1
0
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:
Rui Tomé 2023-12-18 20:53:10 +00:00 committed by GitHub
commit 69c0997f6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
214 changed files with 15561 additions and 107980 deletions

View File

@ -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"

View File

@ -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"
}
}
}

View File

@ -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
View File

@ -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

View File

@ -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"]
},
{

View 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 }}

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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"

View File

@ -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,

View File

@ -27,4 +27,4 @@ jobs:
If youre still working on this, please respond here after youve made the changes weve 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.

View File

@ -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
View File

@ -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
View File

@ -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
}
]
}

View File

@ -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>

View File

@ -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

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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

View File

@ -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())
{

View File

@ -23,6 +23,7 @@
},
"storage": {
"connectionString": "UseDevelopmentStorage=true"
}
},
"developmentDirectory": "../../../dev"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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
View File

@ -15,5 +15,7 @@ data_protection_dev.crt
data_protection_dev.key
data_protection_dev.pfx
signingkey.jwk
# Reverse Proxy Conifg
reverse-proxy.conf

View File

@ -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}"

View File

@ -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}"

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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

View File

@ -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"],
},
};

View File

@ -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"],
},
};

View File

@ -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

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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);

View File

@ -118,3 +118,9 @@ public class OrganizationUserBulkRequestModel
[Required]
public IEnumerable<Guid> Ids { get; set; }
}
public class ResetPasswordWithOrgIdRequestModel : OrganizationUserResetPasswordEnrollmentRequestModel
{
[Required]
public Guid OrganizationId { get; set; }
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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)
{

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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
};
}
}

View File

@ -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

View File

@ -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)
{
}

View File

@ -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);

View 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;
}
}

View 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 });
}
}

View File

@ -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>();
}
}

View File

@ -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));
}
}

View File

@ -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) };
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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; }

View File

@ -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);
}

View File

@ -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)

View File

@ -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
}

View File

@ -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; }
}

View File

@ -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);
}

View File

@ -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>

View File

@ -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);

View File

@ -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>();
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -0,0 +1,8 @@
using Fido2NetLib;
namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin;
public interface IGetWebAuthnLoginCredentialAssertionOptionsCommand
{
public AssertionOptions GetWebAuthnLoginCredentialAssertionOptions();
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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)

View File

@ -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);

View File

@ -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>

View File

@ -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

View 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