mirror of
https://github.com/bitwarden/server.git
synced 2025-02-13 01:21:29 +01:00
Merge branch 'main' into ac/ac-1682/data-migrations-for-deprecated-permissions
This commit is contained in:
commit
69c0997f6c
@ -19,20 +19,11 @@ configure_other_vars() {
|
||||
cp secrets.json .secrets.json.tmp
|
||||
# set DB_PASSWORD equal to .services.mssql.environment.MSSQL_SA_PASSWORD, accounting for quotes
|
||||
DB_PASSWORD="$(grep -oP 'MSSQL_SA_PASSWORD=["'"'"']?\K[^"'"'"'\s]+' $DEV_DIR/.env)"
|
||||
CERT_OUTPUT="$(./create_certificates_linux.sh)"
|
||||
#shellcheck disable=SC2086
|
||||
IDENTITY_SERVER_FINGERPRINT="$(echo $CERT_OUTPUT | awk -F 'Identity Server Dev: ' '{match($2, /[[:alnum:]]+/); print substr($2, RSTART, RLENGTH)}')"
|
||||
#shellcheck disable=SC2086
|
||||
DATA_PROTECTION_FINGERPRINT="$(echo $CERT_OUTPUT | awk -F 'Data Protection Dev: ' '{match($2, /[[:alnum:]]+/); print substr($2, RSTART, RLENGTH)}')"
|
||||
SQL_CONNECTION_STRING="Server=localhost;Database=vault_dev;User Id=SA;Password=$DB_PASSWORD;Encrypt=True;TrustServerCertificate=True"
|
||||
echo "Identity Server Dev: $IDENTITY_SERVER_FINGERPRINT"
|
||||
echo "Data Protection Dev: $DATA_PROTECTION_FINGERPRINT"
|
||||
jq \
|
||||
".globalSettings.sqlServer.connectionString = \"$SQL_CONNECTION_STRING\" |
|
||||
.globalSettings.postgreSql.connectionString = \"Host=localhost;Username=postgres;Password=$DB_PASSWORD;Database=vault_dev;Include Error Detail=true\" |
|
||||
.globalSettings.mySql.connectionString = \"server=localhost;uid=root;pwd=$DB_PASSWORD;database=vault_dev\" |
|
||||
.globalSettings.identityServer.certificateThumbprint = \"$IDENTITY_SERVER_FINGERPRINT\" |
|
||||
.globalSettings.dataProtection.certificateThumbprint = \"$DATA_PROTECTION_FINGERPRINT\"" \
|
||||
.globalSettings.mySql.connectionString = \"server=localhost;uid=root;pwd=$DB_PASSWORD;database=vault_dev\"" \
|
||||
.secrets.json.tmp >secrets.json
|
||||
rm -f .secrets.json.tmp
|
||||
popd >/dev/null || exit
|
||||
@ -51,7 +42,7 @@ Proceed? [y/N] " response
|
||||
pushd ./dev >/dev/null || exit
|
||||
pwsh ./setup_secrets.ps1 || true
|
||||
popd >/dev/null || exit
|
||||
|
||||
|
||||
echo "Running migrations..."
|
||||
sleep 5 # wait for DB container to start
|
||||
dotnet run --project ./util/MsSqlMigratorUtility "$SQL_CONNECTION_STRING"
|
||||
|
@ -12,5 +12,11 @@
|
||||
"extensions": ["ms-dotnettools.csdevkit"]
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "bash .devcontainer/internal_dev/postCreateCommand.sh"
|
||||
"postCreateCommand": "bash .devcontainer/internal_dev/postCreateCommand.sh",
|
||||
"portsAttributes": {
|
||||
"1080": {
|
||||
"label": "Mail Catcher",
|
||||
"onAutoForward": "notify"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,20 +29,11 @@ configure_other_vars() {
|
||||
cp secrets.json .secrets.json.tmp
|
||||
# set DB_PASSWORD equal to .services.mssql.environment.MSSQL_SA_PASSWORD, accounting for quotes
|
||||
DB_PASSWORD="$(grep -oP 'MSSQL_SA_PASSWORD=["'"'"']?\K[^"'"'"'\s]+' $DEV_DIR/.env)"
|
||||
CERT_OUTPUT="$(./create_certificates_linux.sh)"
|
||||
#shellcheck disable=SC2086
|
||||
IDENTITY_SERVER_FINGERPRINT="$(echo $CERT_OUTPUT | awk -F 'Identity Server Dev: ' '{match($2, /[[:alnum:]]+/); print substr($2, RSTART, RLENGTH)}')"
|
||||
#shellcheck disable=SC2086
|
||||
DATA_PROTECTION_FINGERPRINT="$(echo $CERT_OUTPUT | awk -F 'Data Protection Dev: ' '{match($2, /[[:alnum:]]+/); print substr($2, RSTART, RLENGTH)}')"
|
||||
SQL_CONNECTION_STRING="Server=localhost;Database=vault_dev;User Id=SA;Password=$DB_PASSWORD;Encrypt=True;TrustServerCertificate=True"
|
||||
echo "Identity Server Dev: $IDENTITY_SERVER_FINGERPRINT"
|
||||
echo "Data Protection Dev: $DATA_PROTECTION_FINGERPRINT"
|
||||
jq \
|
||||
".globalSettings.sqlServer.connectionString = \"$SQL_CONNECTION_STRING\" |
|
||||
.globalSettings.postgreSql.connectionString = \"Host=localhost;Username=postgres;Password=$DB_PASSWORD;Database=vault_dev;Include Error Detail=true\" |
|
||||
.globalSettings.mySql.connectionString = \"server=localhost;uid=root;pwd=$DB_PASSWORD;database=vault_dev\" |
|
||||
.globalSettings.identityServer.certificateThumbprint = \"$IDENTITY_SERVER_FINGERPRINT\" |
|
||||
.globalSettings.dataProtection.certificateThumbprint = \"$DATA_PROTECTION_FINGERPRINT\"" \
|
||||
.globalSettings.mySql.connectionString = \"server=localhost;uid=root;pwd=$DB_PASSWORD;database=vault_dev\"" \
|
||||
.secrets.json.tmp >secrets.json
|
||||
rm .secrets.json.tmp
|
||||
popd >/dev/null || exit
|
||||
@ -74,7 +65,7 @@ Press <Enter> to continue."
|
||||
echo "Injecting dotnet secrets..."
|
||||
pwsh ./setup_secrets.ps1 || true
|
||||
popd >/dev/null || exit
|
||||
|
||||
|
||||
echo "Running migrations..."
|
||||
sleep 5 # wait for DB container to start
|
||||
dotnet run --project ./util/MsSqlMigratorUtility "$SQL_CONNECTION_STRING"
|
||||
|
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@ -47,6 +47,8 @@ bitwarden_license/src/test/Scim.ScimTest @bitwarden/team-admin-console-dev
|
||||
**/*invoice* @bitwarden/team-billing-dev
|
||||
**/*OrganizationLicense* @bitwarden/team-billing-dev
|
||||
**/Billing @bitwarden/team-billing-dev
|
||||
src/Admin/Controllers/ToolsController.cs @bitwarden/team-billing-dev
|
||||
src/Admin/Views/Tools @bitwarden/team-billing-dev
|
||||
|
||||
# Multiple owners - DO NOT REMOVE (DevOps)
|
||||
**/packages.lock.json
|
||||
|
1
.github/renovate.json
vendored
1
.github/renovate.json
vendored
@ -46,6 +46,7 @@
|
||||
{
|
||||
"matchFileNames": ["src/Admin/package.json", "src/Sso/package.json"],
|
||||
"description": "Admin & SSO npm packages",
|
||||
"commitMessagePrefix": "[deps] Auth:",
|
||||
"reviewers": ["team:team-auth-dev"]
|
||||
},
|
||||
{
|
||||
|
164
.github/workflows/_move_finalization_db_scripts.yml
vendored
Normal file
164
.github/workflows/_move_finalization_db_scripts.yml
vendored
Normal file
@ -0,0 +1,164 @@
|
||||
---
|
||||
|
||||
name: _move_finalization_db_scripts
|
||||
run-name: Move finalization db scripts
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
|
||||
setup:
|
||||
name: Setup
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
migration_filename_prefix: ${{ steps.prefix.outputs.prefix }}
|
||||
copy_finalization_scripts: ${{ steps.check-finalization-scripts-existence.outputs.copy_finalization_scripts }}
|
||||
steps:
|
||||
- name: Login to Azure
|
||||
uses: Azure/login@de95379fe4dadc2defb305917eaa7e5dde727294 # v1.5.1
|
||||
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-pat-bitwarden-devops-bot-repo-scope"
|
||||
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
|
||||
- name: Get script prefix
|
||||
id: prefix
|
||||
run: echo "prefix=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check if any files in db finalization
|
||||
id: check-finalization-scripts-existence
|
||||
run: |
|
||||
if [ -f util/Migrator/DbScripts_finalization/* ]; then
|
||||
echo "copy_finalization_scripts=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "copy_finalization_scripts=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
move-finalization-db-scripts:
|
||||
name: Move finalization db scripts
|
||||
runs-on: ubuntu-22.04
|
||||
needs: setup
|
||||
if: ${{ needs.setup.outputs.copy_finalization_scripts == 'true' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate branch name
|
||||
id: branch_name
|
||||
env:
|
||||
PREFIX: ${{ needs.setup.outputs.migration_filename_prefix }}
|
||||
run: echo "branch_name=move_finalization_db_scripts_$PREFIX" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Create branch"
|
||||
env:
|
||||
BRANCH: ${{ steps.branch_name.outputs.branch_name }}
|
||||
run: git switch -c $BRANCH
|
||||
|
||||
- name: Move DbScripts_finalization
|
||||
id: move-files
|
||||
env:
|
||||
PREFIX: ${{ needs.setup.outputs.migration_filename_prefix }}
|
||||
run: |
|
||||
src_dir="util/Migrator/DbScripts_finalization"
|
||||
dest_dir="util/Migrator/DbScripts"
|
||||
i=0
|
||||
|
||||
moved_files=""
|
||||
for file in "$src_dir"/*; do
|
||||
filenumber=$(printf "%02d" $i)
|
||||
|
||||
filename=$(basename "$file")
|
||||
new_filename="${PREFIX}_${filenumber}_${filename}"
|
||||
dest_file="$dest_dir/$new_filename"
|
||||
|
||||
mv "$file" "$dest_file"
|
||||
moved_files="$moved_files \n $filename -> $new_filename"
|
||||
|
||||
i=$((i+1))
|
||||
done
|
||||
echo "moved_files=$moved_files" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Azure - Prod Subscription
|
||||
uses: Azure/login@de95379fe4dadc2defb305917eaa7e5dde727294 # v1.5.1
|
||||
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,
|
||||
devops-alerts-slack-webhook-url"
|
||||
|
||||
- name: Import GPG keys
|
||||
uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0
|
||||
with:
|
||||
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
||||
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
|
||||
- name: Commit and push changes
|
||||
id: commit
|
||||
run: |
|
||||
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||
git config --local user.name "bitwarden-devops-bot"
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
git add .
|
||||
git commit -m "Move DbScripts_finalization to DbScripts" -a
|
||||
git push -u origin ${{ steps.branch_name.outputs.branch_name }}
|
||||
echo "pr_needed=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "No changes to commit!";
|
||||
echo "pr_needed=false" >> $GITHUB_OUTPUT
|
||||
echo "### :mega: No changes to commit! PR was ommited." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: Create PR for ${{ steps.branch_name.outputs.branch_name }}
|
||||
if: ${{ steps.commit.outputs.pr_needed == 'true' }}
|
||||
id: create-pr
|
||||
env:
|
||||
BRANCH: ${{ steps.branch_name.outputs.branch_name }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
MOVED_FILES: ${{ steps.move-files.outputs.moved_files }}
|
||||
TITLE: "Move finalization db scripts"
|
||||
run: |
|
||||
PR_URL=$(gh pr create --title "$TITLE" \
|
||||
--base "main" \
|
||||
--head "$BRANCH" \
|
||||
--label "automated pr" \
|
||||
--body "
|
||||
## Automated movement of DbScripts_finalization to DbScripts
|
||||
|
||||
## Files moved:
|
||||
$(echo -e "$MOVED_FILES")
|
||||
")
|
||||
echo "pr_url=${PR_URL}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Notify Slack about creation of PR
|
||||
if: ${{ steps.commit.outputs.pr_needed == 'true' }}
|
||||
uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
|
||||
with:
|
||||
message: "Created PR for moving DbScripts_finalization to DbScripts: ${{ steps.create-pr.outputs.pr_url }}"
|
||||
status: ${{ job.status }}
|
37
.github/workflows/build.yml
vendored
37
.github/workflows/build.yml
vendored
@ -250,7 +250,7 @@ jobs:
|
||||
|
||||
- name: Check Branch to Publish
|
||||
env:
|
||||
PUBLISH_BRANCHES: "master,rc,hotfix-rc"
|
||||
PUBLISH_BRANCHES: "main,rc,hotfix-rc"
|
||||
id: publish-branch-check
|
||||
run: |
|
||||
IFS="," read -a publish_branches <<< $PUBLISH_BRANCHES
|
||||
@ -287,7 +287,7 @@ jobs:
|
||||
id: tag
|
||||
run: |
|
||||
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") # slash safe branch name
|
||||
if [[ "$IMAGE_TAG" == "master" ]]; then
|
||||
if [[ "$IMAGE_TAG" == "main" ]]; then
|
||||
IMAGE_TAG=dev
|
||||
fi
|
||||
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
||||
@ -300,12 +300,19 @@ jobs:
|
||||
echo "PROJECT_NAME: $PROJECT_NAME"
|
||||
echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate image full name
|
||||
id: image-name
|
||||
- name: Generate image name(s)
|
||||
id: image-names
|
||||
env:
|
||||
IMAGE_TAG: ${{ steps.tag.outputs.image_tag }}
|
||||
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||
run: echo "name=${_AZ_REGISTRY}/${PROJECT_NAME}:${IMAGE_TAG}" >> $GITHUB_OUTPUT
|
||||
SHA: ${{ github.sha }}
|
||||
run: |
|
||||
NAMES="${_AZ_REGISTRY}/${PROJECT_NAME}:${IMAGE_TAG}"
|
||||
if [[ "${IMAGE_TAG}" == "dev" ]]; then
|
||||
SHORT_SHA=$(git rev-parse --short ${SHA})
|
||||
NAMES=$NAMES",${_AZ_REGISTRY}/${PROJECT_NAME}:dev-${SHORT_SHA}"
|
||||
fi
|
||||
echo "names=$NAMES" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get build artifact
|
||||
if: ${{ matrix.dotnet }}
|
||||
@ -327,7 +334,7 @@ jobs:
|
||||
file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.image-name.outputs.name }}
|
||||
tags: ${{ steps.image-names.outputs.names }}
|
||||
secrets: |
|
||||
"GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}"
|
||||
|
||||
@ -354,13 +361,13 @@ jobs:
|
||||
run: dotnet tool restore
|
||||
|
||||
- name: Make Docker stubs
|
||||
if: github.ref == 'refs/heads/master' ||
|
||||
if: github.ref == 'refs/heads/main' ||
|
||||
github.ref == 'refs/heads/rc' ||
|
||||
github.ref == 'refs/heads/hotfix-rc'
|
||||
run: |
|
||||
# Set proper setup image based on branch
|
||||
case "${{ github.ref }}" in
|
||||
"refs/heads/master")
|
||||
"refs/heads/main")
|
||||
SETUP_IMAGE="$_AZ_REGISTRY/setup:dev"
|
||||
;;
|
||||
"refs/heads/rc")
|
||||
@ -396,13 +403,13 @@ jobs:
|
||||
cd docker-stub/EU; zip -r ../../docker-stub-EU.zip *; cd ../..
|
||||
|
||||
- name: Make Docker stub checksums
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
||||
run: |
|
||||
sha256sum docker-stub-US.zip > docker-stub-US-sha256.txt
|
||||
sha256sum docker-stub-EU.zip > docker-stub-EU-sha256.txt
|
||||
|
||||
- name: Upload Docker stub US artifact
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: docker-stub-US.zip
|
||||
@ -410,7 +417,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Docker stub EU artifact
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: docker-stub-EU.zip
|
||||
@ -418,7 +425,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Docker stub US checksum artifact
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: docker-stub-US-sha256.txt
|
||||
@ -426,7 +433,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Docker stub EU checksum artifact
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: docker-stub-EU-sha256.txt
|
||||
@ -542,7 +549,7 @@ jobs:
|
||||
owner: 'bitwarden',
|
||||
repo: 'self-host',
|
||||
workflow_id: 'build-unified.yml',
|
||||
ref: 'master',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
server_branch: '${{ github.ref }}'
|
||||
}
|
||||
@ -564,7 +571,7 @@ jobs:
|
||||
steps:
|
||||
- name: Check if any job failed
|
||||
if: |
|
||||
github.ref == 'refs/heads/master'
|
||||
github.ref == 'refs/heads/main'
|
||||
|| github.ref == 'refs/heads/rc'
|
||||
|| github.ref == 'refs/heads/hotfix-rc'
|
||||
env:
|
||||
|
@ -74,7 +74,7 @@ jobs:
|
||||
steps:
|
||||
- name: Check if any job failed
|
||||
if: |
|
||||
github.ref == 'refs/heads/master'
|
||||
github.ref == 'refs/heads/main'
|
||||
|| github.ref == 'refs/heads/rc'
|
||||
|| github.ref == 'refs/heads/hotfix-rc'
|
||||
env:
|
||||
|
6
.github/workflows/database.yml
vendored
6
.github/workflows/database.yml
vendored
@ -1,4 +1,4 @@
|
||||
---
|
||||
---
|
||||
name: Validate Database
|
||||
|
||||
on:
|
||||
@ -11,7 +11,7 @@ on:
|
||||
- 'util/Migrator/**'
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'main'
|
||||
- 'rc'
|
||||
paths:
|
||||
- 'src/Sql/**'
|
||||
@ -31,7 +31,7 @@ jobs:
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
dotnet --info
|
||||
|
10
.github/workflows/infrastructure-tests.yml
vendored
10
.github/workflows/infrastructure-tests.yml
vendored
@ -17,7 +17,7 @@ on:
|
||||
- 'test/Infrastructure.IntegrationTest/**' # Any changes to the tests
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'main'
|
||||
- 'rc'
|
||||
paths:
|
||||
- '.github/workflows/infrastructure-tests.yml' # This file
|
||||
@ -55,7 +55,7 @@ jobs:
|
||||
cp .env.example .env
|
||||
docker compose --profile mssql --profile postgres --profile mysql up -d
|
||||
shell: pwsh
|
||||
|
||||
|
||||
# I've seen the SQL Server container not be ready for commands right after starting up and just needing a bit longer to be ready
|
||||
- name: Sleep
|
||||
run: sleep 15s
|
||||
@ -76,13 +76,13 @@ jobs:
|
||||
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:PostgreSql:ConnectionString="$CONN_STR"'
|
||||
env:
|
||||
CONN_STR: "Host=localhost;Username=postgres;Password=SET_A_PASSWORD_HERE_123;Database=vault_dev"
|
||||
|
||||
|
||||
- name: Migrate Sqlite
|
||||
working-directory: 'util/SqliteMigrations'
|
||||
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:Sqlite:ConnectionString="$CONN_STR"'
|
||||
env:
|
||||
CONN_STR: "Data Source=${{ runner.temp }}/test.db"
|
||||
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: 'test/Infrastructure.IntegrationTest'
|
||||
env:
|
||||
@ -109,7 +109,7 @@ jobs:
|
||||
path: "**/*-test-results.trx"
|
||||
reporter: dotnet-trx
|
||||
fail-on-error: true
|
||||
|
||||
|
||||
- name: Docker compose down
|
||||
if: always()
|
||||
working-directory: "dev"
|
||||
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@ -102,7 +102,7 @@ jobs:
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
branch: master
|
||||
branch: main
|
||||
artifacts: ${{ matrix.name }}.zip
|
||||
|
||||
- name: Login to Azure - CI subscription
|
||||
@ -291,7 +291,7 @@ jobs:
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
branch: master
|
||||
branch: main
|
||||
artifacts: "docker-stub-US.zip,
|
||||
docker-stub-US-sha256.txt,
|
||||
docker-stub-EU.zip,
|
||||
|
2
.github/workflows/stale-bot.yml
vendored
2
.github/workflows/stale-bot.yml
vendored
@ -27,4 +27,4 @@ jobs:
|
||||
|
||||
If you’re still working on this, please respond here after you’ve made the changes we’ve requested and our team will re-open it for further review.
|
||||
|
||||
Please make sure to resolve any conflicts with the master branch before requesting another review.
|
||||
Please make sure to resolve any conflicts with the main branch before requesting another review.
|
||||
|
114
.github/workflows/version-bump.yml
vendored
114
.github/workflows/version-bump.yml
vendored
@ -1,21 +1,24 @@
|
||||
---
|
||||
name: Version Bump
|
||||
run-name: Version Bump - v${{ inputs.version_number }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_number:
|
||||
description: "New Version"
|
||||
description: "New version (example: '2024.1.0')"
|
||||
required: true
|
||||
type: string
|
||||
cut_rc_branch:
|
||||
description: "Cut RC branch?"
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
bump_props_version:
|
||||
name: "Create version_bump_${{ github.event.inputs.version_number }} branch"
|
||||
bump_version:
|
||||
name: "Bump Version to v${{ inputs.version_number }}"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
@ -26,7 +29,15 @@ jobs:
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||
secrets: "github-gpg-private-key,
|
||||
github-gpg-private-key-passphrase,
|
||||
github-pat-bitwarden-devops-bot-repo-scope"
|
||||
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
with:
|
||||
ref: main
|
||||
repository: bitwarden/server
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0
|
||||
@ -39,19 +50,40 @@ jobs:
|
||||
- name: Create Version Branch
|
||||
id: create-branch
|
||||
run: |
|
||||
NAME=version_bump_${{ github.ref_name }}_${{ github.event.inputs.version_number }}
|
||||
NAME=version_bump_${{ github.ref_name }}_${{ inputs.version_number }}
|
||||
git switch -c $NAME
|
||||
echo "name=$NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install xmllint
|
||||
run: sudo apt install -y libxml2-utils
|
||||
|
||||
- name: Verify input version
|
||||
env:
|
||||
NEW_VERSION: ${{ inputs.version_number }}
|
||||
run: |
|
||||
CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
|
||||
|
||||
# Error if version has not changed.
|
||||
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
||||
echo "Version has not changed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if version is newer.
|
||||
printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Version check successful."
|
||||
else
|
||||
echo "Version check failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Bump Version - Props
|
||||
uses: bitwarden/gh-actions/version-bump@main
|
||||
with:
|
||||
version: ${{ github.event.inputs.version_number }}
|
||||
version: ${{ inputs.version_number }}
|
||||
file_path: "Directory.Build.props"
|
||||
|
||||
- name: Refresh lockfiles
|
||||
run: dotnet restore -f --force-evaluate --no-cache
|
||||
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||
@ -69,7 +101,7 @@ jobs:
|
||||
|
||||
- name: Commit files
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
run: git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
|
||||
run: git commit -m "Bumped version to ${{ inputs.version_number }}" -a
|
||||
|
||||
- name: Push changes
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
@ -79,13 +111,14 @@ jobs:
|
||||
|
||||
- name: Create Version PR
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
id: create-pr
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
TITLE: "Bump version to ${{ github.event.inputs.version_number }}"
|
||||
TITLE: "Bump version to ${{ inputs.version_number }}"
|
||||
run: |
|
||||
gh pr create --title "$TITLE" \
|
||||
--base "$GITHUB_REF" \
|
||||
PR_URL=$(gh pr create --title "$TITLE" \
|
||||
--base "main" \
|
||||
--head "$PR_BRANCH" \
|
||||
--label "version update" \
|
||||
--label "automated pr" \
|
||||
@ -98,4 +131,49 @@ jobs:
|
||||
- [X] Other
|
||||
|
||||
## Objective
|
||||
Automated version bump to ${{ github.event.inputs.version_number }}"
|
||||
Automated version bump to ${{ inputs.version_number }}")
|
||||
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Approve PR
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||
run: gh pr review $PR_NUMBER --approve
|
||||
|
||||
- name: Merge PR
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.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 }}
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Check if RC branch exists
|
||||
run: |
|
||||
remote_rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
|
||||
if [[ "${remote_rc_branch_check}" -gt 0 ]]; then
|
||||
echo "Remote RC branch exists."
|
||||
echo "Please delete current RC branch before running again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Cut RC branch
|
||||
run: |
|
||||
git switch --quiet --create rc
|
||||
git push --quiet --set-upstream origin rc
|
||||
|
||||
|
||||
move-future-db-scripts:
|
||||
name: Move future DB scripts
|
||||
needs: cut_rc
|
||||
uses: ./.github/workflows/_move_finalization_db_scripts.yml
|
||||
secrets: inherit
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -225,4 +225,4 @@ src/Identity/Identity.zip
|
||||
src/Notifications/Notifications.zip
|
||||
bitwarden_license/src/Portal/Portal.zip
|
||||
bitwarden_license/src/Sso/Sso.zip
|
||||
**/src/*/flags.json
|
||||
**/src/**/flags.json
|
||||
|
36
.vscode/tasks.json
vendored
36
.vscode/tasks.json
vendored
@ -211,6 +211,42 @@
|
||||
"clear": false
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "Setup Secrets",
|
||||
"type": "shell",
|
||||
"command": "pwsh -WorkingDirectory ${workspaceFolder}/dev -Command '${workspaceFolder}/dev/setup_secrets.ps1 -clear:$${input:setupSecretsClear}'",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Install Dev Cert",
|
||||
"type": "shell",
|
||||
"command": "dotnet tool install -g dotnet-certificate-tool -g && certificate-tool add --file ${workspaceFolder}/dev/dev.pfx --password '${input:certPassword}'",
|
||||
"problemMatcher": []
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"id": "setupSecretsClear",
|
||||
"type": "pickString",
|
||||
"default": "true",
|
||||
"description": "Whether or not to clear existing secrets",
|
||||
"options": [
|
||||
{
|
||||
"label": "true",
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"label": "false",
|
||||
"value": "false"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "certPassword",
|
||||
"type": "promptString",
|
||||
"description": "Password for your dev certificate.",
|
||||
"password": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -2,9 +2,8 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Version>2023.12.0</Version>
|
||||
<Version>2023.12.1</Version>
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
</PropertyGroup>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<img src="https://github.com/bitwarden/brand/blob/master/screenshots/apps-combo-logo.png" alt="Bitwarden" />
|
||||
<img src="https://github.com/bitwarden/brand/blob/main/screenshots/apps-combo-logo.png" alt="Bitwarden" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/bitwarden/server/actions/workflows/build.yml?query=branch:master" target="_blank">
|
||||
@ -67,11 +67,11 @@ Interested in contributing in a big way? Consider joining our team! We're hiring
|
||||
|
||||
## Contribute
|
||||
|
||||
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [Contributing Guidelines](https://contributing.bitwarden.com/contributing/). Check out the [Contributing Documentation](https://contributing.bitwarden.com/) for how to get started with your first contribution.
|
||||
Code contributions are welcome! Please commit any pull requests against the `main` branch. Learn more about how to contribute by reading the [Contributing Guidelines](https://contributing.bitwarden.com/contributing/). Check out the [Contributing Documentation](https://contributing.bitwarden.com/) for how to get started with your first contribution.
|
||||
|
||||
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file. We also run a program on [HackerOne](https://hackerone.com/bitwarden).
|
||||
|
||||
No grant of any rights in the trademarks, service marks, or logos of Bitwarden is made (except as may be necessary to comply with the notice requirements as applicable), and use of any Bitwarden trademarks must comply with [Bitwarden Trademark Guidelines](https://github.com/bitwarden/server/blob/master/TRADEMARK_GUIDELINES.md).
|
||||
No grant of any rights in the trademarks, service marks, or logos of Bitwarden is made (except as may be necessary to comply with the notice requirements as applicable), and use of any Bitwarden trademarks must comply with [Bitwarden Trademark Guidelines](https://github.com/bitwarden/server/blob/main/TRADEMARK_GUIDELINES.md).
|
||||
|
||||
### Dotnet-format
|
||||
|
||||
|
@ -1,9 +1,8 @@
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
using Bit.Core.SecretsManager.Models.Data;
|
||||
using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
|
||||
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -11,25 +10,23 @@ using Microsoft.AspNetCore.Authorization;
|
||||
namespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
|
||||
|
||||
public class
|
||||
ProjectPeopleAccessPoliciesAuthorizationHandler : AuthorizationHandler<ProjectPeopleAccessPoliciesOperationRequirement,
|
||||
ProjectPeopleAccessPoliciesAuthorizationHandler : AuthorizationHandler<
|
||||
ProjectPeopleAccessPoliciesOperationRequirement,
|
||||
ProjectPeopleAccessPolicies>
|
||||
{
|
||||
private readonly IAccessClientQuery _accessClientQuery;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly ISameOrganizationQuery _sameOrganizationQuery;
|
||||
|
||||
public ProjectPeopleAccessPoliciesAuthorizationHandler(ICurrentContext currentContext,
|
||||
IAccessClientQuery accessClientQuery,
|
||||
IGroupRepository groupRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ISameOrganizationQuery sameOrganizationQuery,
|
||||
IProjectRepository projectRepository)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_accessClientQuery = accessClientQuery;
|
||||
_groupRepository = groupRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_sameOrganizationQuery = sameOrganizationQuery;
|
||||
_projectRepository = projectRepository;
|
||||
}
|
||||
|
||||
@ -71,9 +68,7 @@ public class
|
||||
if (resource.UserAccessPolicies != null && resource.UserAccessPolicies.Any())
|
||||
{
|
||||
var orgUserIds = resource.UserAccessPolicies.Select(ap => ap.OrganizationUserId!.Value).ToList();
|
||||
var users = await _organizationUserRepository.GetManyAsync(orgUserIds);
|
||||
if (users.Any(user => user.OrganizationId != resource.OrganizationId) ||
|
||||
users.Count != orgUserIds.Count)
|
||||
if (!await _sameOrganizationQuery.OrgUsersInTheSameOrgAsync(orgUserIds, resource.OrganizationId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -82,9 +77,7 @@ public class
|
||||
if (resource.GroupAccessPolicies != null && resource.GroupAccessPolicies.Any())
|
||||
{
|
||||
var groupIds = resource.GroupAccessPolicies.Select(ap => ap.GroupId!.Value).ToList();
|
||||
var groups = await _groupRepository.GetManyByManyIds(groupIds);
|
||||
if (groups.Any(group => group.OrganizationId != resource.OrganizationId) ||
|
||||
groups.Count != groupIds.Count)
|
||||
if (!await _sameOrganizationQuery.GroupsInTheSameOrgAsync(groupIds, resource.OrganizationId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
@ -0,0 +1,89 @@
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
using Bit.Core.SecretsManager.Models.Data;
|
||||
using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
|
||||
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
|
||||
|
||||
public class
|
||||
ServiceAccountPeopleAccessPoliciesAuthorizationHandler : AuthorizationHandler<
|
||||
ServiceAccountPeopleAccessPoliciesOperationRequirement,
|
||||
ServiceAccountPeopleAccessPolicies>
|
||||
{
|
||||
private readonly IAccessClientQuery _accessClientQuery;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ISameOrganizationQuery _sameOrganizationQuery;
|
||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||
|
||||
public ServiceAccountPeopleAccessPoliciesAuthorizationHandler(ICurrentContext currentContext,
|
||||
IAccessClientQuery accessClientQuery,
|
||||
ISameOrganizationQuery sameOrganizationQuery,
|
||||
IServiceAccountRepository serviceAccountRepository)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_accessClientQuery = accessClientQuery;
|
||||
_sameOrganizationQuery = sameOrganizationQuery;
|
||||
_serviceAccountRepository = serviceAccountRepository;
|
||||
}
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
ServiceAccountPeopleAccessPoliciesOperationRequirement requirement,
|
||||
ServiceAccountPeopleAccessPolicies 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 == ServiceAccountPeopleAccessPoliciesOperations.Replace:
|
||||
await CanReplaceServiceAccountPeopleAsync(context, requirement, resource, accessClient, userId);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unsupported operation requirement type provided.",
|
||||
nameof(requirement));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanReplaceServiceAccountPeopleAsync(AuthorizationHandlerContext context,
|
||||
ServiceAccountPeopleAccessPoliciesOperationRequirement requirement, ServiceAccountPeopleAccessPolicies resource,
|
||||
AccessClientType accessClient, Guid userId)
|
||||
{
|
||||
var access = await _serviceAccountRepository.AccessToServiceAccountAsync(resource.Id, userId, accessClient);
|
||||
if (access.Write)
|
||||
{
|
||||
if (resource.UserAccessPolicies != null && resource.UserAccessPolicies.Any())
|
||||
{
|
||||
var orgUserIds = resource.UserAccessPolicies.Select(ap => ap.OrganizationUserId!.Value).ToList();
|
||||
if (!await _sameOrganizationQuery.OrgUsersInTheSameOrgAsync(orgUserIds, resource.OrganizationId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (resource.GroupAccessPolicies != null && resource.GroupAccessPolicies.Any())
|
||||
{
|
||||
var groupIds = resource.GroupAccessPolicies.Select(ap => ap.GroupId!.Value).ToList();
|
||||
if (!await _sameOrganizationQuery.GroupsInTheSameOrgAsync(groupIds, resource.OrganizationId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
|
||||
|
||||
public class SameOrganizationQuery : ISameOrganizationQuery
|
||||
{
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
|
||||
public SameOrganizationQuery(IOrganizationUserRepository organizationUserRepository,
|
||||
IGroupRepository groupRepository)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_groupRepository = groupRepository;
|
||||
}
|
||||
|
||||
public async Task<bool> OrgUsersInTheSameOrgAsync(List<Guid> organizationUserIds, Guid organizationId)
|
||||
{
|
||||
var users = await _organizationUserRepository.GetManyAsync(organizationUserIds);
|
||||
return users.All(user => user.OrganizationId == organizationId) &&
|
||||
users.Count == organizationUserIds.Count;
|
||||
}
|
||||
|
||||
public async Task<bool> GroupsInTheSameOrgAsync(List<Guid> groupIds, Guid organizationId)
|
||||
{
|
||||
var groups = await _groupRepository.GetManyByManyIds(groupIds);
|
||||
return groups.All(group => group.OrganizationId == organizationId) &&
|
||||
groups.Count == groupIds.Count;
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ using Bit.Commercial.Core.SecretsManager.Commands.Secrets;
|
||||
using Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;
|
||||
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.ServiceAccounts;
|
||||
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
||||
@ -19,6 +20,7 @@ using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
|
||||
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
|
||||
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
|
||||
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.ServiceAccounts.Interfaces;
|
||||
@ -36,8 +38,10 @@ public static class SecretsManagerCollectionExtensions
|
||||
services.AddScoped<IAuthorizationHandler, ServiceAccountAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, AccessPolicyAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, ProjectPeopleAccessPoliciesAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, ServiceAccountPeopleAccessPoliciesAuthorizationHandler>();
|
||||
services.AddScoped<IAccessClientQuery, AccessClientQuery>();
|
||||
services.AddScoped<IMaxProjectsQuery, MaxProjectsQuery>();
|
||||
services.AddScoped<ISameOrganizationQuery, SameOrganizationQuery>();
|
||||
services.AddScoped<IServiceAccountSecretsDetailsQuery, ServiceAccountSecretsDetailsQuery>();
|
||||
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
|
||||
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -183,28 +183,6 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
|
||||
return entities.Select(e => MapToCore(e.ap, e.CurrentUserInGroup));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.SecretsManager.Entities.BaseAccessPolicy>> GetManyByGrantedServiceAccountIdAsync(Guid id, Guid userId)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var entities = await dbContext.AccessPolicies.Where(ap =>
|
||||
((UserServiceAccountAccessPolicy)ap).GrantedServiceAccountId == id ||
|
||||
((GroupServiceAccountAccessPolicy)ap).GrantedServiceAccountId == id)
|
||||
.Include(ap => ((UserServiceAccountAccessPolicy)ap).OrganizationUser.User)
|
||||
.Include(ap => ((GroupServiceAccountAccessPolicy)ap).Group)
|
||||
.Select(ap => new
|
||||
{
|
||||
ap,
|
||||
CurrentUserInGroup = ap is GroupServiceAccountAccessPolicy &&
|
||||
((GroupServiceAccountAccessPolicy)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();
|
||||
@ -352,6 +330,81 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
|
||||
return await GetPeoplePoliciesByGrantedProjectIdAsync(peopleAccessPolicies.Id, userId);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.SecretsManager.Entities.BaseAccessPolicy>>
|
||||
GetPeoplePoliciesByGrantedServiceAccountIdAsync(Guid id, Guid userId)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var entities = await dbContext.AccessPolicies.Where(ap =>
|
||||
((UserServiceAccountAccessPolicy)ap).GrantedServiceAccountId == id ||
|
||||
((GroupServiceAccountAccessPolicy)ap).GrantedServiceAccountId == id)
|
||||
.Include(ap => ((UserServiceAccountAccessPolicy)ap).OrganizationUser.User)
|
||||
.Include(ap => ((GroupServiceAccountAccessPolicy)ap).Group)
|
||||
.Select(ap => new
|
||||
{
|
||||
ap,
|
||||
CurrentUserInGroup = ap is GroupServiceAccountAccessPolicy &&
|
||||
((GroupServiceAccountAccessPolicy)ap).Group.GroupUsers.Any(g =>
|
||||
g.OrganizationUser.UserId == userId)
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return entities.Select(e => MapToCore(e.ap, e.CurrentUserInGroup));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.SecretsManager.Entities.BaseAccessPolicy>> ReplaceServiceAccountPeopleAsync(
|
||||
ServiceAccountPeopleAccessPolicies peopleAccessPolicies, Guid userId)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var peoplePolicyEntities = await dbContext.AccessPolicies.Where(ap =>
|
||||
((UserServiceAccountAccessPolicy)ap).GrantedServiceAccountId == peopleAccessPolicies.Id ||
|
||||
((GroupServiceAccountAccessPolicy)ap).GrantedServiceAccountId == peopleAccessPolicies.Id).ToListAsync();
|
||||
|
||||
var userPolicyEntities =
|
||||
peoplePolicyEntities.Where(ap => ap.GetType() == typeof(UserServiceAccountAccessPolicy)).ToList();
|
||||
var groupPolicyEntities =
|
||||
peoplePolicyEntities.Where(ap => ap.GetType() == typeof(GroupServiceAccountAccessPolicy)).ToList();
|
||||
|
||||
|
||||
if (peopleAccessPolicies.UserAccessPolicies == null || !peopleAccessPolicies.UserAccessPolicies.Any())
|
||||
{
|
||||
dbContext.RemoveRange(userPolicyEntities);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var userPolicyEntity in userPolicyEntities.Where(entity =>
|
||||
peopleAccessPolicies.UserAccessPolicies.All(ap =>
|
||||
((Core.SecretsManager.Entities.UserServiceAccountAccessPolicy)ap).OrganizationUserId !=
|
||||
((UserServiceAccountAccessPolicy)entity).OrganizationUserId)))
|
||||
{
|
||||
dbContext.Remove(userPolicyEntity);
|
||||
}
|
||||
}
|
||||
|
||||
if (peopleAccessPolicies.GroupAccessPolicies == null || !peopleAccessPolicies.GroupAccessPolicies.Any())
|
||||
{
|
||||
dbContext.RemoveRange(groupPolicyEntities);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var groupPolicyEntity in groupPolicyEntities.Where(entity =>
|
||||
peopleAccessPolicies.GroupAccessPolicies.All(ap =>
|
||||
((Core.SecretsManager.Entities.GroupServiceAccountAccessPolicy)ap).GroupId !=
|
||||
((GroupServiceAccountAccessPolicy)entity).GroupId)))
|
||||
{
|
||||
dbContext.Remove(groupPolicyEntity);
|
||||
}
|
||||
}
|
||||
|
||||
await UpsertPeoplePoliciesAsync(dbContext,
|
||||
peopleAccessPolicies.ToBaseAccessPolicies().Select(MapToEntity).ToList(), userPolicyEntities,
|
||||
groupPolicyEntities);
|
||||
await dbContext.SaveChangesAsync();
|
||||
return await GetPeoplePoliciesByGrantedServiceAccountIdAsync(peopleAccessPolicies.Id, userId);
|
||||
}
|
||||
|
||||
private static async Task UpsertPeoplePoliciesAsync(DatabaseContext dbContext,
|
||||
List<BaseAccessPolicy> policies, IReadOnlyCollection<AccessPolicy> userPolicyEntities,
|
||||
IReadOnlyCollection<AccessPolicy> groupPolicyEntities)
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -48,6 +48,7 @@ public static class ServiceCollectionExtensions
|
||||
var identityServerBuilder = services
|
||||
.AddIdentityServer(options =>
|
||||
{
|
||||
options.LicenseKey = globalSettings.IdentityServer.LicenseKey;
|
||||
options.IssuerUri = $"{issuerUri.Scheme}://{issuerUri.Host}";
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
|
@ -23,6 +23,7 @@
|
||||
},
|
||||
"storage": {
|
||||
"connectionString": "UseDevelopmentStorage=true"
|
||||
}
|
||||
},
|
||||
"developmentDirectory": "../../../dev"
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,14 +1,11 @@
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
|
||||
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.Models.Data;
|
||||
using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
|
||||
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
|
||||
@ -38,26 +35,16 @@ public class ProjectPeopleAccessPoliciesAuthorizationHandlerTests
|
||||
}
|
||||
|
||||
private static void SetupOrganizationUsers(SutProvider<ProjectPeopleAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
ProjectPeopleAccessPolicies resource)
|
||||
{
|
||||
var orgUsers = resource.UserAccessPolicies.Select(userPolicy =>
|
||||
new OrganizationUser
|
||||
{
|
||||
OrganizationId = resource.OrganizationId,
|
||||
Id = userPolicy.OrganizationUserId!.Value
|
||||
}).ToList();
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(default)
|
||||
.ReturnsForAnyArgs(orgUsers);
|
||||
}
|
||||
ProjectPeopleAccessPolicies resource) =>
|
||||
sutProvider.GetDependency<ISameOrganizationQuery>()
|
||||
.OrgUsersInTheSameOrgAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)
|
||||
.Returns(true);
|
||||
|
||||
private static void SetupGroups(SutProvider<ProjectPeopleAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
ProjectPeopleAccessPolicies resource)
|
||||
{
|
||||
var groups = resource.GroupAccessPolicies.Select(groupPolicy =>
|
||||
new Group { OrganizationId = resource.OrganizationId, Id = groupPolicy.GroupId!.Value }).ToList();
|
||||
sutProvider.GetDependency<IGroupRepository>().GetManyByManyIds(default)
|
||||
.ReturnsForAnyArgs(groups);
|
||||
}
|
||||
ProjectPeopleAccessPolicies resource) =>
|
||||
sutProvider.GetDependency<ISameOrganizationQuery>()
|
||||
.GroupsInTheSameOrgAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)
|
||||
.Returns(true);
|
||||
|
||||
[Fact]
|
||||
public void PeopleAccessPoliciesOperations_OnlyPublicStatic()
|
||||
@ -129,37 +116,10 @@ public class ProjectPeopleAccessPoliciesAuthorizationHandlerTests
|
||||
{
|
||||
var requirement = ProjectPeopleAccessPoliciesOperations.Replace;
|
||||
SetupUserPermission(sutProvider, accessClient, resource, userId);
|
||||
var orgUsers = resource.UserAccessPolicies.Select(userPolicy =>
|
||||
new OrganizationUser { OrganizationId = Guid.NewGuid(), Id = userPolicy.OrganizationUserId!.Value })
|
||||
.ToList();
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(default)
|
||||
.ReturnsForAnyArgs(orgUsers);
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
sutProvider.GetDependency<ISameOrganizationQuery>()
|
||||
.OrgUsersInTheSameOrgAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)
|
||||
.Returns(false);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.User)]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck)]
|
||||
public async Task ReplaceProjectPeople_UserCountMismatch_DoesNotSucceed(AccessClientType accessClient,
|
||||
SutProvider<ProjectPeopleAccessPoliciesAuthorizationHandler> sutProvider, ProjectPeopleAccessPolicies resource,
|
||||
ClaimsPrincipal claimsPrincipal, Guid userId)
|
||||
{
|
||||
var requirement = ProjectPeopleAccessPoliciesOperations.Replace;
|
||||
SetupUserPermission(sutProvider, accessClient, resource, userId);
|
||||
var orgUsers = resource.UserAccessPolicies.Select(userPolicy =>
|
||||
new OrganizationUser
|
||||
{
|
||||
OrganizationId = resource.OrganizationId,
|
||||
Id = userPolicy.OrganizationUserId!.Value
|
||||
}).ToList();
|
||||
orgUsers.RemoveAt(0);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(default)
|
||||
.ReturnsForAnyArgs(orgUsers);
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
@ -179,35 +139,8 @@ public class ProjectPeopleAccessPoliciesAuthorizationHandlerTests
|
||||
SetupUserPermission(sutProvider, accessClient, resource, userId);
|
||||
SetupOrganizationUsers(sutProvider, resource);
|
||||
|
||||
var groups = resource.GroupAccessPolicies.Select(groupPolicy =>
|
||||
new Group { OrganizationId = Guid.NewGuid(), Id = groupPolicy.GroupId!.Value }).ToList();
|
||||
sutProvider.GetDependency<IGroupRepository>().GetManyByManyIds(default)
|
||||
.ReturnsForAnyArgs(groups);
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.User)]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck)]
|
||||
public async Task ReplaceProjectPeople_GroupCountMismatch_DoesNotSucceed(AccessClientType accessClient,
|
||||
SutProvider<ProjectPeopleAccessPoliciesAuthorizationHandler> sutProvider, ProjectPeopleAccessPolicies resource,
|
||||
ClaimsPrincipal claimsPrincipal, Guid userId)
|
||||
{
|
||||
var requirement = ProjectPeopleAccessPoliciesOperations.Replace;
|
||||
SetupUserPermission(sutProvider, accessClient, resource, userId);
|
||||
SetupOrganizationUsers(sutProvider, resource);
|
||||
|
||||
var groups = resource.GroupAccessPolicies.Select(groupPolicy =>
|
||||
new Group { OrganizationId = resource.OrganizationId, Id = groupPolicy.GroupId!.Value }).ToList();
|
||||
groups.RemoveAt(0);
|
||||
sutProvider.GetDependency<IGroupRepository>().GetManyByManyIds(default)
|
||||
.ReturnsForAnyArgs(groups);
|
||||
sutProvider.GetDependency<ISameOrganizationQuery>()
|
||||
.GroupsInTheSameOrgAsync(Arg.Any<List<Guid>>(), resource.OrganizationId).Returns(false);
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
@ -0,0 +1,186 @@
|
||||
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.AccessPolicies.Interfaces;
|
||||
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
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]
|
||||
public class ServiceAccountPeopleAccessPoliciesAuthorizationHandlerTests
|
||||
{
|
||||
private static void SetupUserPermission(
|
||||
SutProvider<ServiceAccountPeopleAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
AccessClientType accessClientType, ServiceAccountPeopleAccessPolicies resource, Guid userId = new(),
|
||||
bool read = true,
|
||||
bool write = true)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.OrganizationId)
|
||||
.ReturnsForAnyArgs(
|
||||
(accessClientType, userId));
|
||||
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||
.AccessToServiceAccountAsync(resource.Id, userId, accessClientType)
|
||||
.Returns((read, write));
|
||||
}
|
||||
|
||||
private static void SetupOrganizationUsers(
|
||||
SutProvider<ServiceAccountPeopleAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
ServiceAccountPeopleAccessPolicies resource) =>
|
||||
sutProvider.GetDependency<ISameOrganizationQuery>()
|
||||
.OrgUsersInTheSameOrgAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)
|
||||
.Returns(true);
|
||||
|
||||
private static void SetupGroups(SutProvider<ServiceAccountPeopleAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
ServiceAccountPeopleAccessPolicies resource) =>
|
||||
sutProvider.GetDependency<ISameOrganizationQuery>()
|
||||
.GroupsInTheSameOrgAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)
|
||||
.Returns(true);
|
||||
|
||||
[Fact]
|
||||
public void ServiceAccountPeopleAccessPoliciesOperations_OnlyPublicStatic()
|
||||
{
|
||||
var publicStaticFields =
|
||||
typeof(ServiceAccountPeopleAccessPoliciesOperations).GetFields(BindingFlags.Public | BindingFlags.Static);
|
||||
var allFields = typeof(ServiceAccountPeopleAccessPoliciesOperations).GetFields();
|
||||
Assert.Equal(publicStaticFields.Length, allFields.Length);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Handler_UnsupportedServiceAccountPeopleAccessPoliciesOperationRequirement_Throws(
|
||||
SutProvider<ServiceAccountPeopleAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
ServiceAccountPeopleAccessPolicies resource,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = new ServiceAccountPeopleAccessPoliciesOperationRequirement();
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.OrganizationId)
|
||||
.ReturnsForAnyArgs(
|
||||
(AccessClientType.NoAccessCheck, new Guid()));
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Handler_AccessSecretsManagerFalse_DoesNotSucceed(
|
||||
SutProvider<ServiceAccountPeopleAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
ServiceAccountPeopleAccessPolicies resource,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = new ServiceAccountPeopleAccessPoliciesOperationRequirement();
|
||||
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 clientType,
|
||||
SutProvider<ServiceAccountPeopleAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
ServiceAccountPeopleAccessPolicies resource,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = new ServiceAccountPeopleAccessPoliciesOperationRequirement();
|
||||
SetupUserPermission(sutProvider, clientType, resource);
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.User)]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck)]
|
||||
public async Task ReplaceServiceAccountPeople_UserNotInOrg_DoesNotSucceed(AccessClientType accessClient,
|
||||
SutProvider<ServiceAccountPeopleAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
ServiceAccountPeopleAccessPolicies resource,
|
||||
ClaimsPrincipal claimsPrincipal, Guid userId)
|
||||
{
|
||||
var requirement = ServiceAccountPeopleAccessPoliciesOperations.Replace;
|
||||
SetupUserPermission(sutProvider, accessClient, resource, userId);
|
||||
sutProvider.GetDependency<ISameOrganizationQuery>()
|
||||
.OrgUsersInTheSameOrgAsync(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.User)]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck)]
|
||||
public async Task ReplaceServiceAccountPeople_GroupNotInOrg_DoesNotSucceed(AccessClientType accessClient,
|
||||
SutProvider<ServiceAccountPeopleAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
ServiceAccountPeopleAccessPolicies resource,
|
||||
ClaimsPrincipal claimsPrincipal, Guid userId)
|
||||
{
|
||||
var requirement = ServiceAccountPeopleAccessPoliciesOperations.Replace;
|
||||
SetupUserPermission(sutProvider, accessClient, resource, userId);
|
||||
SetupOrganizationUsers(sutProvider, resource);
|
||||
|
||||
sutProvider.GetDependency<ISameOrganizationQuery>()
|
||||
.GroupsInTheSameOrgAsync(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.User, false, false, false)]
|
||||
[BitAutoData(AccessClientType.User, false, true, true)]
|
||||
[BitAutoData(AccessClientType.User, true, false, false)]
|
||||
[BitAutoData(AccessClientType.User, true, true, true)]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck, false, false, false)]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck, false, true, true)]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck, true, false, false)]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck, true, true, true)]
|
||||
public async Task ReplaceServiceAccountPeople_AccessCheck(AccessClientType accessClient, bool read, bool write,
|
||||
bool expected,
|
||||
SutProvider<ServiceAccountPeopleAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
ServiceAccountPeopleAccessPolicies resource,
|
||||
ClaimsPrincipal claimsPrincipal, Guid userId)
|
||||
{
|
||||
var requirement = ServiceAccountPeopleAccessPoliciesOperations.Replace;
|
||||
SetupUserPermission(sutProvider, accessClient, resource, userId, read, write);
|
||||
SetupOrganizationUsers(sutProvider, resource);
|
||||
SetupGroups(sutProvider, resource);
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.Equal(expected, authzContext.HasSucceeded);
|
||||
}
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
using Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.SecretsManager.Queries.AccessPolicies;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SameOrganizationQueryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task OrgUsersInTheSameOrg_NoOrgUsers_ReturnsFalse(SutProvider<SameOrganizationQuery> sutProvider,
|
||||
List<OrganizationUser> orgUsers, Guid organizationId)
|
||||
{
|
||||
var orgUserIds = orgUsers.Select(ou => ou.Id).ToList();
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(orgUserIds)
|
||||
.ReturnsForAnyArgs(new List<OrganizationUser>());
|
||||
|
||||
var result = await sutProvider.Sut.OrgUsersInTheSameOrgAsync(orgUserIds, organizationId);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task OrgUsersInTheSameOrg_OrgMismatch_ReturnsFalse(SutProvider<SameOrganizationQuery> sutProvider,
|
||||
List<OrganizationUser> orgUsers, Guid organizationId)
|
||||
{
|
||||
var orgUserIds = orgUsers.Select(ou => ou.Id).ToList();
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(orgUserIds)
|
||||
.ReturnsForAnyArgs(orgUsers);
|
||||
|
||||
var result = await sutProvider.Sut.OrgUsersInTheSameOrgAsync(orgUserIds, organizationId);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task OrgUsersInTheSameOrg_CountMismatch_ReturnsFalse(SutProvider<SameOrganizationQuery> sutProvider,
|
||||
List<OrganizationUser> orgUsers, Guid organizationId)
|
||||
{
|
||||
var orgUserIds = orgUsers.Select(ou => ou.Id).ToList();
|
||||
foreach (var organizationUser in orgUsers)
|
||||
{
|
||||
organizationUser.OrganizationId = organizationId;
|
||||
}
|
||||
|
||||
orgUsers.RemoveAt(0);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(orgUserIds)
|
||||
.ReturnsForAnyArgs(orgUsers);
|
||||
|
||||
var result = await sutProvider.Sut.OrgUsersInTheSameOrgAsync(orgUserIds, organizationId);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task OrgUsersInTheSameOrg_Success_ReturnsTrue(SutProvider<SameOrganizationQuery> sutProvider,
|
||||
List<OrganizationUser> orgUsers, Guid organizationId)
|
||||
{
|
||||
var orgUserIds = orgUsers.Select(ou => ou.Id).ToList();
|
||||
foreach (var organizationUser in orgUsers)
|
||||
{
|
||||
organizationUser.OrganizationId = organizationId;
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(orgUserIds)
|
||||
.ReturnsForAnyArgs(orgUsers);
|
||||
|
||||
var result = await sutProvider.Sut.OrgUsersInTheSameOrgAsync(orgUserIds, organizationId);
|
||||
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GroupsInTheSameOrg_NoGroups_ReturnsFalse(SutProvider<SameOrganizationQuery> sutProvider,
|
||||
List<Group> groups, Guid organizationId)
|
||||
{
|
||||
var groupIds = groups.Select(ou => ou.Id).ToList();
|
||||
sutProvider.GetDependency<IGroupRepository>().GetManyByManyIds(groupIds)
|
||||
.ReturnsForAnyArgs(new List<Group>());
|
||||
|
||||
var result = await sutProvider.Sut.GroupsInTheSameOrgAsync(groupIds, organizationId);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GroupsInTheSameOrg_OrgMismatch_ReturnsFalse(SutProvider<SameOrganizationQuery> sutProvider,
|
||||
List<Group> groups, Guid organizationId)
|
||||
{
|
||||
var groupIds = groups.Select(ou => ou.Id).ToList();
|
||||
sutProvider.GetDependency<IGroupRepository>().GetManyByManyIds(groupIds)
|
||||
.ReturnsForAnyArgs(groups);
|
||||
|
||||
var result = await sutProvider.Sut.GroupsInTheSameOrgAsync(groupIds, organizationId);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GroupsInTheSameOrg_CountMismatch_ReturnsFalse(SutProvider<SameOrganizationQuery> sutProvider,
|
||||
List<Group> groups, Guid organizationId)
|
||||
{
|
||||
var groupIds = groups.Select(ou => ou.Id).ToList();
|
||||
foreach (var group in groups)
|
||||
{
|
||||
group.OrganizationId = organizationId;
|
||||
}
|
||||
|
||||
groups.RemoveAt(0);
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>().GetManyByManyIds(groupIds)
|
||||
.ReturnsForAnyArgs(groups);
|
||||
|
||||
var result = await sutProvider.Sut.GroupsInTheSameOrgAsync(groupIds, organizationId);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GroupsInTheSameOrg_Success_ReturnsTrue(SutProvider<SameOrganizationQuery> sutProvider,
|
||||
List<Group> groups, Guid organizationId)
|
||||
{
|
||||
var groupIds = groups.Select(ou => ou.Id).ToList();
|
||||
foreach (var group in groups)
|
||||
{
|
||||
group.OrganizationId = organizationId;
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>().GetManyByManyIds(groupIds)
|
||||
.ReturnsForAnyArgs(groups);
|
||||
|
||||
|
||||
var result = await sutProvider.Sut.GroupsInTheSameOrgAsync(groupIds, organizationId);
|
||||
|
||||
Assert.True(result);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2
dev/.gitignore
vendored
2
dev/.gitignore
vendored
@ -15,5 +15,7 @@ data_protection_dev.crt
|
||||
data_protection_dev.key
|
||||
data_protection_dev.pfx
|
||||
|
||||
signingkey.jwk
|
||||
|
||||
# Reverse Proxy Conifg
|
||||
reverse-proxy.conf
|
||||
|
@ -4,9 +4,6 @@
|
||||
IDENTITY_SERVER_KEY=identity_server_dev.key
|
||||
IDENTITY_SERVER_CERT=identity_server_dev.crt
|
||||
IDENTITY_SERVER_CN="Bitwarden Identity Server Dev"
|
||||
DATA_PROTECTION_KEY=data_protection_dev.key
|
||||
DATA_PROTECTION_CERT=data_protection_dev.crt
|
||||
DATA_PROTECTION_CN="Bitwarden Data Protection Dev"
|
||||
|
||||
# Detect management command to trust generated certificates.
|
||||
if [ -x "$(command -v update-ca-certificates)" ]; then
|
||||
@ -30,19 +27,10 @@ openssl req -x509 -newkey rsa:4096 -sha256 -nodes -days 3650 \
|
||||
|
||||
sudo cp $IDENTITY_SERVER_CERT $CA_CERT_DIR
|
||||
|
||||
openssl req -x509 -newkey rsa:4096 -sha256 -nodes -days 3650 \
|
||||
-keyout $DATA_PROTECTION_KEY \
|
||||
-out $DATA_PROTECTION_CERT \
|
||||
-subj "/CN=$DATA_PROTECTION_CN"
|
||||
|
||||
sudo cp $DATA_PROTECTION_CERT $CA_CERT_DIR
|
||||
|
||||
sudo $UPDATE_CA_CMD
|
||||
|
||||
identity=($(openssl x509 -in $IDENTITY_SERVER_CERT -outform der | sha1sum | tr a-z A-Z))
|
||||
data=($(openssl x509 -in $DATA_PROTECTION_CERT -outform der | sha1sum | tr a-z A-Z))
|
||||
|
||||
echo "Certificate fingerprints:"
|
||||
|
||||
echo "Identity Server Dev: ${identity}"
|
||||
echo "Data Protection Dev: ${data}"
|
||||
|
@ -7,17 +7,8 @@ openssl pkcs12 -export -legacy -out identity_server_dev.pfx -inkey identity_serv
|
||||
|
||||
security import ./identity_server_dev.pfx -k ~/Library/Keychains/Login.keychain
|
||||
|
||||
openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout data_protection_dev.key -out data_protection_dev.crt \
|
||||
-subj "/CN=Bitwarden Data Protection Dev" -days 3650
|
||||
openssl pkcs12 -export -legacy -out data_protection_dev.pfx -inkey data_protection_dev.key -in data_protection_dev.crt \
|
||||
-certfile data_protection_dev.crt
|
||||
|
||||
security import ./data_protection_dev.pfx -k ~/Library/Keychains/Login.keychain
|
||||
|
||||
identity=($(openssl x509 -in identity_server_dev.crt -outform der | shasum -a 1 | tr a-z A-Z));
|
||||
data=($(openssl x509 -in data_protection_dev.crt -outform der | shasum -a 1 | tr a-z A-Z));
|
||||
|
||||
echo "Certificate fingerprints:"
|
||||
|
||||
echo "Identity Server Dev: ${identity}"
|
||||
echo "Data Protection Dev: ${data}"
|
||||
|
@ -9,6 +9,3 @@ $params = @{
|
||||
|
||||
$params['Subject'] = 'CN=Bitwarden Identity Server Dev';
|
||||
New-SelfSignedCertificate @params;
|
||||
|
||||
$params['Subject'] = 'CN=Bitwarden Data Protection Dev';
|
||||
New-SelfSignedCertificate @params;
|
||||
|
@ -0,0 +1,97 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Bit.Identity.IdentityServer;
|
||||
using Bit.Infrastructure.Dapper.Auth.Repositories;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace Bit.MicroBenchmarks.Identity.IdentityServer;
|
||||
|
||||
[MemoryDiagnoser]
|
||||
public class RedisPersistedGrantStoreTests
|
||||
{
|
||||
const string SQL = nameof(SQL);
|
||||
const string Redis = nameof(Redis);
|
||||
private readonly IPersistedGrantStore _redisGrantStore;
|
||||
private readonly IPersistedGrantStore _sqlGrantStore;
|
||||
private readonly PersistedGrant _updateGrant;
|
||||
|
||||
private IPersistedGrantStore _grantStore = null!;
|
||||
|
||||
// 1) "ConsumedTime"
|
||||
// 2) ""
|
||||
// 3) "Description"
|
||||
// 4) ""
|
||||
// 5) "SubjectId"
|
||||
// 6) "97f31e32-6e44-407f-b8ba-b04c00f51b41"
|
||||
// 7) "CreationTime"
|
||||
// 8) "638350407400000000"
|
||||
// 9) "Data"
|
||||
// 10) "{\"CreationTime\":\"2023-11-08T11:45:40Z\",\"Lifetime\":2592001,\"ConsumedTime\":null,\"AccessToken\":{\"AllowedSigningAlgorithms\":[],\"Confirmation\":null,\"Audiences\":[],\"Issuer\":\"http://localhost\",\"CreationTime\":\"2023-11-08T11:45:40Z\",\"Lifetime\":3600,\"Type\":\"access_token\",\"ClientId\":\"web\",\"AccessTokenType\":0,\"Description\":null,\"Claims\":[{\"Type\":\"client_id\",\"Value\":\"web\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"scope\",\"Value\":\"api\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"scope\",\"Value\":\"offline_access\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"sub\",\"Value\":\"97f31e32-6e44-407f-b8ba-b04c00f51b41\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"auth_time\",\"Value\":\"1699443940\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#integer64\"},{\"Type\":\"idp\",\"Value\":\"bitwarden\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"amr\",\"Value\":\"Application\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"premium\",\"Value\":\"false\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#boolean\"},{\"Type\":\"email\",\"Value\":\"jbaur+test@bitwarden.com\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"email_verified\",\"Value\":\"false\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#boolean\"},{\"Type\":\"sstamp\",\"Value\":\"a4f2e0f3-e9f8-4014-b94e-b761d446a34b\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"name\",\"Value\":\"Justin Test\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"orgowner\",\"Value\":\"8ff8fefb-b035-436b-a25c-b04c00e30351\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"accesssecretsmanager\",\"Value\":\"8ff8fefb-b035-436b-a25c-b04c00e30351\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"device\",\"Value\":\"64b49c58-7768-4c30-8396-f851176daca6\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"jti\",\"Value\":\"CE008210A8276DAB966D9C2607533E0C\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"iat\",\"Value\":\"1699443940\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#integer64\"}],\"Version\":4},\"Version\":4}"
|
||||
// 11) "Type"
|
||||
// 12) "refresh_token"
|
||||
// 13) "SessionId"
|
||||
// 14) ""
|
||||
// 15) "ClientId"
|
||||
// 16) "web"
|
||||
|
||||
public RedisPersistedGrantStoreTests()
|
||||
{
|
||||
_redisGrantStore = new RedisPersistedGrantStore(
|
||||
ConnectionMultiplexer.Connect("localhost"),
|
||||
NullLogger<RedisPersistedGrantStore>.Instance,
|
||||
new InMemoryPersistedGrantStore()
|
||||
);
|
||||
|
||||
var sqlConnectionString = "YOUR CONNECTION STRING HERE";
|
||||
|
||||
_sqlGrantStore = new PersistedGrantStore(
|
||||
new GrantRepository(
|
||||
sqlConnectionString,
|
||||
sqlConnectionString
|
||||
)
|
||||
);
|
||||
|
||||
var creationTime = new DateTime(638350407400000000, DateTimeKind.Utc);
|
||||
_updateGrant = new PersistedGrant
|
||||
{
|
||||
Key = "i11JLqd7PE1yQltB2o5tRpfbMkpDPr+3w0Lc2Hx7kfE=",
|
||||
ConsumedTime = null,
|
||||
Description = null,
|
||||
SubjectId = "97f31e32-6e44-407f-b8ba-b04c00f51b41",
|
||||
CreationTime = creationTime,
|
||||
Data = "{\"CreationTime\":\"2023-11-08T11:45:40Z\",\"Lifetime\":2592001,\"ConsumedTime\":null,\"AccessToken\":{\"AllowedSigningAlgorithms\":[],\"Confirmation\":null,\"Audiences\":[],\"Issuer\":\"http://localhost\",\"CreationTime\":\"2023-11-08T11:45:40Z\",\"Lifetime\":3600,\"Type\":\"access_token\",\"ClientId\":\"web\",\"AccessTokenType\":0,\"Description\":null,\"Claims\":[{\"Type\":\"client_id\",\"Value\":\"web\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"scope\",\"Value\":\"api\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"scope\",\"Value\":\"offline_access\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"sub\",\"Value\":\"97f31e32-6e44-407f-b8ba-b04c00f51b41\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"auth_time\",\"Value\":\"1699443940\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#integer64\"},{\"Type\":\"idp\",\"Value\":\"bitwarden\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"amr\",\"Value\":\"Application\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"premium\",\"Value\":\"false\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#boolean\"},{\"Type\":\"email\",\"Value\":\"jbaur+test@bitwarden.com\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"email_verified\",\"Value\":\"false\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#boolean\"},{\"Type\":\"sstamp\",\"Value\":\"a4f2e0f3-e9f8-4014-b94e-b761d446a34b\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"name\",\"Value\":\"Justin Test\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"orgowner\",\"Value\":\"8ff8fefb-b035-436b-a25c-b04c00e30351\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"accesssecretsmanager\",\"Value\":\"8ff8fefb-b035-436b-a25c-b04c00e30351\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"device\",\"Value\":\"64b49c58-7768-4c30-8396-f851176daca6\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"jti\",\"Value\":\"CE008210A8276DAB966D9C2607533E0C\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#string\"},{\"Type\":\"iat\",\"Value\":\"1699443940\",\"ValueType\":\"http://www.w3.org/2001/XMLSchema#integer64\"}],\"Version\":4},\"Version\":4}",
|
||||
Type = "refresh_token",
|
||||
SessionId = null,
|
||||
ClientId = "web",
|
||||
Expiration = creationTime.AddHours(1),
|
||||
};
|
||||
}
|
||||
|
||||
[Params(Redis, SQL)]
|
||||
public string StoreType { get; set; } = null!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
if (StoreType == Redis)
|
||||
{
|
||||
_grantStore = _redisGrantStore;
|
||||
}
|
||||
else if (StoreType == SQL)
|
||||
{
|
||||
_grantStore = _sqlGrantStore;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidProgramException();
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public async Task StoreAsync()
|
||||
{
|
||||
await _grantStore.StoreAsync(_updateGrant);
|
||||
}
|
||||
}
|
@ -8,11 +8,12 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.10" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Core\Core.csproj" />
|
||||
<ProjectReference Include="..\..\src\Identity\Identity.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -1,4 +1,3 @@
|
||||
using System.Reflection;
|
||||
using BenchmarkDotNet.Running;
|
||||
using BenchmarkDotNet.Running;
|
||||
|
||||
BenchmarkRunner.Run(Assembly.GetEntryAssembly());
|
||||
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -43,7 +43,7 @@ export const options = {
|
||||
},
|
||||
thresholds: {
|
||||
http_req_failed: ["rate<0.01"],
|
||||
http_req_duration: ["p(95)<750"],
|
||||
http_req_duration: ["p(95)<200"],
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -44,7 +44,7 @@ export const options = {
|
||||
},
|
||||
thresholds: {
|
||||
http_req_failed: ["rate<0.01"],
|
||||
http_req_duration: ["p(95)<900"],
|
||||
http_req_duration: ["p(95)<300"],
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -38,7 +38,7 @@ export const options = {
|
||||
},
|
||||
thresholds: {
|
||||
http_req_failed: ["rate<0.01"],
|
||||
http_req_duration: ["p(95)<1500"],
|
||||
http_req_duration: ["p(95)<800"],
|
||||
},
|
||||
};
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,16 @@
|
||||
using Bit.Api.AdminConsole.Models.Request;
|
||||
using Bit.Api.AdminConsole.Models.Response;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Utilities;
|
||||
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;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -23,6 +27,10 @@ public class GroupsController : Controller
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ICreateGroupCommand _createGroupCommand;
|
||||
private readonly IUpdateGroupCommand _updateGroupCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
private bool UseFlexibleCollections => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||
|
||||
public GroupsController(
|
||||
IGroupRepository groupRepository,
|
||||
@ -31,7 +39,9 @@ public class GroupsController : Controller
|
||||
ICurrentContext currentContext,
|
||||
ICreateGroupCommand createGroupCommand,
|
||||
IUpdateGroupCommand updateGroupCommand,
|
||||
IDeleteGroupCommand deleteGroupCommand)
|
||||
IDeleteGroupCommand deleteGroupCommand,
|
||||
IFeatureService featureService,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
_groupRepository = groupRepository;
|
||||
_groupService = groupService;
|
||||
@ -40,6 +50,8 @@ public class GroupsController : Controller
|
||||
_createGroupCommand = createGroupCommand;
|
||||
_updateGroupCommand = updateGroupCommand;
|
||||
_deleteGroupCommand = deleteGroupCommand;
|
||||
_featureService = featureService;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -67,20 +79,26 @@ public class GroupsController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<GroupDetailsResponseModel>> Get(string orgId)
|
||||
public async Task<ListResponseModel<GroupDetailsResponseModel>> Get(Guid orgId)
|
||||
{
|
||||
var orgIdGuid = new Guid(orgId);
|
||||
var canAccess = await _currentContext.ManageGroups(orgIdGuid) ||
|
||||
await _currentContext.ViewAssignedCollections(orgIdGuid) ||
|
||||
await _currentContext.ViewAllCollections(orgIdGuid) ||
|
||||
await _currentContext.ManageUsers(orgIdGuid);
|
||||
if (UseFlexibleCollections)
|
||||
{
|
||||
// New flexible collections logic
|
||||
return await Get_vNext(orgId);
|
||||
}
|
||||
|
||||
// Old pre-flexible collections logic follows
|
||||
var canAccess = await _currentContext.ManageGroups(orgId) ||
|
||||
await _currentContext.ViewAssignedCollections(orgId) ||
|
||||
await _currentContext.ViewAllCollections(orgId) ||
|
||||
await _currentContext.ManageUsers(orgId);
|
||||
|
||||
if (!canAccess)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var groups = await _groupRepository.GetManyWithCollectionsByOrganizationIdAsync(orgIdGuid);
|
||||
var groups = await _groupRepository.GetManyWithCollectionsByOrganizationIdAsync(orgId);
|
||||
var responses = groups.Select(g => new GroupDetailsResponseModel(g.Item1, g.Item2));
|
||||
return new ListResponseModel<GroupDetailsResponseModel>(responses);
|
||||
}
|
||||
@ -185,4 +203,18 @@ public class GroupsController : Controller
|
||||
|
||||
await _groupService.DeleteUserAsync(group, new Guid(orgUserId));
|
||||
}
|
||||
|
||||
private async Task<ListResponseModel<GroupDetailsResponseModel>> Get_vNext(Guid orgId)
|
||||
{
|
||||
var authorized =
|
||||
(await _authorizationService.AuthorizeAsync(User, GroupOperations.ReadAll(orgId))).Succeeded;
|
||||
if (!authorized)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var groups = await _groupRepository.GetManyWithCollectionsByOrganizationIdAsync(orgId);
|
||||
var responses = groups.Select(g => new GroupDetailsResponseModel(g.Item1, g.Item2));
|
||||
return new ListResponseModel<GroupDetailsResponseModel>(responses);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,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.OrganizationUsers;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
@ -36,6 +39,10 @@ public class OrganizationUsersController : Controller
|
||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||
private readonly IUpdateOrganizationUserGroupsCommand _updateOrganizationUserGroupsCommand;
|
||||
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
private bool UseFlexibleCollections => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||
|
||||
public OrganizationUsersController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -49,7 +56,9 @@ public class OrganizationUsersController : Controller
|
||||
ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery,
|
||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
||||
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand,
|
||||
IAcceptOrgUserCommand acceptOrgUserCommand)
|
||||
IAcceptOrgUserCommand acceptOrgUserCommand,
|
||||
IFeatureService featureService,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -63,6 +72,8 @@ public class OrganizationUsersController : Controller
|
||||
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
||||
_updateOrganizationUserGroupsCommand = updateOrganizationUserGroupsCommand;
|
||||
_acceptOrgUserCommand = acceptOrgUserCommand;
|
||||
_featureService = featureService;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -85,18 +96,20 @@ public class OrganizationUsersController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get(string orgId, bool includeGroups = false, bool includeCollections = false)
|
||||
public async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false)
|
||||
{
|
||||
var orgGuidId = new Guid(orgId);
|
||||
if (!await _currentContext.ViewAllCollections(orgGuidId) &&
|
||||
!await _currentContext.ViewAssignedCollections(orgGuidId) &&
|
||||
!await _currentContext.ManageGroups(orgGuidId) &&
|
||||
!await _currentContext.ManageUsers(orgGuidId))
|
||||
var authorized = UseFlexibleCollections
|
||||
? (await _authorizationService.AuthorizeAsync(User, OrganizationUserOperations.ReadAll(orgId))).Succeeded
|
||||
: await _currentContext.ViewAllCollections(orgId) ||
|
||||
await _currentContext.ViewAssignedCollections(orgId) ||
|
||||
await _currentContext.ManageGroups(orgId) ||
|
||||
await _currentContext.ManageUsers(orgId);
|
||||
if (!authorized)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organizationUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(orgGuidId, includeGroups, includeCollections);
|
||||
var organizationUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(orgId, includeGroups, includeCollections);
|
||||
var responseTasks = organizationUsers.Select(async o => new OrganizationUserUserDetailsResponseModel(o,
|
||||
await _userService.TwoFactorIsEnabledAsync(o)));
|
||||
var responses = await Task.WhenAll(responseTasks);
|
||||
|
@ -782,6 +782,7 @@ public class OrganizationsController : Controller
|
||||
|
||||
[HttpPut("{id}/collection-management")]
|
||||
[RequireFeature(FeatureFlagKeys.FlexibleCollections)]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<OrganizationResponseModel> PutCollectionManagement(Guid id, [FromBody] OrganizationCollectionManagementUpdateRequestModel model)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
|
@ -118,3 +118,9 @@ public class OrganizationUserBulkRequestModel
|
||||
[Required]
|
||||
public IEnumerable<Guid> Ids { get; set; }
|
||||
}
|
||||
|
||||
public class ResetPasswordWithOrgIdRequestModel : OrganizationUserResetPasswordEnrollmentRequestModel
|
||||
{
|
||||
[Required]
|
||||
public Guid OrganizationId { get; set; }
|
||||
}
|
||||
|
@ -0,0 +1,64 @@
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Auth.Validators;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Organization user implementation for <see cref="IRotationValidator{T,R}"/>
|
||||
/// Currently responsible for validation of user reset password keys (used by admins to perform account recovery) during user key rotation
|
||||
/// </summary>
|
||||
public class OrganizationUserRotationValidator : IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
|
||||
IReadOnlyList<OrganizationUser>>
|
||||
{
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
|
||||
public OrganizationUserRotationValidator(IOrganizationUserRepository organizationUserRepository) =>
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
|
||||
public async Task<IReadOnlyList<OrganizationUser>> ValidateAsync(User user,
|
||||
IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
var result = new List<OrganizationUser>();
|
||||
if (resetPasswordKeys == null || !resetPasswordKeys.Any())
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var existing = await _organizationUserRepository.GetManyByUserAsync(user.Id);
|
||||
if (existing == null || !existing.Any())
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
// Exclude any account recovery that do not have a key.
|
||||
existing = existing.Where(o => o.ResetPasswordKey != null).ToList();
|
||||
|
||||
|
||||
foreach (var ou in existing)
|
||||
{
|
||||
var organizationUser = resetPasswordKeys.FirstOrDefault(a => a.OrganizationId == ou.OrganizationId);
|
||||
if (organizationUser == null)
|
||||
{
|
||||
throw new BadRequestException("All existing reset password keys must be included in the rotation.");
|
||||
}
|
||||
|
||||
if (organizationUser.ResetPasswordKey == null)
|
||||
{
|
||||
throw new BadRequestException("Reset Password keys cannot be set to null during rotation.");
|
||||
}
|
||||
|
||||
ou.ResetPasswordKey = organizationUser.ResetPasswordKey;
|
||||
result.Add(ou);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
using Bit.Api.AdminConsole.Models.Response;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Models.Response;
|
||||
using Bit.Api.Auth.Models.Request;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Api.Auth.Validators;
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Request.Accounts;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Core;
|
||||
@ -68,8 +70,12 @@ public class AccountsController : Controller
|
||||
|
||||
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
|
||||
private readonly IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> _folderValidator;
|
||||
private readonly IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> _sendValidator;
|
||||
private readonly IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>
|
||||
_emergencyAccessValidator;
|
||||
private readonly IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
|
||||
IReadOnlyList<OrganizationUser>>
|
||||
_organizationUserValidator;
|
||||
|
||||
|
||||
public AccountsController(
|
||||
@ -92,8 +98,11 @@ public class AccountsController : Controller
|
||||
ICurrentContext currentContext,
|
||||
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
|
||||
IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,
|
||||
IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> sendValidator,
|
||||
IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>
|
||||
emergencyAccessValidator
|
||||
emergencyAccessValidator,
|
||||
IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>
|
||||
organizationUserValidator
|
||||
)
|
||||
{
|
||||
_cipherRepository = cipherRepository;
|
||||
@ -115,7 +124,9 @@ public class AccountsController : Controller
|
||||
_currentContext = currentContext;
|
||||
_cipherValidator = cipherValidator;
|
||||
_folderValidator = folderValidator;
|
||||
_sendValidator = sendValidator;
|
||||
_emergencyAccessValidator = emergencyAccessValidator;
|
||||
_organizationUserValidator = organizationUserValidator;
|
||||
}
|
||||
|
||||
#region DEPRECATED (Moved to Identity Service)
|
||||
@ -423,9 +434,9 @@ public class AccountsController : Controller
|
||||
PrivateKey = model.PrivateKey,
|
||||
Ciphers = await _cipherValidator.ValidateAsync(user, model.Ciphers),
|
||||
Folders = await _folderValidator.ValidateAsync(user, model.Folders),
|
||||
Sends = new List<Send>(),
|
||||
EmergencyAccessKeys = await _emergencyAccessValidator.ValidateAsync(user, model.EmergencyAccessKeys),
|
||||
ResetPasswordKeys = new List<OrganizationUser>(),
|
||||
Sends = await _sendValidator.ValidateAsync(user, model.Sends),
|
||||
EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.EmergencyAccessKeys),
|
||||
OrganizationUsers = await _organizationUserValidator.ValidateAsync(user, model.ResetPasswordKeys)
|
||||
};
|
||||
|
||||
result = await _rotateUserKeyCommand.RotateUserKeyAsync(user, dataModel);
|
||||
|
@ -5,8 +5,11 @@ using Bit.Api.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Api.Response.Accounts;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tokens;
|
||||
@ -22,20 +25,36 @@ namespace Bit.Api.Auth.Controllers;
|
||||
public class WebAuthnController : Controller
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IWebAuthnCredentialRepository _credentialRepository;
|
||||
private readonly IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> _createOptionsDataProtector;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
|
||||
private readonly IGetWebAuthnLoginCredentialCreateOptionsCommand _getWebAuthnLoginCredentialCreateOptionsCommand;
|
||||
private readonly ICreateWebAuthnLoginCredentialCommand _createWebAuthnLoginCredentialCommand;
|
||||
private readonly IAssertWebAuthnLoginCredentialCommand _assertWebAuthnLoginCredentialCommand;
|
||||
private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||
|
||||
public WebAuthnController(
|
||||
IUserService userService,
|
||||
IPolicyService policyService,
|
||||
IWebAuthnCredentialRepository credentialRepository,
|
||||
IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> createOptionsDataProtector,
|
||||
IPolicyService policyService)
|
||||
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
|
||||
IGetWebAuthnLoginCredentialCreateOptionsCommand getWebAuthnLoginCredentialCreateOptionsCommand,
|
||||
ICreateWebAuthnLoginCredentialCommand createWebAuthnLoginCredentialCommand,
|
||||
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand,
|
||||
IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand)
|
||||
{
|
||||
_userService = userService;
|
||||
_policyService = policyService;
|
||||
_credentialRepository = credentialRepository;
|
||||
_createOptionsDataProtector = createOptionsDataProtector;
|
||||
_policyService = policyService;
|
||||
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
||||
_getWebAuthnLoginCredentialCreateOptionsCommand = getWebAuthnLoginCredentialCreateOptionsCommand;
|
||||
_createWebAuthnLoginCredentialCommand = createWebAuthnLoginCredentialCommand;
|
||||
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
|
||||
_getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@ -47,12 +66,12 @@ public class WebAuthnController : Controller
|
||||
return new ListResponseModel<WebAuthnCredentialResponseModel>(credentials.Select(c => new WebAuthnCredentialResponseModel(c)));
|
||||
}
|
||||
|
||||
[HttpPost("options")]
|
||||
public async Task<WebAuthnCredentialCreateOptionsResponseModel> PostOptions([FromBody] SecretVerificationRequestModel model)
|
||||
[HttpPost("attestation-options")]
|
||||
public async Task<WebAuthnCredentialCreateOptionsResponseModel> AttestationOptions([FromBody] SecretVerificationRequestModel model)
|
||||
{
|
||||
var user = await VerifyUserAsync(model);
|
||||
await ValidateRequireSsoPolicyDisabledOrNotApplicable(user.Id);
|
||||
var options = await _userService.StartWebAuthnLoginRegistrationAsync(user);
|
||||
var options = await _getWebAuthnLoginCredentialCreateOptionsCommand.GetWebAuthnLoginCredentialCreateOptionsAsync(user);
|
||||
|
||||
var tokenable = new WebAuthnCredentialCreateOptionsTokenable(user, options);
|
||||
var token = _createOptionsDataProtector.Protect(tokenable);
|
||||
@ -64,8 +83,24 @@ public class WebAuthnController : Controller
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("assertion-options")]
|
||||
public async Task<WebAuthnLoginAssertionOptionsResponseModel> AssertionOptions([FromBody] SecretVerificationRequestModel model)
|
||||
{
|
||||
await VerifyUserAsync(model);
|
||||
var options = _getWebAuthnLoginCredentialAssertionOptionsCommand.GetWebAuthnLoginCredentialAssertionOptions();
|
||||
|
||||
var tokenable = new WebAuthnLoginAssertionOptionsTokenable(WebAuthnLoginAssertionOptionsScope.UpdateKeySet, options);
|
||||
var token = _assertionOptionsDataProtector.Protect(tokenable);
|
||||
|
||||
return new WebAuthnLoginAssertionOptionsResponseModel
|
||||
{
|
||||
Options = options,
|
||||
Token = token
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("")]
|
||||
public async Task Post([FromBody] WebAuthnCredentialRequestModel model)
|
||||
public async Task Post([FromBody] WebAuthnLoginCredentialCreateRequestModel model)
|
||||
{
|
||||
var user = await GetUserAsync();
|
||||
await ValidateRequireSsoPolicyDisabledOrNotApplicable(user.Id);
|
||||
@ -76,7 +111,7 @@ public class WebAuthnController : Controller
|
||||
throw new BadRequestException("The token associated with your request is expired. A valid token is required to continue.");
|
||||
}
|
||||
|
||||
var success = await _userService.CompleteWebAuthLoginRegistrationAsync(user, model.Name, tokenable.Options, model.DeviceResponse, model.SupportsPrf, model.EncryptedUserKey, model.EncryptedPublicKey, model.EncryptedPrivateKey);
|
||||
var success = await _createWebAuthnLoginCredentialCommand.CreateWebAuthnLoginCredentialAsync(user, model.Name, tokenable.Options, model.DeviceResponse, model.SupportsPrf, model.EncryptedUserKey, model.EncryptedPublicKey, model.EncryptedPrivateKey);
|
||||
if (!success)
|
||||
{
|
||||
throw new BadRequestException("Unable to complete WebAuthn registration.");
|
||||
@ -93,6 +128,29 @@ public class WebAuthnController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut()]
|
||||
public async Task UpdateCredential([FromBody] WebAuthnLoginCredentialUpdateRequestModel model)
|
||||
{
|
||||
var tokenable = _assertionOptionsDataProtector.Unprotect(model.Token);
|
||||
if (!tokenable.TokenIsValid(WebAuthnLoginAssertionOptionsScope.UpdateKeySet))
|
||||
{
|
||||
throw new BadRequestException("The token associated with your request is invalid or has expired. A valid token is required to continue.");
|
||||
}
|
||||
|
||||
var (_, credential) = await _assertWebAuthnLoginCredentialCommand.AssertWebAuthnLoginCredential(tokenable.Options, model.DeviceResponse);
|
||||
if (credential == null || credential.SupportsPrf != true)
|
||||
{
|
||||
throw new BadRequestException("Unable to update credential.");
|
||||
}
|
||||
|
||||
// assign new keys to credential
|
||||
credential.EncryptedUserKey = model.EncryptedUserKey;
|
||||
credential.EncryptedPrivateKey = model.EncryptedPrivateKey;
|
||||
credential.EncryptedPublicKey = model.EncryptedPublicKey;
|
||||
|
||||
await _credentialRepository.UpdateAsync(credential);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete")]
|
||||
public async Task Delete(Guid id, [FromBody] SecretVerificationRequestModel model)
|
||||
{
|
||||
|
@ -18,6 +18,6 @@ public class UpdateKeyRequestModel
|
||||
public IEnumerable<FolderWithIdRequestModel> Folders { get; set; }
|
||||
public IEnumerable<SendWithIdRequestModel> Sends { get; set; }
|
||||
public IEnumerable<EmergencyAccessWithIdRequestModel> EmergencyAccessKeys { get; set; }
|
||||
public IEnumerable<OrganizationUserUpdateRequestModel> ResetPasswordKeys { get; set; }
|
||||
public IEnumerable<ResetPasswordWithOrgIdRequestModel> ResetPasswordKeys { get; set; }
|
||||
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ using Fido2NetLib;
|
||||
|
||||
namespace Bit.Api.Auth.Models.Request.Webauthn;
|
||||
|
||||
public class WebAuthnCredentialRequestModel
|
||||
public class WebAuthnLoginCredentialCreateRequestModel
|
||||
{
|
||||
[Required]
|
||||
public AuthenticatorAttestationRawResponse DeviceResponse { get; set; }
|
||||
@ -30,4 +30,3 @@ public class WebAuthnCredentialRequestModel
|
||||
[EncryptedStringLength(2000)]
|
||||
public string EncryptedPrivateKey { get; set; }
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Utilities;
|
||||
using Fido2NetLib;
|
||||
|
||||
namespace Bit.Api.Auth.Models.Request.Webauthn;
|
||||
|
||||
public class WebAuthnLoginCredentialUpdateRequestModel
|
||||
{
|
||||
[Required]
|
||||
public AuthenticatorAssertionRawResponse DeviceResponse { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Token { get; set; }
|
||||
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(2000)]
|
||||
public string EncryptedUserKey { get; set; }
|
||||
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(2000)]
|
||||
public string EncryptedPublicKey { get; set; }
|
||||
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(2000)]
|
||||
public string EncryptedPrivateKey { get; set; }
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
|
||||
namespace Bit.Api.Auth.Validators;
|
||||
|
||||
@ -11,5 +12,11 @@ namespace Bit.Api.Auth.Validators;
|
||||
/// <typeparam name="R">Domain model</typeparam>
|
||||
public interface IRotationValidator<T, R>
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates re-encrypted data before being saved to database.
|
||||
/// </summary>
|
||||
/// <param name="user">Request model</param>
|
||||
/// <param name="data">Domain model</param>
|
||||
/// <exception cref="BadRequestException">Throws if data fails validation</exception>
|
||||
Task<R> ValidateAsync(User user, T data);
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
@ -42,6 +43,7 @@ public class CollectionsController : Controller
|
||||
IOrganizationUserRepository organizationUserRepository)
|
||||
{
|
||||
_collectionRepository = collectionRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_collectionService = collectionService;
|
||||
_deleteCollectionCommand = deleteCollectionCommand;
|
||||
_userService = userService;
|
||||
@ -57,18 +59,33 @@ public class CollectionsController : Controller
|
||||
[HttpGet("{id}")]
|
||||
public async Task<CollectionResponseModel> Get(Guid orgId, Guid id)
|
||||
{
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
{
|
||||
// New flexible collections logic
|
||||
return await Get_vNext(id);
|
||||
}
|
||||
|
||||
// Old pre-flexible collections logic follows
|
||||
if (!await CanViewCollectionAsync(orgId, id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var collection = await GetCollectionAsync(id, orgId);
|
||||
|
||||
return new CollectionResponseModel(collection);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/details")]
|
||||
public async Task<CollectionAccessDetailsResponseModel> GetDetails(Guid orgId, Guid id)
|
||||
{
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
{
|
||||
// New flexible collections logic
|
||||
return await GetDetails_vNext(id);
|
||||
}
|
||||
|
||||
// Old pre-flexible collections logic follows
|
||||
if (!await ViewAtLeastOneCollectionAsync(orgId) && !await _currentContext.ManageUsers(orgId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
@ -87,7 +104,7 @@ public class CollectionsController : Controller
|
||||
else
|
||||
{
|
||||
(var collection, var access) = await _collectionRepository.GetByIdWithAccessAsync(id,
|
||||
_currentContext.UserId.Value);
|
||||
_currentContext.UserId.Value, FlexibleCollectionsIsEnabled);
|
||||
if (collection == null || collection.OrganizationId != orgId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
@ -100,15 +117,22 @@ public class CollectionsController : Controller
|
||||
[HttpGet("details")]
|
||||
public async Task<ListResponseModel<CollectionAccessDetailsResponseModel>> GetManyWithDetails(Guid orgId)
|
||||
{
|
||||
if (!await ViewAtLeastOneCollectionAsync(orgId) && !await _currentContext.ManageUsers(orgId) &&
|
||||
!await _currentContext.ManageGroups(orgId))
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
{
|
||||
// New flexible collections logic
|
||||
return await GetManyWithDetails_vNext(orgId);
|
||||
}
|
||||
|
||||
// Old pre-flexible collections logic follows
|
||||
if (!await ViewAtLeastOneCollectionAsync(orgId) && !await _currentContext.ManageUsers(orgId) && !await _currentContext.ManageGroups(orgId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// We always need to know which collections the current user is assigned to
|
||||
var assignedOrgCollections =
|
||||
await _collectionRepository.GetManyByUserIdWithAccessAsync(_currentContext.UserId.Value, orgId);
|
||||
await _collectionRepository.GetManyByUserIdWithAccessAsync(_currentContext.UserId.Value, orgId,
|
||||
FlexibleCollectionsIsEnabled);
|
||||
|
||||
if (await _currentContext.ViewAllCollections(orgId) || await _currentContext.ManageUsers(orgId))
|
||||
{
|
||||
@ -135,7 +159,29 @@ public class CollectionsController : Controller
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<CollectionResponseModel>> Get(Guid orgId)
|
||||
{
|
||||
IEnumerable<Collection> orgCollections = await _collectionService.GetOrganizationCollectionsAsync(orgId);
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
{
|
||||
// New flexible collections logic
|
||||
return await GetByOrgId_vNext(orgId);
|
||||
}
|
||||
|
||||
// Old pre-flexible collections logic follows
|
||||
IEnumerable<Collection> orgCollections = null;
|
||||
if (await _currentContext.ManageGroups(orgId))
|
||||
{
|
||||
// ManageGroups users need to see all collections to manage other users' collection access.
|
||||
// This is not added to collectionService.GetOrganizationCollectionsAsync as that may have
|
||||
// unintended consequences on other logic that also uses that method.
|
||||
// This is a quick fix but it will be properly fixed by permission changes in Flexible Collections.
|
||||
|
||||
// Get all collections for organization
|
||||
orgCollections = await _collectionRepository.GetManyByOrganizationIdAsync(orgId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Returns all collections or collections the user is assigned to, depending on permissions
|
||||
orgCollections = await _collectionService.GetOrganizationCollectionsAsync(orgId);
|
||||
}
|
||||
|
||||
var responses = orgCollections.Select(c => new CollectionResponseModel(c));
|
||||
return new ListResponseModel<CollectionResponseModel>(responses);
|
||||
@ -145,7 +191,7 @@ public class CollectionsController : Controller
|
||||
public async Task<ListResponseModel<CollectionDetailsResponseModel>> GetUser()
|
||||
{
|
||||
var collections = await _collectionRepository.GetManyByUserIdAsync(
|
||||
_userService.GetProperUserId(User).Value);
|
||||
_userService.GetProperUserId(User).Value, FlexibleCollectionsIsEnabled);
|
||||
var responses = collections.Select(c => new CollectionDetailsResponseModel(c));
|
||||
return new ListResponseModel<CollectionDetailsResponseModel>(responses);
|
||||
}
|
||||
@ -153,6 +199,13 @@ public class CollectionsController : Controller
|
||||
[HttpGet("{id}/users")]
|
||||
public async Task<IEnumerable<SelectionReadOnlyResponseModel>> GetUsers(Guid orgId, Guid id)
|
||||
{
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
{
|
||||
// New flexible collections logic
|
||||
return await GetUsers_vNext(id);
|
||||
}
|
||||
|
||||
// Old pre-flexible collections logic follows
|
||||
var collection = await GetCollectionAsync(id, orgId);
|
||||
var collectionUsers = await _collectionRepository.GetManyUsersByIdAsync(collection.Id);
|
||||
var responses = collectionUsers.Select(cu => new SelectionReadOnlyResponseModel(cu));
|
||||
@ -165,7 +218,7 @@ public class CollectionsController : Controller
|
||||
var collection = model.ToCollection(orgId);
|
||||
|
||||
var authorized = FlexibleCollectionsIsEnabled
|
||||
? (await _authorizationService.AuthorizeAsync(User, collection, CollectionOperations.Create)).Succeeded
|
||||
? (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Create)).Succeeded
|
||||
: await CanCreateCollection(orgId, collection.Id) || await CanEditCollectionAsync(orgId, collection.Id);
|
||||
if (!authorized)
|
||||
{
|
||||
@ -205,6 +258,13 @@ public class CollectionsController : Controller
|
||||
[HttpPost("{id}")]
|
||||
public async Task<CollectionResponseModel> Put(Guid orgId, Guid id, [FromBody] CollectionRequestModel model)
|
||||
{
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
{
|
||||
// New flexible collections logic
|
||||
return await Put_vNext(id, model);
|
||||
}
|
||||
|
||||
// Old pre-flexible collections logic follows
|
||||
if (!await CanEditCollectionAsync(orgId, id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
@ -220,6 +280,14 @@ public class CollectionsController : Controller
|
||||
[HttpPut("{id}/users")]
|
||||
public async Task PutUsers(Guid orgId, Guid id, [FromBody] IEnumerable<SelectionReadOnlyRequestModel> model)
|
||||
{
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
{
|
||||
// New flexible collections logic
|
||||
await PutUsers_vNext(id, model);
|
||||
return;
|
||||
}
|
||||
|
||||
// Old pre-flexible collections logic follows
|
||||
if (!await CanEditCollectionAsync(orgId, id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
@ -243,7 +311,7 @@ public class CollectionsController : Controller
|
||||
throw new NotFoundException("One or more collections not found.");
|
||||
}
|
||||
|
||||
var result = await _authorizationService.AuthorizeAsync(User, collections, CollectionOperations.ModifyAccess);
|
||||
var result = await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.ModifyAccess);
|
||||
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
@ -260,16 +328,20 @@ public class CollectionsController : Controller
|
||||
[HttpPost("{id}/delete")]
|
||||
public async Task Delete(Guid orgId, Guid id)
|
||||
{
|
||||
var collection = await GetCollectionAsync(id, orgId);
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
{
|
||||
// New flexible collections logic
|
||||
await Delete_vNext(id);
|
||||
return;
|
||||
}
|
||||
|
||||
var authorized = FlexibleCollectionsIsEnabled
|
||||
? (await _authorizationService.AuthorizeAsync(User, collection, CollectionOperations.Delete)).Succeeded
|
||||
: await CanDeleteCollectionAsync(orgId, id);
|
||||
if (!authorized)
|
||||
// Old pre-flexible collections logic follows
|
||||
if (!await CanDeleteCollectionAsync(orgId, id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var collection = await GetCollectionAsync(id, orgId);
|
||||
await _deleteCollectionCommand.DeleteAsync(collection);
|
||||
}
|
||||
|
||||
@ -281,7 +353,7 @@ public class CollectionsController : Controller
|
||||
{
|
||||
// New flexible collections logic
|
||||
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Ids);
|
||||
var result = await _authorizationService.AuthorizeAsync(User, collections, CollectionOperations.Delete);
|
||||
var result = await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.Delete);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
@ -311,30 +383,18 @@ public class CollectionsController : Controller
|
||||
|
||||
[HttpDelete("{id}/user/{orgUserId}")]
|
||||
[HttpPost("{id}/delete-user/{orgUserId}")]
|
||||
public async Task Delete(string orgId, string id, string orgUserId)
|
||||
public async Task DeleteUser(Guid orgId, Guid id, Guid orgUserId)
|
||||
{
|
||||
var collection = await GetCollectionAsync(new Guid(id), new Guid(orgId));
|
||||
await _collectionService.DeleteUserAsync(collection, new Guid(orgUserId));
|
||||
}
|
||||
|
||||
private async Task<Collection> GetCollectionAsync(Guid id, Guid orgId)
|
||||
{
|
||||
Collection collection = default;
|
||||
if (await _currentContext.ViewAllCollections(orgId))
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
{
|
||||
collection = await _collectionRepository.GetByIdAsync(id);
|
||||
}
|
||||
else if (await _currentContext.ViewAssignedCollections(orgId))
|
||||
{
|
||||
collection = await _collectionRepository.GetByIdAsync(id, _currentContext.UserId.Value);
|
||||
// New flexible collections logic
|
||||
await DeleteUser_vNext(id, orgUserId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (collection == null || collection.OrganizationId != orgId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return collection;
|
||||
// Old pre-flexible collections logic follows
|
||||
var collection = await GetCollectionAsync(id, orgId);
|
||||
await _collectionService.DeleteUserAsync(collection, orgUserId);
|
||||
}
|
||||
|
||||
private void DeprecatedPermissionsGuard()
|
||||
@ -345,6 +405,29 @@ public class CollectionsController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
|
||||
private async Task<Collection> GetCollectionAsync(Guid id, Guid orgId)
|
||||
{
|
||||
DeprecatedPermissionsGuard();
|
||||
|
||||
Collection collection = default;
|
||||
if (await _currentContext.ViewAllCollections(orgId))
|
||||
{
|
||||
collection = await _collectionRepository.GetByIdAsync(id);
|
||||
}
|
||||
else if (await _currentContext.ViewAssignedCollections(orgId))
|
||||
{
|
||||
collection = await _collectionRepository.GetByIdAsync(id, _currentContext.UserId.Value, FlexibleCollectionsIsEnabled);
|
||||
}
|
||||
|
||||
if (collection == null || collection.OrganizationId != orgId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
|
||||
private async Task<bool> CanCreateCollection(Guid orgId, Guid collectionId)
|
||||
{
|
||||
@ -359,8 +442,11 @@ public class CollectionsController : Controller
|
||||
(o.Permissions?.CreateNewCollections ?? false)) ?? false);
|
||||
}
|
||||
|
||||
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
|
||||
private async Task<bool> CanEditCollectionAsync(Guid orgId, Guid collectionId)
|
||||
{
|
||||
DeprecatedPermissionsGuard();
|
||||
|
||||
if (collectionId == default)
|
||||
{
|
||||
return false;
|
||||
@ -374,7 +460,7 @@ public class CollectionsController : Controller
|
||||
if (await _currentContext.EditAssignedCollections(orgId))
|
||||
{
|
||||
var collectionDetails =
|
||||
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value);
|
||||
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value, FlexibleCollectionsIsEnabled);
|
||||
return collectionDetails != null;
|
||||
}
|
||||
|
||||
@ -399,7 +485,7 @@ public class CollectionsController : Controller
|
||||
if (await _currentContext.DeleteAssignedCollections(orgId))
|
||||
{
|
||||
var collectionDetails =
|
||||
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value);
|
||||
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value, FlexibleCollectionsIsEnabled);
|
||||
return collectionDetails != null;
|
||||
}
|
||||
|
||||
@ -416,8 +502,11 @@ public class CollectionsController : Controller
|
||||
&& (o.Permissions?.DeleteAnyCollection ?? false)) ?? false);
|
||||
}
|
||||
|
||||
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
|
||||
private async Task<bool> CanViewCollectionAsync(Guid orgId, Guid collectionId)
|
||||
{
|
||||
DeprecatedPermissionsGuard();
|
||||
|
||||
if (collectionId == default)
|
||||
{
|
||||
return false;
|
||||
@ -431,15 +520,173 @@ public class CollectionsController : Controller
|
||||
if (await _currentContext.ViewAssignedCollections(orgId))
|
||||
{
|
||||
var collectionDetails =
|
||||
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value);
|
||||
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value, FlexibleCollectionsIsEnabled);
|
||||
return collectionDetails != null;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
|
||||
private async Task<bool> ViewAtLeastOneCollectionAsync(Guid orgId)
|
||||
{
|
||||
DeprecatedPermissionsGuard();
|
||||
|
||||
return await _currentContext.ViewAllCollections(orgId) || await _currentContext.ViewAssignedCollections(orgId);
|
||||
}
|
||||
|
||||
private async Task<CollectionResponseModel> Get_vNext(Guid collectionId)
|
||||
{
|
||||
var collection = await _collectionRepository.GetByIdAsync(collectionId);
|
||||
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Read)).Succeeded;
|
||||
if (!authorized)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new CollectionResponseModel(collection);
|
||||
}
|
||||
|
||||
private async Task<CollectionAccessDetailsResponseModel> GetDetails_vNext(Guid id)
|
||||
{
|
||||
// New flexible collections logic
|
||||
var (collection, access) = await _collectionRepository.GetByIdWithAccessAsync(id);
|
||||
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ReadWithAccess)).Succeeded;
|
||||
if (!authorized)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new CollectionAccessDetailsResponseModel(collection, access.Groups, access.Users);
|
||||
}
|
||||
|
||||
private async Task<ListResponseModel<CollectionAccessDetailsResponseModel>> GetManyWithDetails_vNext(Guid orgId)
|
||||
{
|
||||
// We always need to know which collections the current user is assigned to
|
||||
var assignedOrgCollections = await _collectionRepository
|
||||
.GetManyByUserIdWithAccessAsync(_currentContext.UserId.Value, orgId, FlexibleCollectionsIsEnabled);
|
||||
|
||||
var readAllAuthorized =
|
||||
(await _authorizationService.AuthorizeAsync(User, CollectionOperations.ReadAllWithAccess(orgId))).Succeeded;
|
||||
if (readAllAuthorized)
|
||||
{
|
||||
// The user can view all collections, but they may not always be assigned to all of them
|
||||
var allOrgCollections = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(orgId);
|
||||
|
||||
return new ListResponseModel<CollectionAccessDetailsResponseModel>(allOrgCollections.Select(c =>
|
||||
new CollectionAccessDetailsResponseModel(c.Item1, c.Item2.Groups, c.Item2.Users)
|
||||
{
|
||||
// Manually determine which collections they're assigned to
|
||||
Assigned = assignedOrgCollections.Any(ac => ac.Item1.Id == c.Item1.Id)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Filter the assigned collections to only return those where the user has Manage permission
|
||||
var manageableOrgCollections = assignedOrgCollections.Where(c => c.Item1.Manage).ToList();
|
||||
var readAssignedAuthorized = await _authorizationService.AuthorizeAsync(User, manageableOrgCollections.Select(c => c.Item1), BulkCollectionOperations.ReadWithAccess);
|
||||
if (!readAssignedAuthorized.Succeeded)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new ListResponseModel<CollectionAccessDetailsResponseModel>(manageableOrgCollections.Select(c =>
|
||||
new CollectionAccessDetailsResponseModel(c.Item1, c.Item2.Groups, c.Item2.Users)
|
||||
{
|
||||
Assigned = true // Mapping from manageableOrgCollections implies they're all assigned
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async Task<ListResponseModel<CollectionResponseModel>> GetByOrgId_vNext(Guid orgId)
|
||||
{
|
||||
IEnumerable<Collection> orgCollections;
|
||||
|
||||
var readAllAuthorized = (await _authorizationService.AuthorizeAsync(User, CollectionOperations.ReadAll(orgId))).Succeeded;
|
||||
if (readAllAuthorized)
|
||||
{
|
||||
orgCollections = await _collectionRepository.GetManyByOrganizationIdAsync(orgId);
|
||||
}
|
||||
else
|
||||
{
|
||||
var collections = await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId.Value, FlexibleCollectionsIsEnabled);
|
||||
var readAuthorized = (await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.Read)).Succeeded;
|
||||
if (readAuthorized)
|
||||
{
|
||||
orgCollections = collections.Where(c => c.OrganizationId == orgId);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
var responses = orgCollections.Select(c => new CollectionResponseModel(c));
|
||||
return new ListResponseModel<CollectionResponseModel>(responses);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<SelectionReadOnlyResponseModel>> GetUsers_vNext(Guid id)
|
||||
{
|
||||
var collection = await _collectionRepository.GetByIdAsync(id);
|
||||
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ReadAccess)).Succeeded;
|
||||
if (!authorized)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var collectionUsers = await _collectionRepository.GetManyUsersByIdAsync(collection.Id);
|
||||
var responses = collectionUsers.Select(cu => new SelectionReadOnlyResponseModel(cu));
|
||||
return responses;
|
||||
}
|
||||
|
||||
private async Task<CollectionResponseModel> Put_vNext(Guid id, CollectionRequestModel model)
|
||||
{
|
||||
var collection = await _collectionRepository.GetByIdAsync(id);
|
||||
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Update)).Succeeded;
|
||||
if (!authorized)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var groups = model.Groups?.Select(g => g.ToSelectionReadOnly());
|
||||
var users = model.Users?.Select(g => g.ToSelectionReadOnly());
|
||||
await _collectionService.SaveAsync(model.ToCollection(collection), groups, users);
|
||||
return new CollectionResponseModel(collection);
|
||||
}
|
||||
|
||||
private async Task PutUsers_vNext(Guid id, IEnumerable<SelectionReadOnlyRequestModel> model)
|
||||
{
|
||||
var collection = await _collectionRepository.GetByIdAsync(id);
|
||||
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyAccess)).Succeeded;
|
||||
if (!authorized)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _collectionRepository.UpdateUsersAsync(collection.Id, model?.Select(g => g.ToSelectionReadOnly()));
|
||||
}
|
||||
|
||||
private async Task Delete_vNext(Guid id)
|
||||
{
|
||||
var collection = await _collectionRepository.GetByIdAsync(id);
|
||||
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Delete)).Succeeded;
|
||||
if (!authorized)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _deleteCollectionCommand.DeleteAsync(collection);
|
||||
}
|
||||
|
||||
private async Task DeleteUser_vNext(Guid id, Guid orgUserId)
|
||||
{
|
||||
var collection = await _collectionRepository.GetByIdAsync(id);
|
||||
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyAccess)).Succeeded;
|
||||
if (!authorized)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _collectionService.DeleteUserAsync(collection, orgUserId);
|
||||
}
|
||||
}
|
||||
|
@ -89,46 +89,6 @@ public class AccessPoliciesController : Controller
|
||||
return new ProjectAccessPoliciesResponseModel(results);
|
||||
}
|
||||
|
||||
[HttpPost("/service-accounts/{id}/access-policies")]
|
||||
public async Task<ServiceAccountAccessPoliciesResponseModel> CreateServiceAccountAccessPoliciesAsync(
|
||||
[FromRoute] Guid id,
|
||||
[FromBody] AccessPoliciesCreateRequest request)
|
||||
{
|
||||
if (request.Count() > _maxBulkCreation)
|
||||
{
|
||||
throw new BadRequestException($"Can process no more than {_maxBulkCreation} creation requests at once.");
|
||||
}
|
||||
|
||||
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
|
||||
if (serviceAccount == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var policies = request.ToBaseAccessPoliciesForServiceAccount(id, serviceAccount.OrganizationId);
|
||||
foreach (var policy in policies)
|
||||
{
|
||||
var authorizationResult = await _authorizationService.AuthorizeAsync(User, policy, AccessPolicyOperations.Create);
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
var results = await _createAccessPoliciesCommand.CreateManyAsync(policies);
|
||||
return new ServiceAccountAccessPoliciesResponseModel(results);
|
||||
}
|
||||
|
||||
[HttpGet("/service-accounts/{id}/access-policies")]
|
||||
public async Task<ServiceAccountAccessPoliciesResponseModel> GetServiceAccountAccessPoliciesAsync(
|
||||
[FromRoute] Guid id)
|
||||
{
|
||||
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
|
||||
var (_, userId) = await CheckUserHasWriteAccessToServiceAccountAsync(serviceAccount);
|
||||
var results = await _accessPolicyRepository.GetManyByGrantedServiceAccountIdAsync(id, userId);
|
||||
return new ServiceAccountAccessPoliciesResponseModel(results);
|
||||
}
|
||||
|
||||
[HttpPost("/service-accounts/{id}/granted-policies")]
|
||||
public async Task<ListResponseModel<ServiceAccountProjectAccessPolicyResponseModel>>
|
||||
CreateServiceAccountGrantedPoliciesAsync([FromRoute] Guid id,
|
||||
@ -308,6 +268,40 @@ public class AccessPoliciesController : Controller
|
||||
return new ProjectPeopleAccessPoliciesResponseModel(results, userId);
|
||||
}
|
||||
|
||||
[HttpGet("/service-accounts/{id}/access-policies/people")]
|
||||
public async Task<ServiceAccountPeopleAccessPoliciesResponseModel> GetServiceAccountPeopleAccessPoliciesAsync(
|
||||
[FromRoute] Guid id)
|
||||
{
|
||||
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
|
||||
var (_, userId) = await CheckUserHasWriteAccessToServiceAccountAsync(serviceAccount);
|
||||
var results = await _accessPolicyRepository.GetPeoplePoliciesByGrantedServiceAccountIdAsync(id, userId);
|
||||
return new ServiceAccountPeopleAccessPoliciesResponseModel(results, userId);
|
||||
}
|
||||
|
||||
[HttpPut("/service-accounts/{id}/access-policies/people")]
|
||||
public async Task<ServiceAccountPeopleAccessPoliciesResponseModel> PutServiceAccountPeopleAccessPoliciesAsync(
|
||||
[FromRoute] Guid id,
|
||||
[FromBody] PeopleAccessPoliciesRequestModel request)
|
||||
{
|
||||
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
|
||||
if (serviceAccount == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var peopleAccessPolicies = request.ToServiceAccountPeopleAccessPolicies(id, serviceAccount.OrganizationId);
|
||||
|
||||
var authorizationResult = await _authorizationService.AuthorizeAsync(User, peopleAccessPolicies,
|
||||
ServiceAccountPeopleAccessPoliciesOperations.Replace);
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User)!.Value;
|
||||
var results = await _accessPolicyRepository.ReplaceServiceAccountPeopleAsync(peopleAccessPolicies, userId);
|
||||
return new ServiceAccountPeopleAccessPoliciesResponseModel(results, userId);
|
||||
}
|
||||
|
||||
private async Task<(AccessClientType AccessClientType, Guid UserId)> CheckUserHasWriteAccessToProjectAsync(Project project)
|
||||
{
|
||||
@ -345,6 +339,7 @@ public class AccessPoliciesController : Controller
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return (accessClient, userId);
|
||||
}
|
||||
|
||||
|
@ -61,4 +61,39 @@ public class PeopleAccessPoliciesRequestModel
|
||||
GroupAccessPolicies = groupAccessPolicies
|
||||
};
|
||||
}
|
||||
|
||||
public ServiceAccountPeopleAccessPolicies ToServiceAccountPeopleAccessPolicies(Guid grantedServiceAccountId, Guid organizationId)
|
||||
{
|
||||
var userAccessPolicies = UserAccessPolicyRequests?
|
||||
.Select(x => x.ToUserServiceAccountAccessPolicy(grantedServiceAccountId, organizationId)).ToList();
|
||||
|
||||
var groupAccessPolicies = GroupAccessPolicyRequests?
|
||||
.Select(x => x.ToGroupServiceAccountAccessPolicy(grantedServiceAccountId, organizationId)).ToList();
|
||||
|
||||
var policies = new List<BaseAccessPolicy>();
|
||||
if (userAccessPolicies != null)
|
||||
{
|
||||
policies.AddRange(userAccessPolicies);
|
||||
}
|
||||
|
||||
if (groupAccessPolicies != null)
|
||||
{
|
||||
policies.AddRange(groupAccessPolicies);
|
||||
}
|
||||
|
||||
CheckForDistinctAccessPolicies(policies);
|
||||
|
||||
if (!policies.All(ap => ap.Read && ap.Write))
|
||||
{
|
||||
throw new BadRequestException("Service account access must be Can read, write");
|
||||
}
|
||||
|
||||
return new ServiceAccountPeopleAccessPolicies
|
||||
{
|
||||
Id = grantedServiceAccountId,
|
||||
OrganizationId = organizationId,
|
||||
UserAccessPolicies = userAccessPolicies,
|
||||
GroupAccessPolicies = groupAccessPolicies
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -69,10 +69,14 @@ public class UserServiceAccountAccessPolicyResponseModel : BaseAccessPolicyRespo
|
||||
public UserServiceAccountAccessPolicyResponseModel(UserServiceAccountAccessPolicy accessPolicy)
|
||||
: base(accessPolicy, _objectName)
|
||||
{
|
||||
OrganizationUserId = accessPolicy.OrganizationUserId;
|
||||
GrantedServiceAccountId = accessPolicy.GrantedServiceAccountId;
|
||||
OrganizationUserName = GetUserDisplayName(accessPolicy.User);
|
||||
UserId = accessPolicy.User?.Id;
|
||||
SetProperties(accessPolicy);
|
||||
}
|
||||
|
||||
public UserServiceAccountAccessPolicyResponseModel(UserServiceAccountAccessPolicy accessPolicy, Guid userId)
|
||||
: base(accessPolicy, _objectName)
|
||||
{
|
||||
SetProperties(accessPolicy);
|
||||
CurrentUser = accessPolicy.User?.Id == userId;
|
||||
}
|
||||
|
||||
public UserServiceAccountAccessPolicyResponseModel() : base(new UserServiceAccountAccessPolicy(), _objectName)
|
||||
@ -83,6 +87,15 @@ public class UserServiceAccountAccessPolicyResponseModel : BaseAccessPolicyRespo
|
||||
public string? OrganizationUserName { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public Guid? GrantedServiceAccountId { get; set; }
|
||||
public bool CurrentUser { get; set; }
|
||||
|
||||
private void SetProperties(UserServiceAccountAccessPolicy accessPolicy)
|
||||
{
|
||||
OrganizationUserId = accessPolicy.OrganizationUserId;
|
||||
GrantedServiceAccountId = accessPolicy.GrantedServiceAccountId;
|
||||
OrganizationUserName = GetUserDisplayName(accessPolicy.User);
|
||||
UserId = accessPolicy.User?.Id;
|
||||
}
|
||||
}
|
||||
|
||||
public class GroupProjectAccessPolicyResponseModel : BaseAccessPolicyResponseModel
|
||||
|
@ -3,11 +3,11 @@ using Bit.Core.SecretsManager.Entities;
|
||||
|
||||
namespace Bit.Api.SecretsManager.Models.Response;
|
||||
|
||||
public class ServiceAccountAccessPoliciesResponseModel : ResponseModel
|
||||
public class ServiceAccountPeopleAccessPoliciesResponseModel : ResponseModel
|
||||
{
|
||||
private const string _objectName = "serviceAccountAccessPolicies";
|
||||
|
||||
public ServiceAccountAccessPoliciesResponseModel(IEnumerable<BaseAccessPolicy> baseAccessPolicies)
|
||||
public ServiceAccountPeopleAccessPoliciesResponseModel(IEnumerable<BaseAccessPolicy> baseAccessPolicies, Guid userId)
|
||||
: base(_objectName)
|
||||
{
|
||||
if (baseAccessPolicies == null)
|
||||
@ -20,7 +20,7 @@ public class ServiceAccountAccessPoliciesResponseModel : ResponseModel
|
||||
switch (baseAccessPolicy)
|
||||
{
|
||||
case UserServiceAccountAccessPolicy accessPolicy:
|
||||
UserAccessPolicies.Add(new UserServiceAccountAccessPolicyResponseModel(accessPolicy));
|
||||
UserAccessPolicies.Add(new UserServiceAccountAccessPolicyResponseModel(accessPolicy, userId));
|
||||
break;
|
||||
case GroupServiceAccountAccessPolicy accessPolicy:
|
||||
GroupAccessPolicies.Add(new GroupServiceAccountAccessPolicyResponseModel(accessPolicy));
|
||||
@ -29,7 +29,7 @@ public class ServiceAccountAccessPoliciesResponseModel : ResponseModel
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceAccountAccessPoliciesResponseModel() : base(_objectName)
|
||||
public ServiceAccountPeopleAccessPoliciesResponseModel() : base(_objectName)
|
||||
{
|
||||
}
|
||||
|
@ -7,8 +7,12 @@ using Stripe;
|
||||
using Bit.Core.Utilities;
|
||||
using IdentityModel;
|
||||
using System.Globalization;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Validators;
|
||||
using Bit.Api.Auth.Models.Request;
|
||||
using Bit.Api.Auth.Validators;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Tools.Validators;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Api.Vault.Validators;
|
||||
using Bit.Core.Auth.Entities;
|
||||
@ -20,9 +24,10 @@ using Bit.SharedWeb.Utilities;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.UserFeatures.UserKey;
|
||||
using Bit.Core.Auth.UserFeatures.UserKey.Implementations;
|
||||
using Bit.Core.Auth.UserFeatures;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
|
||||
#if !OSS
|
||||
@ -140,16 +145,24 @@ public class Startup
|
||||
services.AddScoped<AuthenticatorTokenProvider>();
|
||||
|
||||
// Key Rotation
|
||||
services.AddScoped<IRotateUserKeyCommand, RotateUserKeyCommand>();
|
||||
services
|
||||
.AddScoped<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>,
|
||||
EmergencyAccessRotationValidator>();
|
||||
services.AddUserKeyCommands(globalSettings);
|
||||
services
|
||||
.AddScoped<IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>,
|
||||
CipherRotationValidator>();
|
||||
services
|
||||
.AddScoped<IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>,
|
||||
FolderRotationValidator>();
|
||||
services
|
||||
.AddScoped<IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>,
|
||||
SendRotationValidator>();
|
||||
services
|
||||
.AddScoped<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>,
|
||||
EmergencyAccessRotationValidator>();
|
||||
services
|
||||
.AddScoped<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
|
||||
IReadOnlyList<OrganizationUser>>
|
||||
, OrganizationUserRotationValidator>();
|
||||
|
||||
|
||||
// Services
|
||||
services.AddBaseServices(globalSettings);
|
||||
|
57
src/Api/Tools/Validators/SendRotationValidator.cs
Normal file
57
src/Api/Tools/Validators/SendRotationValidator.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using Bit.Api.Auth.Validators;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Tools.Services;
|
||||
|
||||
namespace Bit.Api.Tools.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Send implementation for <see cref="IRotationValidator{T,R}"/>
|
||||
/// </summary>
|
||||
public class SendRotationValidator : IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>
|
||||
{
|
||||
private readonly ISendService _sendService;
|
||||
private readonly ISendRepository _sendRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates a new <see cref="SendRotationValidator"/>
|
||||
/// </summary>
|
||||
/// <param name="sendService">Enables conversion of <see cref="SendWithIdRequestModel"/> to <see cref="Send"/></param>
|
||||
/// <param name="sendRepository">Retrieves all user <see cref="Send"/>s</param>
|
||||
public SendRotationValidator(ISendService sendService, ISendRepository sendRepository)
|
||||
{
|
||||
_sendService = sendService;
|
||||
_sendRepository = sendRepository;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Send>> ValidateAsync(User user, IEnumerable<SendWithIdRequestModel> sends)
|
||||
{
|
||||
var result = new List<Send>();
|
||||
if (sends == null || !sends.Any())
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var existingSends = await _sendRepository.GetManyByUserIdAsync(user.Id);
|
||||
if (existingSends == null || !existingSends.Any())
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var existing in existingSends)
|
||||
{
|
||||
var send = sends.FirstOrDefault(c => c.Id == existing.Id);
|
||||
if (send == null)
|
||||
{
|
||||
throw new BadRequestException("All existing folders must be included in the rotation.");
|
||||
}
|
||||
|
||||
result.Add(send.ToSend(existing, _sendService));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
32
src/Api/Utilities/AuthorizationServiceExtensions.cs
Normal file
32
src/Api/Utilities/AuthorizationServiceExtensions.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Api.Utilities;
|
||||
|
||||
public static class AuthorizationServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if a user meets a specific requirement.
|
||||
/// </summary>
|
||||
/// <param name="service">The <see cref="IAuthorizationService"/> providing authorization.</param>
|
||||
/// <param name="user">The user to evaluate the policy against.</param>
|
||||
/// <param name="requirement">The requirement to evaluate the policy against.</param>
|
||||
/// <returns>
|
||||
/// A flag indicating whether requirement evaluation has succeeded or failed.
|
||||
/// This value is <value>true</value> when the user fulfills the policy, otherwise <value>false</value>.
|
||||
/// </returns>
|
||||
public static Task<AuthorizationResult> AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, IAuthorizationRequirement requirement)
|
||||
{
|
||||
if (service == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(service));
|
||||
}
|
||||
|
||||
if (requirement == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(requirement));
|
||||
}
|
||||
|
||||
return service.AuthorizeAsync(user, resource: null, new[] { requirement });
|
||||
}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Groups;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
@ -120,6 +122,9 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
public static void AddAuthorizationHandlers(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IAuthorizationHandler, BulkCollectionAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, CollectionAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, GroupAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, OrganizationUserAuthorizationHandler>();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,265 @@
|
||||
#nullable enable
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Handles authorization logic for Collection objects, including access permissions for users and groups.
|
||||
/// This uses new logic implemented in the Flexible Collections initiative.
|
||||
/// </summary>
|
||||
public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkCollectionOperationRequirement, Collection>
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly IFeatureService _featureService;
|
||||
private Guid _targetOrganizationId;
|
||||
|
||||
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||
|
||||
public BulkCollectionAuthorizationHandler(
|
||||
ICurrentContext currentContext,
|
||||
ICollectionRepository collectionRepository,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_collectionRepository = collectionRepository;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
BulkCollectionOperationRequirement requirement, ICollection<Collection>? resources)
|
||||
{
|
||||
if (!FlexibleCollectionsIsEnabled)
|
||||
{
|
||||
// Flexible collections is OFF, should not be using this handler
|
||||
throw new FeatureUnavailableException("Flexible collections is OFF when it should be ON.");
|
||||
}
|
||||
|
||||
// Establish pattern of authorization handler null checking passed resources
|
||||
if (resources == null || !resources.Any())
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
// Acting user is not authenticated, fail
|
||||
if (!_currentContext.UserId.HasValue)
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
_targetOrganizationId = resources.First().OrganizationId;
|
||||
|
||||
// Ensure all target collections belong to the same organization
|
||||
if (resources.Any(tc => tc.OrganizationId != _targetOrganizationId))
|
||||
{
|
||||
throw new BadRequestException("Requested collections must belong to the same organization.");
|
||||
}
|
||||
|
||||
var org = _currentContext.GetOrganization(_targetOrganizationId);
|
||||
|
||||
switch (requirement)
|
||||
{
|
||||
case not null when requirement == BulkCollectionOperations.Create:
|
||||
await CanCreateAsync(context, requirement, org);
|
||||
break;
|
||||
|
||||
case not null when requirement == BulkCollectionOperations.Read:
|
||||
case not null when requirement == BulkCollectionOperations.ReadAccess:
|
||||
await CanReadAsync(context, requirement, resources, org);
|
||||
break;
|
||||
|
||||
case not null when requirement == BulkCollectionOperations.ReadWithAccess:
|
||||
await CanReadWithAccessAsync(context, requirement, resources, org);
|
||||
break;
|
||||
|
||||
case not null when requirement == BulkCollectionOperations.Update:
|
||||
case not null when requirement == BulkCollectionOperations.ModifyAccess:
|
||||
await CanUpdateCollection(context, requirement, resources, org);
|
||||
break;
|
||||
|
||||
case not null when requirement == BulkCollectionOperations.Delete:
|
||||
await CanDeleteAsync(context, requirement, resources, org);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanCreateAsync(AuthorizationHandlerContext context, IAuthorizationRequirement requirement,
|
||||
CurrentContextOrganization? org)
|
||||
{
|
||||
// If the limit collection management setting is disabled, allow any user to create collections
|
||||
// Otherwise, Owners, Admins, and users with CreateNewCollections permission can always create collections
|
||||
if (org is
|
||||
{ LimitCollectionCreationDeletion: false } or
|
||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||
{ Permissions.CreateNewCollections: true })
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow provider users to create collections if they are a provider for the target organization
|
||||
if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanReadAsync(AuthorizationHandlerContext context, IAuthorizationRequirement requirement,
|
||||
ICollection<Collection> resources, CurrentContextOrganization? org)
|
||||
{
|
||||
// Owners, Admins, and users with EditAnyCollection or DeleteAnyCollection permission can always read a collection
|
||||
if (org is
|
||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||
{ Permissions.EditAnyCollection: true } or
|
||||
{ Permissions.DeleteAnyCollection: true })
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
// The acting user is a member of the target organization,
|
||||
// ensure they have access for the collection being read
|
||||
if (org is not null)
|
||||
{
|
||||
var isAssignedToCollections = await IsAssignedToCollectionsAsync(resources, org, false);
|
||||
if (isAssignedToCollections)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow provider users to read collections if they are a provider for the target organization
|
||||
if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanReadWithAccessAsync(AuthorizationHandlerContext context, IAuthorizationRequirement requirement,
|
||||
ICollection<Collection> resources, CurrentContextOrganization? org)
|
||||
{
|
||||
// Owners, Admins, and users with EditAnyCollection, DeleteAnyCollection or ManageUsers permission can always read a collection
|
||||
if (org is
|
||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||
{ Permissions.EditAnyCollection: true } or
|
||||
{ Permissions.DeleteAnyCollection: true } or
|
||||
{ Permissions.ManageUsers: true })
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
// The acting user is a member of the target organization,
|
||||
// ensure they have access with manage permission for the collection being read
|
||||
if (org is not null)
|
||||
{
|
||||
var isAssignedToCollections = await IsAssignedToCollectionsAsync(resources, org, true);
|
||||
if (isAssignedToCollections)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow provider users to read collections if they are a provider for the target organization
|
||||
if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the acting user is allowed to update the target collections or manage access permissions for them.
|
||||
/// </summary>
|
||||
private async Task CanUpdateCollection(AuthorizationHandlerContext context,
|
||||
IAuthorizationRequirement requirement, ICollection<Collection> resources,
|
||||
CurrentContextOrganization? org)
|
||||
{
|
||||
// Owners, Admins, and users with EditAnyCollection permission can always manage collection access
|
||||
if (org is
|
||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||
{ Permissions.EditAnyCollection: true })
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
// The acting user is a member of the target organization,
|
||||
// ensure they have manage permission for the collection being managed
|
||||
if (org is not null)
|
||||
{
|
||||
var canManageCollections = await IsAssignedToCollectionsAsync(resources, org, true);
|
||||
if (canManageCollections)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow providers to manage collections if they are a provider for the target organization
|
||||
if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanDeleteAsync(AuthorizationHandlerContext context, IAuthorizationRequirement requirement,
|
||||
ICollection<Collection> resources, CurrentContextOrganization? org)
|
||||
{
|
||||
// Owners, Admins, and users with DeleteAnyCollection permission can always delete collections
|
||||
if (org is
|
||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||
{ Permissions.DeleteAnyCollection: true })
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
// The limit collection management setting is disabled,
|
||||
// ensure acting user has manage permissions for all collections being deleted
|
||||
if (org is { LimitCollectionCreationDeletion: false })
|
||||
{
|
||||
var canManageCollections = await IsAssignedToCollectionsAsync(resources, org, true);
|
||||
if (canManageCollections)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow providers to delete collections if they are a provider for the target organization
|
||||
if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> IsAssignedToCollectionsAsync(
|
||||
ICollection<Collection> targetCollections,
|
||||
CurrentContextOrganization org,
|
||||
bool requireManagePermission)
|
||||
{
|
||||
// List of collection Ids the acting user has access to
|
||||
var assignedCollectionIds =
|
||||
(await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId!.Value, useFlexibleCollections: true))
|
||||
.Where(c =>
|
||||
// Check Collections with Manage permission
|
||||
(!requireManagePermission || c.Manage) && c.OrganizationId == org.Id)
|
||||
.Select(c => c.Id)
|
||||
.ToHashSet();
|
||||
|
||||
// Check if the acting user has access to all target collections
|
||||
return targetCollections.All(tc => assignedCollectionIds.Contains(tc.Id));
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||
|
||||
namespace Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
|
||||
public class BulkCollectionOperationRequirement : OperationAuthorizationRequirement { }
|
||||
|
||||
public static class BulkCollectionOperations
|
||||
{
|
||||
public static readonly BulkCollectionOperationRequirement Create = new() { Name = nameof(Create) };
|
||||
public static readonly BulkCollectionOperationRequirement Read = new() { Name = nameof(Read) };
|
||||
public static readonly BulkCollectionOperationRequirement ReadAccess = new() { Name = nameof(ReadAccess) };
|
||||
public static readonly BulkCollectionOperationRequirement ReadWithAccess = new() { Name = nameof(ReadWithAccess) };
|
||||
public static readonly BulkCollectionOperationRequirement Update = new() { Name = nameof(Update) };
|
||||
/// <summary>
|
||||
/// The operation that represents creating, updating, or removing collection access.
|
||||
/// Combined together to allow for a single requirement to be used for each operation
|
||||
/// as they all currently share the same underlying authorization logic.
|
||||
/// </summary>
|
||||
public static readonly BulkCollectionOperationRequirement ModifyAccess = new() { Name = nameof(ModifyAccess) };
|
||||
public static readonly BulkCollectionOperationRequirement Delete = new() { Name = nameof(Delete) };
|
||||
}
|
@ -1,176 +1,108 @@
|
||||
#nullable enable
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Handles authorization logic for Collection objects, including access permissions for users and groups.
|
||||
/// Handles authorization logic for Collection operations.
|
||||
/// This uses new logic implemented in the Flexible Collections initiative.
|
||||
/// </summary>
|
||||
public class CollectionAuthorizationHandler : BulkAuthorizationHandler<CollectionOperationRequirement, Collection>
|
||||
public class CollectionAuthorizationHandler : AuthorizationHandler<CollectionOperationRequirement>
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly IFeatureService _featureService;
|
||||
private Guid _targetOrganizationId;
|
||||
|
||||
public CollectionAuthorizationHandler(ICurrentContext currentContext, ICollectionRepository collectionRepository,
|
||||
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||
|
||||
public CollectionAuthorizationHandler(
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_collectionRepository = collectionRepository;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
CollectionOperationRequirement requirement, ICollection<Collection>? resources)
|
||||
CollectionOperationRequirement requirement)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext))
|
||||
if (!FlexibleCollectionsIsEnabled)
|
||||
{
|
||||
// Flexible collections is OFF, should not be using this handler
|
||||
throw new FeatureUnavailableException("Flexible collections is OFF when it should be ON.");
|
||||
}
|
||||
|
||||
// Establish pattern of authorization handler null checking passed resources
|
||||
if (resources == null || !resources.Any())
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
// Acting user is not authenticated, fail
|
||||
if (!_currentContext.UserId.HasValue)
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
_targetOrganizationId = resources.First().OrganizationId;
|
||||
|
||||
// Ensure all target collections belong to the same organization
|
||||
if (resources.Any(tc => tc.OrganizationId != _targetOrganizationId))
|
||||
if (requirement.OrganizationId == default)
|
||||
{
|
||||
throw new BadRequestException("Requested collections must belong to the same organization.");
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
var org = _currentContext.GetOrganization(_targetOrganizationId);
|
||||
var org = _currentContext.GetOrganization(requirement.OrganizationId);
|
||||
|
||||
switch (requirement)
|
||||
{
|
||||
case not null when requirement == CollectionOperations.Create:
|
||||
await CanCreateAsync(context, requirement, org);
|
||||
case not null when requirement.Name == nameof(CollectionOperations.ReadAll):
|
||||
await CanReadAllAsync(context, requirement, org);
|
||||
break;
|
||||
|
||||
case not null when requirement == CollectionOperations.Delete:
|
||||
await CanDeleteAsync(context, requirement, resources, org);
|
||||
break;
|
||||
|
||||
case not null when requirement == CollectionOperations.ModifyAccess:
|
||||
await CanManageCollectionAccessAsync(context, requirement, resources, org);
|
||||
case not null when requirement.Name == nameof(CollectionOperations.ReadAllWithAccess):
|
||||
await CanReadAllWithAccessAsync(context, requirement, org);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanCreateAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement,
|
||||
private async Task CanReadAllAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement,
|
||||
CurrentContextOrganization? org)
|
||||
{
|
||||
// If the limit collection management setting is disabled, allow any user to create collections
|
||||
// Otherwise, Owners, Admins, and users with CreateNewCollections permission can always create collections
|
||||
// Owners, Admins, and users with EditAnyCollection, DeleteAnyCollection,
|
||||
// or AccessImportExport permission can always read a collection
|
||||
if (org is
|
||||
{ LimitCollectionCreationDeletion: false } or
|
||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||
{ Permissions.CreateNewCollections: true })
|
||||
{ Permissions.EditAnyCollection: true } or
|
||||
{ Permissions.DeleteAnyCollection: true } or
|
||||
{ Permissions.AccessImportExport: true } or
|
||||
{ Permissions.ManageGroups: true })
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow provider users to create collections if they are a provider for the target organization
|
||||
if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId))
|
||||
// Allow provider users to read collections if they are a provider for the target organization
|
||||
if (await _currentContext.ProviderUserForOrgAsync(requirement.OrganizationId))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanDeleteAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement,
|
||||
ICollection<Collection> resources, CurrentContextOrganization? org)
|
||||
{
|
||||
// Owners, Admins, and users with DeleteAnyCollection permission can always delete collections
|
||||
if (org is
|
||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||
{ Permissions.DeleteAnyCollection: true })
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
// The limit collection management setting is disabled,
|
||||
// ensure acting user has manage permissions for all collections being deleted
|
||||
if (org is { LimitCollectionCreationDeletion: false })
|
||||
{
|
||||
var manageableCollectionIds =
|
||||
(await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId!.Value))
|
||||
.Where(c => c.Manage && c.OrganizationId == org.Id)
|
||||
.Select(c => c.Id)
|
||||
.ToHashSet();
|
||||
|
||||
// The acting user has permission to manage all target collections, succeed
|
||||
if (resources.All(c => manageableCollectionIds.Contains(c.Id)))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow providers to delete collections if they are a provider for the target organization
|
||||
if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the acting user is allowed to manage access permissions for the target collections.
|
||||
/// </summary>
|
||||
private async Task CanManageCollectionAccessAsync(AuthorizationHandlerContext context,
|
||||
IAuthorizationRequirement requirement, ICollection<Collection> targetCollections,
|
||||
private async Task CanReadAllWithAccessAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement,
|
||||
CurrentContextOrganization? org)
|
||||
{
|
||||
// Owners, Admins, and users with EditAnyCollection permission can always manage collection access
|
||||
// Owners, Admins, and users with EditAnyCollection or DeleteAnyCollection
|
||||
// permission can always read a collection
|
||||
if (org is
|
||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||
{ Permissions.EditAnyCollection: true })
|
||||
{ Permissions.EditAnyCollection: true } or
|
||||
{ Permissions.DeleteAnyCollection: true } or
|
||||
{ Permissions.ManageUsers: true })
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only check collection management permissions if the user is a member of the target organization (org != null)
|
||||
if (org is not null)
|
||||
{
|
||||
var manageableCollectionIds =
|
||||
(await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId!.Value))
|
||||
.Where(c => c.Manage && c.OrganizationId == org.Id)
|
||||
.Select(c => c.Id)
|
||||
.ToHashSet();
|
||||
|
||||
// The acting user has permission to manage all target collections, succeed
|
||||
if (targetCollections.All(c => manageableCollectionIds.Contains(c.Id)))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow providers to manage collections if they are a provider for the target organization
|
||||
if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId))
|
||||
// Allow provider users to read collections if they are a provider for the target organization
|
||||
if (await _currentContext.ProviderUserForOrgAsync(requirement.OrganizationId))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
|
@ -2,16 +2,26 @@
|
||||
|
||||
namespace Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
|
||||
public class CollectionOperationRequirement : OperationAuthorizationRequirement { }
|
||||
public class CollectionOperationRequirement : OperationAuthorizationRequirement
|
||||
{
|
||||
public Guid OrganizationId { get; init; }
|
||||
|
||||
public CollectionOperationRequirement(string name, Guid organizationId)
|
||||
{
|
||||
Name = name;
|
||||
OrganizationId = organizationId;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CollectionOperations
|
||||
{
|
||||
public static readonly CollectionOperationRequirement Create = new() { Name = nameof(Create) };
|
||||
public static readonly CollectionOperationRequirement Delete = new() { Name = nameof(Delete) };
|
||||
/// <summary>
|
||||
/// The operation that represents creating, updating, or removing collection access.
|
||||
/// Combined together to allow for a single requirement to be used for each operation
|
||||
/// as they all currently share the same underlying authorization logic.
|
||||
/// </summary>
|
||||
public static readonly CollectionOperationRequirement ModifyAccess = new() { Name = nameof(ModifyAccess) };
|
||||
public static CollectionOperationRequirement ReadAll(Guid organizationId)
|
||||
{
|
||||
return new CollectionOperationRequirement(nameof(ReadAll), organizationId);
|
||||
}
|
||||
public static CollectionOperationRequirement ReadAllWithAccess(Guid organizationId)
|
||||
{
|
||||
return new CollectionOperationRequirement(nameof(ReadAllWithAccess), organizationId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,86 @@
|
||||
#nullable enable
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Api.Vault.AuthorizationHandlers.Groups;
|
||||
|
||||
/// <summary>
|
||||
/// Handles authorization logic for Group operations.
|
||||
/// This uses new logic implemented in the Flexible Collections initiative.
|
||||
/// </summary>
|
||||
public class GroupAuthorizationHandler : AuthorizationHandler<GroupOperationRequirement>
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||
|
||||
public GroupAuthorizationHandler(
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
GroupOperationRequirement requirement)
|
||||
{
|
||||
if (!FlexibleCollectionsIsEnabled)
|
||||
{
|
||||
// Flexible collections is OFF, should not be using this handler
|
||||
throw new FeatureUnavailableException("Flexible collections is OFF when it should be ON.");
|
||||
}
|
||||
|
||||
// Acting user is not authenticated, fail
|
||||
if (!_currentContext.UserId.HasValue)
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
if (requirement.OrganizationId == default)
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
var org = _currentContext.GetOrganization(requirement.OrganizationId);
|
||||
|
||||
switch (requirement)
|
||||
{
|
||||
case not null when requirement.Name == nameof(GroupOperations.ReadAll):
|
||||
await CanReadAllAsync(context, requirement, org);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanReadAllAsync(AuthorizationHandlerContext context, GroupOperationRequirement requirement,
|
||||
CurrentContextOrganization? org)
|
||||
{
|
||||
// If the limit collection management setting is disabled, allow any user to read all groups
|
||||
// Otherwise, Owners, Admins, and users with any of ManageGroups, ManageUsers, EditAnyCollection, DeleteAnyCollection, CreateNewCollections permissions can always read all groups
|
||||
if (org is
|
||||
{ LimitCollectionCreationDeletion: false } or
|
||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||
{ Permissions.ManageGroups: true } or
|
||||
{ Permissions.ManageUsers: true } or
|
||||
{ Permissions.EditAnyCollection: true } or
|
||||
{ Permissions.DeleteAnyCollection: true } or
|
||||
{ Permissions.CreateNewCollections: true })
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow provider users to read all groups if they are a provider for the target organization
|
||||
if (await _currentContext.ProviderUserForOrgAsync(requirement.OrganizationId))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||
|
||||
namespace Bit.Api.Vault.AuthorizationHandlers.Groups;
|
||||
|
||||
public class GroupOperationRequirement : OperationAuthorizationRequirement
|
||||
{
|
||||
public Guid OrganizationId { get; init; }
|
||||
|
||||
public GroupOperationRequirement(string name, Guid organizationId)
|
||||
{
|
||||
Name = name;
|
||||
OrganizationId = organizationId;
|
||||
}
|
||||
}
|
||||
|
||||
public static class GroupOperations
|
||||
{
|
||||
public static GroupOperationRequirement ReadAll(Guid organizationId)
|
||||
{
|
||||
return new GroupOperationRequirement(nameof(ReadAll), organizationId);
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
#nullable enable
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers;
|
||||
|
||||
/// <summary>
|
||||
/// Handles authorization logic for OrganizationUser objects.
|
||||
/// This uses new logic implemented in the Flexible Collections initiative.
|
||||
/// </summary>
|
||||
public class OrganizationUserAuthorizationHandler : AuthorizationHandler<OrganizationUserOperationRequirement>
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||
|
||||
public OrganizationUserAuthorizationHandler(
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
OrganizationUserOperationRequirement requirement)
|
||||
{
|
||||
if (!FlexibleCollectionsIsEnabled)
|
||||
{
|
||||
// Flexible collections is OFF, should not be using this handler
|
||||
throw new FeatureUnavailableException("Flexible collections is OFF when it should be ON.");
|
||||
}
|
||||
|
||||
if (!_currentContext.UserId.HasValue)
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
if (requirement.OrganizationId == default)
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
var org = _currentContext.GetOrganization(requirement.OrganizationId);
|
||||
|
||||
switch (requirement)
|
||||
{
|
||||
case not null when requirement.Name == nameof(OrganizationUserOperations.ReadAll):
|
||||
await CanReadAllAsync(context, requirement, org);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanReadAllAsync(AuthorizationHandlerContext context, OrganizationUserOperationRequirement requirement,
|
||||
CurrentContextOrganization? org)
|
||||
{
|
||||
// If the limit collection management setting is disabled, allow any user to read all organization users
|
||||
// Otherwise, Owners, Admins, and users with any of ManageGroups, ManageUsers, EditAnyCollection, DeleteAnyCollection, CreateNewCollections permissions can always read all organization users
|
||||
if (org is
|
||||
{ LimitCollectionCreationDeletion: false } or
|
||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||
{ Permissions.ManageGroups: true } or
|
||||
{ Permissions.ManageUsers: true } or
|
||||
{ Permissions.EditAnyCollection: true } or
|
||||
{ Permissions.DeleteAnyCollection: true } or
|
||||
{ Permissions.CreateNewCollections: true })
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow provider users to read all organization users if they are a provider for the target organization
|
||||
if (await _currentContext.ProviderUserForOrgAsync(requirement.OrganizationId))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||
|
||||
namespace Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers;
|
||||
|
||||
public class OrganizationUserOperationRequirement : OperationAuthorizationRequirement
|
||||
{
|
||||
public Guid OrganizationId { get; }
|
||||
|
||||
public OrganizationUserOperationRequirement(string name, Guid organizationId)
|
||||
{
|
||||
Name = name;
|
||||
OrganizationId = organizationId;
|
||||
}
|
||||
}
|
||||
|
||||
public static class OrganizationUserOperations
|
||||
{
|
||||
public static OrganizationUserOperationRequirement ReadAll(Guid organizationId)
|
||||
{
|
||||
return new OrganizationUserOperationRequirement(nameof(ReadAll), organizationId);
|
||||
}
|
||||
}
|
@ -94,9 +94,8 @@ public class SyncController : Controller
|
||||
|
||||
if (hasEnabledOrgs)
|
||||
{
|
||||
collections = await _collectionRepository.GetManyByUserIdAsync(user.Id);
|
||||
collections = await _collectionRepository.GetManyByUserIdAsync(user.Id, UseFlexibleCollections);
|
||||
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdAsync(user.Id, UseFlexibleCollections);
|
||||
collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);
|
||||
}
|
||||
|
||||
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -236,7 +236,7 @@ public class Organization : ITableObject<Guid>, ISubscriber, IStorable, IStorabl
|
||||
return providers[provider];
|
||||
}
|
||||
|
||||
public void UpdateFromLicense(OrganizationLicense license)
|
||||
public void UpdateFromLicense(OrganizationLicense license, bool flexibleCollectionsIsEnabled)
|
||||
{
|
||||
Name = license.Name;
|
||||
BusinessName = license.BusinessName;
|
||||
@ -267,5 +267,6 @@ public class Organization : ITableObject<Guid>, ISubscriber, IStorable, IStorabl
|
||||
UseSecretsManager = license.UseSecretsManager;
|
||||
SmSeats = license.SmSeats;
|
||||
SmServiceAccounts = license.SmServiceAccounts;
|
||||
LimitCollectionCreationDeletion = !flexibleCollectionsIsEnabled || license.LimitCollectionCreationDeletion;
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,9 @@ public class Permissions
|
||||
public bool CreateNewCollections { get; set; }
|
||||
public bool EditAnyCollection { get; set; }
|
||||
public bool DeleteAnyCollection { get; set; }
|
||||
[Obsolete("Pre-Flexible Collections logic.")]
|
||||
public bool EditAssignedCollections { get; set; }
|
||||
[Obsolete("Pre-Flexible Collections logic.")]
|
||||
public bool DeleteAssignedCollections { get; set; }
|
||||
public bool ManageGroups { get; set; }
|
||||
public bool ManagePolicies { get; set; }
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.Auth.UserFeatures.UserKey;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
@ -42,4 +43,13 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
|
||||
Task RestoreAsync(Guid id, OrganizationUserStatusType status);
|
||||
Task<IEnumerable<OrganizationUserPolicyDetails>> GetByUserIdWithPolicyDetailsAsync(Guid userId, PolicyType policyType);
|
||||
Task<int> GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId);
|
||||
|
||||
/// <summary>
|
||||
/// Updates encrypted data for organization users during a key rotation
|
||||
/// </summary>
|
||||
/// <param name="userId">The user that initiated the key rotation</param>
|
||||
/// <param name="resetPasswordKeys">A list of organization users with updated reset password keys</param>
|
||||
UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId,
|
||||
IEnumerable<OrganizationUser> resetPasswordKeys);
|
||||
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Mail;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
@ -64,6 +65,8 @@ public class OrganizationService : IOrganizationService
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||
|
||||
public OrganizationService(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@ -555,6 +558,9 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
await ValidateSignUpPoliciesAsync(owner.Id);
|
||||
|
||||
var flexibleCollectionsIsEnabled =
|
||||
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||
|
||||
var organization = new Organization
|
||||
{
|
||||
Name = license.Name,
|
||||
@ -594,7 +600,8 @@ public class OrganizationService : IOrganizationService
|
||||
UsePasswordManager = license.UsePasswordManager,
|
||||
UseSecretsManager = license.UseSecretsManager,
|
||||
SmSeats = license.SmSeats,
|
||||
SmServiceAccounts = license.SmServiceAccounts
|
||||
SmServiceAccounts = license.SmServiceAccounts,
|
||||
LimitCollectionCreationDeletion = !flexibleCollectionsIsEnabled || license.LimitCollectionCreationDeletion
|
||||
};
|
||||
|
||||
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
|
||||
@ -1072,6 +1079,54 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
private async Task SendInvitesAsync(IEnumerable<OrganizationUser> orgUsers, Organization organization)
|
||||
{
|
||||
var orgInvitesInfo = await BuildOrganizationInvitesInfoAsync(orgUsers, organization);
|
||||
|
||||
await _mailService.SendOrganizationInviteEmailsAsync(orgInvitesInfo);
|
||||
}
|
||||
|
||||
private async Task SendInviteAsync(OrganizationUser orgUser, Organization organization, bool initOrganization)
|
||||
{
|
||||
// convert single org user into array of 1 org user
|
||||
var orgUsers = new[] { orgUser };
|
||||
|
||||
var orgInvitesInfo = await BuildOrganizationInvitesInfoAsync(orgUsers, organization, initOrganization);
|
||||
|
||||
await _mailService.SendOrganizationInviteEmailsAsync(orgInvitesInfo);
|
||||
}
|
||||
|
||||
private async Task<OrganizationInvitesInfo> BuildOrganizationInvitesInfoAsync(
|
||||
IEnumerable<OrganizationUser> orgUsers,
|
||||
Organization organization,
|
||||
bool initOrganization = false)
|
||||
{
|
||||
// Materialize the sequence into a list to avoid multiple enumeration warnings
|
||||
var orgUsersList = orgUsers.ToList();
|
||||
|
||||
// Email links must include information about the org and user for us to make routing decisions client side
|
||||
// Given an org user, determine if existing BW user exists
|
||||
var orgUserEmails = orgUsersList.Select(ou => ou.Email).ToList();
|
||||
var existingUsers = await _userRepository.GetManyByEmailsAsync(orgUserEmails);
|
||||
|
||||
// hash existing users emails list for O(1) lookups
|
||||
var existingUserEmailsHashSet = new HashSet<string>(existingUsers.Select(u => u.Email));
|
||||
|
||||
// Create a dictionary of org user guids and bools for whether or not they have an existing BW user
|
||||
var orgUserHasExistingUserDict = orgUsersList.ToDictionary(
|
||||
ou => ou.Id,
|
||||
ou => existingUserEmailsHashSet.Contains(ou.Email)
|
||||
);
|
||||
|
||||
// Determine if org has SSO enabled and if user is required to login with SSO
|
||||
// Note: we only want to call the DB after checking if the org can use SSO per plan and if they have any policies enabled.
|
||||
var orgSsoEnabled = organization.UseSso && (await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id)).Enabled;
|
||||
// Even though the require SSO policy can be turned on regardless of SSO being enabled, for this logic, we only
|
||||
// need to check the policy if the org has SSO enabled.
|
||||
var orgSsoLoginRequiredPolicyEnabled = orgSsoEnabled &&
|
||||
organization.UsePolicies &&
|
||||
(await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.RequireSso)).Enabled;
|
||||
|
||||
// Generate the list of org users and expiring tokens
|
||||
// create helper function to create expiring tokens
|
||||
(OrganizationUser, ExpiringToken) MakeOrgUserExpiringTokenPair(OrganizationUser orgUser)
|
||||
{
|
||||
var orgUserInviteTokenable = _orgUserInviteTokenableFactory.CreateToken(orgUser);
|
||||
@ -1081,22 +1136,12 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
var orgUsersWithExpTokens = orgUsers.Select(MakeOrgUserExpiringTokenPair);
|
||||
|
||||
await _mailService.BulkSendOrganizationInviteEmailAsync(
|
||||
organization.Name,
|
||||
return new OrganizationInvitesInfo(
|
||||
organization,
|
||||
orgSsoEnabled,
|
||||
orgSsoLoginRequiredPolicyEnabled,
|
||||
orgUsersWithExpTokens,
|
||||
organization.PlanType == PlanType.Free
|
||||
);
|
||||
}
|
||||
|
||||
private async Task SendInviteAsync(OrganizationUser orgUser, Organization organization, bool initOrganization)
|
||||
{
|
||||
var orgUserInviteTokenable = _orgUserInviteTokenableFactory.CreateToken(orgUser);
|
||||
var protectedToken = _orgUserInviteTokenDataFactory.Protect(orgUserInviteTokenable);
|
||||
await _mailService.SendOrganizationInviteEmailAsync(
|
||||
organization.Name,
|
||||
orgUser,
|
||||
new ExpiringToken(protectedToken, orgUserInviteTokenable.ExpirationDate),
|
||||
organization.PlanType == PlanType.Free,
|
||||
orgUserHasExistingUserDict,
|
||||
initOrganization
|
||||
);
|
||||
}
|
||||
@ -1964,6 +2009,11 @@ public class OrganizationService : IOrganizationService
|
||||
{
|
||||
throw new BadRequestException("Custom users can only grant the same custom permissions that they have.");
|
||||
}
|
||||
|
||||
if (FlexibleCollectionsIsEnabled && newType == OrganizationUserType.Manager && oldType is not OrganizationUserType.Manager)
|
||||
{
|
||||
throw new BadRequestException("Manager role is deprecated after Flexible Collections.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidateOrganizationCustomPermissionsEnabledAsync(Guid organizationId, OrganizationUserType newType)
|
||||
|
@ -2,6 +2,17 @@
|
||||
|
||||
public enum WebAuthnLoginAssertionOptionsScope
|
||||
{
|
||||
/*
|
||||
Authentication is used when a user is trying to login in with a credential.
|
||||
*/
|
||||
Authentication = 0,
|
||||
PrfRegistration = 1
|
||||
/*
|
||||
PrfRegistration is used when a user is trying to register a new credential.
|
||||
*/
|
||||
PrfRegistration = 1,
|
||||
/*
|
||||
UpdateKeySet is used when a user is enabling a credential for passwordless login
|
||||
This is done by adding rotatable keys to the credential.
|
||||
*/
|
||||
UpdateKeySet = 2
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ public class RotateUserKeyData
|
||||
public string PrivateKey { get; set; }
|
||||
public IEnumerable<Cipher> Ciphers { get; set; }
|
||||
public IEnumerable<Folder> Folders { get; set; }
|
||||
public IEnumerable<Send> Sends { get; set; }
|
||||
public IEnumerable<EmergencyAccess> EmergencyAccessKeys { get; set; }
|
||||
public IEnumerable<OrganizationUser> ResetPasswordKeys { get; set; }
|
||||
public IReadOnlyList<Send> Sends { get; set; }
|
||||
public IEnumerable<EmergencyAccess> EmergencyAccesses { get; set; }
|
||||
public IReadOnlyList<OrganizationUser> OrganizationUsers { get; set; }
|
||||
}
|
||||
|
@ -7,4 +7,5 @@ public interface IWebAuthnCredentialRepository : IRepository<WebAuthnCredential,
|
||||
{
|
||||
Task<WebAuthnCredential> GetByIdAsync(Guid id, Guid userId);
|
||||
Task<ICollection<WebAuthnCredential>> GetManyByUserIdAsync(Guid userId);
|
||||
Task<bool> UpdateAsync(WebAuthnCredential credential);
|
||||
}
|
||||
|
@ -5,6 +5,9 @@ using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.UserKey;
|
||||
|
||||
/// <summary>
|
||||
/// Responsible for rotation of a user key and updating database with re-encrypted data
|
||||
/// </summary>
|
||||
public interface IRotateUserKeyCommand
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -2,31 +2,48 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.UserKey.Implementations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class RotateUserKeyCommand : IRotateUserKeyCommand
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly ICipherRepository _cipherRepository;
|
||||
private readonly IFolderRepository _folderRepository;
|
||||
private readonly ISendRepository _sendRepository;
|
||||
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IPushNotificationService _pushService;
|
||||
private readonly IdentityErrorDescriber _identityErrorDescriber;
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates a new <see cref="RotateUserKeyCommand"/>
|
||||
/// </summary>
|
||||
/// <param name="userService">Master password hash validation</param>
|
||||
/// <param name="userRepository">Updates user keys and re-encrypted data if needed</param>
|
||||
/// <param name="cipherRepository">Provides a method to update re-encrypted cipher data</param>
|
||||
/// <param name="folderRepository">Provides a method to update re-encrypted folder data</param>
|
||||
/// <param name="sendRepository">Provides a method to update re-encrypted send data</param>
|
||||
/// <param name="emergencyAccessRepository">Provides a method to update re-encrypted emergency access data</param>
|
||||
/// <param name="pushService">Logs out user from other devices after successful rotation</param>
|
||||
/// <param name="errors">Provides a password mismatch error if master password hash validation fails</param>
|
||||
public RotateUserKeyCommand(IUserService userService, IUserRepository userRepository,
|
||||
ICipherRepository cipherRepository, IFolderRepository folderRepository,
|
||||
IEmergencyAccessRepository emergencyAccessRepository,
|
||||
ICipherRepository cipherRepository, IFolderRepository folderRepository, ISendRepository sendRepository,
|
||||
IEmergencyAccessRepository emergencyAccessRepository, IOrganizationUserRepository organizationUserRepository,
|
||||
IPushNotificationService pushService, IdentityErrorDescriber errors)
|
||||
{
|
||||
_userService = userService;
|
||||
_userRepository = userRepository;
|
||||
_cipherRepository = cipherRepository;
|
||||
_folderRepository = folderRepository;
|
||||
_sendRepository = sendRepository;
|
||||
_emergencyAccessRepository = emergencyAccessRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_pushService = pushService;
|
||||
_identityErrorDescriber = errors;
|
||||
}
|
||||
@ -50,8 +67,8 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand
|
||||
user.SecurityStamp = Guid.NewGuid().ToString();
|
||||
user.Key = model.Key;
|
||||
user.PrivateKey = model.PrivateKey;
|
||||
if (model.Ciphers.Any() || model.Folders.Any() || model.Sends.Any() || model.EmergencyAccessKeys.Any() ||
|
||||
model.ResetPasswordKeys.Any())
|
||||
if (model.Ciphers.Any() || model.Folders.Any() || model.Sends.Any() || model.EmergencyAccesses.Any() ||
|
||||
model.OrganizationUsers.Any())
|
||||
{
|
||||
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = new();
|
||||
|
||||
@ -64,10 +81,22 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand
|
||||
{
|
||||
saveEncryptedDataActions.Add(_folderRepository.UpdateForKeyRotation(user.Id, model.Folders));
|
||||
}
|
||||
if (model.EmergencyAccessKeys.Any())
|
||||
|
||||
if (model.Sends.Any())
|
||||
{
|
||||
saveEncryptedDataActions.Add(_sendRepository.UpdateForKeyRotation(user.Id, model.Sends));
|
||||
}
|
||||
|
||||
if (model.EmergencyAccesses.Any())
|
||||
{
|
||||
saveEncryptedDataActions.Add(
|
||||
_emergencyAccessRepository.UpdateForKeyRotation(user.Id, model.EmergencyAccessKeys));
|
||||
_emergencyAccessRepository.UpdateForKeyRotation(user.Id, model.EmergencyAccesses));
|
||||
}
|
||||
|
||||
if (model.OrganizationUsers.Any())
|
||||
{
|
||||
saveEncryptedDataActions.Add(
|
||||
_organizationUserRepository.UpdateForKeyRotation(user.Id, model.OrganizationUsers));
|
||||
}
|
||||
|
||||
await _userRepository.UpdateUserKeyAndEncryptedDataAsync(user, saveEncryptedDataActions);
|
||||
|
@ -1,7 +1,11 @@
|
||||
|
||||
|
||||
using Bit.Core.Auth.UserFeatures.UserKey;
|
||||
using Bit.Core.Auth.UserFeatures.UserKey.Implementations;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -14,6 +18,12 @@ public static class UserServiceCollectionExtensions
|
||||
{
|
||||
services.AddScoped<IUserService, UserService>();
|
||||
services.AddUserPasswordCommands();
|
||||
services.AddWebAuthnLoginCommands();
|
||||
}
|
||||
|
||||
public static void AddUserKeyCommands(this IServiceCollection services, IGlobalSettings globalSettings)
|
||||
{
|
||||
services.AddScoped<IRotateUserKeyCommand, RotateUserKeyCommand>();
|
||||
}
|
||||
|
||||
private static void AddUserPasswordCommands(this IServiceCollection services)
|
||||
@ -21,4 +31,11 @@ public static class UserServiceCollectionExtensions
|
||||
services.AddScoped<ISetInitialMasterPasswordCommand, SetInitialMasterPasswordCommand>();
|
||||
}
|
||||
|
||||
private static void AddWebAuthnLoginCommands(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IGetWebAuthnLoginCredentialCreateOptionsCommand, GetWebAuthnLoginCredentialCreateOptionsCommand>();
|
||||
services.AddScoped<ICreateWebAuthnLoginCredentialCommand, CreateWebAuthnLoginCredentialCommand>();
|
||||
services.AddScoped<IGetWebAuthnLoginCredentialAssertionOptionsCommand, GetWebAuthnLoginCredentialAssertionOptionsCommand>();
|
||||
services.AddScoped<IAssertWebAuthnLoginCredentialCommand, AssertWebAuthnLoginCredentialCommand>();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,10 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Fido2NetLib;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||
|
||||
public interface IAssertWebAuthnLoginCredentialCommand
|
||||
{
|
||||
public Task<(User, WebAuthnCredential)> AssertWebAuthnLoginCredential(AssertionOptions options, AuthenticatorAssertionRawResponse assertionResponse);
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using Bit.Core.Entities;
|
||||
using Fido2NetLib;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||
|
||||
public interface ICreateWebAuthnLoginCredentialCommand
|
||||
{
|
||||
public Task<bool> CreateWebAuthnLoginCredentialAsync(User user, string name, CredentialCreateOptions options, AuthenticatorAttestationRawResponse attestationResponse, bool supportsPrf, string encryptedUserKey = null, string encryptedPublicKey = null, string encryptedPrivateKey = null);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Fido2NetLib;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||
|
||||
public interface IGetWebAuthnLoginCredentialAssertionOptionsCommand
|
||||
{
|
||||
public AssertionOptions GetWebAuthnLoginCredentialAssertionOptions();
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
using Bit.Core.Entities;
|
||||
using Fido2NetLib;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||
|
||||
/// <summary>
|
||||
/// Get the options required to create a Passkey for login.
|
||||
/// </summary>
|
||||
public interface IGetWebAuthnLoginCredentialCreateOptionsCommand
|
||||
{
|
||||
public Task<CredentialCreateOptions> GetWebAuthnLoginCredentialCreateOptionsAsync(User user);
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.Utilities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Fido2NetLib;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations;
|
||||
|
||||
internal class AssertWebAuthnLoginCredentialCommand : IAssertWebAuthnLoginCredentialCommand
|
||||
{
|
||||
private readonly IFido2 _fido2;
|
||||
private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public AssertWebAuthnLoginCredentialCommand(IFido2 fido2, IWebAuthnCredentialRepository webAuthnCredentialRepository, IUserRepository userRepository)
|
||||
{
|
||||
_fido2 = fido2;
|
||||
_webAuthnCredentialRepository = webAuthnCredentialRepository;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task<(User, WebAuthnCredential)> AssertWebAuthnLoginCredential(AssertionOptions options, AuthenticatorAssertionRawResponse assertionResponse)
|
||||
{
|
||||
if (!GuidUtilities.TryParseBytes(assertionResponse.Response.UserHandle, out var userId))
|
||||
{
|
||||
throw new BadRequestException("Invalid credential.");
|
||||
}
|
||||
|
||||
var user = await _userRepository.GetByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
throw new BadRequestException("Invalid credential.");
|
||||
}
|
||||
|
||||
var userCredentials = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id);
|
||||
var assertedCredentialId = CoreHelpers.Base64UrlEncode(assertionResponse.Id);
|
||||
var credential = userCredentials.FirstOrDefault(c => c.CredentialId == assertedCredentialId);
|
||||
if (credential == null)
|
||||
{
|
||||
throw new BadRequestException("Invalid credential.");
|
||||
}
|
||||
|
||||
// Always return true, since we've already filtered the credentials after user id
|
||||
IsUserHandleOwnerOfCredentialIdAsync callback = (args, cancellationToken) => Task.FromResult(true);
|
||||
var credentialPublicKey = CoreHelpers.Base64UrlDecode(credential.PublicKey);
|
||||
var assertionVerificationResult = await _fido2.MakeAssertionAsync(
|
||||
assertionResponse, options, credentialPublicKey, (uint)credential.Counter, callback);
|
||||
|
||||
// Update SignatureCounter
|
||||
credential.Counter = (int)assertionVerificationResult.Counter;
|
||||
await _webAuthnCredentialRepository.ReplaceAsync(credential);
|
||||
|
||||
if (assertionVerificationResult.Status != "ok")
|
||||
{
|
||||
throw new BadRequestException("Invalid credential.");
|
||||
}
|
||||
|
||||
return (user, credential);
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
using Fido2NetLib;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations;
|
||||
|
||||
internal class CreateWebAuthnLoginCredentialCommand : ICreateWebAuthnLoginCredentialCommand
|
||||
{
|
||||
public const int MaxCredentialsPerUser = 5;
|
||||
|
||||
private readonly IFido2 _fido2;
|
||||
private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository;
|
||||
|
||||
public CreateWebAuthnLoginCredentialCommand(IFido2 fido2, IWebAuthnCredentialRepository webAuthnCredentialRepository)
|
||||
{
|
||||
_fido2 = fido2;
|
||||
_webAuthnCredentialRepository = webAuthnCredentialRepository;
|
||||
}
|
||||
|
||||
public async Task<bool> CreateWebAuthnLoginCredentialAsync(User user, string name, CredentialCreateOptions options, AuthenticatorAttestationRawResponse attestationResponse, bool supportsPrf, string encryptedUserKey = null, string encryptedPublicKey = null, string encryptedPrivateKey = null)
|
||||
{
|
||||
var existingCredentials = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id);
|
||||
if (existingCredentials.Count >= MaxCredentialsPerUser)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var existingCredentialIds = existingCredentials.Select(c => c.CredentialId);
|
||||
IsCredentialIdUniqueToUserAsyncDelegate callback = (args, cancellationToken) => Task.FromResult(!existingCredentialIds.Contains(CoreHelpers.Base64UrlEncode(args.CredentialId)));
|
||||
|
||||
var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback);
|
||||
|
||||
var credential = new WebAuthnCredential
|
||||
{
|
||||
Name = name,
|
||||
CredentialId = CoreHelpers.Base64UrlEncode(success.Result.CredentialId),
|
||||
PublicKey = CoreHelpers.Base64UrlEncode(success.Result.PublicKey),
|
||||
Type = success.Result.CredType,
|
||||
AaGuid = success.Result.Aaguid,
|
||||
Counter = (int)success.Result.Counter,
|
||||
UserId = user.Id,
|
||||
SupportsPrf = supportsPrf,
|
||||
EncryptedUserKey = encryptedUserKey,
|
||||
EncryptedPublicKey = encryptedPublicKey,
|
||||
EncryptedPrivateKey = encryptedPrivateKey
|
||||
};
|
||||
|
||||
await _webAuthnCredentialRepository.CreateAsync(credential);
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
using Fido2NetLib;
|
||||
using Fido2NetLib.Objects;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations;
|
||||
|
||||
internal class GetWebAuthnLoginCredentialAssertionOptionsCommand : IGetWebAuthnLoginCredentialAssertionOptionsCommand
|
||||
{
|
||||
private readonly IFido2 _fido2;
|
||||
|
||||
public GetWebAuthnLoginCredentialAssertionOptionsCommand(IFido2 fido2)
|
||||
{
|
||||
_fido2 = fido2;
|
||||
}
|
||||
|
||||
public AssertionOptions GetWebAuthnLoginCredentialAssertionOptions()
|
||||
{
|
||||
return _fido2.GetAssertionOptions(Enumerable.Empty<PublicKeyCredentialDescriptor>(), UserVerificationRequirement.Required);
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
using Fido2NetLib;
|
||||
using Fido2NetLib.Objects;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations;
|
||||
|
||||
internal class GetWebAuthnLoginCredentialCreateOptionsCommand : IGetWebAuthnLoginCredentialCreateOptionsCommand
|
||||
{
|
||||
private readonly IFido2 _fido2;
|
||||
private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository;
|
||||
|
||||
public GetWebAuthnLoginCredentialCreateOptionsCommand(IFido2 fido2, IWebAuthnCredentialRepository webAuthnCredentialRepository)
|
||||
{
|
||||
_fido2 = fido2;
|
||||
_webAuthnCredentialRepository = webAuthnCredentialRepository;
|
||||
}
|
||||
|
||||
public async Task<CredentialCreateOptions> GetWebAuthnLoginCredentialCreateOptionsAsync(User user)
|
||||
{
|
||||
var fidoUser = new Fido2User
|
||||
{
|
||||
DisplayName = user.Name,
|
||||
Name = user.Email,
|
||||
Id = user.Id.ToByteArray(),
|
||||
};
|
||||
|
||||
// Get existing keys to exclude
|
||||
var existingKeys = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id);
|
||||
var excludeCredentials = existingKeys
|
||||
.Select(k => new PublicKeyCredentialDescriptor(CoreHelpers.Base64UrlDecode(k.CredentialId)))
|
||||
.ToList();
|
||||
|
||||
var authenticatorSelection = new AuthenticatorSelection
|
||||
{
|
||||
AuthenticatorAttachment = null,
|
||||
RequireResidentKey = true,
|
||||
UserVerification = UserVerificationRequirement.Required
|
||||
};
|
||||
|
||||
var extensions = new AuthenticationExtensionsClientInputs { };
|
||||
|
||||
var options = _fido2.RequestNewCredential(fidoUser, excludeCredentials, authenticatorSelection,
|
||||
AttestationConveyancePreference.None, extensions);
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
@ -5,9 +5,11 @@ using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@ -18,11 +20,14 @@ public class CurrentContext : ICurrentContext
|
||||
{
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IFeatureService _featureService;
|
||||
private bool _builtHttpContext;
|
||||
private bool _builtClaimsPrincipal;
|
||||
private IEnumerable<ProviderOrganizationProviderDetails> _providerOrganizationProviderDetails;
|
||||
private IEnumerable<ProviderUserOrganizationDetails> _providerUserOrganizations;
|
||||
|
||||
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, this);
|
||||
|
||||
public virtual HttpContext HttpContext { get; set; }
|
||||
public virtual Guid? UserId { get; set; }
|
||||
public virtual User User { get; set; }
|
||||
@ -44,10 +49,12 @@ public class CurrentContext : ICurrentContext
|
||||
|
||||
public CurrentContext(
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IProviderUserRepository providerUserRepository)
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
public async virtual Task BuildAsync(HttpContext httpContext, GlobalSettings globalSettings)
|
||||
@ -276,6 +283,11 @@ public class CurrentContext : ICurrentContext
|
||||
|
||||
public async Task<bool> OrganizationManager(Guid orgId)
|
||||
{
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
{
|
||||
throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF.");
|
||||
}
|
||||
|
||||
return await OrganizationAdmin(orgId) ||
|
||||
(Organizations?.Any(o => o.Id == orgId && o.Type == OrganizationUserType.Manager) ?? false);
|
||||
}
|
||||
@ -338,12 +350,22 @@ public class CurrentContext : ICurrentContext
|
||||
|
||||
public async Task<bool> EditAssignedCollections(Guid orgId)
|
||||
{
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
{
|
||||
throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF.");
|
||||
}
|
||||
|
||||
return await OrganizationManager(orgId) || (Organizations?.Any(o => o.Id == orgId
|
||||
&& (o.Permissions?.EditAssignedCollections ?? false)) ?? false);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAssignedCollections(Guid orgId)
|
||||
{
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
{
|
||||
throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF.");
|
||||
}
|
||||
|
||||
return await OrganizationManager(orgId) || (Organizations?.Any(o => o.Id == orgId
|
||||
&& (o.Permissions?.DeleteAssignedCollections ?? false)) ?? false);
|
||||
}
|
||||
@ -355,6 +377,12 @@ public class CurrentContext : ICurrentContext
|
||||
* Owner, Admin, Manager, and Provider checks are handled via the EditAssigned/DeleteAssigned context calls.
|
||||
* This entire method will be moved to the CollectionAuthorizationHandler in the future
|
||||
*/
|
||||
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
{
|
||||
throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF.");
|
||||
}
|
||||
|
||||
var canCreateNewCollections = false;
|
||||
var org = GetOrganization(orgId);
|
||||
if (org != null)
|
||||
|
@ -36,6 +36,7 @@ public interface ICurrentContext
|
||||
|
||||
|
||||
Task<bool> OrganizationUser(Guid orgId);
|
||||
[Obsolete("Manager role is deprecated after Flexible Collections.")]
|
||||
Task<bool> OrganizationManager(Guid orgId);
|
||||
Task<bool> OrganizationAdmin(Guid orgId);
|
||||
Task<bool> OrganizationOwner(Guid orgId);
|
||||
@ -45,8 +46,11 @@ public interface ICurrentContext
|
||||
Task<bool> AccessReports(Guid orgId);
|
||||
Task<bool> EditAnyCollection(Guid orgId);
|
||||
Task<bool> ViewAllCollections(Guid orgId);
|
||||
[Obsolete("Pre-Flexible Collections logic.")]
|
||||
Task<bool> EditAssignedCollections(Guid orgId);
|
||||
[Obsolete("Pre-Flexible Collections logic.")]
|
||||
Task<bool> DeleteAssignedCollections(Guid orgId);
|
||||
[Obsolete("Pre-Flexible Collections logic.")]
|
||||
Task<bool> ViewAssignedCollections(Guid orgId);
|
||||
Task<bool> ManageGroups(Guid orgId);
|
||||
Task<bool> ManagePolicies(Guid orgId);
|
||||
|
@ -31,8 +31,8 @@
|
||||
<PackageReference Include="DnsClient" Version="1.7.0" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
|
||||
<PackageReference Include="Handlebars.Net" Version="2.1.4" />
|
||||
<PackageReference Include="MailKit" Version="4.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.4" />
|
||||
<PackageReference Include="MailKit" Version="4.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.25" />
|
||||
<PackageReference Include="Microsoft.Azure.Cosmos.Table" Version="1.0.8" />
|
||||
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.1.0" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.0.1" />
|
||||
@ -40,23 +40,23 @@
|
||||
<PackageReference Include="Azure.Identity" Version="1.10.2"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="6.0.25" />
|
||||
<PackageReference Include="Quartz" Version="3.4.0" />
|
||||
<PackageReference Include="SendGrid" Version="9.27.0" />
|
||||
<PackageReference Include="SendGrid" Version="9.28.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="5.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging.File" Version="2.0.0" />
|
||||
<PackageReference Include="Sentry.Serilog" Version="3.16.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging.File" Version="3.0.0" />
|
||||
<PackageReference Include="Sentry.Serilog" Version="3.41.3" />
|
||||
<PackageReference Include="Duende.IdentityServer" Version="6.0.4" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog.Sinks.AzureCosmosDB" Version="2.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="2.0.9" />
|
||||
<PackageReference Include="AspNetCoreRateLimit" Version="4.0.2" />
|
||||
<PackageReference Include="Braintree" Version="5.19.0" />
|
||||
<PackageReference Include="Stripe.net" Version="40.0.0" />
|
||||
<PackageReference Include="Braintree" Version="5.21.0" />
|
||||
<PackageReference Include="Stripe.net" Version="40.16.0" />
|
||||
<PackageReference Include="Otp.NET" Version="1.2.2" />
|
||||
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="6.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="6.0.25" />
|
||||
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -52,6 +52,7 @@ public class OrganizationLicense : ILicense
|
||||
UseSecretsManager = org.UseSecretsManager;
|
||||
SmSeats = org.SmSeats;
|
||||
SmServiceAccounts = org.SmServiceAccounts;
|
||||
LimitCollectionCreationDeletion = org.LimitCollectionCreationDeletion;
|
||||
|
||||
if (subscriptionInfo?.Subscription == null)
|
||||
{
|
||||
@ -135,6 +136,7 @@ public class OrganizationLicense : ILicense
|
||||
public bool UseSecretsManager { get; set; }
|
||||
public int? SmSeats { get; set; }
|
||||
public int? SmServiceAccounts { get; set; }
|
||||
public bool LimitCollectionCreationDeletion { get; set; } = true;
|
||||
public bool Trial { get; set; }
|
||||
public LicenseType? LicenseType { get; set; }
|
||||
public string Hash { get; set; }
|
||||
@ -146,11 +148,10 @@ public class OrganizationLicense : ILicense
|
||||
/// </summary>
|
||||
/// <remarks>Intentionally set one version behind to allow self hosted users some time to update before
|
||||
/// getting out of date license errors</remarks>
|
||||
public const int CurrentLicenseFileVersion = 12;
|
||||
|
||||
public const int CurrentLicenseFileVersion = 13;
|
||||
private bool ValidLicenseVersion
|
||||
{
|
||||
get => Version is >= 1 and <= 13;
|
||||
get => Version is >= 1 and <= 14;
|
||||
}
|
||||
|
||||
public byte[] GetDataBytes(bool forHash = false)
|
||||
@ -191,6 +192,8 @@ public class OrganizationLicense : ILicense
|
||||
(Version >= 13 || !p.Name.Equals(nameof(UsePasswordManager))) &&
|
||||
(Version >= 13 || !p.Name.Equals(nameof(SmSeats))) &&
|
||||
(Version >= 13 || !p.Name.Equals(nameof(SmServiceAccounts))) &&
|
||||
// LimitCollectionCreationDeletion was added in Version 14
|
||||
(Version >= 14 || !p.Name.Equals(nameof(LimitCollectionCreationDeletion))) &&
|
||||
(
|
||||
!forHash ||
|
||||
(
|
||||
@ -338,6 +341,13 @@ public class OrganizationLicense : ILicense
|
||||
organization.SmServiceAccounts == SmServiceAccounts;
|
||||
}
|
||||
|
||||
// Restore validity check when Flexible Collections are enabled for cloud and self-host
|
||||
// https://bitwarden.atlassian.net/browse/AC-1875
|
||||
// if (valid && Version >= 14)
|
||||
// {
|
||||
// valid = organization.LimitCollectionCreationDeletion == LimitCollectionCreationDeletion;
|
||||
// }
|
||||
|
||||
return valid;
|
||||
}
|
||||
else
|
||||
|
41
src/Core/Models/Mail/OrganizationInvitesInfo.cs
Normal file
41
src/Core/Models/Mail/OrganizationInvitesInfo.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Models.Business;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Mail;
|
||||
public class OrganizationInvitesInfo
|
||||
{
|
||||
public OrganizationInvitesInfo(
|
||||
Organization org,
|
||||
bool orgSsoEnabled,
|
||||
bool orgSsoLoginRequiredPolicyEnabled,
|
||||
IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> orgUserTokenPairs,
|
||||
Dictionary<Guid, bool> orgUserHasExistingUserDict,
|
||||
bool initOrganization = false
|
||||
)
|
||||
{
|
||||
OrganizationName = org.Name;
|
||||
OrgSsoIdentifier = org.Identifier;
|
||||
|
||||
IsFreeOrg = org.PlanType == PlanType.Free;
|
||||
InitOrganization = initOrganization;
|
||||
|
||||
OrgSsoEnabled = orgSsoEnabled;
|
||||
OrgSsoLoginRequiredPolicyEnabled = orgSsoLoginRequiredPolicyEnabled;
|
||||
|
||||
OrgUserTokenPairs = orgUserTokenPairs;
|
||||
OrgUserHasExistingUserDict = orgUserHasExistingUserDict;
|
||||
}
|
||||
|
||||
public string OrganizationName { get; }
|
||||
public bool IsFreeOrg { get; }
|
||||
public bool InitOrganization { get; } = false;
|
||||
public bool OrgSsoEnabled { get; }
|
||||
public string OrgSsoIdentifier { get; }
|
||||
public bool OrgSsoLoginRequiredPolicyEnabled { get; }
|
||||
|
||||
public IEnumerable<(OrganizationUser OrgUser, ExpiringToken Token)> OrgUserTokenPairs { get; }
|
||||
public Dictionary<Guid, bool> OrgUserHasExistingUserDict { get; }
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user