1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-05 21:18:13 -05:00

Merge branch 'main' into add-docker-arm64-builds

This commit is contained in:
Vince Grassia 2024-05-08 14:02:47 -04:00
commit 31d0480316
No known key found for this signature in database
GPG Key ID: 9AD7505E8448CC08
1241 changed files with 134089 additions and 122241 deletions

13
.checkmarx/config.yml Normal file
View File

@ -0,0 +1,13 @@
version: 1
# Checkmarx configuration file
#
# https://checkmarx.com/resource/documents/en/34965-68549-configuring-projects-using-config-as-code-files.html
checkmarx:
scan:
configs:
sast:
# Exclude test directory
filter: "!test"
kics:
filter: "!dev,!.devcontainer"

View File

@ -4,27 +4,11 @@
"tools": {
"swashbuckle.aspnetcore.cli": {
"version": "6.5.0",
"commands": [
"swagger"
]
},
"coverlet.console": {
"version": "3.1.2",
"commands": [
"coverlet"
]
},
"dotnet-reportgenerator-globaltool": {
"version": "5.1.6",
"commands": [
"reportgenerator"
]
"commands": ["swagger"]
},
"dotnet-ef": {
"version": "7.0.8",
"commands": [
"dotnet-ef"
]
"version": "8.0.2",
"commands": ["dotnet-ef"]
}
}
}
}

View File

@ -2,7 +2,7 @@ version: '3'
services:
bitwarden_server:
image: mcr.microsoft.com/devcontainers/dotnet:dev-6.0
image: mcr.microsoft.com/devcontainers/dotnet:8.0
volumes:
- ../../:/workspace:cached
# Overrides default command so things don't shut down after the process ends.

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"

20
.github/CODEOWNERS vendored
View File

@ -1,11 +1,9 @@
# Please sort lines alphabetically, this will ensure we don't accidentally add duplicates.
# Please sort into logical groups with comment headers. Sort groups in order of specificity.
# For example, default owners should always be the first group.
# Sort lines alphabetically within these groups to avoid accidentally adding duplicates.
#
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# The following owners will be the default owners for everything in the repo
# unless a later match takes precedence
* @bitwarden/tech-leads
# DevOps for Actions and other workflow changes
.github/workflows @bitwarden/dept-devops
@ -16,7 +14,12 @@
# Database Operations for database changes
src/Sql/** @bitwarden/dept-dbops
util/EfShared/** @bitwarden/dept-dbops
util/Migrator/** @bitwarden/dept-dbops
util/MySqlMigrations/** @bitwarden/dept-dbops
util/PostgresMigrations/** @bitwarden/dept-dbops
util/SqlServerEFScaffold/** @bitwarden/dept-dbops
util/SqliteMigrations/** @bitwarden/dept-dbops
# Auth team
**/Auth @bitwarden/team-auth-dev
@ -45,8 +48,13 @@ bitwarden_license/src/test/Scim.ScimTest @bitwarden/team-admin-console-dev
**/*paypal* @bitwarden/team-billing-dev
**/*stripe* @bitwarden/team-billing-dev
**/*subscription* @bitwarden/team-billing-dev
**/*payment* @bitwarden/team-billing-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
# Multiple owners - DO NOT REMOVE (DevOps)
**/packages.lock.json
Directory.Build.props

3
.github/codecov.yml vendored Normal file
View File

@ -0,0 +1,3 @@
ignore:
- "test" # Tests
- "util" # Utils (migrators)

103
.github/renovate.json vendored
View File

@ -1,17 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base",
":combinePatchMinorReleases",
":dependencyDashboard",
":maintainLockFilesWeekly",
":pinAllExceptPeerDependencies",
":prConcurrentLimit10",
":rebaseStalePrs",
":separateMajorReleases",
"group:monorepos",
"schedule:weekends"
],
"extends": ["github>bitwarden/renovate-config"],
"enabledManagers": [
"dockerfile",
"docker-compose",
@ -19,8 +8,6 @@
"npm",
"nuget"
],
"commitMessagePrefix": "[deps]:",
"commitMessageTopic": "{{depName}}",
"packageRules": [
{
"groupName": "dockerfile minor",
@ -37,6 +24,10 @@
"matchManagers": ["github-actions"],
"matchUpdateTypes": ["minor", "patch"]
},
{
"matchManagers": ["github-actions", "dockerfile", "docker-compose"],
"commitMessagePrefix": "[deps] DevOps:"
},
{
"matchPackageNames": ["DnsClient", "Quartz"],
"description": "Admin Console owned dependencies",
@ -46,22 +37,29 @@
{
"matchFileNames": ["src/Admin/package.json", "src/Sso/package.json"],
"description": "Admin & SSO npm packages",
"commitMessagePrefix": "[deps] Auth:",
"reviewers": ["team:team-auth-dev"]
},
{
"matchPackageNames": ["bootstrap", "del", "gulp"],
"matchUpdateTypes": ["major"],
"description": "Lock bootstrap, del, and gulp major versions due to ASP.NET conflicts",
"enabled": false
},
{
"matchPackageNames": [
"AspNetCoreRateLimit",
"AspNetCoreRateLimit.Redis",
"Azure.Data.Tables",
"Azure.Extensions.AspNetCore.DataProtection.Blobs",
"Azure.Messaging.EventGrid",
"Azure.Messaging.ServiceBus",
"Azure.Storage.Blobs",
"Azure.Storage.Queues",
"DuoUniversal",
"Fido2.AspNet",
"IdentityServer4",
"IdentityServer4.AccessTokenValidation",
"Duende.IdentityServer",
"Microsoft.Azure.Cosmos",
"Microsoft.Azure.Cosmos.Table",
"Microsoft.Extensions.Caching.StackExchangeRedis",
"Microsoft.Extensions.Identity.Stores",
"Otp.NET",
@ -104,25 +102,17 @@
"reviewers": ["team:team-billing-dev"]
},
{
"matchPackageNames": ["CommandDotNet", "dbup-sqlserver", "YamlDotNet"],
"description": "DevOps owned dependencies",
"commitMessagePrefix": "[deps] DevOps:",
"reviewers": ["team:team-devops"]
},
{
"matchPackageNames": [
"Microsoft.AspNetCore.Authentication.JwtBearer",
"Microsoft.AspNetCore.Http",
"Microsoft.Data.SqlClient"
],
"description": "Platform owned dependencies",
"commitMessagePrefix": "[deps] Platform:",
"reviewers": ["team:team-platform-dev"]
"matchPackagePatterns": ["^Microsoft.Extensions.Logging"],
"groupName": "Microsoft.Extensions.Logging",
"description": "Group Microsoft.Extensions.Logging to exclude them from the dotnet monorepo preset"
},
{
"matchPackageNames": [
"Dapper",
"dbup-sqlserver",
"dotnet-ef",
"linq2db.EntityFrameworkCore",
"Microsoft.Data.SqlClient",
"Microsoft.EntityFrameworkCore.Design",
"Microsoft.EntityFrameworkCore.InMemory",
"Microsoft.EntityFrameworkCore.Relational",
@ -131,9 +121,29 @@
"Npgsql.EntityFrameworkCore.PostgreSQL",
"Pomelo.EntityFrameworkCore.MySql"
],
"description": "Secrets Manager owned dependencies",
"commitMessagePrefix": "[deps] SM:",
"reviewers": ["team:team-secrets-manager-dev"]
"description": "DbOps owned dependencies",
"commitMessagePrefix": "[deps] DbOps:",
"reviewers": ["team:dept-dbops"]
},
{
"matchPackageNames": ["CommandDotNet", "YamlDotNet"],
"description": "DevOps owned dependencies",
"commitMessagePrefix": "[deps] DevOps:",
"reviewers": ["team:dept-devops"]
},
{
"matchPackageNames": [
"Microsoft.AspNetCore.Authentication.JwtBearer",
"Microsoft.AspNetCore.Http"
],
"description": "Platform owned dependencies",
"commitMessagePrefix": "[deps] Platform:",
"reviewers": ["team:team-platform-dev"]
},
{
"matchPackagePatterns": ["EntityFrameworkCore", "^dotnet-ef"],
"groupName": "EntityFrameworkCore",
"description": "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset"
},
{
"matchPackageNames": [
@ -146,17 +156,32 @@
"Microsoft.AspNetCore.SignalR.Protocols.MessagePack",
"Microsoft.AspNetCore.SignalR.StackExchangeRedis",
"Microsoft.Azure.NotificationHubs",
"Microsoft.Extensions.Configuration",
"Microsoft.Extensions.Configuration.EnvironmentVariables",
"Microsoft.Extensions.Configuration.UserSecrets",
"Microsoft.Extensions.DependencyInjection",
"Microsoft.Extensions.Configuration",
"Microsoft.Extensions.DependencyInjection.Abstractions",
"Microsoft.Extensions.DependencyInjection",
"SendGrid"
],
"description": "Tools owned dependencies",
"commitMessagePrefix": "[deps] Tools:",
"reviewers": ["team:team-tools-dev"]
},
{
"matchPackagePatterns": ["^Microsoft.AspNetCore.SignalR"],
"groupName": "SignalR",
"description": "Group SignalR to exclude them from the dotnet monorepo preset"
},
{
"matchPackagePatterns": ["^Microsoft.Extensions.Configuration"],
"groupName": "Microsoft.Extensions.Configuration",
"description": "Group Microsoft.Extensions.Configuration to exclude them from the dotnet monorepo preset"
},
{
"matchPackagePatterns": ["^Microsoft.Extensions.DependencyInjection"],
"groupName": "Microsoft.Extensions.DependencyInjection",
"description": "Group Microsoft.Extensions.DependencyInjection to exclude them from the dotnet monorepo preset"
},
{
"matchPackageNames": [
"AngleSharp",
@ -173,9 +198,5 @@
"reviewers": ["team:team-vault-dev"]
}
],
"force": {
"constraints": {
"dotnet": "6.0.413"
}
}
"ignoreDeps": ["dotnet-sdk"]
}

View File

@ -1,7 +0,0 @@
{
"release": {
"head": {
"ref": "master"
}
}
}

View File

@ -0,0 +1,162 @@
---
name: _move_finalization_db_scripts
run-name: Move finalization database 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: Log in 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: Check out 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 directory
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 database 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: Log in to Azure - production 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 database 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

@ -6,8 +6,8 @@ on:
- labeled
jobs:
close-issue:
name: 'Close issue with automatic response'
runs-on: ubuntu-20.04
name: Close issue with automatic response
runs-on: ubuntu-22.04
permissions:
issues: write
steps:
@ -24,7 +24,7 @@ jobs:
This issue will now be closed. Thanks!
# Intended behavior
- if: github.event.label.name == 'intended-behavior'
name: Intended behaviour
name: Intended behavior
uses: peter-evans/close-issue@1373cadf1f0c96c1420bc000cfba2273ea307fd1 # v2.2.0
with:
comment: |

View File

@ -2,33 +2,18 @@
name: Build
on:
push:
branches-ignore:
- "l10n_master"
- "gh-pages"
paths-ignore:
- ".github/workflows/**"
workflow_dispatch:
push:
branches:
- "main"
- "rc"
- "hotfix-rc"
pull_request:
env:
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
jobs:
cloc:
name: CLOC
runs-on: ubuntu-22.04
steps:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Install cloc
run: |
sudo apt-get update
sudo apt-get -y install cloc
- name: Print lines of code
run: cloc --include-lang C#,SQL,Razor,"Bourne Shell",PowerShell,HTML,CSS,Sass,JavaScript,TypeScript --vcs git
lint:
name: Lint
runs-on: ubuntu-22.04
@ -36,67 +21,17 @@ jobs:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up dotnet
- name: Set up .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
- name: Verify Format
- name: Verify format
run: dotnet format --verify-no-changes
testing:
name: Testing
build-artifacts:
name: Build artifacts
runs-on: ubuntu-22.04
env:
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
steps:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up dotnet
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
- name: Print environment
run: |
dotnet --info
nuget help | grep Version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
- name: Restore
run: dotnet restore --locked-mode
shell: pwsh
- name: Remove SQL proj
run: dotnet sln bitwarden-server.sln remove src/Sql/Sql.sqlproj
- name: Build OSS solution
run: dotnet build bitwarden-server.sln -p:Configuration=Debug -p:DefineConstants="OSS" --verbosity minimal
shell: pwsh
- name: Build solution
run: dotnet build bitwarden-server.sln -p:Configuration=Debug --verbosity minimal
shell: pwsh
- name: Test OSS solution
run: dotnet test ./test --configuration Debug --no-build --logger "trx;LogFileName=oss-test-results.trx"
shell: pwsh
- name: Test Bitwarden solution
run: dotnet test ./bitwarden_license/test --configuration Debug --no-build --logger "trx;LogFileName=bw-test-results.trx"
shell: pwsh
- name: Report test results
uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226 # v1.6.0
if: always()
with:
name: Test Results
path: "**/*-test-results.trx"
reporter: dotnet-trx
fail-on-error: true
build:
name: Build artifacts and images
runs-on: ubuntu-22.04
needs: testing
needs:
- lint
strategy:
fail-fast: false
matrix:
@ -106,7 +41,97 @@ jobs:
upload_artifact: true
- project_name: Api
base_path: ./src
upload_artifact: true
- project_name: Billing
base_path: ./src
- project_name: Events
base_path: ./src
- project_name: EventsProcessor
base_path: ./src
- project_name: Icons
base_path: ./src
- project_name: Identity
base_path: ./src
- project_name: MsSqlMigratorUtility
base_path: ./util
dotnet: true
- project_name: Notifications
base_path: ./src
- project_name: Scim
base_path: ./bitwarden_license/src
dotnet: true
- project_name: Server
base_path: ./util
- project_name: Setup
base_path: ./util
- project_name: Sso
base_path: ./bitwarden_license/src
node: true
steps:
- name: Check out repo
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
- name: Set up .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
- name: Set up Node
uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
with:
cache: "npm"
cache-dependency-path: "**/package-lock.json"
node-version: "16"
- name: Print environment
run: |
whoami
dotnet --info
node --version
npm --version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
- name: Build node
if: ${{ matrix.node }}
working-directory: ${{ matrix.base_path }}/${{ matrix.project_name }}
run: |
npm ci
npm run build
- name: Publish project
working-directory: ${{ matrix.base_path }}/${{ matrix.project_name }}
run: |
echo "Publish"
dotnet publish -c "Release" -o obj/build-output/publish
cd obj/build-output/publish
zip -r ${{ matrix.project_name }}.zip .
mv ${{ matrix.project_name }}.zip ../../../
pwd
ls -atlh ../../../
- name: Upload project artifact
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: ${{ matrix.project_name }}.zip
path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip
if-no-files-found: error
build-docker:
name: Build Docker images
runs-on: ubuntu-22.04
permissions:
security-events: write
needs: build-artifacts
strategy:
fail-fast: false
matrix:
include:
- project_name: Admin
base_path: ./src
dotnet: true
- project_name: Api
base_path: ./src
dotnet: true
- project_name: Attachments
base_path: ./util
- project_name: Billing
@ -147,9 +172,9 @@ jobs:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Check Branch to Publish
- 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
@ -168,7 +193,7 @@ jobs:
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
########## ACRs ##########
- name: Login to Azure - PROD Subscription
- name: Log in to Azure - production subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
@ -180,13 +205,20 @@ jobs:
- name: Generate Docker image tag
id: tag
run: |
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") # slash safe branch name
if [[ "$IMAGE_TAG" == "master" ]]; then
if [[ $(grep "pull" <<< "${GITHUB_REF}") ]]; then
IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s#/#-#g")
else
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
fi
if [[ "$IMAGE_TAG" == "main" ]]; then
IMAGE_TAG=dev
fi
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
- name: Setup project name
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
echo "### :mega: Docker Image Tag: $IMAGE_TAG" >> $GITHUB_STEP_SUMMARY
- name: Set up project name
id: setup
run: |
PROJECT_NAME=$(echo "${{ matrix.project_name }}" | awk '{print tolower($0)}')
@ -194,12 +226,20 @@ jobs:
echo "PROJECT_NAME: $PROJECT_NAME"
echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT
- name: Generate image full name
id: image-name
- name: Generate image tags(s)
id: image-tags
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: |
TAGS="${_AZ_REGISTRY}/${PROJECT_NAME}:${IMAGE_TAG}"
echo "primary_tag=$TAGS" >> $GITHUB_OUTPUT
if [[ "${IMAGE_TAG}" == "dev" ]]; then
SHORT_SHA=$(git rev-parse --short ${SHA})
TAGS=$TAGS",${_AZ_REGISTRY}/${PROJECT_NAME}:dev-${SHORT_SHA}"
fi
echo "tags=$TAGS" >> $GITHUB_OUTPUT
- name: Build Docker image
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
@ -238,6 +278,19 @@ jobs:
path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip
if-no-files-found: error
- name: Scan Docker image
id: container-scan
uses: anchore/scan-action@3343887d815d7b07465f6fdcd395bd66508d486a # v3.6.4
with:
image: ${{ steps.image-tags.outputs.primary_tag }}
fail-build: false
output-format: sarif
- name: Upload Grype results to GitHub
uses: github/codeql-action/upload-sarif@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2
with:
sarif_file: ${{ steps.container-scan.outputs.sarif }}
- name: Log out of Docker
run: docker logout
@ -249,10 +302,10 @@ jobs:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up dotnet
- name: Set up .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
- name: Login to Azure - PROD Subscription
- name: Log in to Azure - production subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
@ -260,17 +313,14 @@ jobs:
- name: Login to PROD ACR
run: az acr login -n ${_AZ_REGISTRY%.azurecr.io}
- name: Restore
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")
@ -306,13 +356,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
@ -320,7 +370,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
@ -328,7 +378,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
@ -336,7 +386,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
@ -370,7 +420,7 @@ jobs:
if-no-files-found: error
build-mssqlmigratorutility:
name: Build MsSqlMigratorUtility
name: Build MSSQL migrator utility
runs-on: ubuntu-22.04
needs: testing
defaults:
@ -388,7 +438,7 @@ jobs:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up dotnet
- name: Set up .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
- name: Print environment
@ -408,7 +458,7 @@ jobs:
dotnet publish -c "Release" -o obj/build-output/publish -r ${{ matrix.target }} -p:PublishSingleFile=true \
-p:IncludeNativeLibrariesForSelfExtract=true --self-contained true
- name: Upload project artifact Windows
- name: Upload project artifact for Windows
if: ${{ contains(matrix.target, 'win') == true }}
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
@ -424,18 +474,17 @@ jobs:
path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility
if-no-files-found: error
self-host-build:
name: Trigger self-host build
runs-on: ubuntu-22.04
needs: testing
needs: build-docker
steps:
- name: Login to Azure - CI Subscription
- name: Log in to Azure - CI subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve github PAT secrets
- name: Retrieve GitHub PAT secrets
id: retrieve-secret-pat
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
@ -451,56 +500,68 @@ jobs:
owner: 'bitwarden',
repo: 'self-host',
workflow_id: 'build-unified.yml',
ref: 'master',
ref: 'main',
inputs: {
server_branch: '${{ github.ref }}'
}
})
trigger-k8s-deploy:
name: Trigger k8s deploy
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-22.04
needs: build-docker
steps:
- name: Log in to Azure - CI subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve GitHub PAT secrets
id: retrieve-secret-pat
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: Trigger k8s deploy
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
with:
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
script: |
await github.rest.actions.createWorkflowDispatch({
owner: 'bitwarden',
repo: 'devops',
workflow_id: 'deploy-k8s.yml',
ref: 'main',
inputs: {
environment: 'US-DEV Cloud',
tag: 'main'
}
})
check-failures:
name: Check for failures
if: always()
runs-on: ubuntu-22.04
needs:
- cloc
- lint
- testing
- build
- build-stub-swagger
- build-artifacts
- build-docker
- upload
- build-mssqlmigratorutility
- self-host-build
- trigger-k8s-deploy
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:
CLOC_STATUS: ${{ needs.cloc.result }}
LINT_STATUS: ${{ needs.lint.result }}
TESTING_STATUS: ${{ needs.testing.result }}
BUILD_STATUS: ${{ needs.build.result }}
BUILD_STUB_SWAGGER_STATUS: ${{ needs.build-stub-swagger.result }}
BUILD_MSSQLMIGRATORUTILITY_STATUS: ${{ needs.build-mssqlmigratorutility.result }}
TRIGGER_SELF_HOST_BUILD_STATUS: ${{ needs.self-host-build.result }}
run: |
if [ "$CLOC_STATUS" = "failure" ]; then
exit 1
elif [ "$LINT_STATUS" = "failure" ]; then
exit 1
elif [ "$TESTING_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_STUB_SWAGGER_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_MSSQLMIGRATORUTILITY_STATUS" = "failure" ]; then
exit 1
elif [ "$TRIGGER_SELF_HOST_BUILD_STATUS" = "failure" ]; then
exit 1
fi
|| github.ref == 'refs/heads/hotfix-rc')
&& contains(needs.*.result, 'failure')
run: exit 1
- name: Login to Azure - CI subscription
- name: Log in to Azure - CI subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
if: failure()
with:

View File

@ -1,42 +1,30 @@
---
name: Clean After PR
name: Container registry cleanup
on:
pull_request:
types: [closed]
env:
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
jobs:
build-docker:
name: Remove feature branch docker images
runs-on: ubuntu-20.04
name: Remove branch-specific Docker images
runs-on: ubuntu-22.04
steps:
- name: Checkout repo
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
########## ACR ##########
- name: Login to Azure - QA Subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
- name: Login to Azure ACR
run: az acr login -n bitwardenqa
- name: Login to Azure - PROD Subscription
- name: Log in to Azure - production subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Login to Azure ACR
run: az acr login -n bitwardenprod
- name: Log in to Azure ACR
run: az acr login -n $_AZ_REGISTRY --only-show-errors
########## Remove Docker images ##########
- name: Remove the docker image from ACR
- name: Remove the Docker image from ACR
env:
REGISTRIES: |
registries:
- bitwardenprod
- bitwardenqa
REF: ${{ github.event.pull_request.head.ref }}
SERVICES: |
services:
- Admin
@ -56,24 +44,21 @@ jobs:
run: |
for SERVICE in $(echo "${{ env.SERVICES }}" | yq e ".services[]" - )
do
for REGISTRY in $( echo "${{ env.REGISTRIES }}" | yq e ".registries[]" - )
do
SERVICE_NAME=$(echo $SERVICE | awk '{print tolower($0)}')
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") # slash safe branch name
SERVICE_NAME=$(echo $SERVICE | awk '{print tolower($0)}')
IMAGE_TAG=$(echo "${REF}" | sed "s#/#-#g") # slash safe branch name
echo "[*] Checking if remote exists: $REGISTRY.azurecr.io/$SERVICE_NAME:$IMAGE_TAG"
TAG_EXISTS=$(
az acr repository show-tags --name $REGISTRY --repository $SERVICE_NAME \
| jq --arg $TAG "$IMAGE_TAG" -e '. | any(. == "$TAG")'
)
echo "[*] Checking if remote exists: $_AZ_REGISTRY/$SERVICE_NAME:$IMAGE_TAG"
TAG_EXISTS=$(
az acr repository show-tags --name $_AZ_REGISTRY --repository $SERVICE_NAME \
| jq --arg $TAG "$IMAGE_TAG" -e '. | any(. == "$TAG")'
)
if [[ "$TAG_EXISTS" == "true" ]]; then
echo "[*] Tag exists. Removing tag"
az acr repository delete --name $REGISTRY --image $SERVICE_NAME:$IMAGE_TAG --yes
else
echo "[*] Tag does not exist. No action needed"
fi
done
if [[ "$TAG_EXISTS" == "true" ]]; then
echo "[*] Tag exists. Removing tag"
az acr repository delete --name $_AZ_REGISTRY --image $SERVICE_NAME:$IMAGE_TAG --yes
else
echo "[*] Tag does not exist. No action needed"
fi
done
- name: Log out of Docker

53
.github/workflows/cleanup-rc-branch.yml vendored Normal file
View File

@ -0,0 +1,53 @@
---
name: Cleanup RC Branch
on:
push:
tags:
- v**
jobs:
delete-rc:
name: Delete RC Branch
runs-on: ubuntu-22.04
steps:
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve bot secrets
id: retrieve-bot-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: bitwarden-ci
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: Checkout main
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: main
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
- name: Check if a RC branch exists
id: branch-check
run: |
hotfix_rc_branch_check=$(git ls-remote --heads origin hotfix-rc | wc -l)
rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
if [[ "${hotfix_rc_branch_check}" -gt 0 ]]; then
echo "hotfix-rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
echo "name=hotfix-rc" >> $GITHUB_OUTPUT
elif [[ "${rc_branch_check}" -gt 0 ]]; then
echo "rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
echo "name=rc" >> $GITHUB_OUTPUT
fi
- name: Delete RC branch
env:
BRANCH_NAME: ${{ steps.branch-check.outputs.name }}
run: |
if ! [[ -z "$BRANCH_NAME" ]]; then
git push --quiet origin --delete $BRANCH_NAME
echo "Deleted $BRANCH_NAME branch." | tee -a $GITHUB_STEP_SUMMARY
fi

42
.github/workflows/code-references.yml vendored Normal file
View File

@ -0,0 +1,42 @@
---
name: Collect code references
on:
pull_request:
branches-ignore:
- "renovate/**"
permissions:
contents: read
pull-requests: write
jobs:
refs:
name: Code reference collection
runs-on: ubuntu-22.04
steps:
- name: Check out repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Collect
id: collect
uses: launchdarkly/find-code-references-in-pull-request@2e9333c88539377cfbe818c265ba8b9ebced3c91 # v1.1.0
with:
project-key: default
environment-key: dev
access-token: ${{ secrets.LD_ACCESS_TOKEN }}
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Add label
if: steps.collect.outputs.any-changed == 'true'
run: gh pr edit $PR_NUMBER --add-label feature-flag
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
- name: Remove label
if: steps.collect.outputs.any-changed == 'false'
run: gh pr edit $PR_NUMBER --remove-label feature-flag
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}

View File

@ -1,18 +1,18 @@
---
name: Container Registry Purge
name: Container registry purge
on:
schedule:
- cron: '0 0 * * SUN'
- cron: "0 0 * * SUN"
workflow_dispatch:
inputs: {}
jobs:
purge:
name: Purge old images
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Login to Azure
- name: Log in to Azure
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
@ -68,23 +68,18 @@ jobs:
check-failures:
name: Check for failures
if: always()
runs-on: ubuntu-20.04
needs:
- purge
runs-on: ubuntu-22.04
needs: [purge]
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:
PURGE_STATUS: ${{ needs.purge.result }}
run: |
if [ "$PURGE_STATUS" = "failure" ]; then
exit 1
fi
|| github.ref == 'refs/heads/hotfix-rc')
&& contains(needs.*.result, 'failure')
run: exit 1
- name: Login to Azure - CI subscription
- name: Log in to Azure - CI subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
if: failure()
with:

View File

@ -1,95 +0,0 @@
---
name: Validate Database
on:
pull_request:
branches-ignore:
- 'l10n_master'
- 'gh-pages'
paths:
- 'src/Sql/**'
- 'util/Migrator/**'
push:
branches:
- 'master'
- 'rc'
paths:
- 'src/Sql/**'
- 'util/Migrator/**'
workflow_dispatch:
inputs: {}
jobs:
validate:
name: Validate
runs-on: ubuntu-22.04
steps:
- name: Checkout repo
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
- name: Set up dotnet
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with:
dotnet-version: '6.0.x'
- name: Print environment
run: |
dotnet --info
nuget help | grep Version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
- name: Build DACPAC
run: dotnet build src/Sql --configuration Release --verbosity minimal --output .
shell: pwsh
- name: Upload DACPAC
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: sql.dacpac
path: Sql.dacpac
- name: Docker Compose up
working-directory: "dev"
run: |
cp .env.example .env
docker compose --profile mssql up -d
shell: pwsh
- name: Migrate
working-directory: "dev"
run: "pwsh ./migrate.ps1"
shell: pwsh
- name: Diff sqlproj to migrations
run: /usr/local/sqlpackage/sqlpackage /action:DeployReport /SourceFile:"Sql.dacpac" /TargetConnectionString:"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;" /OutputPath:"report.xml" /p:IgnoreColumnOrder=True /p:IgnoreComments=True
shell: pwsh
- name: Generate SQL file
run: /usr/local/sqlpackage/sqlpackage /action:Script /SourceFile:"Sql.dacpac" /TargetConnectionString:"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;" /OutputPath:"diff.sql" /p:IgnoreColumnOrder=True /p:IgnoreComments=True
shell: pwsh
- name: Upload Report
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: report.xml
path: |
report.xml
diff.sql
- name: Validate XML
run: |
if grep -q "<Operations>" "report.xml"; then
echo
echo "Migrations are out of sync with sqlproj!"
exit 1
else
echo "Report looks good"
fi
shell: bash
- name: Docker compose down
if: ${{ always() }}
working-directory: "dev"
run: docker compose down
shell: pwsh

View File

@ -2,15 +2,18 @@
name: Enforce PR labels
on:
workflow_call:
pull_request:
types: [labeled, unlabeled, opened, edited, synchronize]
types: [labeled, unlabeled, opened, reopened, synchronize]
jobs:
enforce-label:
name: EnforceLabel
runs-on: ubuntu-20.04
if: ${{ contains(github.event.*.labels.*.name, 'hold') || contains(github.event.*.labels.*.name, 'needs-qa') || contains(github.event.*.labels.*.name, 'DB-migrations-changed') }}
name: Enforce label
runs-on: ubuntu-22.04
steps:
- name: Enforce Label
uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024 # 2.2.2
with:
BANNED_LABELS: "hold,DB-migrations-changed,needs-qa"
- name: Check for label
run: |
echo "PRs with the hold or needs-qa labels cannot be merged"
echo "### :x: PRs with the hold or needs-qa labels cannot be merged" >> $GITHUB_STEP_SUMMARY
exit 1

View File

@ -1,117 +0,0 @@
---
name: Run Database Infrastructure Tests
on:
pull_request:
branches-ignore:
- 'l10n_master'
- 'gh-pages'
paths:
- '.github/workflows/infrastructure-tests.yml' # This file
- 'src/Sql/**' # SQL Server Database Changes
- 'util/Migrator/**' # New SQL Server Migrations
- 'util/MySqlMigrations/**' # Changes to MySQL
- 'util/PostgresMigrations/**' # Changes to Postgres
- 'util/SqliteMigrations/**' # Changes to Sqlite
- 'src/Infrastructure.Dapper/**' # Changes to SQL Server Dapper Repository Layer
- 'src/Infrastructure.EntityFramework/**' # Changes to Entity Framework Repository Layer
- 'test/Infrastructure.IntegrationTest/**' # Any changes to the tests
push:
branches:
- 'master'
- 'rc'
paths:
- '.github/workflows/infrastructure-tests.yml' # This file
- 'src/Sql/**' # SQL Server Database Changes
- 'util/Migrator/**' # New SQL Server Migrations
- 'util/MySqlMigrations/**' # Changes to MySQL
- 'util/PostgresMigrations/**' # Changes to Postgres
- 'util/SqliteMigrations/**' # Changes to Sqlite
- 'src/Infrastructure.Dapper/**' # Changes to SQL Server Dapper Repository Layer
- 'src/Infrastructure.EntityFramework/**' # Changes to Entity Framework Repository Layer
- 'test/Infrastructure.IntegrationTest/**' # Any changes to the tests
workflow_dispatch:
inputs: {}
jobs:
test:
name: 'Run Infrastructure.IntegrationTest'
runs-on: ubuntu-22.04
steps:
- name: Checkout repo
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
- name: Set up dotnet
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with:
dotnet-version: '6.0.x'
- name: Restore Tools
run: dotnet tool restore
- name: Compose Databases
working-directory: 'dev'
# We could think about not using profiles and pulling images directly to cover multiple versions
run: |
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
- name: Migrate SQL Server
working-directory: 'dev'
run: "pwsh ./migrate.ps1"
shell: pwsh
- name: Migrate MySQL
working-directory: 'util/MySqlMigrations'
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:MySql:ConnectionString="$CONN_STR"'
env:
CONN_STR: "server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev;Allow User Variables=true"
- name: Migrate Postgres
working-directory: 'util/PostgresMigrations'
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:
# Default Postgres:
BW_TEST_DATABASES__0__TYPE: "Postgres"
BW_TEST_DATABASES__0__CONNECTIONSTRING: "Host=localhost;Username=postgres;Password=SET_A_PASSWORD_HERE_123;Database=vault_dev"
# Default MySql
BW_TEST_DATABASES__1__TYPE: "MySql"
BW_TEST_DATABASES__1__CONNECTIONSTRING: "server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev"
# Default Dapper SqlServer
BW_TEST_DATABASES__2__TYPE: "SqlServer"
BW_TEST_DATABASES__2__CONNECTIONSTRING: "Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;"
# Default Sqlite
BW_TEST_DATABASES__3__TYPE: "Sqlite"
BW_TEST_DATABASES__3__CONNECTIONSTRING: "Data Source=${{ runner.temp }}/test.db"
run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx"
shell: pwsh
- name: Report test results
uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226 # v1.6.0
if: always()
with:
name: Test Results
path: "**/*-test-results.trx"
reporter: dotnet-trx
fail-on-error: true
- name: Docker compose down
if: always()
working-directory: "dev"
run: docker compose down
shell: pwsh

View File

@ -2,8 +2,7 @@
# Starts a matrix job to check for modified files, then sets output based on the results.
# The input decides if the label job is ran, adding a label to the PR.
---
name: Protect Files
name: Protect files
on:
pull_request:
@ -17,7 +16,7 @@ on:
jobs:
changed-files:
name: Check for file changes
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
outputs:
changes: ${{steps.check-changes.outputs.changes_detected}}
@ -29,7 +28,7 @@ jobs:
path: util/Migrator/DbScripts
label: "DB-migrations-changed"
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
with:
fetch-depth: 2

View File

@ -16,7 +16,7 @@ on:
- Dry Run
env:
_AZ_REGISTRY: 'bitwardenprod.azurecr.io'
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
jobs:
setup:
@ -36,10 +36,10 @@ jobs:
exit 1
fi
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
- name: Check Release Version
- name: Check release version
id: version
uses: bitwarden/gh-actions/release-version-check@main
with:
@ -87,7 +87,7 @@ jobs:
task: "deploy"
description: "Deploy from ${{ needs.setup.outputs.branch-name }} branch"
- name: Download latest Release ${{ matrix.name }} asset
- name: Download latest release ${{ matrix.name }} asset
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: bitwarden/gh-actions/download-artifacts@main
with:
@ -96,16 +96,16 @@ jobs:
branch: ${{ needs.setup.outputs.branch-name }}
artifacts: ${{ matrix.name }}.zip
- name: Dry Run - Download latest Release ${{ matrix.name }} asset
- name: Dry run - Download latest release ${{ matrix.name }} asset
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
uses: bitwarden/gh-actions/download-artifacts@main
with:
workflow: build.yml
workflow_conclusion: success
branch: master
branch: main
artifacts: ${{ matrix.name }}.zip
- name: Login to Azure - CI subscription
- name: Log in to Azure - CI subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
@ -130,12 +130,12 @@ jobs:
echo "::add-mask::$publish_profile"
echo "publish-profile=$publish_profile" >> $GITHUB_OUTPUT
- name: Login to Azure
- name: Log in to Azure
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Deploy App
- name: Deploy app
uses: azure/webapps-deploy@4bca689e4c7129e55923ea9c45401b22dc6aa96f # v2.2.11
with:
app-name: ${{ steps.retrieve-secrets.outputs.webapp-name }}
@ -156,7 +156,7 @@ jobs:
fi
az webapp start -n $WEBAPP_NAME -g $RESOURCE_GROUP -s staging
- name: Update ${{ matrix.name }} deployment status to Success
- name: Update ${{ matrix.name }} deployment status to success
if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }}
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
with:
@ -164,7 +164,7 @@ jobs:
state: "success"
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
- name: Update ${{ matrix.name }} deployment status to Failure
- name: Update ${{ matrix.name }} deployment status to failure
if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }}
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
with:
@ -210,10 +210,10 @@ jobs:
echo "GitHub event: $GITHUB_EVENT"
echo "Github Release Option: $RELEASE_OPTION"
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
- name: Setup project name
- name: Set up project name
id: setup
run: |
PROJECT_NAME=$(echo "${{ matrix.project_name }}" | awk '{print tolower($0)}')
@ -222,12 +222,12 @@ jobs:
echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT
########## ACR PROD ##########
- name: Login to Azure - PROD Subscription
- name: Log in to Azure - production subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Login to Azure ACR
- name: Log in to Azure ACR
run: az acr login -n $_AZ_REGISTRY --only-show-errors
- name: Pull latest project image
@ -266,13 +266,13 @@ jobs:
run: docker logout
release:
name: Create GitHub Release
name: Create GitHub release
runs-on: ubuntu-22.04
needs:
- setup
- deploy
steps:
- name: Download latest Release Docker Stubs
- name: Download latest release Docker stubs
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: bitwarden/gh-actions/download-artifacts@main
with:
@ -285,13 +285,13 @@ jobs:
docker-stub-EU-sha256.txt,
swagger.json"
- name: Dry Run - Download latest Release Docker Stubs
- name: Dry Run - Download latest release Docker stubs
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
uses: bitwarden/gh-actions/download-artifacts@main
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,

77
.github/workflows/scan.yml vendored Normal file
View File

@ -0,0 +1,77 @@
name: Scan
on:
workflow_dispatch:
push:
branches:
- "main"
- "rc"
- "hotfix-rc"
pull_request_target:
types: [opened, synchronize]
jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
sast:
name: SAST scan
runs-on: ubuntu-22.04
needs: check-run
permissions:
contents: read
pull-requests: write
security-events: write
steps:
- name: Check out repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with Checkmarx
uses: checkmarx/ast-github-action@749fec53e0db0f6404a97e2e0807c3e80e3583a7 #2.0.23
env:
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
with:
project_name: ${{ github.repository }}
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
base_uri: https://ast.checkmarx.net/
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
additional_params: |
--report-format sarif \
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
--output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
with:
sarif_file: cx_result.sarif
quality:
name: Quality scan
runs-on: ubuntu-22.04
needs: check-run
permissions:
contents: read
pull-requests: write
steps:
- name: Check out repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with SonarCloud
uses: sonarsource/sonarcloud-github-action@49e6cd3b187936a73b8280d59ffd9da69df63ec9 # v2.1.1
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: >
-Dsonar.organization=${{ github.repository_owner }}
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
-Dsonar.tests=test/

View File

@ -1,23 +1,23 @@
---
name: 'Close stale issues and PRs'
name: Staleness
on:
workflow_dispatch:
schedule: # Run once a day at 5.23am (arbitrary but should avoid peak loads on the hour)
- cron: '23 5 * * *'
schedule: # Run once a day at 5.23am (arbitrary but should avoid peak loads on the hour)
- cron: "23 5 * * *"
jobs:
stale:
name: 'Check for stale issues and PRs'
runs-on: ubuntu-20.04
name: Check for stale issues and PRs
runs-on: ubuntu-22.04
steps:
- name: 'Run stale action'
- name: Check
uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
with:
stale-issue-label: 'needs-reply'
stale-pr-label: 'needs-changes'
days-before-stale: -1 # Do not apply the stale labels automatically, this is a manual process
days-before-issue-close: 14 # Close issue if no further activity after X days
days-before-pr-close: 21 # Close PR if no further activity after X days
stale-issue-label: "needs-reply"
stale-pr-label: "needs-changes"
days-before-stale: -1 # Do not apply the stale labels automatically, this is a manual process
days-before-issue-close: 14 # Close issue if no further activity after X days
days-before-pr-close: 21 # Close PR if no further activity after X days
close-issue-message: |
We need more information before we can help you with your problem. As we havent heard from you recently, this issue will be closed.
@ -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,5 +1,5 @@
---
name: Stop Staging Slots
name: Stop staging slots
on:
workflow_dispatch:
@ -7,8 +7,8 @@ on:
jobs:
stop-slots:
name: Stop Slots
runs-on: ubuntu-20.04
name: Stop slots
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
@ -28,7 +28,7 @@ jobs:
echo "NAME_LOWER: $NAME_LOWER"
echo "name_lower=$NAME_LOWER" >> $GITHUB_OUTPUT
- name: Login to Azure - CI Subscription
- name: Log in to Azure - CI subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
@ -46,7 +46,7 @@ jobs:
echo "::add-mask::$webapp_name"
echo "webapp-name=$webapp_name" >> $GITHUB_OUTPUT
- name: Login to Azure
- name: Log in to Azure
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}

185
.github/workflows/test-database.yml vendored Normal file
View File

@ -0,0 +1,185 @@
---
name: Database testing
on:
workflow_dispatch:
push:
branches:
- "main"
- "rc"
- "hotfix-rc"
paths:
- ".github/workflows/infrastructure-tests.yml" # This file
- "src/Sql/**" # SQL Server Database Changes
- "util/Migrator/**" # New SQL Server Migrations
- "util/MySqlMigrations/**" # Changes to MySQL
- "util/PostgresMigrations/**" # Changes to Postgres
- "util/SqliteMigrations/**" # Changes to Sqlite
- "src/Infrastructure.Dapper/**" # Changes to SQL Server Dapper Repository Layer
- "src/Infrastructure.EntityFramework/**" # Changes to Entity Framework Repository Layer
- "test/Infrastructure.IntegrationTest/**" # Any changes to the tests
pull_request:
paths:
- ".github/workflows/infrastructure-tests.yml" # This file
- "src/Sql/**" # SQL Server Database Changes
- "util/Migrator/**" # New SQL Server Migrations
- "util/MySqlMigrations/**" # Changes to MySQL
- "util/PostgresMigrations/**" # Changes to Postgres
- "util/SqliteMigrations/**" # Changes to Sqlite
- "src/Infrastructure.Dapper/**" # Changes to SQL Server Dapper Repository Layer
- "src/Infrastructure.EntityFramework/**" # Changes to Entity Framework Repository Layer
- "test/Infrastructure.IntegrationTest/**" # Any changes to the tests
jobs:
test:
name: Run tests
runs-on: ubuntu-22.04
steps:
- name: Check out repo
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
- name: Set up .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
- name: Restore tools
run: dotnet tool restore
- name: Docker Compose databases
working-directory: "dev"
# We could think about not using profiles and pulling images directly to cover multiple versions
run: |
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
- name: Migrate SQL Server
run: 'dotnet run --project util/MsSqlMigratorUtility/ "$CONN_STR"'
env:
CONN_STR: "Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;"
- name: Migrate MySQL
working-directory: "util/MySqlMigrations"
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:MySql:ConnectionString="$CONN_STR"'
env:
CONN_STR: "server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev;Allow User Variables=true"
- name: Migrate Postgres
working-directory: "util/PostgresMigrations"
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:
# Default Postgres:
BW_TEST_DATABASES__0__TYPE: "Postgres"
BW_TEST_DATABASES__0__CONNECTIONSTRING: "Host=localhost;Username=postgres;Password=SET_A_PASSWORD_HERE_123;Database=vault_dev"
# Default MySql
BW_TEST_DATABASES__1__TYPE: "MySql"
BW_TEST_DATABASES__1__CONNECTIONSTRING: "server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev"
# Default Dapper SqlServer
BW_TEST_DATABASES__2__TYPE: "SqlServer"
BW_TEST_DATABASES__2__CONNECTIONSTRING: "Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;"
# Default Sqlite
BW_TEST_DATABASES__3__TYPE: "Sqlite"
BW_TEST_DATABASES__3__CONNECTIONSTRING: "Data Source=${{ runner.temp }}/test.db"
run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx"
shell: pwsh
- name: Report test results
uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226 # v1.6.0
if: always()
with:
name: Test Results
path: "**/*-test-results.trx"
reporter: dotnet-trx
fail-on-error: true
- name: Docker Compose down
if: always()
working-directory: "dev"
run: docker compose down
shell: pwsh
validate:
name: Run validation
runs-on: ubuntu-22.04
steps:
- name: Check out repo
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
- name: Set up .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
- name: Print environment
run: |
dotnet --info
nuget help | grep Version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
- name: Build DACPAC
run: dotnet build src/Sql --configuration Release --verbosity minimal --output .
shell: pwsh
- name: Upload DACPAC
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: sql.dacpac
path: Sql.dacpac
- name: Docker Compose up
working-directory: "dev"
run: |
cp .env.example .env
docker compose --profile mssql up -d
shell: pwsh
- name: Migrate
run: 'dotnet run --project util/MsSqlMigratorUtility/ "$CONN_STR"'
env:
CONN_STR: "Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;"
- name: Diff .sqlproj to migrations
run: /usr/local/sqlpackage/sqlpackage /action:DeployReport /SourceFile:"Sql.dacpac" /TargetConnectionString:"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;" /OutputPath:"report.xml" /p:IgnoreColumnOrder=True /p:IgnoreComments=True
shell: pwsh
- name: Generate SQL file
run: /usr/local/sqlpackage/sqlpackage /action:Script /SourceFile:"Sql.dacpac" /TargetConnectionString:"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;" /OutputPath:"diff.sql" /p:IgnoreColumnOrder=True /p:IgnoreComments=True
shell: pwsh
- name: Report validation results
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: report.xml
path: |
report.xml
diff.sql
- name: Validate XML
run: |
if grep -q "<Operations>" "report.xml"; then
echo
echo "Migrations are out of sync with sqlproj!"
exit 1
else
echo "Report looks good"
fi
shell: bash
- name: Docker Compose down
if: ${{ always() }}
working-directory: "dev"
run: docker compose down
shell: pwsh

58
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,58 @@
---
name: Testing
on:
workflow_dispatch:
push:
branches:
- "main"
- "rc"
- "hotfix-rc"
pull_request:
env:
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
jobs:
testing:
name: Run tests
if: ${{ startsWith(github.head_ref, 'version_bump_') == false }}
runs-on: ubuntu-22.04
env:
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
steps:
- name: Check out repo
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
- name: Set up .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
- name: Print environment
run: |
dotnet --info
nuget help | grep Version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
- name: Remove SQL project
run: dotnet sln bitwarden-server.sln remove src/Sql/Sql.sqlproj
- name: Test OSS solution
run: dotnet test ./test --configuration Debug --logger "trx;LogFileName=oss-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
- name: Test Bitwarden solution
run: dotnet test ./bitwarden_license/test --configuration Debug --logger "trx;LogFileName=bw-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
- name: Report test results
uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226 # v1.6.0
if: always()
with:
name: Test Results
path: "**/*-test-results.trx"
reporter: dotnet-trx
fail-on-error: true
- name: Upload to codecov.io
uses: codecov/codecov-action@0cfda1dd0a4ad9efc75517f399d859cd1ea4ced1 # v4.0.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@ -4,19 +4,44 @@ name: Version Bump
on:
workflow_dispatch:
inputs:
version_number:
description: "New Version"
required: true
version_number_override:
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
required: false
type: string
cut_rc_branch:
description: "Cut RC branch?"
default: true
type: boolean
jobs:
bump_props_version:
name: "Create version_bump_${{ github.event.inputs.version_number }} branch"
runs-on: ubuntu-20.04
bump_version:
name: Bump Version
runs-on: ubuntu-22.04
outputs:
version: ${{ steps.set-final-version-output.outputs.version }}
steps:
- name: Checkout Branch
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
- name: Validate version input
if: ${{ inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-check@main
with:
version: ${{ inputs.version_number_override }}
- name: Login to Azure - CI Subscription
- name: Check out branch
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
with:
ref: main
- name: Check if RC branch exists
if: ${{ inputs.cut_rc_branch == true }}
run: |
remote_rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
if [[ "${remote_rc_branch_check}" -gt 0 ]]; then
echo "Remote RC branch exists."
echo "Please delete current RC branch before running again."
exit 1
fi
- name: Log in to Azure - CI subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
@ -26,7 +51,9 @@ 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: Import GPG key
uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0
@ -36,23 +63,82 @@ jobs:
git_user_signingkey: true
git_commit_gpgsign: true
- name: Create Version Branch
run: git switch -c version_bump_${{ github.event.inputs.version_number }}
- name: Bump Version - Props
uses: bitwarden/gh-actions/version-bump@main
with:
version: ${{ github.event.inputs.version_number }}
file_path: "Directory.Build.props"
- name: Refresh lockfiles
run: dotnet restore -f --force-evaluate --no-cache
- name: Setup git
- name: Set up Git
run: |
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
git config --local user.name "bitwarden-devops-bot"
- name: Create version branch
id: create-branch
run: |
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
git switch -c $NAME
echo "name=$NAME" >> $GITHUB_OUTPUT
- name: Install xmllint
run: |
sudo apt-get update
sudo apt-get install -y libxml2-utils
- name: Get current version
id: current-version
run: |
CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
- name: Verify input version
if: ${{ inputs.version_number_override != '' }}
env:
CURRENT_VERSION: ${{ steps.current-version.outputs.version }}
NEW_VERSION: ${{ inputs.version_number_override }}
run: |
# Error if version has not changed.
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
echo "Version has not changed."
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: Calculate next release version
if: ${{ inputs.version_number_override == '' }}
id: calculate-next-version
uses: bitwarden/gh-actions/version-next@main
with:
version: ${{ steps.current-version.outputs.version }}
- name: Bump version props - Version Override
if: ${{ inputs.version_number_override != '' }}
id: bump-version-override
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "Directory.Build.props"
version: ${{ inputs.version_number_override }}
- name: Bump version props - Automatic Calculation
if: ${{ inputs.version_number_override == '' }}
id: bump-version-automatic
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "Directory.Build.props"
version: ${{ steps.calculate-next-version.outputs.version }}
- name: Set final version output
id: set-final-version-output
run: |
if [[ "${{ steps.bump-version-override.outcome }}" = "success" ]]; then
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-version-automatic.outcome }}" = "success" ]]; then
echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT
fi
- name: Check if version changed
id: version-changed
run: |
@ -65,22 +151,24 @@ 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 ${{ steps.set-final-version-output.outputs.version }}" -a
- name: Push changes
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
- name: Create Version PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
BASE_BRANCH: master
TITLE: "Bump version to ${{ github.event.inputs.version_number }}"
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
run: git push -u origin $PR_BRANCH
- 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 }}
TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}"
run: |
gh pr create --title "$TITLE" \
--base "$BASE" \
PR_URL=$(gh pr create --title "$TITLE" \
--base "main" \
--head "$PR_BRANCH" \
--label "version update" \
--label "automated pr" \
@ -93,4 +181,62 @@ jobs:
- [X] Other
## Objective
Automated version bump to ${{ github.event.inputs.version_number }}"
Automated version bump to ${{ steps.set-final-version-output.outputs.version }}")
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
- name: Approve PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr review $PR_NUMBER --approve
- name: Merge PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
cut_rc:
name: Cut RC branch
if: ${{ inputs.cut_rc_branch == true }}
needs: bump_version
runs-on: ubuntu-22.04
steps:
- name: Check out branch
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: main
- name: Install xmllint
run: |
sudo apt-get update
sudo apt-get install -y libxml2-utils
- name: Verify version has been updated
env:
NEW_VERSION: ${{ needs.bump_version.outputs.version }}
run: |
# Wait for version to change.
while : ; do
echo "Waiting for version to be updated..."
git pull --force
CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
# If the versions don't match we continue the loop, otherwise we break out of the loop.
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break
sleep 10
done
- name: Cut RC branch
run: |
git switch --quiet --create rc
git push --quiet --set-upstream origin rc
move-future-db-scripts:
name: Move finalization database scripts
needs: cut_rc
uses: ./.github/workflows/_move_finalization_db_scripts.yml
secrets: inherit

View File

@ -1,11 +0,0 @@
---
name: Workflow Linter
on:
pull_request:
paths:
- .github/workflows/**
jobs:
call-workflow:
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@main

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

376
.vscode/launch.json vendored
View File

@ -7,95 +7,266 @@
{
"name": "Min Server",
"configurations": [
"Identity",
"API"
"run-Identity",
"run-API"
],
"presentation": {
"hidden": false,
"group": "AA_compounds",
"order": 1
},
"preLaunchTask": "buildIdentityApi",
"stopAll": true
},
{
"name": "Admin, API, Identity",
"configurations": [
"Admin",
"API",
"Identity"
"run-Admin",
"run-API",
"run-Identity"
],
"presentation": {
"hidden": false,
"group": "AA_compounds",
"order": 3
},
"preLaunchTask": "buildIdentityApiAdmin",
"stopAll": true
},
{
"name": "Full Server",
"configurations": [
"Admin",
"API",
"EventsProcessor",
"Identity",
"Sso",
"Icons",
"Billing",
"Notifications"
"run-Admin",
"run-API",
"run-Events",
"run-EventsProcessor",
"run-Identity",
"run-Sso",
"run-Icons",
"run-Billing",
"run-Notifications"
],
"presentation": {
"hidden": false,
"group": "AA_compounds",
"order": 4
},
"preLaunchTask": "buildFullServer",
"stopAll": true
},
{
"name": "Self Host: Bit",
"configurations": [
"Admin-SelfHost",
"API-SelfHost",
"EventsProcessor-SelfHost",
"Identity-SelfHost",
"Sso-SelfHost",
"Notifications-SelfHost"
"run-Admin-SelfHost",
"run-API-SelfHost",
"run-Events-SelfHost",
"run-EventsProcessor-SelfHost",
"run-Identity-SelfHost",
"run-Sso-SelfHost",
"run-Notifications-SelfHost"
],
"presentation": {
"hidden": false,
"group": "AA_compounds",
"order": 2
},
"preLaunchTask": "buildSelfHostBit",
"stopAll": true
},
{
"name": "Self Host: OSS",
"configurations": [
"Admin-SelfHost",
"API-SelfHost",
"EventsProcessor-SelfHost",
"Identity-SelfHost",
"run-Admin-SelfHost",
"run-API-SelfHost",
"run-Events-SelfHost",
"run-EventsProcessor-SelfHost",
"run-Identity-SelfHost",
],
"presentation": {
"hidden": false,
"group": "AA_compounds",
"order": 99
},
"preLaunchTask": "buildSelfHostOss",
"stopAll": true
}
],
"configurations": [
},
{
"name": "Identity",
"name": "Admin",
"configurations": [
"run-Admin"
],
"presentation": {
"hidden": false,
"group": "cloud",
"order": 10
},
"preLaunchTask": "buildAdmin",
},
{
"name": "API",
"configurations": [
"run-API"
],
"presentation": {
"hidden": false,
"group": "cloud",
},
"preLaunchTask": "buildAPI",
},
{
"name": "Billing",
"configurations": [
"run-Billing"
],
"presentation": {
"hidden": false,
"group": "cloud",
},
"preLaunchTask": "buildBilling",
},
{
"name": "Events",
"configurations": [
"run-Events"
],
"presentation": {
"hidden": false,
"group": "cloud",
},
"preLaunchTask": "buildEvents",
},
{
"name": "Events Processor",
"configurations": [
"run-EventsProcessor"
],
"presentation": {
"hidden": false,
"group": "cloud",
},
"preLaunchTask": "buildEventsProcessor",
},
{
"name": "Icons",
"configurations": [
"run-Icons"
],
"presentation": {
"hidden": false,
"group": "cloud",
},
"preLaunchTask": "buildIcons",
},
{
"name": "Identity",
"configurations": [
"run-Identity"
],
"presentation": {
"hidden": false,
"group": "cloud",
},
"preLaunchTask": "buildIdentity",
},
{
"name": "Notifications",
"configurations": [
"run-Notifications"
],
"presentation": {
"hidden": false,
"group": "cloud",
},
"preLaunchTask": "buildNotifications",
},
{
"name": "SSO",
"configurations": [
"run-Sso"
],
"presentation": {
"hidden": false,
"group": "cloud",
},
"preLaunchTask": "buildSso",
},
{
"name": "Admin Self Host",
"configurations": [
"run-Admin-SelfHost"
],
"presentation": {
"hidden": false,
"group": "self-host",
},
"preLaunchTask": "buildAdmin",
},
{
"name": "API Self Host",
"configurations": [
"run-API-SelfHost"
],
"presentation": {
"hidden": false,
"group": "self-host",
},
"preLaunchTask": "buildAPI",
},
{
"name": "Events Processor Self Host",
"configurations": [
"run-EventsProcessor-SelfHost"
],
"presentation": {
"hidden": false,
"group": "self-host",
},
"preLaunchTask": "buildEventsProcessor",
},
{
"name": "Identity Self Host",
"configurations": [
"run-Identity-SelfHost"
],
"presentation": {
"hidden": false,
"group": "self-host",
},
"preLaunchTask": "buildIdentity",
},
{
"name": "Notifications Self Host",
"configurations": [
"run-Notifications-SelfHost"
],
"presentation": {
"hidden": false,
"group": "self-host",
},
"preLaunchTask": "buildNotifications",
},
{
"name": "SSO Self Host",
"configurations": [
"run-Sso-SelfHost"
],
"presentation": {
"hidden": false,
"group": "self-host",
},
"preLaunchTask": "buildSso",
},
],
"configurations": [
// Configurations represent run-only scenarios so that they can be used in multiple compounds
{
"name": "run-Identity",
"presentation": {
"hidden": true,
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"preLaunchTask": "buildIdentity",
"program": "${workspaceFolder}/src/Identity/bin/Debug/net6.0/Identity.dll",
"program": "${workspaceFolder}/src/Identity/bin/Debug/net8.0/Identity.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Identity",
"stopAtEntry": false,
@ -107,17 +278,14 @@
}
},
{
"name": "API",
"name": "run-API",
"presentation": {
"hidden": false,
"group": "cloud",
"order": 10
"hidden": true,
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"preLaunchTask": "buildAPI",
"program": "${workspaceFolder}/src/Api/bin/Debug/net6.0/Api.dll",
"program": "${workspaceFolder}/src/Api/bin/Debug/net8.0/Api.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Api",
"stopAtEntry": false,
@ -129,17 +297,14 @@
}
},
{
"name": "Billing",
"name": "run-Billing",
"presentation": {
"hidden": false,
"group": "cloud",
"order": 10
"hidden": true,
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"preLaunchTask": "buildBilling",
"program": "${workspaceFolder}/src/Billing/bin/Debug/net6.0/Billing.dll",
"program": "${workspaceFolder}/src/Billing/bin/Debug/net8.0/Billing.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Billing",
"stopAtEntry": false,
@ -151,18 +316,15 @@
}
},
{
"name": "Admin",
"name": "run-Admin",
"presentation": {
"hidden": false,
"group": "cloud",
"order": 20
"hidden": true,
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"preLaunchTask": "buildAdmin",
"OS-COMMENT4": "If you have changed target frameworks, make sure to update the program path.",
"program": "${workspaceFolder}/src/Admin/bin/Debug/net6.0/Admin.dll",
"program": "${workspaceFolder}/src/Admin/bin/Debug/net8.0/Admin.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Admin",
"stopAtEntry": false,
@ -175,17 +337,14 @@
}
},
{
"name": "Sso",
"name": "run-Sso",
"presentation": {
"hidden": false,
"group": "cloud",
"order": 50
"hidden": true,
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"preLaunchTask": "buildSso",
"program": "${workspaceFolder}/bitwarden_license/src/Sso/bin/Debug/net6.0/Sso.dll",
"program": "${workspaceFolder}/bitwarden_license/src/Sso/bin/Debug/net8.0/Sso.dll",
"args": [],
"cwd": "${workspaceFolder}/bitwarden_license/src/Sso",
"stopAtEntry": false,
@ -197,17 +356,33 @@
}
},
{
"name": "EventsProcessor",
"name": "run-Events",
"presentation": {
"hidden": false,
"group": "cloud",
"order": 90
"hidden": true,
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"preLaunchTask": "buildEventsProcessor",
"program": "${workspaceFolder}/src/EventsProcessor/bin/Debug/net6.0/EventsProcessor.dll",
"program": "${workspaceFolder}/src/Events/bin/Debug/net8.0/Events.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Events",
"stopAtEntry": false,
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": "run-EventsProcessor",
"presentation": {
"hidden": true,
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/src/EventsProcessor/bin/Debug/net8.0/EventsProcessor.dll",
"args": [],
"cwd": "${workspaceFolder}/src/EventsProcessor",
"stopAtEntry": false,
@ -219,17 +394,14 @@
}
},
{
"name": "Icons",
"name": "run-Icons",
"presentation": {
"hidden": false,
"group": "cloud",
"order": 90
"hidden": true,
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"preLaunchTask": "buildIcons",
"program": "${workspaceFolder}/src/Icons/bin/Debug/net6.0/Icons.dll",
"program": "${workspaceFolder}/src/Icons/bin/Debug/net8.0/Icons.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Icons",
"stopAtEntry": false,
@ -241,17 +413,14 @@
}
},
{
"name": "Notifications",
"name": "run-Notifications",
"presentation": {
"hidden": true,
"group": "cloud",
"order": 100
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"preLaunchTask": "buildNotifications",
"program": "${workspaceFolder}/src/Notifications/bin/Debug/net6.0/Notifications.dll",
"program": "${workspaceFolder}/src/Notifications/bin/Debug/net8.0/Notifications.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Notifications",
"stopAtEntry": false,
@ -263,17 +432,14 @@
}
},
{
"name": "Identity-SelfHost",
"name": "run-Identity-SelfHost",
"presentation": {
"hidden": true,
"group": "self-host",
"order": 999
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"preLaunchTask": "buildIdentity",
"program": "${workspaceFolder}/src/Identity/bin/Debug/net6.0/Identity.dll",
"program": "${workspaceFolder}/src/Identity/bin/Debug/net8.0/Identity.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Identity",
"stopAtEntry": false,
@ -287,17 +453,14 @@
}
},
{
"name": "API-SelfHost",
"name": "run-API-SelfHost",
"presentation": {
"hidden": true,
"group": "self-host",
"order": 999
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"preLaunchTask": "buildAPI",
"program": "${workspaceFolder}/src/Api/bin/Debug/net6.0/Api.dll",
"program": "${workspaceFolder}/src/Api/bin/Debug/net8.0/Api.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Api",
"stopAtEntry": false,
@ -311,18 +474,15 @@
}
},
{
"name": "Admin-SelfHost",
"name": "run-Admin-SelfHost",
"presentation": {
"hidden": true,
"group": "self-host",
"order": 999
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"preLaunchTask": "buildAdmin",
"OS-COMMENT4": "If you have changed target frameworks, make sure to update the program path.",
"program": "${workspaceFolder}/src/Admin/bin/Debug/net6.0/Admin.dll",
"program": "${workspaceFolder}/src/Admin/bin/Debug/net8.0/Admin.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Admin",
"stopAtEntry": false,
@ -337,17 +497,14 @@
}
},
{
"name": "Sso-SelfHost",
"name": "run-Sso-SelfHost",
"presentation": {
"hidden": true,
"group": "self-host",
"order": 999
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"preLaunchTask": "buildSso",
"program": "${workspaceFolder}/bitwarden_license/src/Sso/bin/Debug/net6.0/Sso.dll",
"program": "${workspaceFolder}/bitwarden_license/src/Sso/bin/Debug/net8.0/Sso.dll",
"args": [],
"cwd": "${workspaceFolder}/bitwarden_license/src/Sso",
"stopAtEntry": false,
@ -361,17 +518,14 @@
}
},
{
"name": "Notifications-SelfHost",
"name": "run-Notifications-SelfHost",
"presentation": {
"hidden": true,
"group": "self-host",
"order": 999
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"preLaunchTask": "buildNotifications",
"program": "${workspaceFolder}/src/Notifications/bin/Debug/net6.0/Notifications.dll",
"program": "${workspaceFolder}/src/Notifications/bin/Debug/net8.0/Notifications.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Notifications",
"stopAtEntry": false,
@ -385,23 +539,41 @@
}
},
{
"name": "EventsProcessor-SelfHost",
"name": "run-Events-SelfHost",
"presentation": {
"hidden": true,
"group": "self-host",
"order": 999
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"preLaunchTask": "buildEventsProcessor",
"program": "${workspaceFolder}/src/EventsProcessor/bin/Debug/net6.0/EventsProcessor.dll",
"program": "${workspaceFolder}/src/Events/bin/Debug/net8.0/Events.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Events",
"stopAtEntry": false,
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "http://localhost:46274",
"developSelfHosted": "true",
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": "run-EventsProcessor-SelfHost",
"presentation": {
"hidden": true,
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/src/EventsProcessor/bin/Debug/net8.0/EventsProcessor.dll",
"args": [],
"cwd": "${workspaceFolder}/src/EventsProcessor",
"stopAtEntry": false,
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "http://localhost:46274",
"ASPNETCORE_URLS": "http://localhost:54103",
"developSelfHosted": "true",
},
"sourceFileMap": {

107
.vscode/tasks.json vendored
View File

@ -1,6 +1,65 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "buildIdentityApi",
"dependsOrder": "sequence",
"dependsOn": [
"buildIdentity",
"buildAPI"
],
"problemMatcher": [
"$msCompile"
]
},
{
"label": "buildIdentityApiAdmin",
"dependsOrder": "sequence",
"dependsOn": [
"buildIdentity",
"buildAPI",
"buildAdmin"
],
"problemMatcher": [
"$msCompile"
]
},
{
"label": "buildFullServer",
"dependsOrder": "sequence",
"dependsOn": [
"buildAdmin",
"buildAPI",
"buildEventsProcessor",
"buildIdentity",
"buildSso",
"buildIcons",
"buildBilling",
"buildNotifications",
],
},
{
"label": "buildSelfHostBit",
"dependsOrder": "sequence",
"dependsOn": [
"buildAdmin",
"buildAPI",
"buildEventsProcessor",
"buildIdentity",
"buildSso",
"buildNotifications",
],
},
{
"label": "buildSelfHostOss",
"dependsOrder": "sequence",
"dependsOn": [
"buildAdmin",
"buildAPI",
"buildEventsProcessor",
"buildIdentity",
],
},
{
"label": "buildIcons",
"command": "dotnet",
@ -37,6 +96,18 @@
],
"problemMatcher": "$msCompile"
},
{
"label": "buildEvents",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/src/Events/Events.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "buildEventsProcessor",
"command": "dotnet",
@ -152,6 +223,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

@ -1,10 +1,11 @@
<Project>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Version>2023.10.2</Version>
<TargetFramework>net8.0</TargetFramework>
<Version>2024.5.0</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<ImplicitUsings>enable</ImplicitUsings>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>
@ -18,31 +19,31 @@
<!--
NuGet: https://www.nuget.org/packages/Microsoft.NET.Test.Sdk
-->
<MicrosoftNetTestSdkVersion>17.1.0</MicrosoftNetTestSdkVersion>
<MicrosoftNetTestSdkVersion>17.8.0</MicrosoftNetTestSdkVersion>
<!--
NuGet: https://www.nuget.org/packages/xunit
-->
<XUnitVersion>2.4.1</XUnitVersion>
<XUnitVersion>2.6.6</XUnitVersion>
<!--
NuGet: https://www.nuget.org/packages/xunit
NuGet: https://www.nuget.org/packages/xunit.runner.visualstudio
-->
<XUnitRunnerVisualStudioVersion>2.4.3</XUnitRunnerVisualStudioVersion>
<XUnitRunnerVisualStudioVersion>2.5.6</XUnitRunnerVisualStudioVersion>
<!--
NuGet: https://www.nuget.org/packages/coverlet.collector/
NuGet: https://www.nuget.org/packages/coverlet.collector
-->
<CoverletCollectorVersion>3.1.2</CoverletCollectorVersion>
<CoverletCollectorVersion>6.0.0</CoverletCollectorVersion>
<!--
NuGet: https://www.nuget.org/packages/NSubstitute/
NuGet: https://www.nuget.org/packages/NSubstitute
-->
<NSubstituteVersion>4.3.0</NSubstituteVersion>
<NSubstituteVersion>5.1.0</NSubstituteVersion>
<!--
NuGet: https://www.nuget.org/packages/AutoFixture.Xunit2/
NuGet: https://www.nuget.org/packages/AutoFixture.Xunit2
-->
<AutoFixtureXUnit2Version>4.17.0</AutoFixtureXUnit2Version>
<AutoFixtureXUnit2Version>4.18.1</AutoFixtureXUnit2Version>
<!--
NuGet: https://www.nuget.org/packages/AutoFixture.AutoNSubstitute/
NuGet: https://www.nuget.org/packages/AutoFixture.AutoNSubstitute
-->
<AutoFixtureAutoNSubstituteVersion>4.17.0</AutoFixtureAutoNSubstituteVersion>
<AutoFixtureAutoNSubstituteVersion>4.18.1</AutoFixtureAutoNSubstituteVersion>
</PropertyGroup>
<!--

View File

@ -5,13 +5,13 @@ specifies another license. Bitwarden Licensed code is found only in the
/bitwarden_license directory.
AGPL v3.0:
https://github.com/bitwarden/server/blob/master/LICENSE_AGPL.txt
https://github.com/bitwarden/server/blob/main/LICENSE_AGPL.txt
Bitwarden License v1.0:
https://github.com/bitwarden/server/blob/master/LICENSE_BITWARDEN.txt
https://github.com/bitwarden/server/blob/main/LICENSE_BITWARDEN.txt
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>.
<https://github.com/bitwarden/server/blob/main/TRADEMARK_GUIDELINES.md>.

View File

@ -56,7 +56,7 @@ such Open Source Software only.
logos of any Contributor (except as may be necessary to comply with the notice
requirements in Section 2.3), and use of any Bitwarden trademarks must comply with
Bitwarden Trademark Guidelines
<https://github.com/bitwarden/server/blob/master/TRADEMARK_GUIDELINES.md>.
<https://github.com/bitwarden/server/blob/main/TRADEMARK_GUIDELINES.md>.
3. TERMINATION

View File

@ -8,7 +8,7 @@ As an open solution, Bitwarden publishes the source code for various modules und
# Bitwarden Software Licensing
We have two tiers of licensing for our software. The core products are offered under one of the GPL open source licenses: GPL 3 and A-GPL 3. A select number of features, primarily those designed for use by larger organizations rather than individuals and families, are licensed under a "Source Available" commercial license [here](https://github.com/bitwarden/server/blob/master/LICENSE_BITWARDEN.txt).
We have two tiers of licensing for our software. The core products are offered under one of the GPL open source licenses: GPL 3 and A-GPL 3. A select number of features, primarily those designed for use by larger organizations rather than individuals and families, are licensed under a "Source Available" commercial license [here](https://github.com/bitwarden/server/blob/main/LICENSE_BITWARDEN.txt).
Our current software products have the following licenses:
@ -49,7 +49,7 @@ As detailed above, the Bitwarden password management clients for individual use,
***If I redistribute or provide services related to Bitwarden open source software can I use the "Bitwarden" name?***
Our licenses do not grant any rights in the trademarks, service marks, or logos of Bitwarden (except as may be necessary to comply with the notice requirements as applicable). The Bitwarden trademark is a trusted mark applied to products distributed by Bitwarden, Inc., owner of the Bitwarden trademarks and products. We have adopted and enforce strict rules governing use of our trademarks. Use of any Bitwarden trademarks must comply with Bitwarden [Trademark Guidelines](https://github.com/bitwarden/server/blob/master/TRADEMARK_GUIDELINES.md).
Our licenses do not grant any rights in the trademarks, service marks, or logos of Bitwarden (except as may be necessary to comply with the notice requirements as applicable). The Bitwarden trademark is a trusted mark applied to products distributed by Bitwarden, Inc., owner of the Bitwarden trademarks and products. We have adopted and enforce strict rules governing use of our trademarks. Use of any Bitwarden trademarks must comply with Bitwarden [Trademark Guidelines](https://github.com/bitwarden/server/blob/main/TRADEMARK_GUIDELINES.md).
***Bitwarden Trademark Usage***
@ -61,10 +61,10 @@ You don't need permission to use our marks when truthfully referring to our prod
***How should I use the Bitwarden Trademarks when allowed?***
Use the Bitwarden Trademarks exactly as [shown](https://github.com/bitwarden/server/blob/master/TRADEMARK_GUIDELINES.md) and without modification. For example, do not abbreviate, hyphenate, or remove elements and separate them from surrounding text, images and other features. Always use the Bitwarden Trademarks as adjectives followed by a generic term, never as a noun or verb.
Use the Bitwarden Trademarks exactly as [shown](https://github.com/bitwarden/server/blob/main/TRADEMARK_GUIDELINES.md) and without modification. For example, do not abbreviate, hyphenate, or remove elements and separate them from surrounding text, images and other features. Always use the Bitwarden Trademarks as adjectives followed by a generic term, never as a noun or verb.
Use the Bitwarden Trademarks only to reference one of our products or services, but never in a way that implies sponsorship or affiliation by Bitwarden. For example, do not use any part of the Bitwarden Trademarks as the name of your business, product or service name, application, domain name, publication or other offering this can be confusing to others.
***Where can I find more information?***
For more information on how to use the Bitwarden Trademarks, including in connection with self-hosted options and open-source code, see our [Trademark Guidelines](https://github.com/bitwarden/server/blob/master/TRADEMARK_GUIDELINES.md) or [contacts us](https://bitwarden.com/contact/).
For more information on how to use the Bitwarden Trademarks, including in connection with self-hosted options and open-source code, see our [Trademark Guidelines](https://github.com/bitwarden/server/blob/main/TRADEMARK_GUIDELINES.md) or [contacts us](https://bitwarden.com/contact/).

View File

@ -1,9 +1,9 @@
<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">
<img src="https://github.com/bitwarden/server/actions/workflows/build.yml/badge.svg?branch=master" alt="Github Workflow build on master" />
<a href="https://github.com/bitwarden/server/actions/workflows/build.yml?query=branch:main" target="_blank">
<img src="https://github.com/bitwarden/server/actions/workflows/build.yml/badge.svg?branch=main" alt="Github Workflow build on main" />
</a>
<a href="https://hub.docker.com/u/bitwarden/" target="_blank">
<img src="https://img.shields.io/docker/pulls/bitwarden/api.svg" alt="DockerHub" />
@ -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

@ -116,6 +116,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqliteMigrations", "util\Sq
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MsSqlMigratorUtility", "util\MsSqlMigratorUtility\MsSqlMigratorUtility.csproj", "{D9A2CCBB-FB0A-4BBA-A9ED-BA9FF277C880}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.Test", "test\Admin.Test\Admin.Test.csproj", "{52D22B52-26D3-463A-8EB5-7FDC849D3761}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events.Test", "test\Events.Test\Events.Test.csproj", "{916AFD8C-30AF-49B6-A5C9-28CA1B5D9298}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventsProcessor.Test", "test\EventsProcessor.Test\EventsProcessor.Test.csproj", "{81673EFB-7134-4B4B-A32F-1EA05F0EF3CE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Notifications.Test", "test\Notifications.Test\Notifications.Test.csproj", "{90D85D8F-5577-4570-A96E-5A2E185F0F6F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -284,6 +292,22 @@ Global
{D9A2CCBB-FB0A-4BBA-A9ED-BA9FF277C880}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D9A2CCBB-FB0A-4BBA-A9ED-BA9FF277C880}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D9A2CCBB-FB0A-4BBA-A9ED-BA9FF277C880}.Release|Any CPU.Build.0 = Release|Any CPU
{52D22B52-26D3-463A-8EB5-7FDC849D3761}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{52D22B52-26D3-463A-8EB5-7FDC849D3761}.Debug|Any CPU.Build.0 = Debug|Any CPU
{52D22B52-26D3-463A-8EB5-7FDC849D3761}.Release|Any CPU.ActiveCfg = Release|Any CPU
{52D22B52-26D3-463A-8EB5-7FDC849D3761}.Release|Any CPU.Build.0 = Release|Any CPU
{916AFD8C-30AF-49B6-A5C9-28CA1B5D9298}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{916AFD8C-30AF-49B6-A5C9-28CA1B5D9298}.Debug|Any CPU.Build.0 = Debug|Any CPU
{916AFD8C-30AF-49B6-A5C9-28CA1B5D9298}.Release|Any CPU.ActiveCfg = Release|Any CPU
{916AFD8C-30AF-49B6-A5C9-28CA1B5D9298}.Release|Any CPU.Build.0 = Release|Any CPU
{81673EFB-7134-4B4B-A32F-1EA05F0EF3CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{81673EFB-7134-4B4B-A32F-1EA05F0EF3CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{81673EFB-7134-4B4B-A32F-1EA05F0EF3CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{81673EFB-7134-4B4B-A32F-1EA05F0EF3CE}.Release|Any CPU.Build.0 = Release|Any CPU
{90D85D8F-5577-4570-A96E-5A2E185F0F6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{90D85D8F-5577-4570-A96E-5A2E185F0F6F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90D85D8F-5577-4570-A96E-5A2E185F0F6F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{90D85D8F-5577-4570-A96E-5A2E185F0F6F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -329,6 +353,10 @@ Global
{7E9A7DD5-EB78-4AC5-BFD5-64573FD2533B} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{07143DFA-F242-47A4-A15E-39C9314D4140} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
{D9A2CCBB-FB0A-4BBA-A9ED-BA9FF277C880} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
{52D22B52-26D3-463A-8EB5-7FDC849D3761} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{916AFD8C-30AF-49B6-A5C9-28CA1B5D9298} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{81673EFB-7134-4B4B-A32F-1EA05F0EF3CE} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{90D85D8F-5577-4570-A96E-5A2E185F0F6F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}

View File

@ -1,3 +1,3 @@
# Bitwarden Licensed Code
All source code under this directory is licensed under the [Bitwarden License Agreement](https://github.com/bitwarden/server/blob/master/LICENSE_BITWARDEN.txt).
All source code under this directory is licensed under the [Bitwarden License Agreement](https://github.com/bitwarden/server/blob/main/LICENSE_BITWARDEN.txt).

View File

@ -1,10 +1,15 @@
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Repositories;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
namespace Bit.Commercial.Core.AdminConsole.Providers;
@ -14,20 +19,26 @@ public class CreateProviderCommand : ICreateProviderCommand
private readonly IProviderUserRepository _providerUserRepository;
private readonly IProviderService _providerService;
private readonly IUserRepository _userRepository;
private readonly IProviderPlanRepository _providerPlanRepository;
private readonly IFeatureService _featureService;
public CreateProviderCommand(
IProviderRepository providerRepository,
IProviderUserRepository providerUserRepository,
IProviderService providerService,
IUserRepository userRepository)
IUserRepository userRepository,
IProviderPlanRepository providerPlanRepository,
IFeatureService featureService)
{
_providerRepository = providerRepository;
_providerUserRepository = providerUserRepository;
_providerService = providerService;
_userRepository = userRepository;
_providerPlanRepository = providerPlanRepository;
_featureService = featureService;
}
public async Task CreateMspAsync(Provider provider, string ownerEmail)
public async Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats)
{
var owner = await _userRepository.GetByEmailAsync(ownerEmail);
if (owner == null)
@ -44,6 +55,23 @@ public class CreateProviderCommand : ICreateProviderCommand
Type = ProviderUserType.ProviderAdmin,
Status = ProviderUserStatusType.Confirmed,
};
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
if (isConsolidatedBillingEnabled)
{
var providerPlans = new List<ProviderPlan>
{
CreateProviderPlan(provider.Id, PlanType.TeamsMonthly, teamsMinimumSeats),
CreateProviderPlan(provider.Id, PlanType.EnterpriseMonthly, enterpriseMinimumSeats)
};
foreach (var providerPlan in providerPlans)
{
await _providerPlanRepository.CreateAsync(providerPlan);
}
}
await _providerUserRepository.CreateAsync(providerUser);
await _providerService.SendProviderSetupInviteEmailAsync(provider, owner.Email);
}
@ -60,4 +88,16 @@ public class CreateProviderCommand : ICreateProviderCommand
provider.UseEvents = true;
await _providerRepository.CreateAsync(provider);
}
private ProviderPlan CreateProviderPlan(Guid providerId, PlanType planType, int seatMinimum)
{
return new ProviderPlan
{
ProviderId = providerId,
PlanType = planType,
SeatMinimum = seatMinimum,
PurchasedSeats = 0,
AllocatedSeats = 0
};
}
}

View File

@ -0,0 +1,135 @@
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Commands;
using Bit.Core.Billing.Constants;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Microsoft.Extensions.Logging;
using Stripe;
namespace Bit.Commercial.Core.AdminConsole.Providers;
public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProviderCommand
{
private readonly IEventService _eventService;
private readonly ILogger<RemoveOrganizationFromProviderCommand> _logger;
private readonly IMailService _mailService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationService _organizationService;
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IStripeAdapter _stripeAdapter;
private readonly IScaleSeatsCommand _scaleSeatsCommand;
private readonly IFeatureService _featureService;
public RemoveOrganizationFromProviderCommand(
IEventService eventService,
ILogger<RemoveOrganizationFromProviderCommand> logger,
IMailService mailService,
IOrganizationRepository organizationRepository,
IOrganizationService organizationService,
IProviderOrganizationRepository providerOrganizationRepository,
IStripeAdapter stripeAdapter,
IScaleSeatsCommand scaleSeatsCommand,
IFeatureService featureService)
{
_eventService = eventService;
_logger = logger;
_mailService = mailService;
_organizationRepository = organizationRepository;
_organizationService = organizationService;
_providerOrganizationRepository = providerOrganizationRepository;
_stripeAdapter = stripeAdapter;
_scaleSeatsCommand = scaleSeatsCommand;
_featureService = featureService;
}
public async Task RemoveOrganizationFromProvider(
Provider provider,
ProviderOrganization providerOrganization,
Organization organization)
{
if (provider == null ||
providerOrganization == null ||
organization == null ||
providerOrganization.ProviderId != provider.Id)
{
throw new BadRequestException("Failed to remove organization. Please contact support.");
}
if (!await _organizationService.HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
Array.Empty<Guid>(),
includeProvider: false))
{
throw new BadRequestException("Organization must have at least one confirmed owner.");
}
var organizationOwnerEmails =
(await _organizationRepository.GetOwnerEmailAddressesById(organization.Id)).ToList();
organization.BillingEmail = organizationOwnerEmails.MinBy(email => email);
var customerUpdateOptions = new CustomerUpdateOptions
{
Coupon = string.Empty,
Email = organization.BillingEmail
};
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, customerUpdateOptions);
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
if (isConsolidatedBillingEnabled && provider.Status == ProviderStatusType.Billable)
{
var plan = StaticStore.GetPlan(organization.PlanType).PasswordManager;
var subscriptionCreateOptions = new SubscriptionCreateOptions
{
Customer = organization.GatewayCustomerId,
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
DaysUntilDue = 30,
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
Metadata = new Dictionary<string, string>
{
{ "organizationId", organization.Id.ToString() }
},
OffSession = true,
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
Items = [new SubscriptionItemOptions { Price = plan.StripeSeatPlanId, Quantity = organization.Seats }]
};
var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
organization.GatewaySubscriptionId = subscription.Id;
await _scaleSeatsCommand.ScalePasswordManagerSeats(provider, organization.PlanType,
-(organization.Seats ?? 0));
}
else
{
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
{
CollectionMethod = "send_invoice",
DaysUntilDue = 30
};
await _stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, subscriptionUpdateOptions);
}
await _organizationRepository.ReplaceAsync(organization);
await _mailService.SendProviderUpdatePaymentMethod(
organization.Id,
organization.Name,
provider.Name,
organizationOwnerEmails);
await _providerOrganizationRepository.DeleteAsync(providerOrganization);
await _eventService.LogProviderOrganizationEventAsync(
providerOrganization,
EventType.ProviderOrganization_Removed);
}
}

View File

@ -1,8 +1,13 @@
using Bit.Core.AdminConsole.Entities.Provider;
using System.ComponentModel.DataAnnotations;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business.Provider;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Extensions;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
@ -12,8 +17,10 @@ using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.DataProtection;
using Stripe;
namespace Bit.Commercial.Core.AdminConsole.Services;
@ -33,13 +40,19 @@ public class ProviderService : IProviderService
private readonly IUserService _userService;
private readonly IOrganizationService _organizationService;
private readonly ICurrentContext _currentContext;
private readonly IStripeAdapter _stripeAdapter;
private readonly IFeatureService _featureService;
private readonly IDataProtectorTokenFactory<ProviderDeleteTokenable> _providerDeleteTokenDataFactory;
private readonly IApplicationCacheService _applicationCacheService;
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
IUserService userService, IOrganizationService organizationService, IMailService mailService,
IDataProtectionProvider dataProtectionProvider, IEventService eventService,
IOrganizationRepository organizationRepository, GlobalSettings globalSettings,
ICurrentContext currentContext)
ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService,
IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory,
IApplicationCacheService applicationCacheService)
{
_providerRepository = providerRepository;
_providerUserRepository = providerUserRepository;
@ -53,6 +66,10 @@ public class ProviderService : IProviderService
_globalSettings = globalSettings;
_dataProtector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
_currentContext = currentContext;
_stripeAdapter = stripeAdapter;
_featureService = featureService;
_providerDeleteTokenDataFactory = providerDeleteTokenDataFactory;
_applicationCacheService = applicationCacheService;
}
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key)
@ -252,7 +269,7 @@ public class ProviderService : IProviderService
await _providerUserRepository.ReplaceAsync(providerUser);
events.Add((providerUser, EventType.ProviderUser_Confirmed, null));
await _mailService.SendProviderConfirmedEmailAsync(provider.Name, user.Email);
await _mailService.SendProviderConfirmedEmailAsync(provider.DisplayName(), user.Email);
result.Add(Tuple.Create(providerUser, ""));
}
catch (BadRequestException e)
@ -326,7 +343,7 @@ public class ProviderService : IProviderService
var email = user == null ? providerUser.Email : user.Email;
if (!string.IsNullOrWhiteSpace(email))
{
await _mailService.SendProviderUserRemoved(provider.Name, email);
await _mailService.SendProviderUserRemoved(provider.DisplayName(), email);
}
result.Add(Tuple.Create(providerUser, ""));
@ -354,6 +371,7 @@ public class ProviderService : IProviderService
}
var organization = await _organizationRepository.GetByIdAsync(organizationId);
ThrowOnInvalidPlanType(organization.PlanType);
if (organization.UseSecretsManager)
@ -369,7 +387,22 @@ public class ProviderService : IProviderService
Key = key,
};
var provider = await _providerRepository.GetByIdAsync(providerId);
await ApplyProviderPriceRateAsync(organization, provider);
await _providerOrganizationRepository.CreateAsync(providerOrganization);
organization.BillingEmail = provider.BillingEmail;
await _organizationRepository.ReplaceAsync(organization);
if (!string.IsNullOrEmpty(organization.GatewayCustomerId))
{
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
{
Email = provider.BillingEmail
});
}
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Added);
}
@ -381,24 +414,120 @@ public class ProviderService : IProviderService
throw new BadRequestException("Provider must be of type Reseller in order to assign Organizations to it.");
}
var existingProviderOrganizationsCount = await _providerOrganizationRepository.GetCountByOrganizationIdsAsync(organizationIds);
var orgIdsList = organizationIds.ToList();
var existingProviderOrganizationsCount = await _providerOrganizationRepository.GetCountByOrganizationIdsAsync(orgIdsList);
if (existingProviderOrganizationsCount > 0)
{
throw new BadRequestException("Organizations must not be assigned to any Provider.");
}
var providerOrganizationsToInsert = organizationIds.Select(orgId => new ProviderOrganization { ProviderId = providerId, OrganizationId = orgId });
var providerOrganizationsToInsert = orgIdsList.Select(orgId => new ProviderOrganization { ProviderId = providerId, OrganizationId = orgId });
var insertedProviderOrganizations = await _providerOrganizationRepository.CreateManyAsync(providerOrganizationsToInsert);
await _eventService.LogProviderOrganizationEventsAsync(insertedProviderOrganizations.Select(ipo => (ipo, EventType.ProviderOrganization_Added, (DateTime?)null)));
}
private async Task ApplyProviderPriceRateAsync(Organization organization, Provider provider)
{
// if a provider was created before Nov 6, 2023.If true, the organization plan assigned to that provider is updated to a 2020 plan.
if (provider.CreationDate >= Constants.ProviderCreatedPriorNov62023)
{
return;
}
var subscriptionItem = await GetSubscriptionItemAsync(organization.GatewaySubscriptionId, GetStripeSeatPlanId(organization.PlanType));
var extractedPlanType = PlanTypeMappings(organization);
if (subscriptionItem != null)
{
await UpdateSubscriptionAsync(subscriptionItem, GetStripeSeatPlanId(extractedPlanType), organization);
}
await _organizationRepository.UpsertAsync(organization);
}
private async Task<Stripe.SubscriptionItem> GetSubscriptionItemAsync(string subscriptionId, string oldPlanId)
{
var subscriptionDetails = await _stripeAdapter.SubscriptionGetAsync(subscriptionId);
return subscriptionDetails.Items.Data.FirstOrDefault(item => item.Price.Id == oldPlanId);
}
private static string GetStripeSeatPlanId(PlanType planType)
{
return StaticStore.GetPlan(planType).PasswordManager.StripeSeatPlanId;
}
private async Task UpdateSubscriptionAsync(Stripe.SubscriptionItem subscriptionItem, string extractedPlanType, Organization organization)
{
try
{
if (subscriptionItem.Price.Id != extractedPlanType)
{
await _stripeAdapter.SubscriptionUpdateAsync(subscriptionItem.Subscription,
new Stripe.SubscriptionUpdateOptions
{
Items = new List<Stripe.SubscriptionItemOptions>
{
new()
{
Id = subscriptionItem.Id,
Price = extractedPlanType,
Quantity = organization.Seats.Value,
},
}
});
}
}
catch (Exception)
{
throw new Exception("Unable to update existing plan on stripe");
}
}
private static PlanType PlanTypeMappings(Organization organization)
{
var planTypeMappings = new Dictionary<PlanType, string>
{
{ PlanType.EnterpriseAnnually2020, GetEnumDisplayName(PlanType.EnterpriseAnnually2020) },
{ PlanType.EnterpriseMonthly2020, GetEnumDisplayName(PlanType.EnterpriseMonthly2020) },
{ PlanType.TeamsMonthly2020, GetEnumDisplayName(PlanType.TeamsMonthly2020) },
{ PlanType.TeamsAnnually2020, GetEnumDisplayName(PlanType.TeamsAnnually2020) }
};
foreach (var mapping in planTypeMappings)
{
if (mapping.Value.IndexOf(organization.Plan, StringComparison.Ordinal) != -1)
{
organization.PlanType = mapping.Key;
organization.Plan = mapping.Value;
return organization.PlanType;
}
}
throw new ArgumentException("Invalid PlanType selected");
}
private static string GetEnumDisplayName(Enum value)
{
var fieldInfo = value.GetType().GetField(value.ToString());
var displayAttribute = (DisplayAttribute)Attribute.GetCustomAttribute(fieldInfo!, typeof(DisplayAttribute));
return displayAttribute?.Name ?? value.ToString();
}
public async Task<ProviderOrganization> CreateOrganizationAsync(Guid providerId,
OrganizationSignup organizationSignup, string clientOwnerEmail, User user)
{
ThrowOnInvalidPlanType(organizationSignup.Plan);
var provider = await _providerRepository.GetByIdAsync(providerId);
var (organization, _) = await _organizationService.SignUpAsync(organizationSignup, true);
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && provider.IsBillable();
ThrowOnInvalidPlanType(organizationSignup.Plan, consolidatedBillingEnabled);
var (organization, _, defaultCollection) = consolidatedBillingEnabled
? await _organizationService.SignupClientAsync(organizationSignup)
: await _organizationService.SignUpAsync(organizationSignup, true);
var providerOrganization = new ProviderOrganization
{
@ -410,6 +539,21 @@ public class ProviderService : IProviderService
await _providerOrganizationRepository.CreateAsync(providerOrganization);
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Created);
// If using Flexible Collections, give the owner Can Manage access over the default collection
// The orgUser is not available when the org is created so we have to do it here as part of the invite
var defaultOwnerAccess = organization.FlexibleCollections && defaultCollection != null
?
[
new CollectionAccessSelection
{
Id = defaultCollection.Id,
HidePasswords = false,
ReadOnly = false,
Manage = true
}
]
: Array.Empty<CollectionAccessSelection>();
await _organizationService.InviteUsersAsync(organization.Id, user.Id,
new (OrganizationUserInvite, string)[]
{
@ -417,10 +561,13 @@ public class ProviderService : IProviderService
new OrganizationUserInvite
{
Emails = new[] { clientOwnerEmail },
AccessAll = true,
// If using Flexible Collections, AccessAll is deprecated and set to false.
// If not using Flexible Collections, set AccessAll to true (previous behavior)
AccessAll = !organization.FlexibleCollections,
Type = OrganizationUserType.Owner,
Permissions = null,
Collections = Array.Empty<CollectionAccessSelection>(),
Collections = defaultOwnerAccess,
},
null
)
@ -429,23 +576,6 @@ public class ProviderService : IProviderService
return providerOrganization;
}
public async Task RemoveOrganizationAsync(Guid providerId, Guid providerOrganizationId, Guid removingUserId)
{
var providerOrganization = await _providerOrganizationRepository.GetByIdAsync(providerOrganizationId);
if (providerOrganization == null || providerOrganization.ProviderId != providerId)
{
throw new BadRequestException("Invalid organization.");
}
if (!await _organizationService.HasConfirmedOwnersExceptAsync(providerOrganization.OrganizationId, new Guid[] { }, includeProvider: false))
{
throw new BadRequestException("Organization needs to have at least one confirmed owner.");
}
await _providerOrganizationRepository.DeleteAsync(providerOrganization);
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);
}
public async Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid ownerId)
{
var provider = await _providerRepository.GetByIdAsync(providerId);
@ -482,12 +612,50 @@ public class ProviderService : IProviderService
}
}
public async Task InitiateDeleteAsync(Provider provider, string providerAdminEmail)
{
if (string.IsNullOrWhiteSpace(provider.Name))
{
throw new BadRequestException("Provider name not found.");
}
var providerAdmin = await _userRepository.GetByEmailAsync(providerAdminEmail);
if (providerAdmin == null)
{
throw new BadRequestException("Provider admin not found.");
}
var providerAdminOrgUser = await _providerUserRepository.GetByProviderUserAsync(provider.Id, providerAdmin.Id);
if (providerAdminOrgUser == null || providerAdminOrgUser.Status != ProviderUserStatusType.Confirmed ||
providerAdminOrgUser.Type != ProviderUserType.ProviderAdmin)
{
throw new BadRequestException("Org admin not found.");
}
var token = _providerDeleteTokenDataFactory.Protect(new ProviderDeleteTokenable(provider, 1));
await _mailService.SendInitiateDeletProviderEmailAsync(providerAdminEmail, provider, token);
}
public async Task DeleteAsync(Provider provider, string token)
{
if (!_providerDeleteTokenDataFactory.TryUnprotect(token, out var data) || !data.IsValid(provider))
{
throw new BadRequestException("Invalid token.");
}
await DeleteAsync(provider);
}
public async Task DeleteAsync(Provider provider)
{
await _providerRepository.DeleteAsync(provider);
await _applicationCacheService.DeleteProviderAbilityAsync(provider.Id);
}
private async Task SendInviteAsync(ProviderUser providerUser, Provider provider)
{
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
var token = _dataProtector.Protect(
$"ProviderUserInvite {providerUser.Id} {providerUser.Email} {nowMillis}");
await _mailService.SendProviderInviteEmailAsync(provider.Name, providerUser, token, providerUser.Email);
await _mailService.SendProviderInviteEmailAsync(provider.DisplayName(), providerUser, token, providerUser.Email);
}
private async Task<bool> HasConfirmedProviderAdminExceptAsync(Guid providerId, IEnumerable<Guid> providerUserIds)
@ -499,8 +667,13 @@ public class ProviderService : IProviderService
return confirmedOwnersIds.Except(providerUserIds).Any();
}
private void ThrowOnInvalidPlanType(PlanType requestedType)
private void ThrowOnInvalidPlanType(PlanType requestedType, bool consolidatedBillingEnabled = false)
{
if (consolidatedBillingEnabled && requestedType is not (PlanType.TeamsMonthly or PlanType.EnterpriseMonthly))
{
throw new BadRequestException($"Providers cannot manage organizations with the plan type {requestedType}. Only Teams (Monthly) and Enterprise (Monthly) are allowed.");
}
if (ProviderDisallowedOrganizationTypes.Contains(requestedType))
{
throw new BadRequestException($"Providers cannot manage organizations with the requested plan type ({requestedType}). Only Teams and Enterprise accounts are allowed.");

View File

@ -1,267 +0,0 @@
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Microsoft.AspNetCore.Authorization;
namespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
public class AccessPolicyAuthorizationHandler : AuthorizationHandler<AccessPolicyOperationRequirement, BaseAccessPolicy>
{
private readonly ICurrentContext _currentContext;
private readonly IAccessClientQuery _accessClientQuery;
private readonly IGroupRepository _groupRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IProjectRepository _projectRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
public AccessPolicyAuthorizationHandler(ICurrentContext currentContext,
IAccessClientQuery accessClientQuery,
IGroupRepository groupRepository,
IOrganizationUserRepository organizationUserRepository,
IProjectRepository projectRepository,
IServiceAccountRepository serviceAccountRepository)
{
_currentContext = currentContext;
_accessClientQuery = accessClientQuery;
_groupRepository = groupRepository;
_organizationUserRepository = organizationUserRepository;
_projectRepository = projectRepository;
_serviceAccountRepository = serviceAccountRepository;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
AccessPolicyOperationRequirement requirement,
BaseAccessPolicy resource)
{
switch (requirement)
{
case not null when requirement == AccessPolicyOperations.Create:
await CanCreateAccessPolicyAsync(context, requirement, resource);
break;
case not null when requirement == AccessPolicyOperations.Update:
await CanUpdateAccessPolicyAsync(context, requirement, resource);
break;
case not null when requirement == AccessPolicyOperations.Delete:
await CanDeleteAccessPolicyAsync(context, requirement, resource);
break;
default:
throw new ArgumentException("Unsupported operation requirement type provided.",
nameof(requirement));
}
}
private async Task CanCreateAccessPolicyAsync(AuthorizationHandlerContext context,
AccessPolicyOperationRequirement requirement, BaseAccessPolicy resource)
{
switch (resource)
{
case UserProjectAccessPolicy ap:
await CanCreateAsync(context, requirement, ap);
break;
case GroupProjectAccessPolicy ap:
await CanCreateAsync(context, requirement, ap);
break;
case ServiceAccountProjectAccessPolicy ap:
await CanCreateAsync(context, requirement, ap);
break;
case UserServiceAccountAccessPolicy ap:
await CanCreateAsync(context, requirement, ap);
break;
case GroupServiceAccountAccessPolicy ap:
await CanCreateAsync(context, requirement, ap);
break;
default:
throw new ArgumentException("Unsupported access policy type provided.");
}
}
private async Task CanUpdateAccessPolicyAsync(AuthorizationHandlerContext context,
AccessPolicyOperationRequirement requirement, BaseAccessPolicy resource)
{
var access = await GetAccessPolicyAccessAsync(context, resource);
if (access.Write)
{
context.Succeed(requirement);
}
}
private async Task CanDeleteAccessPolicyAsync(AuthorizationHandlerContext context,
AccessPolicyOperationRequirement requirement, BaseAccessPolicy resource)
{
var access = await GetAccessPolicyAccessAsync(context, resource);
if (access.Write)
{
context.Succeed(requirement);
}
}
private async Task CanCreateAsync(AuthorizationHandlerContext context,
AccessPolicyOperationRequirement requirement, UserProjectAccessPolicy resource)
{
var user = await _organizationUserRepository.GetByIdAsync(resource.OrganizationUserId!.Value);
if (user.OrganizationId != resource.GrantedProject?.OrganizationId)
{
return;
}
var access = await GetAccessAsync(context, resource.GrantedProject!.OrganizationId, resource.GrantedProjectId);
if (access.Write)
{
context.Succeed(requirement);
}
}
private async Task CanCreateAsync(AuthorizationHandlerContext context,
AccessPolicyOperationRequirement requirement, GroupProjectAccessPolicy resource)
{
var group = await _groupRepository.GetByIdAsync(resource.GroupId!.Value);
if (group.OrganizationId != resource.GrantedProject?.OrganizationId)
{
return;
}
var access = await GetAccessAsync(context, resource.GrantedProject!.OrganizationId, resource.GrantedProjectId);
if (access.Write)
{
context.Succeed(requirement);
}
}
private async Task CanCreateAsync(AuthorizationHandlerContext context,
AccessPolicyOperationRequirement requirement, ServiceAccountProjectAccessPolicy resource)
{
var projectOrganizationId = resource.GrantedProject?.OrganizationId;
var serviceAccountOrgId = resource.ServiceAccount?.OrganizationId;
if (projectOrganizationId == null)
{
var project = await _projectRepository.GetByIdAsync(resource.GrantedProjectId!.Value);
projectOrganizationId = project?.OrganizationId;
}
if (serviceAccountOrgId == null)
{
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(resource.ServiceAccountId!.Value);
serviceAccountOrgId = serviceAccount?.OrganizationId;
}
if (!serviceAccountOrgId.HasValue || !projectOrganizationId.HasValue ||
serviceAccountOrgId != projectOrganizationId)
{
return;
}
var access = await GetAccessAsync(context, projectOrganizationId.Value, resource.GrantedProjectId,
resource.ServiceAccountId);
if (access.Write)
{
context.Succeed(requirement);
}
}
private async Task CanCreateAsync(AuthorizationHandlerContext context,
AccessPolicyOperationRequirement requirement, UserServiceAccountAccessPolicy resource)
{
var user = await _organizationUserRepository.GetByIdAsync(resource.OrganizationUserId!.Value);
if (user.OrganizationId != resource.GrantedServiceAccount!.OrganizationId)
{
return;
}
var access = await GetAccessAsync(context, resource.GrantedServiceAccount!.OrganizationId,
serviceAccountIdToCheck: resource.GrantedServiceAccountId);
if (access.Write)
{
context.Succeed(requirement);
}
}
private async Task CanCreateAsync(AuthorizationHandlerContext context,
AccessPolicyOperationRequirement requirement, GroupServiceAccountAccessPolicy resource)
{
var group = await _groupRepository.GetByIdAsync(resource.GroupId!.Value);
if (group.OrganizationId != resource.GrantedServiceAccount!.OrganizationId)
{
return;
}
var access = await GetAccessAsync(context, resource.GrantedServiceAccount!.OrganizationId,
serviceAccountIdToCheck: resource.GrantedServiceAccountId);
if (access.Write)
{
context.Succeed(requirement);
}
}
private async Task<(bool Read, bool Write)> GetAccessPolicyAccessAsync(AuthorizationHandlerContext context,
BaseAccessPolicy resource) =>
resource switch
{
UserProjectAccessPolicy ap => await GetAccessAsync(context, ap.GrantedProject!.OrganizationId,
ap.GrantedProjectId),
GroupProjectAccessPolicy ap => await GetAccessAsync(context, ap.GrantedProject!.OrganizationId,
ap.GrantedProjectId),
ServiceAccountProjectAccessPolicy ap => await GetAccessAsync(context, ap.GrantedProject!.OrganizationId,
ap.GrantedProjectId),
UserServiceAccountAccessPolicy ap => await GetAccessAsync(context, ap.GrantedServiceAccount!.OrganizationId,
serviceAccountIdToCheck: ap.GrantedServiceAccountId),
GroupServiceAccountAccessPolicy ap => await GetAccessAsync(context,
ap.GrantedServiceAccount!.OrganizationId, serviceAccountIdToCheck: ap.GrantedServiceAccountId),
_ => throw new ArgumentException("Unsupported access policy type provided."),
};
private async Task<(bool Read, bool Write)> GetAccessAsync(AuthorizationHandlerContext context,
Guid organizationId, Guid? projectIdToCheck = null,
Guid? serviceAccountIdToCheck = null)
{
if (!_currentContext.AccessSecretsManager(organizationId))
{
return (false, false);
}
var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(context.User, organizationId);
// Only users and admins should be able to manipulate access policies
if (accessClient != AccessClientType.User && accessClient != AccessClientType.NoAccessCheck)
{
return (false, false);
}
if (projectIdToCheck.HasValue && serviceAccountIdToCheck.HasValue)
{
var projectAccess =
await _projectRepository.AccessToProjectAsync(projectIdToCheck.Value, userId, accessClient);
var serviceAccountAccess =
await _serviceAccountRepository.AccessToServiceAccountAsync(serviceAccountIdToCheck.Value, userId,
accessClient);
return (
projectAccess.Read && serviceAccountAccess.Read,
projectAccess.Write && serviceAccountAccess.Write);
}
if (projectIdToCheck.HasValue)
{
return await _projectRepository.AccessToProjectAsync(projectIdToCheck.Value, userId, accessClient);
}
if (serviceAccountIdToCheck.HasValue)
{
return await _serviceAccountRepository.AccessToServiceAccountAsync(serviceAccountIdToCheck.Value, userId,
accessClient);
}
throw new ArgumentException("No ID to check provided.");
}
}

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,107 @@
#nullable enable
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Enums.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Microsoft.AspNetCore.Authorization;
namespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
public class ProjectServiceAccountsAccessPoliciesAuthorizationHandler : AuthorizationHandler<
ProjectServiceAccountsAccessPoliciesOperationRequirement,
ProjectServiceAccountsAccessPoliciesUpdates>
{
private readonly IAccessClientQuery _accessClientQuery;
private readonly ICurrentContext _currentContext;
private readonly IProjectRepository _projectRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
public ProjectServiceAccountsAccessPoliciesAuthorizationHandler(ICurrentContext currentContext,
IAccessClientQuery accessClientQuery,
IProjectRepository projectRepository,
IServiceAccountRepository serviceAccountRepository)
{
_currentContext = currentContext;
_accessClientQuery = accessClientQuery;
_serviceAccountRepository = serviceAccountRepository;
_projectRepository = projectRepository;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
ProjectServiceAccountsAccessPoliciesOperationRequirement requirement,
ProjectServiceAccountsAccessPoliciesUpdates resource)
{
if (!_currentContext.AccessSecretsManager(resource.OrganizationId))
{
return;
}
// Only users and admins should be able to manipulate access policies
var (accessClient, userId) =
await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);
if (accessClient != AccessClientType.User && accessClient != AccessClientType.NoAccessCheck)
{
return;
}
switch (requirement)
{
case not null when requirement == ProjectServiceAccountsAccessPoliciesOperations.Updates:
await CanUpdateAsync(context, requirement, resource, accessClient,
userId);
break;
default:
throw new ArgumentException("Unsupported operation requirement type provided.",
nameof(requirement));
}
}
private async Task CanUpdateAsync(AuthorizationHandlerContext context,
ProjectServiceAccountsAccessPoliciesOperationRequirement requirement,
ProjectServiceAccountsAccessPoliciesUpdates resource,
AccessClientType accessClient, Guid userId)
{
var access =
await _projectRepository.AccessToProjectAsync(resource.ProjectId, userId,
accessClient);
if (!access.Write)
{
return;
}
var serviceAccountIds = resource.ServiceAccountAccessPolicyUpdates.Select(update =>
update.AccessPolicy.ServiceAccountId!.Value).ToList();
var inSameOrganization =
await _serviceAccountRepository.ServiceAccountsAreInOrganizationAsync(serviceAccountIds,
resource.OrganizationId);
if (!inSameOrganization)
{
return;
}
// Users can only create access policies for service accounts they have access to.
// User can delete and update any service account access policy if they have write access to the project.
var serviceAccountIdsToCheck = resource.ServiceAccountAccessPolicyUpdates
.Where(update => update.Operation == AccessPolicyOperation.Create).Select(update =>
update.AccessPolicy.ServiceAccountId!.Value).ToList();
if (serviceAccountIdsToCheck.Count == 0)
{
context.Succeed(requirement);
return;
}
var serviceAccountsAccess =
await _serviceAccountRepository.AccessToServiceAccountsAsync(serviceAccountIdsToCheck, userId,
accessClient);
if (serviceAccountsAccess.Count == serviceAccountIdsToCheck.Count &&
serviceAccountsAccess.All(a => a.Value.Write))
{
context.Succeed(requirement);
}
}
}

View File

@ -0,0 +1,88 @@
#nullable enable
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Microsoft.AspNetCore.Authorization;
namespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
public class ServiceAccountGrantedPoliciesAuthorizationHandler : AuthorizationHandler<
ServiceAccountGrantedPoliciesOperationRequirement,
ServiceAccountGrantedPoliciesUpdates>
{
private readonly IAccessClientQuery _accessClientQuery;
private readonly ICurrentContext _currentContext;
private readonly IProjectRepository _projectRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
public ServiceAccountGrantedPoliciesAuthorizationHandler(ICurrentContext currentContext,
IAccessClientQuery accessClientQuery,
IProjectRepository projectRepository,
IServiceAccountRepository serviceAccountRepository)
{
_currentContext = currentContext;
_accessClientQuery = accessClientQuery;
_serviceAccountRepository = serviceAccountRepository;
_projectRepository = projectRepository;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
ServiceAccountGrantedPoliciesOperationRequirement requirement,
ServiceAccountGrantedPoliciesUpdates resource)
{
if (!_currentContext.AccessSecretsManager(resource.OrganizationId))
{
return;
}
// Only users and admins should be able to manipulate access policies
var (accessClient, userId) =
await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);
if (accessClient != AccessClientType.User && accessClient != AccessClientType.NoAccessCheck)
{
return;
}
switch (requirement)
{
case not null when requirement == ServiceAccountGrantedPoliciesOperations.Updates:
await CanUpdateAsync(context, requirement, resource, accessClient,
userId);
break;
default:
throw new ArgumentException("Unsupported operation requirement type provided.",
nameof(requirement));
}
}
private async Task CanUpdateAsync(AuthorizationHandlerContext context,
ServiceAccountGrantedPoliciesOperationRequirement requirement, ServiceAccountGrantedPoliciesUpdates resource,
AccessClientType accessClient, Guid userId)
{
var access =
await _serviceAccountRepository.AccessToServiceAccountAsync(resource.ServiceAccountId, userId,
accessClient);
if (access.Write)
{
var projectIdsToCheck = resource.ProjectGrantedPolicyUpdates.Select(update =>
update.AccessPolicy.GrantedProjectId!.Value).ToList();
var sameOrganization =
await _projectRepository.ProjectsAreInOrganization(projectIdsToCheck, resource.OrganizationId);
if (!sameOrganization)
{
return;
}
var projectsAccess =
await _projectRepository.AccessToProjectsAsync(projectIdsToCheck, userId, accessClient);
if (projectsAccess.Count == projectIdsToCheck.Count && projectsAccess.All(a => a.Value.Write))
{
context.Succeed(requirement);
}
}
}
}

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

@ -1,33 +0,0 @@
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
public class CreateAccessPoliciesCommand : ICreateAccessPoliciesCommand
{
private readonly IAccessPolicyRepository _accessPolicyRepository;
public CreateAccessPoliciesCommand(IAccessPolicyRepository accessPolicyRepository)
{
_accessPolicyRepository = accessPolicyRepository;
}
public async Task<IEnumerable<BaseAccessPolicy>> CreateManyAsync(List<BaseAccessPolicy> accessPolicies)
{
await CheckAccessPoliciesDoNotExistAsync(accessPolicies);
return await _accessPolicyRepository.CreateManyAsync(accessPolicies);
}
private async Task CheckAccessPoliciesDoNotExistAsync(List<BaseAccessPolicy> accessPolicies)
{
foreach (var accessPolicy in accessPolicies)
{
if (await _accessPolicyRepository.AccessPolicyExists(accessPolicy))
{
throw new BadRequestException("Resource already exists");
}
}
}
}

View File

@ -1,19 +0,0 @@
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
public class DeleteAccessPolicyCommand : IDeleteAccessPolicyCommand
{
private readonly IAccessPolicyRepository _accessPolicyRepository;
public DeleteAccessPolicyCommand(IAccessPolicyRepository accessPolicyRepository)
{
_accessPolicyRepository = accessPolicyRepository;
}
public async Task DeleteAsync(Guid id)
{
await _accessPolicyRepository.DeleteAsync(id);
}
}

View File

@ -1,31 +0,0 @@
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
public class UpdateAccessPolicyCommand : IUpdateAccessPolicyCommand
{
private readonly IAccessPolicyRepository _accessPolicyRepository;
public UpdateAccessPolicyCommand(IAccessPolicyRepository accessPolicyRepository)
{
_accessPolicyRepository = accessPolicyRepository;
}
public async Task<BaseAccessPolicy> UpdateAsync(Guid id, bool read, bool write)
{
var accessPolicy = await _accessPolicyRepository.GetByIdAsync(id);
if (accessPolicy == null)
{
throw new NotFoundException();
}
accessPolicy.Read = read;
accessPolicy.Write = write;
accessPolicy.RevisionDate = DateTime.UtcNow;
await _accessPolicyRepository.ReplaceAsync(accessPolicy);
return accessPolicy;
}
}

View File

@ -0,0 +1,26 @@
#nullable enable
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
public class UpdateProjectServiceAccountsAccessPoliciesCommand : IUpdateProjectServiceAccountsAccessPoliciesCommand
{
private readonly IAccessPolicyRepository _accessPolicyRepository;
public UpdateProjectServiceAccountsAccessPoliciesCommand(IAccessPolicyRepository accessPolicyRepository)
{
_accessPolicyRepository = accessPolicyRepository;
}
public async Task UpdateAsync(ProjectServiceAccountsAccessPoliciesUpdates accessPoliciesUpdates)
{
if (!accessPoliciesUpdates.ServiceAccountAccessPolicyUpdates.Any())
{
return;
}
await _accessPolicyRepository.UpdateProjectServiceAccountsAccessPoliciesAsync(accessPoliciesUpdates);
}
}

View File

@ -0,0 +1,26 @@
#nullable enable
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
public class UpdateServiceAccountGrantedPoliciesCommand : IUpdateServiceAccountGrantedPoliciesCommand
{
private readonly IAccessPolicyRepository _accessPolicyRepository;
public UpdateServiceAccountGrantedPoliciesCommand(IAccessPolicyRepository accessPolicyRepository)
{
_accessPolicyRepository = accessPolicyRepository;
}
public async Task UpdateAsync(ServiceAccountGrantedPoliciesUpdates grantedPoliciesUpdates)
{
if (!grantedPoliciesUpdates.ProjectGrantedPolicyUpdates.Any())
{
return;
}
await _accessPolicyRepository.UpdateServiceAccountGrantedPoliciesAsync(grantedPoliciesUpdates);
}
}

View File

@ -0,0 +1,44 @@
#nullable enable
using Bit.Core.SecretsManager.Enums.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
public class ProjectServiceAccountsAccessPoliciesUpdatesQuery : IProjectServiceAccountsAccessPoliciesUpdatesQuery
{
private readonly IAccessPolicyRepository _accessPolicyRepository;
public ProjectServiceAccountsAccessPoliciesUpdatesQuery(IAccessPolicyRepository accessPolicyRepository)
{
_accessPolicyRepository = accessPolicyRepository;
}
public async Task<ProjectServiceAccountsAccessPoliciesUpdates> GetAsync(
ProjectServiceAccountsAccessPolicies projectServiceAccountsAccessPolicies)
{
var currentPolicies =
await _accessPolicyRepository.GetProjectServiceAccountsAccessPoliciesAsync(
projectServiceAccountsAccessPolicies.ProjectId);
if (currentPolicies == null)
{
return new ProjectServiceAccountsAccessPoliciesUpdates
{
ProjectId = projectServiceAccountsAccessPolicies.ProjectId,
OrganizationId = projectServiceAccountsAccessPolicies.OrganizationId,
ServiceAccountAccessPolicyUpdates =
projectServiceAccountsAccessPolicies.ServiceAccountAccessPolicies.Select(p =>
new ServiceAccountProjectAccessPolicyUpdate
{
Operation = AccessPolicyOperation.Create,
AccessPolicy = p
})
};
}
return currentPolicies.GetPolicyUpdates(projectServiceAccountsAccessPolicies);
}
}

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

@ -0,0 +1,41 @@
#nullable enable
using Bit.Core.SecretsManager.Enums.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
public class ServiceAccountGrantedPolicyUpdatesQuery : IServiceAccountGrantedPolicyUpdatesQuery
{
private readonly IAccessPolicyRepository _accessPolicyRepository;
public ServiceAccountGrantedPolicyUpdatesQuery(IAccessPolicyRepository accessPolicyRepository)
{
_accessPolicyRepository = accessPolicyRepository;
}
public async Task<ServiceAccountGrantedPoliciesUpdates> GetAsync(
ServiceAccountGrantedPolicies grantedPolicies)
{
var currentPolicies =
await _accessPolicyRepository.GetServiceAccountGrantedPoliciesAsync(grantedPolicies.ServiceAccountId);
if (currentPolicies == null)
{
return new ServiceAccountGrantedPoliciesUpdates
{
ServiceAccountId = grantedPolicies.ServiceAccountId,
OrganizationId = grantedPolicies.OrganizationId,
ProjectGrantedPolicyUpdates = grantedPolicies.ProjectGrantedPolicies.Select(p =>
new ServiceAccountProjectAccessPolicyUpdate
{
Operation = AccessPolicyOperation.Create,
AccessPolicy = p
})
};
}
return currentPolicies.GetPolicyUpdates(grantedPolicies);
}
}

View File

@ -0,0 +1,50 @@
#nullable enable
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Queries.Secrets.Interfaces;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Queries.Secrets;
public class SecretsSyncQuery : ISecretsSyncQuery
{
private readonly ISecretRepository _secretRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
public SecretsSyncQuery(
ISecretRepository secretRepository,
IServiceAccountRepository serviceAccountRepository)
{
_secretRepository = secretRepository;
_serviceAccountRepository = serviceAccountRepository;
}
public async Task<(bool HasChanges, IEnumerable<Secret>? Secrets)> GetAsync(SecretsSyncRequest syncRequest)
{
if (syncRequest.LastSyncedDate == null)
{
return await GetSecretsAsync(syncRequest);
}
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(syncRequest.ServiceAccountId);
if (serviceAccount == null)
{
throw new NotFoundException();
}
if (syncRequest.LastSyncedDate.Value <= serviceAccount.RevisionDate)
{
return await GetSecretsAsync(syncRequest);
}
return (HasChanges: false, null);
}
private async Task<(bool HasChanges, IEnumerable<Secret>? Secrets)> GetSecretsAsync(SecretsSyncRequest syncRequest)
{
var secrets = await _secretRepository.GetManyByOrganizationIdAsync(syncRequest.OrganizationId,
syncRequest.ServiceAccountId, syncRequest.AccessClientType);
return (HasChanges: true, Secrets: secrets);
}
}

View File

@ -26,7 +26,7 @@ public class CountNewServiceAccountSlotsRequiredQuery : ICountNewServiceAccountS
throw new NotFoundException();
}
if (!organization.SmServiceAccounts.HasValue || serviceAccountsToAdd == 0 || organization.SecretsManagerBeta)
if (!organization.SmServiceAccounts.HasValue || serviceAccountsToAdd == 0)
{
return 0;
}

View File

@ -10,7 +10,9 @@ 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.Secrets;
using Bit.Commercial.Core.SecretsManager.Queries.ServiceAccounts;
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;
@ -19,8 +21,10 @@ 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.Secrets.Interfaces;
using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
@ -34,11 +38,17 @@ public static class SecretsManagerCollectionExtensions
services.AddScoped<IAuthorizationHandler, ProjectAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, SecretAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, ServiceAccountAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, AccessPolicyAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, ProjectPeopleAccessPoliciesAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, ServiceAccountPeopleAccessPoliciesAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, ServiceAccountGrantedPoliciesAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, ProjectServiceAccountsAccessPoliciesAuthorizationHandler>();
services.AddScoped<IAccessClientQuery, AccessClientQuery>();
services.AddScoped<IMaxProjectsQuery, MaxProjectsQuery>();
services.AddScoped<ISameOrganizationQuery, SameOrganizationQuery>();
services.AddScoped<IServiceAccountSecretsDetailsQuery, ServiceAccountSecretsDetailsQuery>();
services.AddScoped<IServiceAccountGrantedPolicyUpdatesQuery, ServiceAccountGrantedPolicyUpdatesQuery>();
services.AddScoped<ISecretsSyncQuery, SecretsSyncQuery>();
services.AddScoped<IProjectServiceAccountsAccessPoliciesUpdatesQuery, ProjectServiceAccountsAccessPoliciesUpdatesQuery>();
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
services.AddScoped<IDeleteSecretCommand, DeleteSecretCommand>();
@ -51,11 +61,10 @@ public static class SecretsManagerCollectionExtensions
services.AddScoped<ICountNewServiceAccountSlotsRequiredQuery, CountNewServiceAccountSlotsRequiredQuery>();
services.AddScoped<IRevokeAccessTokensCommand, RevokeAccessTokensCommand>();
services.AddScoped<ICreateAccessTokenCommand, CreateAccessTokenCommand>();
services.AddScoped<ICreateAccessPoliciesCommand, CreateAccessPoliciesCommand>();
services.AddScoped<IUpdateAccessPolicyCommand, UpdateAccessPolicyCommand>();
services.AddScoped<IDeleteAccessPolicyCommand, DeleteAccessPolicyCommand>();
services.AddScoped<IImportCommand, ImportCommand>();
services.AddScoped<IEmptyTrashCommand, EmptyTrashCommand>();
services.AddScoped<IRestoreTrashCommand, RestoreTrashCommand>();
services.AddScoped<IUpdateServiceAccountGrantedPoliciesCommand, UpdateServiceAccountGrantedPoliciesCommand>();
services.AddScoped<IUpdateProjectServiceAccountsAccessPoliciesCommand, UpdateProjectServiceAccountsAccessPoliciesCommand>();
}
}

View File

@ -12,5 +12,6 @@ public static class ServiceCollectionExtensions
{
services.AddScoped<IProviderService, ProviderService>();
services.AddScoped<ICreateProviderCommand, CreateProviderCommand>();
services.AddScoped<IRemoveOrganizationFromProviderCommand, RemoveOrganizationFromProviderCommand>();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,8 @@
using System.Linq.Expressions;
using AutoMapper;
using AutoMapper;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Enums.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
using Bit.Core.SecretsManager.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.SecretsManager.Discriminators;
@ -19,16 +20,13 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
{
}
private static Expression<Func<ServiceAccountProjectAccessPolicy, bool>> UserHasWriteAccessToProject(Guid userId) =>
policy =>
policy.GrantedProject.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
policy.GrantedProject.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write));
public async Task<List<Core.SecretsManager.Entities.BaseAccessPolicy>> CreateManyAsync(List<Core.SecretsManager.Entities.BaseAccessPolicy> baseAccessPolicies)
{
using var scope = ServiceScopeFactory.CreateScope();
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
await using var transaction = await dbContext.Database.BeginTransactionAsync();
var serviceAccountIds = new List<Guid>();
foreach (var baseAccessPolicy in baseAccessPolicies)
{
baseAccessPolicy.SetNewId();
@ -64,182 +62,25 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
{
var entity = Mapper.Map<ServiceAccountProjectAccessPolicy>(accessPolicy);
await dbContext.AddAsync(entity);
serviceAccountIds.Add(entity.ServiceAccountId!.Value);
break;
}
}
}
if (serviceAccountIds.Count > 0)
{
var utcNow = DateTime.UtcNow;
await dbContext.ServiceAccount
.Where(sa => serviceAccountIds.Contains(sa.Id))
.ExecuteUpdateAsync(setters => setters.SetProperty(sa => sa.RevisionDate, utcNow));
}
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
return baseAccessPolicies;
}
public async Task<bool> AccessPolicyExists(Core.SecretsManager.Entities.BaseAccessPolicy baseAccessPolicy)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
switch (baseAccessPolicy)
{
case Core.SecretsManager.Entities.UserProjectAccessPolicy accessPolicy:
{
var policy = await dbContext.UserProjectAccessPolicy
.Where(c => c.OrganizationUserId == accessPolicy.OrganizationUserId &&
c.GrantedProjectId == accessPolicy.GrantedProjectId)
.FirstOrDefaultAsync();
return policy != null;
}
case Core.SecretsManager.Entities.GroupProjectAccessPolicy accessPolicy:
{
var policy = await dbContext.GroupProjectAccessPolicy
.Where(c => c.GroupId == accessPolicy.GroupId &&
c.GrantedProjectId == accessPolicy.GrantedProjectId)
.FirstOrDefaultAsync();
return policy != null;
}
case Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy accessPolicy:
{
var policy = await dbContext.ServiceAccountProjectAccessPolicy
.Where(c => c.ServiceAccountId == accessPolicy.ServiceAccountId &&
c.GrantedProjectId == accessPolicy.GrantedProjectId)
.FirstOrDefaultAsync();
return policy != null;
}
case Core.SecretsManager.Entities.UserServiceAccountAccessPolicy accessPolicy:
{
var policy = await dbContext.UserServiceAccountAccessPolicy
.Where(c => c.OrganizationUserId == accessPolicy.OrganizationUserId &&
c.GrantedServiceAccountId == accessPolicy.GrantedServiceAccountId)
.FirstOrDefaultAsync();
return policy != null;
}
case Core.SecretsManager.Entities.GroupServiceAccountAccessPolicy accessPolicy:
{
var policy = await dbContext.GroupServiceAccountAccessPolicy
.Where(c => c.GroupId == accessPolicy.GroupId &&
c.GrantedServiceAccountId == accessPolicy.GrantedServiceAccountId)
.FirstOrDefaultAsync();
return policy != null;
}
default:
throw new ArgumentException("Unsupported access policy type provided.", nameof(baseAccessPolicy));
}
}
public async Task<Core.SecretsManager.Entities.BaseAccessPolicy?> GetByIdAsync(Guid id)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var entity = await dbContext.AccessPolicies.Where(ap => ap.Id == id)
.Include(ap => ((UserProjectAccessPolicy)ap).OrganizationUser.User)
.Include(ap => ((UserProjectAccessPolicy)ap).GrantedProject)
.Include(ap => ((GroupProjectAccessPolicy)ap).Group)
.Include(ap => ((GroupProjectAccessPolicy)ap).GrantedProject)
.Include(ap => ((ServiceAccountProjectAccessPolicy)ap).ServiceAccount)
.Include(ap => ((ServiceAccountProjectAccessPolicy)ap).GrantedProject)
.Include(ap => ((UserServiceAccountAccessPolicy)ap).OrganizationUser.User)
.Include(ap => ((UserServiceAccountAccessPolicy)ap).GrantedServiceAccount)
.Include(ap => ((GroupServiceAccountAccessPolicy)ap).Group)
.Include(ap => ((GroupServiceAccountAccessPolicy)ap).GrantedServiceAccount)
.FirstOrDefaultAsync();
return entity == null ? null : MapToCore(entity);
}
public async Task ReplaceAsync(Core.SecretsManager.Entities.BaseAccessPolicy baseAccessPolicy)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var entity = await dbContext.AccessPolicies.FindAsync(baseAccessPolicy.Id);
if (entity != null)
{
dbContext.AccessPolicies.Attach(entity);
entity.Write = baseAccessPolicy.Write;
entity.Read = baseAccessPolicy.Read;
entity.RevisionDate = baseAccessPolicy.RevisionDate;
await dbContext.SaveChangesAsync();
}
}
public async Task<IEnumerable<Core.SecretsManager.Entities.BaseAccessPolicy>> GetManyByGrantedProjectIdAsync(Guid id, Guid userId)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var entities = await dbContext.AccessPolicies.Where(ap =>
((UserProjectAccessPolicy)ap).GrantedProjectId == id ||
((GroupProjectAccessPolicy)ap).GrantedProjectId == id ||
((ServiceAccountProjectAccessPolicy)ap).GrantedProjectId == id)
.Include(ap => ((UserProjectAccessPolicy)ap).OrganizationUser.User)
.Include(ap => ((GroupProjectAccessPolicy)ap).Group)
.Include(ap => ((ServiceAccountProjectAccessPolicy)ap).ServiceAccount)
.Select(ap => new
{
ap,
CurrentUserInGroup = ap is GroupProjectAccessPolicy &&
((GroupProjectAccessPolicy)ap).Group.GroupUsers.Any(g =>
g.OrganizationUser.User.Id == userId),
})
.ToListAsync();
return entities.Select(e => MapToCore(e.ap, e.CurrentUserInGroup));
}
public async Task<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();
var dbContext = GetDatabaseContext(scope);
var entity = await dbContext.AccessPolicies.FindAsync(id);
if (entity != null)
{
dbContext.Remove(entity);
await dbContext.SaveChangesAsync();
}
}
public async Task<IEnumerable<Core.SecretsManager.Entities.BaseAccessPolicy>> GetManyByServiceAccountIdAsync(Guid id, Guid userId,
AccessClientType accessType)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var query = dbContext.ServiceAccountProjectAccessPolicy.Where(ap =>
ap.ServiceAccountId == id);
query = accessType switch
{
AccessClientType.NoAccessCheck => query,
AccessClientType.User => query.Where(UserHasWriteAccessToProject(userId)),
_ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),
};
var entities = await query
.Include(ap => ap.ServiceAccount)
.Include(ap => ap.GrantedProject)
.ToListAsync();
return entities.Select(MapToCore);
}
public async Task<PeopleGrantees> GetPeopleGranteesAsync(Guid organizationId, Guid currentUserId)
{
using var scope = ServiceScopeFactory.CreateScope();
@ -352,6 +193,208 @@ 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);
}
public async Task<ServiceAccountGrantedPolicies?> GetServiceAccountGrantedPoliciesAsync(Guid serviceAccountId)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var entities = await dbContext.ServiceAccountProjectAccessPolicy
.Where(ap => ap.ServiceAccountId == serviceAccountId)
.Include(ap => ap.ServiceAccount)
.Include(ap => ap.GrantedProject)
.ToListAsync();
if (entities.Count == 0)
{
return null;
}
return new ServiceAccountGrantedPolicies(serviceAccountId, entities.Select(MapToCore).ToList());
}
public async Task<ServiceAccountGrantedPoliciesPermissionDetails?>
GetServiceAccountGrantedPoliciesPermissionDetailsAsync(Guid serviceAccountId, Guid userId,
AccessClientType accessClientType)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var accessPolicyQuery = dbContext.ServiceAccountProjectAccessPolicy
.Where(ap => ap.ServiceAccountId == serviceAccountId)
.Include(ap => ap.ServiceAccount)
.Include(ap => ap.GrantedProject);
var accessPoliciesPermissionDetails =
await ToPermissionDetails(accessPolicyQuery, userId, accessClientType).ToListAsync();
if (accessPoliciesPermissionDetails.Count == 0)
{
return null;
}
return new ServiceAccountGrantedPoliciesPermissionDetails
{
ServiceAccountId = serviceAccountId,
OrganizationId = accessPoliciesPermissionDetails.First().AccessPolicy.GrantedProject!.OrganizationId,
ProjectGrantedPolicies = accessPoliciesPermissionDetails
};
}
public async Task UpdateServiceAccountGrantedPoliciesAsync(ServiceAccountGrantedPoliciesUpdates updates)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var currentAccessPolicies = await dbContext.ServiceAccountProjectAccessPolicy
.Where(ap => ap.ServiceAccountId == updates.ServiceAccountId)
.ToListAsync();
if (currentAccessPolicies.Count != 0)
{
var projectIdsToDelete = updates.ProjectGrantedPolicyUpdates
.Where(pu => pu.Operation == AccessPolicyOperation.Delete)
.Select(pu => pu.AccessPolicy.GrantedProjectId!.Value)
.ToList();
var policiesToDelete = currentAccessPolicies
.Where(entity => projectIdsToDelete.Contains(entity.GrantedProjectId!.Value))
.ToList();
dbContext.RemoveRange(policiesToDelete);
}
await UpsertServiceAccountProjectPoliciesAsync(dbContext, currentAccessPolicies,
updates.ProjectGrantedPolicyUpdates.Where(pu => pu.Operation != AccessPolicyOperation.Delete).ToList());
await UpdateServiceAccountRevisionAsync(dbContext, updates.ServiceAccountId);
await dbContext.SaveChangesAsync();
}
public async Task<ProjectServiceAccountsAccessPolicies?> GetProjectServiceAccountsAccessPoliciesAsync(Guid projectId)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var entities = await dbContext.ServiceAccountProjectAccessPolicy
.Where(ap => ap.GrantedProjectId == projectId)
.Include(ap => ap.ServiceAccount)
.Include(ap => ap.GrantedProject)
.ToListAsync();
if (entities.Count == 0)
{
return null;
}
return new ProjectServiceAccountsAccessPolicies(projectId, entities.Select(MapToCore).ToList());
}
public async Task UpdateProjectServiceAccountsAccessPoliciesAsync(
ProjectServiceAccountsAccessPoliciesUpdates updates)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
await using var transaction = await dbContext.Database.BeginTransactionAsync();
var currentAccessPolicies = await dbContext.ServiceAccountProjectAccessPolicy
.Where(ap => ap.GrantedProjectId == updates.ProjectId)
.ToListAsync();
if (currentAccessPolicies.Count != 0)
{
var serviceAccountIdsToDelete = updates.ServiceAccountAccessPolicyUpdates
.Where(pu => pu.Operation == AccessPolicyOperation.Delete)
.Select(pu => pu.AccessPolicy.ServiceAccountId!.Value)
.ToList();
var accessPolicyIdsToDelete = currentAccessPolicies
.Where(entity => serviceAccountIdsToDelete.Contains(entity.ServiceAccountId!.Value))
.Select(ap => ap.Id)
.ToList();
await dbContext.ServiceAccountProjectAccessPolicy
.Where(ap => accessPolicyIdsToDelete.Contains(ap.Id))
.ExecuteDeleteAsync();
}
await UpsertServiceAccountProjectPoliciesAsync(dbContext, currentAccessPolicies,
updates.ServiceAccountAccessPolicyUpdates.Where(update => update.Operation != AccessPolicyOperation.Delete)
.ToList());
var effectedServiceAccountIds = updates.ServiceAccountAccessPolicyUpdates
.Select(sa => sa.AccessPolicy.ServiceAccountId!.Value).ToList();
await UpdateServiceAccountsRevisionAsync(dbContext, effectedServiceAccountIds);
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
}
private static async Task UpsertPeoplePoliciesAsync(DatabaseContext dbContext,
List<BaseAccessPolicy> policies, IReadOnlyCollection<AccessPolicy> userPolicyEntities,
IReadOnlyCollection<AccessPolicy> groupPolicyEntities)
@ -387,6 +430,37 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
}
}
private async Task UpsertServiceAccountProjectPoliciesAsync(DatabaseContext dbContext,
IReadOnlyCollection<ServiceAccountProjectAccessPolicy> currentPolices,
List<ServiceAccountProjectAccessPolicyUpdate> policyUpdates)
{
var currentDate = DateTime.UtcNow;
foreach (var policyUpdate in policyUpdates)
{
var updatedEntity = MapToEntity(policyUpdate.AccessPolicy);
var currentEntity = currentPolices.FirstOrDefault(e =>
e.GrantedProjectId == policyUpdate.AccessPolicy.GrantedProjectId!.Value &&
e.ServiceAccountId == policyUpdate.AccessPolicy.ServiceAccountId!.Value);
switch (policyUpdate.Operation)
{
case AccessPolicyOperation.Create when currentEntity == null:
updatedEntity.SetNewId();
await dbContext.AddAsync(updatedEntity);
break;
case AccessPolicyOperation.Update when currentEntity != null:
dbContext.AccessPolicies.Attach(currentEntity);
currentEntity.Read = updatedEntity.Read;
currentEntity.Write = updatedEntity.Write;
currentEntity.RevisionDate = currentDate;
break;
default:
throw new InvalidOperationException("Policy updates failed due to unexpected state.");
}
}
}
private Core.SecretsManager.Entities.BaseAccessPolicy MapToCore(
BaseAccessPolicy baseAccessPolicyEntity) =>
baseAccessPolicyEntity switch
@ -441,4 +515,51 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
return MapToCore(baseAccessPolicyEntity);
}
}
private IQueryable<ServiceAccountProjectAccessPolicyPermissionDetails> ToPermissionDetails(
IQueryable<ServiceAccountProjectAccessPolicy>
query, Guid userId, AccessClientType accessClientType)
{
var permissionDetails = accessClientType switch
{
AccessClientType.NoAccessCheck => query.Select(ap => new ServiceAccountProjectAccessPolicyPermissionDetails
{
AccessPolicy =
Mapper.Map<Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy>(ap),
HasPermission = true
}),
AccessClientType.User => query.Select(ap => new ServiceAccountProjectAccessPolicyPermissionDetails
{
AccessPolicy =
Mapper.Map<Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy>(ap),
HasPermission =
(ap.GrantedProject.UserAccessPolicies.Any(p => p.OrganizationUser.UserId == userId && p.Write) ||
ap.GrantedProject.GroupAccessPolicies.Any(p =>
p.Group.GroupUsers.Any(gu => gu.OrganizationUser.UserId == userId && p.Write))) &&
(ap.ServiceAccount.UserAccessPolicies.Any(p => p.OrganizationUser.UserId == userId && p.Write) ||
ap.ServiceAccount.GroupAccessPolicies.Any(p =>
p.Group.GroupUsers.Any(gu => gu.OrganizationUser.UserId == userId && p.Write)))
}),
_ => throw new ArgumentOutOfRangeException(nameof(accessClientType), accessClientType, null)
};
return permissionDetails;
}
private static async Task UpdateServiceAccountRevisionAsync(DatabaseContext dbContext, Guid serviceAccountId)
{
var entity = await dbContext.ServiceAccount.FindAsync(serviceAccountId);
if (entity != null)
{
entity.RevisionDate = DateTime.UtcNow;
}
}
private static async Task UpdateServiceAccountsRevisionAsync(DatabaseContext dbContext, List<Guid> serviceAccountIds)
{
var utcNow = DateTime.UtcNow;
await dbContext.ServiceAccount
.Where(sa => serviceAccountIds.Contains(sa.Id))
.ExecuteUpdateAsync(setters =>
setters.SetProperty(sa => sa.RevisionDate, utcNow));
}
}

View File

@ -70,23 +70,43 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
{
using var scope = ServiceScopeFactory.CreateScope();
var utcNow = DateTime.UtcNow;
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var projects = dbContext.Project
.Where(c => ids.Contains(c.Id))
.Include(p => p.Secrets);
await projects.ForEachAsync(project =>
await using var transaction = await dbContext.Database.BeginTransactionAsync();
var serviceAccountIds = await dbContext.Project
.Where(p => ids.Contains(p.Id))
.Include(p => p.ServiceAccountAccessPolicies)
.SelectMany(p => p.ServiceAccountAccessPolicies.Select(ap => ap.ServiceAccountId!.Value))
.Distinct()
.ToListAsync();
var secretIds = await dbContext.Project
.Where(p => ids.Contains(p.Id))
.Include(p => p.Secrets)
.SelectMany(p => p.Secrets.Select(s => s.Id))
.Distinct()
.ToListAsync();
var utcNow = DateTime.UtcNow;
if (serviceAccountIds.Count > 0)
{
foreach (var projectSecret in project.Secrets)
{
projectSecret.RevisionDate = utcNow;
}
await dbContext.ServiceAccount
.Where(sa => serviceAccountIds.Contains(sa.Id))
.ExecuteUpdateAsync(setters =>
setters.SetProperty(sa => sa.RevisionDate, utcNow));
}
dbContext.Remove(project);
});
if (secretIds.Count > 0)
{
await dbContext.Secret
.Where(s => secretIds.Contains(s.Id))
.ExecuteUpdateAsync(setters =>
setters.SetProperty(s => s.RevisionDate, utcNow));
}
await dbContext.SaveChangesAsync();
await dbContext.Project.Where(p => ids.Contains(p.Id)).ExecuteDeleteAsync();
await transaction.CommitAsync();
}
public async Task<IEnumerable<Core.SecretsManager.Entities.Project>> GetManyWithSecretsByIds(IEnumerable<Guid> ids)
@ -120,27 +140,8 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
var projectQuery = dbContext.Project
.Where(s => s.Id == id);
var query = accessType switch
{
AccessClientType.NoAccessCheck => projectQuery.Select(_ => new { Read = true, Write = true }),
AccessClientType.User => projectQuery.Select(p => new
{
Read = p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read)
|| p.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),
Write = p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
p.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write)),
}),
AccessClientType.ServiceAccount => projectQuery.Select(p => new
{
Read = p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Read),
Write = p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Write),
}),
_ => projectQuery.Select(_ => new { Read = false, Write = false }),
};
var policy = await query.FirstOrDefaultAsync();
var accessQuery = BuildProjectAccessQuery(projectQuery, userId, accessType);
var policy = await accessQuery.FirstOrDefaultAsync();
return policy == null ? (false, false) : (policy.Read, policy.Write);
}
@ -154,6 +155,46 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
return projectIds.Count == results.Count;
}
public async Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToProjectsAsync(
IEnumerable<Guid> projectIds,
Guid userId,
AccessClientType accessType)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var projectsQuery = dbContext.Project.Where(p => projectIds.Contains(p.Id));
var accessQuery = BuildProjectAccessQuery(projectsQuery, userId, accessType);
return await accessQuery.ToDictionaryAsync(pa => pa.Id, pa => (pa.Read, pa.Write));
}
private record ProjectAccess(Guid Id, bool Read, bool Write);
private static IQueryable<ProjectAccess> BuildProjectAccessQuery(IQueryable<Project> projectQuery, Guid userId,
AccessClientType accessType) =>
accessType switch
{
AccessClientType.NoAccessCheck => projectQuery.Select(p => new ProjectAccess(p.Id, true, true)),
AccessClientType.User => projectQuery.Select(p => new ProjectAccess
(
p.Id,
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
p.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
p.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write))
)),
AccessClientType.ServiceAccount => projectQuery.Select(p => new ProjectAccess
(
p.Id,
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Read),
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Write)
)),
_ => projectQuery.Select(p => new ProjectAccess(p.Id, false, false))
};
private IQueryable<ProjectPermissionDetails> ProjectToPermissionDetails(IQueryable<Project> query, Guid userId, AccessClientType accessType)
{
var projects = accessType switch
@ -199,8 +240,4 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
private static Expression<Func<Project, bool>> ServiceAccountHasReadAccessToProject(Guid serviceAccountId) => p =>
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Read);
private static Expression<Func<Project, bool>> ServiceAccountHasWriteAccessToProject(Guid serviceAccountId) => p =>
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Write);
}

View File

@ -43,7 +43,28 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
}
}
public async Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByOrganizationIdAsync(
Guid organizationId, Guid userId, AccessClientType accessType)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var query = dbContext.Secret
.Include(c => c.Projects)
.Where(c => c.OrganizationId == organizationId && c.DeletedDate == null);
query = accessType switch
{
AccessClientType.NoAccessCheck => query,
AccessClientType.User => query.Where(UserHasReadAccessToSecret(userId)),
AccessClientType.ServiceAccount => query.Where(ServiceAccountHasReadAccessToSecret(userId)),
_ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null)
};
var secrets = await query.OrderBy(c => c.RevisionDate).ToListAsync();
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
}
public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
@ -82,7 +103,7 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
}
}
public async Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdInTrashAsync(Guid organizationId)
public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdInTrashAsync(Guid organizationId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
@ -103,7 +124,7 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
}
}
public async Task<IEnumerable<SecretPermissionDetails>> GetManyByProjectIdAsync(Guid projectId, Guid userId, AccessClientType accessType)
public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByProjectIdAsync(Guid projectId, Guid userId, AccessClientType accessType)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
@ -115,106 +136,124 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
return await secrets.ToListAsync();
}
public override async Task<Core.SecretsManager.Entities.Secret> CreateAsync(Core.SecretsManager.Entities.Secret secret)
public override async Task<Core.SecretsManager.Entities.Secret> CreateAsync(
Core.SecretsManager.Entities.Secret secret)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
secret.SetNewId();
var entity = Mapper.Map<Secret>(secret);
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
secret.SetNewId();
var entity = Mapper.Map<Secret>(secret);
if (secret.Projects?.Count > 0)
await using var transaction = await dbContext.Database.BeginTransactionAsync();
if (secret.Projects?.Count > 0)
{
foreach (var project in entity.Projects)
{
foreach (var p in entity.Projects)
{
dbContext.Attach(p);
}
dbContext.Attach(project);
}
await dbContext.AddAsync(entity);
await dbContext.SaveChangesAsync();
secret.Id = entity.Id;
return secret;
var projectIds = entity.Projects.Select(p => p.Id).ToList();
await UpdateServiceAccountRevisionsByProjectIdsAsync(dbContext, projectIds);
}
await dbContext.AddAsync(entity);
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
secret.Id = entity.Id;
return secret;
}
public async Task<Core.SecretsManager.Entities.Secret> UpdateAsync(Core.SecretsManager.Entities.Secret secret)
{
using (var scope = ServiceScopeFactory.CreateScope())
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var mappedEntity = Mapper.Map<Secret>(secret);
await using var transaction = await dbContext.Database.BeginTransactionAsync();
var entity = await dbContext.Secret
.Include(s => s.Projects)
.FirstAsync(s => s.Id == secret.Id);
var projectsToRemove = entity.Projects.Where(p => mappedEntity.Projects.All(mp => mp.Id != p.Id)).ToList();
var projectsToAdd = mappedEntity.Projects.Where(p => entity.Projects.All(ep => ep.Id != p.Id)).ToList();
foreach (var p in projectsToRemove)
{
var dbContext = GetDatabaseContext(scope);
var mappedEntity = Mapper.Map<Secret>(secret);
var entity = await dbContext.Secret
.Include("Projects")
.FirstAsync(s => s.Id == secret.Id);
foreach (var p in entity.Projects.Where(p => mappedEntity.Projects.All(mp => mp.Id != p.Id)))
{
entity.Projects.Remove(p);
}
// Add new relationships
foreach (var project in mappedEntity.Projects.Where(p => entity.Projects.All(ep => ep.Id != p.Id)))
{
var p = dbContext.AttachToOrGet<Project>(_ => _.Id == project.Id, () => project);
entity.Projects.Add(p);
}
dbContext.Entry(entity).CurrentValues.SetValues(mappedEntity);
await dbContext.SaveChangesAsync();
entity.Projects.Remove(p);
}
foreach (var project in projectsToAdd)
{
var p = dbContext.AttachToOrGet<Project>(x => x.Id == project.Id, () => project);
entity.Projects.Add(p);
}
var projectIds = projectsToRemove.Select(p => p.Id).Concat(projectsToAdd.Select(p => p.Id)).ToList();
if (projectIds.Count > 0)
{
await UpdateServiceAccountRevisionsByProjectIdsAsync(dbContext, projectIds);
}
await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, [entity.Id]);
dbContext.Entry(entity).CurrentValues.SetValues(mappedEntity);
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
return secret;
}
public async Task SoftDeleteManyByIdAsync(IEnumerable<Guid> ids)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var utcNow = DateTime.UtcNow;
var secrets = dbContext.Secret.Where(c => ids.Contains(c.Id));
await secrets.ForEachAsync(secret =>
{
dbContext.Attach(secret);
secret.DeletedDate = utcNow;
secret.RevisionDate = utcNow;
});
await dbContext.SaveChangesAsync();
}
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
await using var transaction = await dbContext.Database.BeginTransactionAsync();
var secretIds = ids.ToList();
await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, secretIds);
var utcNow = DateTime.UtcNow;
await dbContext.Secret.Where(c => secretIds.Contains(c.Id))
.ExecuteUpdateAsync(setters =>
setters.SetProperty(s => s.RevisionDate, utcNow)
.SetProperty(s => s.DeletedDate, utcNow));
await transaction.CommitAsync();
}
public async Task HardDeleteManyByIdAsync(IEnumerable<Guid> ids)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var secrets = dbContext.Secret.Where(c => ids.Contains(c.Id));
await secrets.ForEachAsync(secret =>
{
dbContext.Attach(secret);
dbContext.Remove(secret);
});
await dbContext.SaveChangesAsync();
}
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
await using var transaction = await dbContext.Database.BeginTransactionAsync();
var secretIds = ids.ToList();
await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, secretIds);
await dbContext.Secret.Where(c => secretIds.Contains(c.Id))
.ExecuteDeleteAsync();
await transaction.CommitAsync();
}
public async Task RestoreManyByIdAsync(IEnumerable<Guid> ids)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var utcNow = DateTime.UtcNow;
var secrets = dbContext.Secret.Where(c => ids.Contains(c.Id));
await secrets.ForEachAsync(secret =>
{
dbContext.Attach(secret);
secret.DeletedDate = null;
secret.RevisionDate = utcNow;
});
await dbContext.SaveChangesAsync();
}
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
await using var transaction = await dbContext.Database.BeginTransactionAsync();
var secretIds = ids.ToList();
await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, secretIds);
var utcNow = DateTime.UtcNow;
await dbContext.Secret.Where(c => secretIds.Contains(c.Id))
.ExecuteUpdateAsync(setters =>
setters.SetProperty(s => s.RevisionDate, utcNow)
.SetProperty(s => s.DeletedDate, (DateTime?)null));
await transaction.CommitAsync();
}
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> ImportAsync(IEnumerable<Core.SecretsManager.Entities.Secret> secrets)
@ -248,24 +287,6 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
return secrets;
}
public async Task UpdateRevisionDates(IEnumerable<Guid> ids)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var utcNow = DateTime.UtcNow;
var secrets = dbContext.Secret.Where(s => ids.Contains(s.Id));
await secrets.ForEachAsync(secret =>
{
dbContext.Attach(secret);
secret.RevisionDate = utcNow;
});
await dbContext.SaveChangesAsync();
}
}
public async Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType)
{
using var scope = ServiceScopeFactory.CreateScope();
@ -357,4 +378,60 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.UserId == userId && ap.Read) ||
p.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.UserId == userId && ap.Read)));
private static async Task UpdateServiceAccountRevisionsByProjectIdsAsync(DatabaseContext dbContext,
List<Guid> projectIds)
{
if (projectIds.Count == 0)
{
return;
}
var serviceAccountIds = await dbContext.Project.Where(p => projectIds.Contains(p.Id))
.Include(p => p.ServiceAccountAccessPolicies)
.SelectMany(p => p.ServiceAccountAccessPolicies.Select(ap => ap.ServiceAccountId!.Value))
.Distinct()
.ToListAsync();
await UpdateServiceAccountRevisionsAsync(dbContext, serviceAccountIds);
}
private static async Task UpdateServiceAccountRevisionsBySecretIdsAsync(DatabaseContext dbContext,
List<Guid> secretIds)
{
if (secretIds.Count == 0)
{
return;
}
var projectAccessServiceAccountIds = await dbContext.Secret
.Where(s => secretIds.Contains(s.Id))
.SelectMany(s =>
s.Projects.SelectMany(p => p.ServiceAccountAccessPolicies.Select(ap => ap.ServiceAccountId!.Value)))
.Distinct()
.ToListAsync();
var directAccessServiceAccountIds = await dbContext.Secret
.Where(s => secretIds.Contains(s.Id))
.SelectMany(s => s.ServiceAccountAccessPolicies.Select(ap => ap.ServiceAccountId!.Value))
.Distinct()
.ToListAsync();
var serviceAccountIds =
directAccessServiceAccountIds.Concat(projectAccessServiceAccountIds).Distinct().ToList();
await UpdateServiceAccountRevisionsAsync(dbContext, serviceAccountIds);
}
private static async Task UpdateServiceAccountRevisionsAsync(DatabaseContext dbContext,
List<Guid> serviceAccountIds)
{
if (serviceAccountIds.Count > 0)
{
var utcNow = DateTime.UtcNow;
await dbContext.ServiceAccount
.Where(sa => serviceAccountIds.Contains(sa.Id))
.ExecuteUpdateAsync(setters => setters.SetProperty(b => b.RevisionDate, utcNow));
}
}
}

View File

@ -43,28 +43,6 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
return Mapper.Map<List<Core.SecretsManager.Entities.ServiceAccount>>(serviceAccounts);
}
public async Task<bool> UserHasReadAccessToServiceAccount(Guid id, Guid userId)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var query = dbContext.ServiceAccount
.Where(sa => sa.Id == id)
.Where(UserHasReadAccessToServiceAccount(userId));
return await query.AnyAsync();
}
public async Task<bool> UserHasWriteAccessToServiceAccount(Guid id, Guid userId)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var query = dbContext.ServiceAccount
.Where(sa => sa.Id == id)
.Where(UserHasWriteAccessToServiceAccount(userId));
return await query.AnyAsync();
}
public async Task<IEnumerable<Core.SecretsManager.Entities.ServiceAccount>> GetManyByOrganizationIdWriteAccessAsync(Guid organizationId, Guid userId, AccessClientType accessType)
{
using var scope = ServiceScopeFactory.CreateScope();
@ -84,51 +62,57 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
{
var targetIds = ids.ToList();
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
await using var transaction = await dbContext.Database.BeginTransactionAsync();
// Policies can't have a cascade delete, so we need to delete them manually.
var policies = dbContext.AccessPolicies.Where(ap =>
((ServiceAccountProjectAccessPolicy)ap).ServiceAccountId.HasValue && ids.Contains(((ServiceAccountProjectAccessPolicy)ap).ServiceAccountId!.Value) ||
((GroupServiceAccountAccessPolicy)ap).GrantedServiceAccountId.HasValue && ids.Contains(((GroupServiceAccountAccessPolicy)ap).GrantedServiceAccountId!.Value) ||
((UserServiceAccountAccessPolicy)ap).GrantedServiceAccountId.HasValue && ids.Contains(((UserServiceAccountAccessPolicy)ap).GrantedServiceAccountId!.Value));
dbContext.RemoveRange(policies);
await dbContext.AccessPolicies.Where(ap =>
targetIds.Contains(((ServiceAccountProjectAccessPolicy)ap).ServiceAccountId!.Value) ||
targetIds.Contains(((ServiceAccountSecretAccessPolicy)ap).ServiceAccountId!.Value) ||
targetIds.Contains(((GroupServiceAccountAccessPolicy)ap).GrantedServiceAccountId!.Value) ||
targetIds.Contains(((UserServiceAccountAccessPolicy)ap).GrantedServiceAccountId!.Value))
.ExecuteDeleteAsync();
var apiKeys = dbContext.ApiKeys.Where(a => a.ServiceAccountId.HasValue && ids.Contains(a.ServiceAccountId!.Value));
dbContext.RemoveRange(apiKeys);
await dbContext.ApiKeys
.Where(a => targetIds.Contains(a.ServiceAccountId!.Value))
.ExecuteDeleteAsync();
var serviceAccounts = dbContext.ServiceAccount.Where(c => ids.Contains(c.Id));
dbContext.RemoveRange(serviceAccounts);
await dbContext.SaveChangesAsync();
await dbContext.ServiceAccount
.Where(c => targetIds.Contains(c.Id))
.ExecuteDeleteAsync();
await transaction.CommitAsync();
}
public async Task<(bool Read, bool Write)> AccessToServiceAccountAsync(Guid id, Guid userId,
AccessClientType accessType)
{
using var scope = ServiceScopeFactory.CreateScope();
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var serviceAccount = dbContext.ServiceAccount.Where(sa => sa.Id == id);
var serviceAccountQuery = dbContext.ServiceAccount.Where(sa => sa.Id == id);
var query = accessType switch
{
AccessClientType.NoAccessCheck => serviceAccount.Select(_ => new { Read = true, Write = true }),
AccessClientType.User => serviceAccount.Select(sa => new
{
Read = sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
sa.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),
Write = sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
sa.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write)),
}),
AccessClientType.ServiceAccount => serviceAccount.Select(_ => new { Read = false, Write = false }),
_ => serviceAccount.Select(_ => new { Read = false, Write = false }),
};
var accessQuery = BuildServiceAccountAccessQuery(serviceAccountQuery, userId, accessType);
var access = await accessQuery.FirstOrDefaultAsync();
var policy = await query.FirstOrDefaultAsync();
return access == null ? (false, false) : (access.Read, access.Write);
}
return policy == null ? (false, false) : (policy.Read, policy.Write);
public async Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToServiceAccountsAsync(
IEnumerable<Guid> ids,
Guid userId,
AccessClientType accessType)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var serviceAccountsQuery = dbContext.ServiceAccount.Where(p => ids.Contains(p.Id));
var accessQuery = BuildServiceAccountAccessQuery(serviceAccountsQuery, userId, accessType);
return await accessQuery.ToDictionaryAsync(access => access.Id, access => (access.Read, access.Write));
}
public async Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId)
@ -141,6 +125,15 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
}
}
public async Task<bool> ServiceAccountsAreInOrganizationAsync(List<Guid> serviceAccountIds, Guid organizationId)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var result = await dbContext.ServiceAccount.CountAsync(sa =>
sa.OrganizationId == organizationId && serviceAccountIds.Contains(sa.Id));
return serviceAccountIds.Count == result;
}
public async Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(
Guid organizationId, Guid userId, AccessClientType accessType)
{
@ -179,6 +172,27 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
return results;
}
private record ServiceAccountAccess(Guid Id, bool Read, bool Write);
private static IQueryable<ServiceAccountAccess> BuildServiceAccountAccessQuery(IQueryable<ServiceAccount> serviceAccountQuery, Guid userId,
AccessClientType accessType) =>
accessType switch
{
AccessClientType.NoAccessCheck => serviceAccountQuery.Select(sa => new ServiceAccountAccess(sa.Id, true, true)),
AccessClientType.User => serviceAccountQuery.Select(sa => new ServiceAccountAccess
(
sa.Id,
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
sa.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
sa.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write))
)),
AccessClientType.ServiceAccount => serviceAccountQuery.Select(sa => new ServiceAccountAccess(sa.Id, false, false)),
_ => serviceAccountQuery.Select(sa => new ServiceAccountAccess(sa.Id, false, false))
};
private static Expression<Func<ServiceAccount, bool>> UserHasReadAccessToServiceAccount(Guid userId) => sa =>
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
sa.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read));

View File

@ -1,6 +1,6 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Bit.Core.Settings;

View File

@ -1,6 +1,6 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Settings;

View File

@ -1,7 +1,7 @@
###############################################
# Build stage #
###############################################
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:6.0 AS build
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
# Docker buildx supplies the value for this arg
ARG TARGETPLATFORM
@ -48,7 +48,7 @@ WORKDIR /app
###############################################
# App stage #
###############################################
FROM mcr.microsoft.com/dotnet/aspnet:6.0
FROM mcr.microsoft.com/dotnet/aspnet:8.0
ARG TARGETPLATFORM
LABEL com.bitwarden.product="bitwarden"
@ -58,8 +58,8 @@ EXPOSE 5000
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gosu \
curl \
gosu \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy app from the build stage

View File

@ -1,4 +1,4 @@
using Bit.Core.Entities;
using Bit.Core.AdminConsole.Entities;
using Bit.Scim.Models;
namespace Bit.Scim.Groups.Interfaces;

View File

@ -1,5 +1,4 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Entities;
using Bit.Scim.Models;
namespace Bit.Scim.Groups.Interfaces;

View File

@ -1,5 +1,4 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Entities;
using Bit.Scim.Models;
namespace Bit.Scim.Groups.Interfaces;

View File

@ -1,8 +1,8 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Scim.Groups.Interfaces;

View File

@ -2,7 +2,6 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;

View File

@ -2,7 +2,6 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Scim.Context;

View File

@ -30,10 +30,6 @@
"connectionString": "SECRET",
"applicationCacheTopicName": "SECRET"
},
"documentDb": {
"uri": "SECRET",
"key": "SECRET"
},
"sentry": {
"dsn": "SECRET"
},
@ -58,6 +54,5 @@
"region": "SECRET"
}
},
"scimSettings": {
}
"scimSettings": {}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,7 @@
using System.Security.Claims;
using Bit.Core;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
@ -16,14 +18,15 @@ using Bit.Core.Tokens;
using Bit.Core.Utilities;
using Bit.Sso.Models;
using Bit.Sso.Utilities;
using Duende.IdentityServer;
using Duende.IdentityServer.Services;
using Duende.IdentityServer.Stores;
using IdentityModel;
using IdentityServer4;
using IdentityServer4.Extensions;
using IdentityServer4.Services;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using AuthenticationSchemes = Bit.Core.AuthenticationSchemes;
using DIM = Duende.IdentityServer.Models;
namespace Bit.Sso.Controllers;
@ -206,6 +209,8 @@ public class AccountController : Controller
returnUrl = "~/";
}
// Clean the returnUrl
returnUrl = CoreHelpers.ReplaceWhiteSpace(returnUrl, string.Empty);
if (!Url.IsLocalUrl(returnUrl) && !_interaction.IsValidReturnUrl(returnUrl))
{
throw new Exception(_i18nService.T("InvalidReturnUrl"));
@ -478,7 +483,7 @@ public class AccountController : Controller
if (orgUser.Status == OrganizationUserStatusType.Invited)
{
// Org User is invited - they must manually accept the invite via email and authenticate with MP
throw new Exception(_i18nService.T("UserAlreadyInvited", email, organization.Name));
throw new Exception(_i18nService.T("UserAlreadyInvited", email, organization.DisplayName()));
}
// Accepted or Confirmed - create SSO link and return;
@ -511,7 +516,7 @@ public class AccountController : Controller
await _organizationService.AdjustSeatsAsync(orgId, initialSeatCount - organization.Seats.Value, prorationDate);
}
_logger.LogInformation(e, "SSO auto provisioning failed");
throw new Exception(_i18nService.T("NoSeatsAvailable", organization.Name));
throw new Exception(_i18nService.T("NoSeatsAvailable", organization.DisplayName()));
}
}
}
@ -698,8 +703,10 @@ public class AccountController : Controller
var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
if (idp != null && idp != IdentityServerConstants.LocalIdentityProvider)
{
var providerSupportsSignout = await HttpContext.GetSchemeSupportsSignOutAsync(idp);
if (providerSupportsSignout)
var provider = HttpContext.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
var handler = await provider.GetHandlerAsync(HttpContext, idp);
if (handler is IAuthenticationSignOutHandler)
{
if (logoutId == null)
{
@ -717,7 +724,7 @@ public class AccountController : Controller
return (logoutId, logout?.PostLogoutRedirectUri, externalAuthenticationScheme);
}
public bool IsNativeClient(IdentityServer4.Models.AuthorizationRequest context)
public bool IsNativeClient(DIM.AuthorizationRequest context)
{
return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal)
&& !context.RedirectUri.StartsWith("http", StringComparison.Ordinal);

View File

@ -1,6 +1,6 @@
using System.Diagnostics;
using Bit.Sso.Models;
using IdentityServer4.Services;
using Duende.IdentityServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;

View File

@ -1,7 +1,7 @@
###############################################
# Build stage #
###############################################
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:6.0 AS build
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
# Docker buildx supplies the value for this arg
ARG TARGETPLATFORM
@ -67,7 +67,7 @@ WORKDIR /app
###############################################
# App stage #
###############################################
FROM mcr.microsoft.com/dotnet/aspnet:6.0
FROM mcr.microsoft.com/dotnet/aspnet:8.0
ARG TARGETPLATFORM
LABEL com.bitwarden.product="bitwarden"
@ -77,8 +77,8 @@ EXPOSE 5000
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gosu \
curl \
gosu \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy app from the build stage

View File

@ -1,6 +1,6 @@
using Bit.Core.Settings;
using IdentityServer4;
using IdentityServer4.Models;
using Duende.IdentityServer;
using Duende.IdentityServer.Models;
namespace Bit.Sso.IdentityServer;

View File

@ -1,4 +1,4 @@
using IdentityServer4.Models;
using Duende.IdentityServer.Models;
namespace Bit.Sso.Models;

View File

@ -8,9 +8,9 @@
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Sso-SelfHost' " />
<ItemGroup>
<!-- This is a transitive dependency to Sustainsys.Saml2.AspNetCore2 -->
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.22" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Sustainsys.Saml2.AspNetCore2" Version="2.9.0" />
<PackageReference Include="Sustainsys.Saml2.AspNetCore2" Version="2.9.2" />
</ItemGroup>
<ItemGroup>

View File

@ -6,7 +6,7 @@ using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.SharedWeb.Utilities;
using Bit.Sso.Utilities;
using IdentityServer4.Extensions;
using Duende.IdentityServer.Services;
using Microsoft.IdentityModel.Logging;
using Stripe;
@ -65,7 +65,7 @@ public class Startup
}
// Authentication
services.AddDistributedIdentityServices(globalSettings);
services.AddDistributedIdentityServices();
services.AddAuthentication()
.AddCookie(AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);
services.AddSsoServices(globalSettings);
@ -108,7 +108,7 @@ public class Startup
var uri = new Uri(globalSettings.BaseServiceUri.Sso);
app.Use(async (ctx, next) =>
{
ctx.SetIdentityServerOrigin($"{uri.Scheme}://{uri.Host}");
ctx.RequestServices.GetRequiredService<IServerUrls>().Origin = $"{uri.Scheme}://{uri.Host}";
await next();
});
}

View File

@ -1,13 +1,14 @@
using Bit.Core.Settings;
using Bit.Core.Utilities;
using IdentityServer4.Configuration;
using IdentityServer4.Services;
using IdentityServer4.Stores;
using IdentityServer4.Validation;
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Services;
using Duende.IdentityServer.Stores;
using Duende.IdentityServer.Validation;
using DIR = Duende.IdentityServer.ResponseHandling;
namespace Bit.Sso.Utilities;
public class DiscoveryResponseGenerator : IdentityServer4.ResponseHandling.DiscoveryResponseGenerator
public class DiscoveryResponseGenerator : DIR.DiscoveryResponseGenerator
{
private readonly GlobalSettings _globalSettings;

View File

@ -7,9 +7,9 @@ using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.Sso.Models;
using Bit.Sso.Utilities;
using Duende.IdentityServer;
using Duende.IdentityServer.Infrastructure;
using IdentityModel;
using IdentityServer4;
using IdentityServer4.Infrastructure;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.Options;
@ -34,7 +34,7 @@ public class DynamicAuthenticationSchemeProvider : AuthenticationSchemeProvider
private readonly Dictionary<string, DynamicAuthenticationScheme> _cachedSchemes;
private readonly Dictionary<string, DynamicAuthenticationScheme> _cachedHandlerSchemes;
private readonly SemaphoreSlim _semaphore;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IServiceProvider _serviceProvider;
private DateTime? _lastSchemeLoad;
private IEnumerable<DynamicAuthenticationScheme> _schemesCopy = Array.Empty<DynamicAuthenticationScheme>();
@ -50,7 +50,7 @@ public class DynamicAuthenticationSchemeProvider : AuthenticationSchemeProvider
ILogger<DynamicAuthenticationSchemeProvider> logger,
GlobalSettings globalSettings,
SamlEnvironment samlEnvironment,
IHttpContextAccessor httpContextAccessor)
IServiceProvider serviceProvider)
: base(options)
{
_oidcPostConfigureOptions = oidcPostConfigureOptions;
@ -77,7 +77,7 @@ public class DynamicAuthenticationSchemeProvider : AuthenticationSchemeProvider
_cachedSchemes = new Dictionary<string, DynamicAuthenticationScheme>();
_cachedHandlerSchemes = new Dictionary<string, DynamicAuthenticationScheme>();
_semaphore = new SemaphoreSlim(1);
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
private bool CacheIsValid
@ -324,7 +324,7 @@ public class DynamicAuthenticationSchemeProvider : AuthenticationSchemeProvider
oidcOptions.Scope.AddIfNotExists(OpenIdConnectScopes.Acr);
}
oidcOptions.StateDataFormat = new DistributedCacheStateDataFormatter(_httpContextAccessor, name);
oidcOptions.StateDataFormat = new DistributedCacheStateDataFormatter(_serviceProvider, name);
// see: https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest (acr_values)
if (!string.IsNullOrWhiteSpace(config.AcrValues))
@ -349,7 +349,9 @@ public class DynamicAuthenticationSchemeProvider : AuthenticationSchemeProvider
}
var spEntityId = new Sustainsys.Saml2.Metadata.EntityId(
SsoConfigurationData.BuildSaml2ModulePath(_globalSettings.BaseServiceUri.Sso));
SsoConfigurationData.BuildSaml2ModulePath(
_globalSettings.BaseServiceUri.Sso,
config.SpUniqueEntityId ? name : null));
bool? allowCreate = null;
if (config.SpNameIdFormat != Saml2NameIdFormat.Transient)
{
@ -415,7 +417,7 @@ public class DynamicAuthenticationSchemeProvider : AuthenticationSchemeProvider
};
options.IdentityProviders.Add(idp);
return new DynamicAuthenticationScheme(name, name, typeof(Saml2BitHandler), options, SsoType.Saml2);
return new DynamicAuthenticationScheme(name, name, typeof(Saml2Handler), options, SsoType.Saml2);
}
private NameIdFormat GetNameIdFormat(Saml2NameIdFormat format)

View File

@ -1,205 +0,0 @@
using System.Text;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Options;
using Sustainsys.Saml2.AspNetCore2;
using Sustainsys.Saml2.WebSso;
namespace Bit.Sso.Utilities;
// Temporary handler for validating Saml2 requests
// Most of this is taken from Sustainsys.Saml2.AspNetCore2.Saml2Handler
// TODO: PM-3641 - Remove this handler once there is a proper solution
public class Saml2BitHandler : IAuthenticationRequestHandler
{
private readonly Saml2Handler _saml2Handler;
private string _scheme;
private readonly IOptionsMonitorCache<Saml2Options> _optionsCache;
private Saml2Options _options;
private HttpContext _context;
private readonly IDataProtector _dataProtector;
private readonly IOptionsFactory<Saml2Options> _optionsFactory;
private bool _emitSameSiteNone;
public Saml2BitHandler(
IOptionsMonitorCache<Saml2Options> optionsCache,
IDataProtectionProvider dataProtectorProvider,
IOptionsFactory<Saml2Options> optionsFactory)
{
if (dataProtectorProvider == null)
{
throw new ArgumentNullException(nameof(dataProtectorProvider));
}
_optionsFactory = optionsFactory;
_optionsCache = optionsCache;
_saml2Handler = new Saml2Handler(optionsCache, dataProtectorProvider, optionsFactory);
_dataProtector = dataProtectorProvider.CreateProtector(_saml2Handler.GetType().FullName);
}
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_options = _optionsCache.GetOrAdd(scheme.Name, () => _optionsFactory.Create(scheme.Name));
_emitSameSiteNone = _options.Notifications.EmitSameSiteNone(context.Request.GetUserAgent());
_scheme = scheme.Name;
return _saml2Handler.InitializeAsync(scheme, context);
}
public async Task<bool> HandleRequestAsync()
{
if (!_context.Request.Path.StartsWithSegments(_options.SPOptions.ModulePath, StringComparison.Ordinal))
{
return false;
}
var commandName = _context.Request.Path.Value.Substring(
_options.SPOptions.ModulePath.Length).TrimStart('/');
var commandResult = CommandFactory.GetCommand(commandName).Run(
_context.ToHttpRequestData(_options.CookieManager, _dataProtector.Unprotect), _options);
// Scheme is the organization ID since we use dynamic handlers for authentication schemes.
// We need to compare this to the scheme returned in the RelayData to ensure this value hasn't been
// tampered with
if (commandResult.RelayData["scheme"] != _scheme)
{
return false;
}
await commandResult.Apply(
_context, _dataProtector, _options.CookieManager, _options.SignInScheme, _options.SignOutScheme, _emitSameSiteNone);
return true;
}
public Task<AuthenticateResult> AuthenticateAsync() => _saml2Handler.AuthenticateAsync();
public Task ChallengeAsync(AuthenticationProperties properties) => _saml2Handler.ChallengeAsync(properties);
public Task ForbidAsync(AuthenticationProperties properties) => _saml2Handler.ForbidAsync(properties);
}
static class HttpRequestExtensions
{
public static HttpRequestData ToHttpRequestData(
this HttpContext httpContext,
ICookieManager cookieManager,
Func<byte[], byte[]> cookieDecryptor)
{
var request = httpContext.Request;
var uri = new Uri(
request.Scheme
+ "://"
+ request.Host
+ request.Path
+ request.QueryString);
var pathBase = httpContext.Request.PathBase.Value;
pathBase = string.IsNullOrEmpty(pathBase) ? "/" : pathBase;
IEnumerable<KeyValuePair<string, IEnumerable<string>>> formData = null;
if (httpContext.Request.Method == "POST" && httpContext.Request.HasFormContentType)
{
formData = request.Form.Select(
f => new KeyValuePair<string, IEnumerable<string>>(f.Key, f.Value));
}
return new HttpRequestData(
httpContext.Request.Method,
uri,
pathBase,
formData,
cookieName => cookieManager.GetRequestCookie(httpContext, cookieName),
cookieDecryptor,
httpContext.User);
}
public static string GetUserAgent(this HttpRequest request)
{
return request.Headers["user-agent"].FirstOrDefault() ?? "";
}
}
static class CommandResultExtensions
{
public static async Task Apply(
this CommandResult commandResult,
HttpContext httpContext,
IDataProtector dataProtector,
ICookieManager cookieManager,
string signInScheme,
string signOutScheme,
bool emitSameSiteNone)
{
httpContext.Response.StatusCode = (int)commandResult.HttpStatusCode;
if (commandResult.Location != null)
{
httpContext.Response.Headers["Location"] = commandResult.Location.OriginalString;
}
if (!string.IsNullOrEmpty(commandResult.SetCookieName))
{
var cookieData = HttpRequestData.ConvertBinaryData(
dataProtector.Protect(commandResult.GetSerializedRequestState()));
cookieManager.AppendResponseCookie(
httpContext,
commandResult.SetCookieName,
cookieData,
new CookieOptions()
{
HttpOnly = true,
Secure = commandResult.SetCookieSecureFlag,
// We are expecting a different site to POST back to us,
// so the ASP.Net Core default of Lax is not appropriate in this case
SameSite = emitSameSiteNone ? SameSiteMode.None : (SameSiteMode)(-1),
IsEssential = true
});
}
foreach (var h in commandResult.Headers)
{
httpContext.Response.Headers.Add(h.Key, h.Value);
}
if (!string.IsNullOrEmpty(commandResult.ClearCookieName))
{
cookieManager.DeleteCookie(
httpContext,
commandResult.ClearCookieName,
new CookieOptions
{
Secure = commandResult.SetCookieSecureFlag
});
}
if (!string.IsNullOrEmpty(commandResult.Content))
{
var buffer = Encoding.UTF8.GetBytes(commandResult.Content);
httpContext.Response.ContentType = commandResult.ContentType;
await httpContext.Response.Body.WriteAsync(buffer, 0, buffer.Length);
}
if (commandResult.Principal != null)
{
var authProps = new AuthenticationProperties(commandResult.RelayData)
{
RedirectUri = commandResult.Location.OriginalString
};
await httpContext.SignInAsync(signInScheme, commandResult.Principal, authProps);
}
if (commandResult.TerminateLocalSession)
{
await httpContext.SignOutAsync(signOutScheme ?? signInScheme);
}
}
}

View File

@ -4,8 +4,8 @@ using Bit.Core.Utilities;
using Bit.SharedWeb.Utilities;
using Bit.Sso.IdentityServer;
using Bit.Sso.Models;
using IdentityServer4.Models;
using IdentityServer4.ResponseHandling;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.ResponseHandling;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Sustainsys.Saml2.AspNetCore2;
@ -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())
{
@ -59,6 +60,7 @@ public static class ServiceCollectionExtensions
options.UserInteraction.ErrorIdParameter = "errorId";
}
options.InputLengthRestrictions.UserName = 256;
options.KeyManagement.Enabled = false;
})
.AddInMemoryCaching()
.AddInMemoryClients(new List<Client>

View File

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

View File

@ -31,10 +31,6 @@
"connectionString": "SECRET",
"applicationCacheTopicName": "SECRET"
},
"documentDb": {
"uri": "SECRET",
"key": "SECRET"
},
"notificationHub": {
"connectionString": "SECRET",
"hubName": "SECRET"

View File

@ -9,15 +9,15 @@
"version": "0.0.0",
"license": "-",
"devDependencies": {
"bootstrap": "4.5.0",
"del": "6.0.0",
"bootstrap": "4.6.2",
"del": "6.1.1",
"font-awesome": "4.7.0",
"gulp": "4.0.2",
"gulp-sass": "5.1.0",
"jquery": "3.5.1",
"jquery": "3.7.1",
"merge-stream": "2.0.0",
"popper.js": "1.16.1",
"sass": "1.49.7"
"sass": "1.75.0"
}
},
"node_modules/@nodelib/fs.scandir": {
@ -598,17 +598,23 @@
}
},
"node_modules/bootstrap": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.0.tgz",
"integrity": "sha512-Z93QoXvodoVslA+PWNdk23Hze4RBYIkpb5h8I2HY2Tu2h7A0LpAgLcyrhrSUyo2/Oxm2l1fRZPs1e5hnxnliXA==",
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz",
"integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==",
"dev": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
},
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"peerDependencies": {
"jquery": "1.9.1 - 3",
"popper.js": "^1.16.0"
"popper.js": "^1.16.1"
}
},
"node_modules/brace-expansion": {
@ -1028,9 +1034,9 @@
}
},
"node_modules/del": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz",
"integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==",
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz",
"integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==",
"dev": true,
"dependencies": {
"globby": "^11.0.1",
@ -2383,9 +2389,9 @@
}
},
"node_modules/jquery": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==",
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
"dev": true
},
"node_modules/json-stable-stringify-without-jsonify": {
@ -3946,9 +3952,9 @@
}
},
"node_modules/sass": {
"version": "1.49.7",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.49.7.tgz",
"integrity": "sha512-13dml55EMIR2rS4d/RDHHP0sXMY3+30e1TKsyXaSz3iLWVoDWEoboY8WzJd5JMnxrRHffKO3wq2mpJ0jxRJiEQ==",
"version": "1.75.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.75.0.tgz",
"integrity": "sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
@ -3959,7 +3965,7 @@
"sass": "sass.js"
},
"engines": {
"node": ">=12.0.0"
"node": ">=14.0.0"
}
},
"node_modules/sass/node_modules/anymatch": {
@ -3976,12 +3982,15 @@
}
},
"node_modules/sass/node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/sass/node_modules/braces": {
@ -3997,16 +4006,10 @@
}
},
"node_modules/sass/node_modules/chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@ -4019,6 +4022,9 @@
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}

View File

@ -8,14 +8,14 @@
"build": "gulp build"
},
"devDependencies": {
"bootstrap": "4.5.0",
"del": "6.0.0",
"bootstrap": "4.6.2",
"del": "6.1.1",
"font-awesome": "4.7.0",
"gulp": "4.0.2",
"gulp-sass": "5.1.0",
"jquery": "3.5.1",
"jquery": "3.7.1",
"merge-stream": "2.0.0",
"popper.js": "1.16.1",
"sass": "1.49.7"
"sass": "1.75.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,7 @@ public class CreateProviderCommandTests
provider.Type = ProviderType.Msp;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.CreateMspAsync(provider, default));
() => sutProvider.Sut.CreateMspAsync(provider, default, default, default));
Assert.Contains("Invalid owner.", exception.Message);
}
@ -34,7 +34,7 @@ public class CreateProviderCommandTests
var userRepository = sutProvider.GetDependency<IUserRepository>();
userRepository.GetByEmailAsync(user.Email).Returns(user);
await sutProvider.Sut.CreateMspAsync(provider, user.Email);
await sutProvider.Sut.CreateMspAsync(provider, user.Email, default, default);
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);

View File

@ -0,0 +1,195 @@
using Bit.Commercial.Core.AdminConsole.Providers;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Commands;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Stripe;
using Xunit;
namespace Bit.Commercial.Core.Test.AdminConsole.ProviderFeatures;
[SutProviderCustomize]
public class RemoveOrganizationFromProviderCommandTests
{
[Theory, BitAutoData]
public async Task RemoveOrganizationFromProvider_NoProvider_BadRequest(
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
{
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RemoveOrganizationFromProvider(null, null, null));
Assert.Equal("Failed to remove organization. Please contact support.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveOrganizationFromProvider_NoProviderOrganization_BadRequest(
Provider provider,
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
{
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RemoveOrganizationFromProvider(provider, null, null));
Assert.Equal("Failed to remove organization. Please contact support.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveOrganizationFromProvider_NoOrganization_BadRequest(
Provider provider,
ProviderOrganization providerOrganization,
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
{
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RemoveOrganizationFromProvider(
provider, providerOrganization, null));
Assert.Equal("Failed to remove organization. Please contact support.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveOrganizationFromProvider_MismatchedProviderOrganization_BadRequest(
Provider provider,
ProviderOrganization providerOrganization,
Organization organization,
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
{
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization));
Assert.Equal("Failed to remove organization. Please contact support.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveOrganizationFromProvider_NoConfirmedOwners_BadRequest(
Provider provider,
ProviderOrganization providerOrganization,
Organization organization,
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
{
providerOrganization.ProviderId = provider.Id;
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
Array.Empty<Guid>(),
includeProvider: false)
.Returns(false);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization));
Assert.Equal("Organization must have at least one confirmed owner.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveOrganizationFromProvider_MakesCorrectInvocations__FeatureFlagOff(
Provider provider,
ProviderOrganization providerOrganization,
Organization organization,
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
{
providerOrganization.ProviderId = provider.Id;
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
Array.Empty<Guid>(),
includeProvider: false)
.Returns(true);
var organizationOwnerEmails = new List<string> { "a@example.com", "b@example.com" };
organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns(organizationOwnerEmails);
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
{
Id = "S-1",
CurrentPeriodEnd = DateTime.Today.AddDays(10),
});
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(
org => org.Id == organization.Id && org.BillingEmail == "a@example.com"));
await stripeAdapter.Received(1).CustomerUpdateAsync(
organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
options => options.Coupon == string.Empty && options.Email == "a@example.com"));
await sutProvider.GetDependency<IMailService>().Received(1).SendProviderUpdatePaymentMethod(
organization.Id,
organization.Name,
provider.Name,
Arg.Is<IEnumerable<string>>(emails => emails.Contains("a@example.com") && emails.Contains("b@example.com")));
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
.DeleteAsync(providerOrganization);
await sutProvider.GetDependency<IEventService>().Received(1).LogProviderOrganizationEventAsync(
providerOrganization,
EventType.ProviderOrganization_Removed);
}
[Theory, BitAutoData]
public async Task RemoveOrganizationFromProvider_CreatesSubscriptionAndScalesSeats_FeatureFlagON(Provider provider,
ProviderOrganization providerOrganization,
Organization organization,
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
{
providerOrganization.ProviderId = provider.Id;
provider.Status = ProviderStatusType.Billable;
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
Array.Empty<Guid>(),
includeProvider: false)
.Returns(true);
var organizationOwnerEmails = new List<string> { "a@example.com", "b@example.com" };
organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns(organizationOwnerEmails);
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
{
Id = "S-1",
CurrentPeriodEnd = DateTime.Today.AddDays(10),
});
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
await stripeAdapter.Received(1).CustomerUpdateAsync(
organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
options => options.Coupon == string.Empty && options.Email == "a@example.com"));
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(c =>
c.Customer == organization.GatewayCustomerId &&
c.CollectionMethod == "send_invoice" &&
c.DaysUntilDue == 30 &&
c.Items.Count == 1
));
await sutProvider.GetDependency<IScaleSeatsCommand>().Received(1)
.ScalePasswordManagerSeats(provider, organization.PlanType, -(int)organization.Seats);
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(
org => org.Id == organization.Id && org.BillingEmail == "a@example.com" &&
org.GatewaySubscriptionId == "S-1"));
await sutProvider.GetDependency<IMailService>().Received(1).SendProviderUpdatePaymentMethod(
organization.Id,
organization.Name,
provider.Name,
Arg.Is<IEnumerable<string>>(emails =>
emails.Contains("a@example.com") && emails.Contains("b@example.com")));
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
.DeleteAsync(providerOrganization);
await sutProvider.GetDependency<IEventService>().Received(1).LogProviderOrganizationEventAsync(
providerOrganization,
EventType.ProviderOrganization_Removed);
}
}

View File

@ -1,8 +1,11 @@
using Bit.Commercial.Core.AdminConsole.Services;
using Bit.Commercial.Core.Test.AdminConsole.AutoFixture;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business.Provider;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
@ -11,12 +14,15 @@ using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
using Bit.Core.Tokens;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.DataProtection;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Stripe;
using Xunit;
using Provider = Bit.Core.AdminConsole.Entities.Provider.Provider;
using ProviderUser = Bit.Core.AdminConsole.Entities.Provider.ProviderUser;
@ -455,17 +461,112 @@ public class ProviderServiceTests
{
organization.PlanType = PlanType.EnterpriseAnnually;
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
await providerOrganizationRepository.Received(1)
.CreateAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
providerOrganization.ProviderId == provider.Id &&
providerOrganization.OrganizationId == organization.Id &&
providerOrganization.Key == key));
await organizationRepository.Received(1)
.ReplaceAsync(Arg.Is<Organization>(org => org.BillingEmail == provider.BillingEmail));
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerUpdateAsync(
organization.GatewayCustomerId,
Arg.Is<CustomerUpdateOptions>(options => options.Email == provider.BillingEmail));
await sutProvider.GetDependency<IEventService>()
.Received().LogProviderOrganizationEventAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
providerOrganization.ProviderId == provider.Id &&
providerOrganization.OrganizationId == organization.Id &&
providerOrganization.Key == key),
EventType.ProviderOrganization_Added);
}
[Theory, BitAutoData]
public async Task AddOrganization_CreateAfterNov62023_PlanTypeDoesNotUpdated(Provider provider, Organization organization, string key,
SutProvider<ProviderService> sutProvider)
{
provider.Type = ProviderType.Msp;
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
var expectedPlanType = PlanType.EnterpriseAnnually;
organization.PlanType = PlanType.EnterpriseAnnually;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
await providerOrganizationRepository.Received(1)
.CreateAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
providerOrganization.ProviderId == provider.Id &&
providerOrganization.OrganizationId == organization.Id &&
providerOrganization.Key == key));
await sutProvider.GetDependency<IEventService>()
.Received().LogProviderOrganizationEventAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
providerOrganization.ProviderId == provider.Id &&
providerOrganization.OrganizationId == organization.Id &&
providerOrganization.Key == key),
EventType.ProviderOrganization_Added);
Assert.Equal(organization.PlanType, expectedPlanType);
}
[Theory, BitAutoData]
public async Task AddOrganization_CreateBeforeNov62023_PlanTypeUpdated(Provider provider, Organization organization, string key,
SutProvider<ProviderService> sutProvider)
{
var newCreationDate = new DateTime(2023, 11, 5);
BackdateProviderCreationDate(provider, newCreationDate);
provider.Type = ProviderType.Msp;
organization.PlanType = PlanType.EnterpriseAnnually;
organization.Plan = "Enterprise (Annually)";
var expectedPlanType = PlanType.EnterpriseAnnually2020;
var expectedPlanId = "2020-enterprise-org-seat-annually";
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var subscriptionItem = GetSubscription(organization.GatewaySubscriptionId);
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
.Returns(GetSubscription(organization.GatewaySubscriptionId));
await sutProvider.GetDependency<IStripeAdapter>().SubscriptionUpdateAsync(
organization.GatewaySubscriptionId, SubscriptionUpdateRequest(expectedPlanId, subscriptionItem));
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);
await providerOrganizationRepository.Received(1)
.CreateAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
providerOrganization.ProviderId == provider.Id &&
providerOrganization.OrganizationId == organization.Id &&
providerOrganization.Key == key));
await sutProvider.GetDependency<IEventService>()
.Received().LogProviderOrganizationEventAsync(Arg.Any<ProviderOrganization>(),
.Received().LogProviderOrganizationEventAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
providerOrganization.ProviderId == provider.Id &&
providerOrganization.OrganizationId == organization.Id &&
providerOrganization.Key == key),
EventType.ProviderOrganization_Added);
Assert.Equal(organization.PlanType, expectedPlanType);
}
[Theory, BitAutoData]
@ -511,7 +612,7 @@ public class ProviderServiceTests
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogProviderOrganizationEventsAsync(default);
}
[Theory, BitAutoData]
[Theory, OrganizationCustomize(FlexibleCollections = false), BitAutoData]
public async Task CreateOrganizationAsync_Success(Provider provider, OrganizationSignup organizationSignup,
Organization organization, string clientOwnerEmail, User user, SutProvider<ProviderService> sutProvider)
{
@ -520,7 +621,7 @@ public class ProviderServiceTests
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup, true)
.Returns(Tuple.Create(organization, null as OrganizationUser));
.Returns((organization, null as OrganizationUser, new Collection()));
var providerOrganization =
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
@ -536,65 +637,243 @@ public class ProviderServiceTests
t.First().Item1.Emails.First() == clientOwnerEmail &&
t.First().Item1.Type == OrganizationUserType.Owner &&
t.First().Item1.AccessAll &&
!t.First().Item1.Collections.Any() &&
t.First().Item2 == null));
}
[Theory, OrganizationCustomize(FlexibleCollections = false), BitAutoData]
public async Task CreateOrganizationAsync_ConsolidatedBillingEnabled_InvalidPlanType_ThrowsBadRequestException(
Provider provider,
OrganizationSignup organizationSignup,
Organization organization,
string clientOwnerEmail,
User user,
SutProvider<ProviderService> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
provider.Type = ProviderType.Msp;
provider.Status = ProviderStatusType.Billable;
organizationSignup.Plan = PlanType.EnterpriseAnnually;
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
.Returns((organization, null as OrganizationUser, new Collection()));
await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user));
await providerOrganizationRepository.DidNotReceiveWithAnyArgs().CreateAsync(default);
}
[Theory, OrganizationCustomize(FlexibleCollections = false), BitAutoData]
public async Task CreateOrganizationAsync_ConsolidatedBillingEnabled_InvokeSignupClientAsync(
Provider provider,
OrganizationSignup organizationSignup,
Organization organization,
string clientOwnerEmail,
User user,
SutProvider<ProviderService> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
provider.Type = ProviderType.Msp;
provider.Status = ProviderStatusType.Billable;
organizationSignup.Plan = PlanType.EnterpriseMonthly;
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
.Returns((organization, null as OrganizationUser, new Collection()));
var providerOrganization = await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
await providerOrganizationRepository.Received(1).CreateAsync(Arg.Is<ProviderOrganization>(
po =>
po.ProviderId == provider.Id &&
po.OrganizationId == organization.Id));
await sutProvider.GetDependency<IEventService>()
.Received()
.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Created);
await sutProvider.GetDependency<IOrganizationService>()
.Received()
.InviteUsersAsync(
organization.Id,
user.Id,
Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(
t =>
t.Count() == 1 &&
t.First().Item1.Emails.Count() == 1 &&
t.First().Item1.Emails.First() == clientOwnerEmail &&
t.First().Item1.Type == OrganizationUserType.Owner &&
t.First().Item1.AccessAll &&
!t.First().Item1.Collections.Any() &&
t.First().Item2 == null));
}
[Theory, OrganizationCustomize(FlexibleCollections = true), BitAutoData]
public async Task CreateOrganizationAsync_WithFlexibleCollections_SetsAccessAllToFalse
(Provider provider, OrganizationSignup organizationSignup, Organization organization, string clientOwnerEmail,
User user, SutProvider<ProviderService> sutProvider, Collection defaultCollection)
{
organizationSignup.Plan = PlanType.EnterpriseAnnually;
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup, true)
.Returns((organization, null as OrganizationUser, defaultCollection));
var providerOrganization =
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);
await sutProvider.GetDependency<IEventService>()
.Received().LogProviderOrganizationEventAsync(providerOrganization,
EventType.ProviderOrganization_Created);
await sutProvider.GetDependency<IOrganizationService>()
.Received().InviteUsersAsync(organization.Id, user.Id, Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(
t => t.Count() == 1 &&
t.First().Item1.Emails.Count() == 1 &&
t.First().Item1.Emails.First() == clientOwnerEmail &&
t.First().Item1.Type == OrganizationUserType.Owner &&
t.First().Item1.AccessAll == false &&
t.First().Item1.Collections.Single().Id == defaultCollection.Id &&
!t.First().Item1.Collections.Single().HidePasswords &&
!t.First().Item1.Collections.Single().ReadOnly &&
t.First().Item1.Collections.Single().Manage &&
t.First().Item2 == null));
}
[Theory, BitAutoData]
public async Task RemoveOrganization_ProviderOrganizationIsInvalid_Throws(Provider provider,
ProviderOrganization providerOrganization, User user, SutProvider<ProviderService> sutProvider)
public async Task Delete_Success(Provider provider, SutProvider<ProviderService> sutProvider)
{
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganization.Id)
.ReturnsNull();
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveOrganizationAsync(provider.Id, providerOrganization.Id, user.Id));
Assert.Equal("Invalid organization.", exception.Message);
await sutProvider.Sut.DeleteAsync(provider);
await providerRepository.Received().DeleteAsync(provider);
await applicationCacheService.Received().DeleteProviderAbilityAsync(provider.Id);
}
[Theory, BitAutoData]
public async Task RemoveOrganization_ProviderOrganizationBelongsToWrongProvider_Throws(Provider provider,
ProviderOrganization providerOrganization, User user, SutProvider<ProviderService> sutProvider)
public async Task InitiateDeleteAsync_ThrowsBadRequestException_WhenProviderNameIsEmpty(string providerAdminEmail, SutProvider<ProviderService> sutProvider)
{
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganization.Id)
.Returns(providerOrganization);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveOrganizationAsync(provider.Id, providerOrganization.Id, user.Id));
Assert.Equal("Invalid organization.", exception.Message);
var provider = new Provider { Name = "" };
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InitiateDeleteAsync(provider, providerAdminEmail));
}
[Theory, BitAutoData]
public async Task RemoveOrganization_HasNoOwners_Throws(Provider provider,
ProviderOrganization providerOrganization, User user, SutProvider<ProviderService> sutProvider)
public async Task InitiateDeleteAsync_ThrowsBadRequestException_WhenProviderAdminNotFound(Provider provider, SutProvider<ProviderService> sutProvider)
{
providerOrganization.ProviderId = provider.Id;
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganization.Id)
.Returns(providerOrganization);
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(default, default, default)
.ReturnsForAnyArgs(false);
var providerAdminEmail = "nonexistent@example.com";
var userRepository = sutProvider.GetDependency<IUserRepository>();
userRepository.GetByEmailAsync(providerAdminEmail).Returns(Task.FromResult<User>(null));
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveOrganizationAsync(provider.Id, providerOrganization.Id, user.Id));
Assert.Equal("Organization needs to have at least one confirmed owner.", exception.Message);
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InitiateDeleteAsync(provider, providerAdminEmail));
}
[Theory, BitAutoData]
public async Task RemoveOrganization_Success(Provider provider,
ProviderOrganization providerOrganization, User user, SutProvider<ProviderService> sutProvider)
public async Task InitiateDeleteAsync_ThrowsBadRequestException_WhenProviderAdminStatusIsNotConfirmed(
Provider provider
, User providerAdmin
, ProviderUser providerUser
, SutProvider<ProviderService> sutProvider)
{
providerOrganization.ProviderId = provider.Id;
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
providerOrganizationRepository.GetByIdAsync(providerOrganization.Id).Returns(providerOrganization);
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(default, default, default)
.ReturnsForAnyArgs(true);
var providerAdminEmail = "nonexistent@example.com";
providerUser.Status = ProviderUserStatusType.Confirmed;
providerUser.Type = ProviderUserType.ServiceUser;
await sutProvider.Sut.RemoveOrganizationAsync(provider.Id, providerOrganization.Id, user.Id);
await providerOrganizationRepository.Received().DeleteAsync(providerOrganization);
await sutProvider.GetDependency<IEventService>().Received()
.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);
var userRepository = sutProvider.GetDependency<IUserRepository>();
userRepository.GetByEmailAsync(providerAdminEmail).Returns(Task.FromResult<User>(providerAdmin));
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
providerUserRepository.GetByProviderUserAsync(provider.Id, providerAdmin.Id).Returns(providerUser);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InitiateDeleteAsync(provider, providerAdminEmail));
Assert.Contains("Org admin not found.", exception.Message);
}
[Theory, BitAutoData]
public async Task InitiateDeleteAsync_SendsInitiateDeleteProviderEmail(Provider provider, User providerAdmin
, ProviderUser providerUser, SutProvider<ProviderService> sutProvider)
{
var providerAdminEmail = providerAdmin.Email;
providerUser.Status = ProviderUserStatusType.Confirmed;
providerUser.Type = ProviderUserType.ProviderAdmin;
var userRepository = sutProvider.GetDependency<IUserRepository>();
userRepository.GetByEmailAsync(providerAdminEmail).Returns(Task.FromResult<User>(providerAdmin));
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
providerUserRepository.GetByProviderUserAsync(provider.Id, providerAdmin.Id).Returns(providerUser);
var mailService = sutProvider.GetDependency<IMailService>();
await sutProvider.Sut.InitiateDeleteAsync(provider, providerAdminEmail);
await mailService.Received().SendInitiateDeletProviderEmailAsync(providerAdminEmail, provider, Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task DeleteAsync_ThrowsBadRequestException_WhenInvalidToken(Provider provider, string invalidToken
, SutProvider<ProviderService> sutProvider)
{
var providerDeleteTokenDataFactory = sutProvider.GetDependency<IDataProtectorTokenFactory<ProviderDeleteTokenable>>();
providerDeleteTokenDataFactory.TryUnprotect(invalidToken, out Arg.Any<ProviderDeleteTokenable>()).Returns(false);
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.DeleteAsync(provider, invalidToken));
}
[Theory, BitAutoData]
public async Task DeleteAsync_ThrowsBadRequestException_WhenInvalidTokenData(Provider provider, string validToken
, SutProvider<ProviderService> sutProvider)
{
var validTokenData = new ProviderDeleteTokenable();
var providerDeleteTokenDataFactory = sutProvider.GetDependency<IDataProtectorTokenFactory<ProviderDeleteTokenable>>();
providerDeleteTokenDataFactory.TryUnprotect(validToken, out validTokenData).Returns(false);
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.DeleteAsync(provider, validToken));
}
private static SubscriptionUpdateOptions SubscriptionUpdateRequest(string expectedPlanId, Subscription subscriptionItem) =>
new()
{
Items = new List<Stripe.SubscriptionItemOptions>
{
new() { Id = subscriptionItem.Id, Price = expectedPlanId },
}
};
private static Subscription GetSubscription(string subscriptionId) =>
new()
{
Id = subscriptionId,
Items = new StripeList<SubscriptionItem>
{
Data = new List<SubscriptionItem>
{
new()
{
Id = "sub_item_123",
Price = new Price()
{
Id = "2023-enterprise-org-seat-annually"
}
}
}
}
};
private static void BackdateProviderCreationDate(Provider provider, DateTime newCreationDate)
{
// Set the CreationDate to the desired value
provider.GetType().GetProperty("CreationDate")?.SetValue(provider, newCreationDate, null);
}
}

View File

@ -1,763 +0,0 @@
using System.Reflection;
using System.Security.Claims;
using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
using Bit.Commercial.Core.Test.SecretsManager.Enums;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization;
using NSubstitute;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.AccessPolicies;
[SutProviderCustomize]
[ProjectCustomize]
public class AccessPolicyAuthorizationHandlerTests
{
private static void SetupCurrentUserPermission(SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
PermissionType permissionType, Guid organizationId, Guid userId = new())
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId)
.Returns(true);
switch (permissionType)
{
case PermissionType.RunAsAdmin:
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId).ReturnsForAnyArgs(
(AccessClientType.NoAccessCheck, userId));
break;
case PermissionType.RunAsUserWithPermission:
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId).ReturnsForAnyArgs(
(AccessClientType.User, userId));
break;
default:
throw new ArgumentOutOfRangeException(nameof(permissionType), permissionType, null);
}
}
private static BaseAccessPolicy CreatePolicy(AccessPolicyType accessPolicyType, Project grantedProject,
ServiceAccount grantedServiceAccount, Guid? serviceAccountId = null)
{
switch (accessPolicyType)
{
case AccessPolicyType.UserProjectAccessPolicy:
return
new UserProjectAccessPolicy
{
Id = Guid.NewGuid(),
OrganizationUserId = Guid.NewGuid(),
Read = true,
Write = true,
GrantedProjectId = grantedProject.Id,
GrantedProject = grantedProject,
};
case AccessPolicyType.GroupProjectAccessPolicy:
return
new GroupProjectAccessPolicy
{
Id = Guid.NewGuid(),
GroupId = Guid.NewGuid(),
GrantedProjectId = grantedProject.Id,
Read = true,
Write = true,
GrantedProject = grantedProject,
};
case AccessPolicyType.ServiceAccountProjectAccessPolicy:
return new ServiceAccountProjectAccessPolicy
{
Id = Guid.NewGuid(),
ServiceAccountId = serviceAccountId,
GrantedProjectId = grantedProject.Id,
Read = true,
Write = true,
GrantedProject = grantedProject,
};
case AccessPolicyType.UserServiceAccountAccessPolicy:
return
new UserServiceAccountAccessPolicy
{
Id = Guid.NewGuid(),
OrganizationUserId = Guid.NewGuid(),
Read = true,
Write = true,
GrantedServiceAccountId = grantedServiceAccount.Id,
GrantedServiceAccount = grantedServiceAccount,
};
case AccessPolicyType.GroupServiceAccountAccessPolicy:
return new GroupServiceAccountAccessPolicy
{
Id = Guid.NewGuid(),
GroupId = Guid.NewGuid(),
GrantedServiceAccountId = grantedServiceAccount.Id,
GrantedServiceAccount = grantedServiceAccount,
Read = true,
Write = true,
};
default:
throw new ArgumentOutOfRangeException(nameof(accessPolicyType), accessPolicyType, null);
}
}
private static void SetupMockAccess(SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Guid userId, BaseAccessPolicy accessPolicy, bool read, bool write)
{
switch (accessPolicy)
{
case UserProjectAccessPolicy ap:
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(ap.GrantedProjectId!.Value, userId, Arg.Any<AccessClientType>())
.Returns((read, write));
break;
case GroupProjectAccessPolicy ap:
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(ap.GrantedProjectId!.Value, userId, Arg.Any<AccessClientType>())
.Returns((read, write));
break;
case UserServiceAccountAccessPolicy ap:
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountAsync(ap.GrantedServiceAccountId!.Value, userId, Arg.Any<AccessClientType>())
.Returns((read, write));
break;
case GroupServiceAccountAccessPolicy ap:
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountAsync(ap.GrantedServiceAccountId!.Value, userId, Arg.Any<AccessClientType>())
.Returns((read, write));
break;
case ServiceAccountProjectAccessPolicy ap:
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(ap.GrantedProjectId!.Value, userId, Arg.Any<AccessClientType>())
.Returns((read, write));
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountAsync(ap.ServiceAccountId!.Value, userId, Arg.Any<AccessClientType>())
.Returns((read, write));
break;
}
}
private static void SetupOrganizationMismatch(SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
BaseAccessPolicy accessPolicy)
{
switch (accessPolicy)
{
case UserProjectAccessPolicy resource:
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(resource.OrganizationUserId!.Value)
.Returns(new OrganizationUser
{
Id = resource.OrganizationUserId!.Value,
OrganizationId = Guid.NewGuid()
});
break;
case GroupProjectAccessPolicy resource:
sutProvider.GetDependency<IGroupRepository>().GetByIdAsync(resource.GroupId!.Value)
.Returns(new Group { Id = resource.GroupId!.Value, OrganizationId = Guid.NewGuid() });
break;
case UserServiceAccountAccessPolicy resource:
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(resource.OrganizationUserId!.Value)
.Returns(new OrganizationUser
{
Id = resource.OrganizationUserId!.Value,
OrganizationId = Guid.NewGuid()
});
break;
case GroupServiceAccountAccessPolicy resource:
sutProvider.GetDependency<IGroupRepository>().GetByIdAsync(resource.GroupId!.Value)
.Returns(new Group { Id = resource.GroupId!.Value, OrganizationId = Guid.NewGuid() });
break;
default:
throw new ArgumentOutOfRangeException(nameof(accessPolicy), accessPolicy, null);
}
}
private static void SetupOrganizationMatch(SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
BaseAccessPolicy accessPolicy, Guid organizationId)
{
switch (accessPolicy)
{
case UserProjectAccessPolicy resource:
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(resource.OrganizationUserId!.Value)
.Returns(new OrganizationUser
{
Id = resource.OrganizationUserId!.Value,
OrganizationId = organizationId
});
break;
case GroupProjectAccessPolicy resource:
sutProvider.GetDependency<IGroupRepository>().GetByIdAsync(resource.GroupId!.Value)
.Returns(new Group { Id = resource.GroupId!.Value, OrganizationId = organizationId });
break;
case UserServiceAccountAccessPolicy resource:
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(resource.OrganizationUserId!.Value)
.Returns(new OrganizationUser
{
Id = resource.OrganizationUserId!.Value,
OrganizationId = organizationId
});
break;
case GroupServiceAccountAccessPolicy resource:
sutProvider.GetDependency<IGroupRepository>().GetByIdAsync(resource.GroupId!.Value)
.Returns(new Group { Id = resource.GroupId!.Value, OrganizationId = organizationId });
break;
default:
throw new ArgumentOutOfRangeException(nameof(accessPolicy), accessPolicy, null);
}
}
[Fact]
public void AccessPolicyOperations_OnlyPublicStatic()
{
var publicStaticFields = typeof(AccessPolicyOperations).GetFields(BindingFlags.Public | BindingFlags.Static);
var allFields = typeof(AccessPolicyOperations).GetFields();
Assert.Equal(publicStaticFields.Length, allFields.Length);
}
[Theory]
[BitAutoData]
public async Task Handler_UnsupportedAccessPolicyOperationRequirement_Throws(
SutProvider<AccessPolicyAuthorizationHandler> sutProvider, UserProjectAccessPolicy resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = new AccessPolicyOperationRequirement();
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));
}
[Theory]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy)]
public async Task CanCreate_OrgMismatch_DoesNotSucceed(
AccessPolicyType accessPolicyType,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Project mockGrantedProject,
ServiceAccount mockGrantedServiceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Create;
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
SetupOrganizationMismatch(sutProvider, resource);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy)]
public async Task CanCreate_AccessToSecretsManagerFalse_DoesNotSucceed(
AccessPolicyType accessPolicyType,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Guid organizationId,
Project mockGrantedProject,
ServiceAccount mockGrantedServiceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Create;
mockGrantedProject.OrganizationId = organizationId;
mockGrantedServiceAccount.OrganizationId = organizationId;
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
SetupOrganizationMatch(sutProvider, resource, organizationId);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId)
.Returns(false);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.UserProjectAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.GroupProjectAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.UserServiceAccountAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.GroupServiceAccountAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.UserProjectAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.GroupProjectAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.UserServiceAccountAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.GroupServiceAccountAccessPolicy)]
public async Task CanCreate_UnsupportedClientTypes_DoesNotSucceed(
AccessClientType clientType,
AccessPolicyType accessPolicyType,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Guid organizationId,
Project mockGrantedProject,
ServiceAccount mockGrantedServiceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Create;
mockGrantedProject.OrganizationId = organizationId;
mockGrantedServiceAccount.OrganizationId = organizationId;
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
SetupOrganizationMatch(sutProvider, resource, organizationId);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId)
.Returns(true);
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId).ReturnsForAnyArgs(
(clientType, Guid.NewGuid()));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
public async Task CanCreate_AccessCheck(
AccessPolicyType accessPolicyType,
PermissionType permissionType,
bool read, bool write, bool expected,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Guid organizationId,
Guid userId,
Guid serviceAccountId,
Project mockGrantedProject,
ServiceAccount mockGrantedServiceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Create;
mockGrantedProject.OrganizationId = organizationId;
mockGrantedServiceAccount.OrganizationId = organizationId;
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount, serviceAccountId);
SetupCurrentUserPermission(sutProvider, permissionType, organizationId, userId);
SetupOrganizationMatch(sutProvider, resource, organizationId);
SetupMockAccess(sutProvider, userId, resource, read, write);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.Equal(expected, authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(false, false)]
[BitAutoData(false, true)]
[BitAutoData(true, false)]
public async Task CanCreate_ServiceAccountProjectAccessPolicy_TargetsDontExist_DoesNotSucceed(bool projectExists,
bool serviceAccountExists,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider, ServiceAccountProjectAccessPolicy resource,
Project mockProject, ServiceAccount mockServiceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Create;
resource.GrantedProject = null;
resource.ServiceAccount = null;
if (projectExists)
{
resource.GrantedProject = null;
mockProject.Id = resource.GrantedProjectId!.Value;
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(resource.GrantedProjectId!.Value)
.Returns(mockProject);
}
if (serviceAccountExists)
{
resource.ServiceAccount = null;
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(resource.ServiceAccountId!.Value)
.Returns(mockServiceAccount);
}
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(false, false)]
[BitAutoData(false, true)]
[BitAutoData(true, false)]
[BitAutoData(true, true)]
public async Task CanCreate_ServiceAccountProjectAccessPolicy_OrgMismatch_DoesNotSucceed(bool fetchProject,
bool fetchSa,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider, ServiceAccountProjectAccessPolicy resource,
Project mockProject, ServiceAccount mockServiceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Create;
if (fetchProject)
{
resource.GrantedProject = null;
mockProject.Id = resource.GrantedProjectId!.Value;
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(resource.GrantedProjectId!.Value)
.Returns(mockProject);
}
if (fetchSa)
{
resource.ServiceAccount = null;
mockServiceAccount.Id = resource.ServiceAccountId!.Value;
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(resource.ServiceAccountId!.Value)
.Returns(mockServiceAccount);
}
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task CanCreate_ServiceAccountProjectAccessPolicy_AccessToSecretsManagerFalse_DoesNotSucceed(
SutProvider<AccessPolicyAuthorizationHandler> sutProvider, ServiceAccountProjectAccessPolicy resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Create;
resource.ServiceAccount!.OrganizationId = resource.GrantedProject!.OrganizationId;
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.GrantedProject!.OrganizationId)
.Returns(false);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.ServiceAccount)]
[BitAutoData(AccessClientType.Organization)]
public async Task CanCreate_ServiceAccountProjectAccessPolicy_UnsupportedClientTypes_DoesNotSucceed(
AccessClientType clientType,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider, ServiceAccountProjectAccessPolicy resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Create;
resource.ServiceAccount!.OrganizationId = resource.GrantedProject!.OrganizationId;
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.GrantedProject!.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.ServiceAccount!.OrganizationId).ReturnsForAnyArgs(
(clientType, new Guid()));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(PermissionType.RunAsAdmin, true, true, true, true, true)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false, true, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false, true, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false, true, true, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true, false, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true, false, true, true)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true, true, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true, true, true, true)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false, false, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false, false, true, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false, true, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false, true, true, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true, false, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true, false, true, true)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true, true, true)]
public async Task CanCreate_ServiceAccountProjectAccessPolicy_AccessCheck(PermissionType permissionType,
bool projectRead,
bool projectWrite, bool saRead, bool saWrite, bool expected,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider, ServiceAccountProjectAccessPolicy resource,
ClaimsPrincipal claimsPrincipal, Guid userId)
{
var requirement = AccessPolicyOperations.Create;
resource.ServiceAccount!.OrganizationId = resource.GrantedProject!.OrganizationId;
SetupCurrentUserPermission(sutProvider, permissionType, resource.GrantedProject!.OrganizationId, userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(resource.GrantedProjectId!.Value, userId, Arg.Any<AccessClientType>())
.Returns((projectRead, projectWrite));
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountAsync(resource.ServiceAccountId!.Value, userId, Arg.Any<AccessClientType>())
.Returns((saRead, saWrite));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.Equal(expected, authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy)]
public async Task CanUpdate_AccessToSecretsManagerFalse_DoesNotSucceed(AccessPolicyType accessPolicyType,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Guid organizationId,
Project mockGrantedProject,
ServiceAccount mockGrantedServiceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Update;
mockGrantedProject.OrganizationId = organizationId;
mockGrantedServiceAccount.OrganizationId = organizationId;
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(false);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.UserProjectAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.GroupProjectAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.ServiceAccountProjectAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.UserServiceAccountAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.GroupServiceAccountAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.UserProjectAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.GroupProjectAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.ServiceAccountProjectAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.UserServiceAccountAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.GroupServiceAccountAccessPolicy)]
public async Task CanUpdate_UnsupportedClientTypes_DoesNotSucceed(
AccessClientType clientType,
AccessPolicyType accessPolicyType,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Guid organizationId,
Project mockGrantedProject,
ServiceAccount mockGrantedServiceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Update;
mockGrantedProject.OrganizationId = organizationId;
mockGrantedServiceAccount.OrganizationId = organizationId;
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId).ReturnsForAnyArgs(
(clientType, new Guid()));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
public async Task CanUpdate_AccessCheck(
AccessPolicyType accessPolicyType,
PermissionType permissionType, bool read,
bool write, bool expected,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Guid organizationId,
Project mockGrantedProject,
ServiceAccount mockGrantedServiceAccount,
ClaimsPrincipal claimsPrincipal, Guid userId, Guid serviceAccountId)
{
var requirement = AccessPolicyOperations.Update;
mockGrantedProject.OrganizationId = organizationId;
mockGrantedServiceAccount.OrganizationId = organizationId;
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount,
serviceAccountId);
SetupCurrentUserPermission(sutProvider, permissionType, organizationId, userId);
SetupMockAccess(sutProvider, userId, resource, read, write);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.Equal(expected, authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy)]
public async Task CanDelete_AccessToSecretsManagerFalse_DoesNotSucceed(AccessPolicyType accessPolicyType,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Guid organizationId,
Project mockGrantedProject,
ServiceAccount mockGrantedServiceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Delete;
mockGrantedProject.OrganizationId = organizationId;
mockGrantedServiceAccount.OrganizationId = organizationId;
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(false);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.UserProjectAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.GroupProjectAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.ServiceAccountProjectAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.UserServiceAccountAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.GroupServiceAccountAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.UserProjectAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.GroupProjectAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.ServiceAccountProjectAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.UserServiceAccountAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.GroupServiceAccountAccessPolicy)]
public async Task CanDelete_UnsupportedClientTypes_DoesNotSucceed(
AccessClientType clientType,
AccessPolicyType accessPolicyType,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Guid organizationId,
Project mockGrantedProject,
ServiceAccount mockGrantedServiceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Delete;
mockGrantedProject.OrganizationId = organizationId;
mockGrantedServiceAccount.OrganizationId = organizationId;
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId).ReturnsForAnyArgs(
(clientType, new Guid()));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
public async Task CanDelete_AccessCheck(
AccessPolicyType accessPolicyType,
PermissionType permissionType,
bool read, bool write, bool expected,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Guid organizationId,
Project mockGrantedProject,
ServiceAccount mockGrantedServiceAccount,
ClaimsPrincipal claimsPrincipal, Guid userId, Guid serviceAccountId)
{
var requirement = AccessPolicyOperations.Delete;
mockGrantedProject.OrganizationId = organizationId;
mockGrantedServiceAccount.OrganizationId = organizationId;
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount,
serviceAccountId);
SetupCurrentUserPermission(sutProvider, permissionType, organizationId, userId);
SetupMockAccess(sutProvider, userId, resource, read, write);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.Equal(expected, authzContext.HasSucceeded);
}
}

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,342 @@
#nullable enable
using System.Reflection;
using System.Security.Claims;
using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Enums.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization;
using NSubstitute;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.AccessPolicies;
[SutProviderCustomize]
[ProjectCustomize]
public class ProjectServiceAccountsAccessPoliciesAuthorizationHandlerTests
{
[Fact]
public void ServiceAccountGrantedPoliciesOperations_OnlyPublicStatic()
{
var publicStaticFields =
typeof(ProjectServiceAccountsAccessPoliciesOperations).GetFields(BindingFlags.Public | BindingFlags.Static);
var allFields = typeof(ProjectServiceAccountsAccessPoliciesOperations).GetFields();
Assert.Equal(publicStaticFields.Length, allFields.Length);
}
[Theory]
[BitAutoData]
public async Task Handler_AccessSecretsManagerFalse_DoesNotSucceed(
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
.Returns(false);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.ServiceAccount)]
[BitAutoData(AccessClientType.Organization)]
public async Task Handler_UnsupportedClientTypes_DoesNotSucceed(
AccessClientType accessClientType,
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
SetupUserSubstitutes(sutProvider, accessClientType, resource);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task Handler_UnsupportedProjectServiceAccountsPoliciesOperationRequirement_Throws(
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = new ProjectServiceAccountsAccessPoliciesOperationRequirement();
SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck, false, false)]
[BitAutoData(AccessClientType.NoAccessCheck, true, false)]
[BitAutoData(AccessClientType.User, false, false)]
[BitAutoData(AccessClientType.User, true, false)]
public async Task Handler_UserHasNoWriteAccessToProject_DoesNotSucceed(
AccessClientType accessClientType,
bool projectReadAccess,
bool projectWriteAccess,
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(resource.ProjectId, userId, accessClientType)
.Returns((projectReadAccess, projectWriteAccess));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task Handler_ServiceAccountsInDifferentOrganization_DoesNotSucceed(
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource, userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(resource.ProjectId, userId, AccessClientType.NoAccessCheck)
.Returns((true, true));
sutProvider.GetDependency<IServiceAccountRepository>()
.ServiceAccountsAreInOrganizationAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)
.Returns(false);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_UserHasAccessToProject_NoCreatesRequested_Success(
AccessClientType accessClientType,
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
resource = RemoveAllCreates(resource);
SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.True(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_UserHasNoAccessToCreateServiceAccounts_DoesNotSucceed(
AccessClientType accessClientType,
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
resource = AddServiceAccountCreateUpdate(resource);
SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);
var accessResult = resource.ServiceAccountAccessPolicyUpdates
.Where(x => x.Operation == AccessPolicyOperation.Create)
.Select(x => x.AccessPolicy.ServiceAccountId!.Value)
.ToDictionary(id => id, _ => (false, false));
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns(accessResult);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_AccessResultsPartial_DoesNotSucceed(
AccessClientType accessClientType,
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
resource = AddServiceAccountCreateUpdate(resource);
SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);
var accessResult = resource.ServiceAccountAccessPolicyUpdates
.Where(x => x.Operation == AccessPolicyOperation.Create)
.Select(x => x.AccessPolicy.ServiceAccountId!.Value)
.ToDictionary(id => id, _ => (false, false));
accessResult[accessResult.First().Key] = (true, true);
accessResult.Remove(accessResult.Last().Key);
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns(accessResult);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_UserHasAccessToSomeCreateServiceAccounts_DoesNotSucceed(
AccessClientType accessClientType,
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
resource = AddServiceAccountCreateUpdate(resource);
SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);
var accessResult = resource.ServiceAccountAccessPolicyUpdates
.Where(x => x.Operation == AccessPolicyOperation.Create)
.Select(x => x.AccessPolicy.ServiceAccountId!.Value)
.ToDictionary(id => id, _ => (false, false));
accessResult[accessResult.First().Key] = (true, true);
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns(accessResult);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_UserHasAccessToAllCreateServiceAccounts_Success(
AccessClientType accessClientType,
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
resource = AddServiceAccountCreateUpdate(resource);
SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);
var accessResult = resource.ServiceAccountAccessPolicyUpdates
.Where(x => x.Operation == AccessPolicyOperation.Create)
.Select(x => x.AccessPolicy.ServiceAccountId!.Value)
.ToDictionary(id => id, _ => (true, true));
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns(accessResult);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.True(authzContext.HasSucceeded);
}
private static void SetupUserSubstitutes(
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
AccessClientType accessClientType,
ProjectServiceAccountsAccessPoliciesUpdates resource,
Guid userId = new())
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.OrganizationId)
.ReturnsForAnyArgs((accessClientType, userId));
}
private static void SetupServiceAccountsAccessTest(
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
AccessClientType accessClientType,
ProjectServiceAccountsAccessPoliciesUpdates resource,
Guid userId = new())
{
SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(resource.ProjectId, userId, accessClientType)
.Returns((true, true));
sutProvider.GetDependency<IServiceAccountRepository>()
.ServiceAccountsAreInOrganizationAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)
.Returns(true);
}
private static ProjectServiceAccountsAccessPoliciesUpdates AddServiceAccountCreateUpdate(
ProjectServiceAccountsAccessPoliciesUpdates resource)
{
resource.ServiceAccountAccessPolicyUpdates = resource.ServiceAccountAccessPolicyUpdates.Append(
new ServiceAccountProjectAccessPolicyUpdate
{
AccessPolicy = new ServiceAccountProjectAccessPolicy
{
ServiceAccountId = Guid.NewGuid(),
GrantedProjectId = resource.ProjectId,
Read = true,
Write = true
}
});
return resource;
}
private static ProjectServiceAccountsAccessPoliciesUpdates RemoveAllCreates(
ProjectServiceAccountsAccessPoliciesUpdates resource)
{
resource.ServiceAccountAccessPolicyUpdates =
resource.ServiceAccountAccessPolicyUpdates.Where(x => x.Operation != AccessPolicyOperation.Create);
return resource;
}
}

View File

@ -0,0 +1,273 @@
#nullable enable
using System.Reflection;
using System.Security.Claims;
using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization;
using NSubstitute;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.AccessPolicies;
[SutProviderCustomize]
[ProjectCustomize]
public class ServiceAccountGrantedPoliciesAuthorizationHandlerTests
{
[Fact]
public void ServiceAccountGrantedPoliciesOperations_OnlyPublicStatic()
{
var publicStaticFields =
typeof(ServiceAccountGrantedPoliciesOperations).GetFields(BindingFlags.Public | BindingFlags.Static);
var allFields = typeof(ServiceAccountGrantedPoliciesOperations).GetFields();
Assert.Equal(publicStaticFields.Length, allFields.Length);
}
[Theory]
[BitAutoData]
public async Task Handler_AccessSecretsManagerFalse_DoesNotSucceed(
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
.Returns(false);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.ServiceAccount)]
[BitAutoData(AccessClientType.Organization)]
public async Task Handler_UnsupportedClientTypes_DoesNotSucceed(
AccessClientType accessClientType,
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
SetupUserSubstitutes(sutProvider, accessClientType, resource);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task Handler_UnsupportedServiceAccountGrantedPoliciesOperationRequirement_Throws(
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = new ServiceAccountGrantedPoliciesOperationRequirement();
SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck, false, false)]
[BitAutoData(AccessClientType.NoAccessCheck, true, false)]
[BitAutoData(AccessClientType.User, false, false)]
[BitAutoData(AccessClientType.User, true, false)]
public async Task Handler_UserHasNoWriteAccessToServiceAccount_DoesNotSucceed(
AccessClientType accessClientType,
bool saReadAccess,
bool saWriteAccess,
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountAsync(resource.ServiceAccountId, userId, accessClientType)
.Returns((saReadAccess, saWriteAccess));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task Handler_GrantedProjectsInDifferentOrganization_DoesNotSucceed(
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource, userId);
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountAsync(resource.ServiceAccountId, userId, AccessClientType.NoAccessCheck)
.Returns((true, true));
sutProvider.GetDependency<IProjectRepository>()
.ProjectsAreInOrganization(Arg.Any<List<Guid>>(), resource.OrganizationId)
.Returns(false);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_UserHasNoAccessToGrantedProjects_DoesNotSucceed(
AccessClientType accessClientType,
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns(projectIds.ToDictionary(projectId => projectId, _ => (false, false)));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_UserHasAccessToSomeGrantedProjects_DoesNotSucceed(
AccessClientType accessClientType,
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId);
var accessResult = projectIds.ToDictionary(projectId => projectId, _ => (false, false));
accessResult[projectIds.First()] = (true, true);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns(accessResult);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_AccessResultsPartial_DoesNotSucceed(
AccessClientType accessClientType,
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId);
var accessResult = projectIds.ToDictionary(projectId => projectId, _ => (false, false));
accessResult.Remove(projectIds.First());
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns(accessResult);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_UserHasAccessToAllGrantedProjects_Success(
AccessClientType accessClientType,
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns(projectIds.ToDictionary(projectId => projectId, _ => (true, true)));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.True(authzContext.HasSucceeded);
}
private static void SetupUserSubstitutes(
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
AccessClientType accessClientType,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId = new())
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.OrganizationId)
.ReturnsForAnyArgs((accessClientType, userId));
}
private static List<Guid> SetupProjectAccessTest(
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
AccessClientType accessClientType,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId = new())
{
SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountAsync(resource.ServiceAccountId, userId, accessClientType)
.Returns((true, true));
sutProvider.GetDependency<IProjectRepository>()
.ProjectsAreInOrganization(Arg.Any<List<Guid>>(), resource.OrganizationId)
.Returns(true);
return resource.ProjectGrantedPolicyUpdates
.Select(pu => pu.AccessPolicy.GrantedProjectId!.Value)
.ToList();
}
}

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

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