mirror of
https://github.com/bitwarden/server
synced 2025-12-21 18:53:41 +00:00
Merge branch 'main' of github.com:bitwarden/server into arch/seeder-sdk
# Conflicts: # .gitignore # bitwarden-server.sln
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"swashbuckle.aspnetcore.cli": {
|
"swashbuckle.aspnetcore.cli": {
|
||||||
"version": "7.3.2",
|
"version": "9.0.4",
|
||||||
"commands": ["swagger"]
|
"commands": ["swagger"]
|
||||||
},
|
},
|
||||||
"dotnet-ef": {
|
"dotnet-ef": {
|
||||||
|
|||||||
24
.github/CODEOWNERS
vendored
24
.github/CODEOWNERS
vendored
@@ -4,17 +4,18 @@
|
|||||||
#
|
#
|
||||||
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||||
|
|
||||||
## Docker files have shared ownership ##
|
## Docker-related files
|
||||||
**/Dockerfile
|
**/Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre
|
||||||
**/*.Dockerfile
|
**/*.Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre
|
||||||
**/.dockerignore
|
**/*.dockerignore @bitwarden/team-appsec @bitwarden/dept-bre
|
||||||
**/entrypoint.sh
|
**/docker-compose.yml @bitwarden/team-appsec @bitwarden/dept-bre
|
||||||
|
**/entrypoint.sh @bitwarden/team-appsec @bitwarden/dept-bre
|
||||||
|
|
||||||
## BRE team owns these workflows ##
|
## BRE team owns these workflows ##
|
||||||
.github/workflows/publish.yml @bitwarden/dept-bre
|
.github/workflows/publish.yml @bitwarden/dept-bre
|
||||||
|
|
||||||
## These are shared workflows ##
|
## These are shared workflows ##
|
||||||
.github/workflows/_move_finalization_db_scripts.yml
|
.github/workflows/_move_edd_db_scripts.yml
|
||||||
.github/workflows/release.yml
|
.github/workflows/release.yml
|
||||||
|
|
||||||
# Database Operations for database changes
|
# Database Operations for database changes
|
||||||
@@ -33,6 +34,9 @@ util/SqliteMigrations/** @bitwarden/dept-dbops
|
|||||||
# Shared util projects
|
# Shared util projects
|
||||||
util/Setup/** @bitwarden/dept-bre @bitwarden/team-platform-dev
|
util/Setup/** @bitwarden/dept-bre @bitwarden/team-platform-dev
|
||||||
|
|
||||||
|
# UIF
|
||||||
|
src/Core/MailTemplates/Mjml @bitwarden/team-ui-foundation # Teams are expected to own sub-directories of this project
|
||||||
|
|
||||||
# Auth team
|
# Auth team
|
||||||
**/Auth @bitwarden/team-auth-dev
|
**/Auth @bitwarden/team-auth-dev
|
||||||
bitwarden_license/src/Sso @bitwarden/team-auth-dev
|
bitwarden_license/src/Sso @bitwarden/team-auth-dev
|
||||||
@@ -47,11 +51,7 @@ src/Core/IdentityServer @bitwarden/team-auth-dev
|
|||||||
**/Tools @bitwarden/team-tools-dev
|
**/Tools @bitwarden/team-tools-dev
|
||||||
|
|
||||||
# Dirt (Data Insights & Reporting) team
|
# Dirt (Data Insights & Reporting) team
|
||||||
src/Api/Controllers/Dirt @bitwarden/team-data-insights-and-reporting-dev
|
**/Dirt @bitwarden/team-data-insights-and-reporting-dev
|
||||||
src/Core/Dirt @bitwarden/team-data-insights-and-reporting-dev
|
|
||||||
src/Infrastructure.Dapper/Dirt @bitwarden/team-data-insights-and-reporting-dev
|
|
||||||
test/Api.Test/Dirt @bitwarden/team-data-insights-and-reporting-dev
|
|
||||||
test/Core.Test/Dirt @bitwarden/team-data-insights-and-reporting-dev
|
|
||||||
|
|
||||||
# Vault team
|
# Vault team
|
||||||
**/Vault @bitwarden/team-vault-dev
|
**/Vault @bitwarden/team-vault-dev
|
||||||
@@ -93,6 +93,8 @@ src/Admin/Views/Tools @bitwarden/team-billing-dev
|
|||||||
**/.dockerignore @bitwarden/team-platform-dev
|
**/.dockerignore @bitwarden/team-platform-dev
|
||||||
**/Dockerfile @bitwarden/team-platform-dev
|
**/Dockerfile @bitwarden/team-platform-dev
|
||||||
**/entrypoint.sh @bitwarden/team-platform-dev
|
**/entrypoint.sh @bitwarden/team-platform-dev
|
||||||
|
# The PushType enum is expected to be editted by anyone without need for Platform review
|
||||||
|
src/Core/Platform/Push/PushType.cs
|
||||||
|
|
||||||
# Multiple owners - DO NOT REMOVE (BRE)
|
# Multiple owners - DO NOT REMOVE (BRE)
|
||||||
**/packages.lock.json
|
**/packages.lock.json
|
||||||
|
|||||||
1
.github/ISSUE_TEMPLATE/bw-unified.yml
vendored
1
.github/ISSUE_TEMPLATE/bw-unified.yml
vendored
@@ -1,4 +1,3 @@
|
|||||||
name: Bitwarden Unified Bug Report
|
|
||||||
name: Bitwarden Unified Deployment Bug Report
|
name: Bitwarden Unified Deployment Bug Report
|
||||||
description: File a bug report
|
description: File a bug report
|
||||||
labels: [bug, bw-unified-deploy]
|
labels: [bug, bw-unified-deploy]
|
||||||
|
|||||||
14
.github/renovate.json5
vendored
14
.github/renovate.json5
vendored
@@ -9,18 +9,6 @@
|
|||||||
"nuget",
|
"nuget",
|
||||||
],
|
],
|
||||||
packageRules: [
|
packageRules: [
|
||||||
{
|
|
||||||
// Group all release-related workflows for GitHub Actions together for BRE.
|
|
||||||
groupName: "github-action",
|
|
||||||
matchManagers: ["github-actions"],
|
|
||||||
matchFileNames: [
|
|
||||||
".github/workflows/publish.yml",
|
|
||||||
".github/workflows/release.yml"
|
|
||||||
],
|
|
||||||
commitMessagePrefix: "[deps] BRE:",
|
|
||||||
reviewers: ["team:dept-bre"],
|
|
||||||
addLabels: ["hold"],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
groupName: "dockerfile minor",
|
groupName: "dockerfile minor",
|
||||||
matchManagers: ["dockerfile"],
|
matchManagers: ["dockerfile"],
|
||||||
@@ -35,6 +23,7 @@
|
|||||||
groupName: "github-action minor",
|
groupName: "github-action minor",
|
||||||
matchManagers: ["github-actions"],
|
matchManagers: ["github-actions"],
|
||||||
matchUpdateTypes: ["minor"],
|
matchUpdateTypes: ["minor"],
|
||||||
|
addLabels: ["hold"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// For any Microsoft.Extensions.* and Microsoft.AspNetCore.* packages, we want to create PRs for patch updates.
|
// For any Microsoft.Extensions.* and Microsoft.AspNetCore.* packages, we want to create PRs for patch updates.
|
||||||
@@ -95,7 +84,6 @@
|
|||||||
"Serilog.AspNetCore",
|
"Serilog.AspNetCore",
|
||||||
"Serilog.Extensions.Logging",
|
"Serilog.Extensions.Logging",
|
||||||
"Serilog.Extensions.Logging.File",
|
"Serilog.Extensions.Logging.File",
|
||||||
"Serilog.Sinks.AzureCosmosDB",
|
|
||||||
"Serilog.Sinks.SyslogMessages",
|
"Serilog.Sinks.SyslogMessages",
|
||||||
"Stripe.net",
|
"Stripe.net",
|
||||||
"Swashbuckle.AspNetCore",
|
"Swashbuckle.AspNetCore",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: _move_finalization_db_scripts
|
name: _move_edd_db_scripts
|
||||||
run-name: Move finalization database scripts
|
run-name: Move EDD database scripts
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
@@ -12,14 +12,20 @@ jobs:
|
|||||||
setup:
|
setup:
|
||||||
name: Setup
|
name: Setup
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
outputs:
|
outputs:
|
||||||
migration_filename_prefix: ${{ steps.prefix.outputs.prefix }}
|
migration_filename_prefix: ${{ steps.prefix.outputs.prefix }}
|
||||||
copy_finalization_scripts: ${{ steps.check-finalization-scripts-existence.outputs.copy_finalization_scripts }}
|
copy_edd_scripts: ${{ steps.check-script-existence.outputs.copy_edd_scripts }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Log in to Azure
|
- name: Log in to Azure
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: bitwarden/gh-actions/azure-login@main
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
@@ -28,6 +34,9 @@ jobs:
|
|||||||
keyvault: "bitwarden-ci"
|
keyvault: "bitwarden-ci"
|
||||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
|
||||||
|
- name: Log out from Azure
|
||||||
|
uses: bitwarden/gh-actions/azure-logout@main
|
||||||
|
|
||||||
- name: Check out branch
|
- name: Check out branch
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
@@ -37,22 +46,27 @@ jobs:
|
|||||||
id: prefix
|
id: prefix
|
||||||
run: echo "prefix=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
run: echo "prefix=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Check if any files in DB finalization directory
|
- name: Check if any files in DB transition or finalization directories
|
||||||
id: check-finalization-scripts-existence
|
id: check-script-existence
|
||||||
run: |
|
run: |
|
||||||
if [ -f util/Migrator/DbScripts_finalization/* ]; then
|
if [ -f util/Migrator/DbScripts_transition/* -o -f util/Migrator/DbScripts_finalization/* ]; then
|
||||||
echo "copy_finalization_scripts=true" >> $GITHUB_OUTPUT
|
echo "copy_edd_scripts=true" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo "copy_finalization_scripts=false" >> $GITHUB_OUTPUT
|
echo "copy_edd_scripts=false" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
move-finalization-db-scripts:
|
move-scripts:
|
||||||
name: Move finalization database scripts
|
name: Move scripts
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs: setup
|
needs: setup
|
||||||
if: ${{ needs.setup.outputs.copy_finalization_scripts == 'true' }}
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
id-token: write
|
||||||
|
actions: read
|
||||||
|
if: ${{ needs.setup.outputs.copy_edd_scripts == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Check out repo
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -61,41 +75,70 @@ jobs:
|
|||||||
id: branch_name
|
id: branch_name
|
||||||
env:
|
env:
|
||||||
PREFIX: ${{ needs.setup.outputs.migration_filename_prefix }}
|
PREFIX: ${{ needs.setup.outputs.migration_filename_prefix }}
|
||||||
run: echo "branch_name=move_finalization_db_scripts_$PREFIX" >> $GITHUB_OUTPUT
|
run: echo "branch_name=move_edd_db_scripts_$PREFIX" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: "Create branch"
|
- name: "Create branch"
|
||||||
env:
|
env:
|
||||||
BRANCH: ${{ steps.branch_name.outputs.branch_name }}
|
BRANCH: ${{ steps.branch_name.outputs.branch_name }}
|
||||||
run: git switch -c $BRANCH
|
run: git switch -c $BRANCH
|
||||||
|
|
||||||
- name: Move DbScripts_finalization
|
- name: Move scripts and finalization database schema
|
||||||
id: move-files
|
id: move-files
|
||||||
env:
|
env:
|
||||||
PREFIX: ${{ needs.setup.outputs.migration_filename_prefix }}
|
PREFIX: ${{ needs.setup.outputs.migration_filename_prefix }}
|
||||||
run: |
|
run: |
|
||||||
src_dir="util/Migrator/DbScripts_finalization"
|
# scripts
|
||||||
|
moved_files="Migration scripts moved:\n\n"
|
||||||
|
|
||||||
|
src_dirs="util/Migrator/DbScripts_transition,util/Migrator/DbScripts_finalization"
|
||||||
dest_dir="util/Migrator/DbScripts"
|
dest_dir="util/Migrator/DbScripts"
|
||||||
i=0
|
i=0
|
||||||
|
|
||||||
moved_files=""
|
for src_dir in ${src_dirs//,/ }; do
|
||||||
for file in "$src_dir"/*; do
|
for file in "$src_dir"/*; do
|
||||||
filenumber=$(printf "%02d" $i)
|
filenumber=$(printf "%02d" $i)
|
||||||
|
|
||||||
filename=$(basename "$file")
|
filename=$(basename "$file")
|
||||||
new_filename="${PREFIX}_${filenumber}_${filename}"
|
new_filename="${PREFIX}_${filenumber}_${filename}"
|
||||||
dest_file="$dest_dir/$new_filename"
|
dest_file="$dest_dir/$new_filename"
|
||||||
|
|
||||||
mv "$file" "$dest_file"
|
# Replace any finalization references due to the move
|
||||||
moved_files="$moved_files \n $filename -> $new_filename"
|
sed -i -e 's/dbo_finalization/dbo/g' "$file"
|
||||||
|
|
||||||
i=$((i+1))
|
mv "$file" "$dest_file"
|
||||||
|
moved_files="$moved_files \n $filename -> $new_filename"
|
||||||
|
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# schema
|
||||||
|
moved_files="$moved_files\n\nFinalization scripts moved:\n\n"
|
||||||
|
|
||||||
|
src_dir="src/Sql/dbo_finalization"
|
||||||
|
dest_dir="src/Sql/dbo"
|
||||||
|
|
||||||
|
# sync finalization schema back to dbo, maintaining structure
|
||||||
|
rsync -r "$src_dir/" "$dest_dir/"
|
||||||
|
rm -rf $src_dir/*
|
||||||
|
|
||||||
|
# Replace any finalization references due to the move
|
||||||
|
find ./src/Sql/dbo -name "*.sql" -type f -exec sed -i \
|
||||||
|
-e 's/\[dbo_finalization\]/[dbo]/g' \
|
||||||
|
-e 's/dbo_finalization\./dbo./g' {} +
|
||||||
|
|
||||||
|
for file in "$src_dir"/**/*; do
|
||||||
|
moved_files="$moved_files \n $file"
|
||||||
|
done
|
||||||
|
|
||||||
echo "moved_files=$moved_files" >> $GITHUB_OUTPUT
|
echo "moved_files=$moved_files" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Log in to Azure - production subscription
|
- name: Log in to Azure
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: bitwarden/gh-actions/azure-login@main
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
@@ -106,8 +149,11 @@ jobs:
|
|||||||
github-gpg-private-key-passphrase,
|
github-gpg-private-key-passphrase,
|
||||||
devops-alerts-slack-webhook-url"
|
devops-alerts-slack-webhook-url"
|
||||||
|
|
||||||
|
- name: Log out from Azure
|
||||||
|
uses: bitwarden/gh-actions/azure-logout@main
|
||||||
|
|
||||||
- name: Import GPG keys
|
- name: Import GPG keys
|
||||||
uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6.2.0
|
uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0
|
||||||
with:
|
with:
|
||||||
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
||||||
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
||||||
@@ -121,7 +167,7 @@ jobs:
|
|||||||
git config --local user.name "bitwarden-devops-bot"
|
git config --local user.name "bitwarden-devops-bot"
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
git add .
|
git add .
|
||||||
git commit -m "Move DbScripts_finalization to DbScripts" -a
|
git commit -m "Move EDD database scripts" -a
|
||||||
git push -u origin ${{ steps.branch_name.outputs.branch_name }}
|
git push -u origin ${{ steps.branch_name.outputs.branch_name }}
|
||||||
echo "pr_needed=true" >> $GITHUB_OUTPUT
|
echo "pr_needed=true" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
@@ -137,16 +183,16 @@ jobs:
|
|||||||
BRANCH: ${{ steps.branch_name.outputs.branch_name }}
|
BRANCH: ${{ steps.branch_name.outputs.branch_name }}
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
MOVED_FILES: ${{ steps.move-files.outputs.moved_files }}
|
MOVED_FILES: ${{ steps.move-files.outputs.moved_files }}
|
||||||
TITLE: "Move finalization database scripts"
|
TITLE: "Move EDD database scripts"
|
||||||
run: |
|
run: |
|
||||||
PR_URL=$(gh pr create --title "$TITLE" \
|
PR_URL=$(gh pr create --title "$TITLE" \
|
||||||
--base "main" \
|
--base "main" \
|
||||||
--head "$BRANCH" \
|
--head "$BRANCH" \
|
||||||
--label "automated pr" \
|
--label "automated pr" \
|
||||||
--body "
|
--body "
|
||||||
## Automated movement of DbScripts_finalization to DbScripts
|
Automated movement of EDD database scripts.
|
||||||
|
|
||||||
## Files moved:
|
Files moved:
|
||||||
$(echo -e "$MOVED_FILES")
|
$(echo -e "$MOVED_FILES")
|
||||||
")
|
")
|
||||||
echo "pr_url=${PR_URL}" >> $GITHUB_OUTPUT
|
echo "pr_url=${PR_URL}" >> $GITHUB_OUTPUT
|
||||||
@@ -157,5 +203,5 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
|
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
|
||||||
with:
|
with:
|
||||||
message: "Created PR for moving DbScripts_finalization to DbScripts: ${{ steps.create-pr.outputs.pr_url }}"
|
message: "Created PR for moving EDD database scripts: ${{ steps.create-pr.outputs.pr_url }}"
|
||||||
status: ${{ job.status }}
|
status: ${{ job.status }}
|
||||||
153
.github/workflows/build.yml
vendored
153
.github/workflows/build.yml
vendored
@@ -11,7 +11,7 @@ on:
|
|||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs: {}
|
inputs: {}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||||
|
|
||||||
- name: Verify format
|
- name: Verify format
|
||||||
run: dotnet format --verify-no-changes
|
run: dotnet format --verify-no-changes
|
||||||
@@ -95,10 +95,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check secrets
|
- name: Check secrets
|
||||||
id: check-secrets
|
id: check-secrets
|
||||||
env:
|
|
||||||
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
|
||||||
run: |
|
run: |
|
||||||
has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }}
|
has_secrets=${{ secrets.AZURE_CLIENT_ID != '' }}
|
||||||
echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT
|
echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
@@ -119,10 +117,10 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: "**/package-lock.json"
|
cache-dependency-path: "**/package-lock.json"
|
||||||
@@ -168,25 +166,22 @@ jobs:
|
|||||||
|
|
||||||
########## Set up Docker ##########
|
########## Set up Docker ##########
|
||||||
- name: Set up QEMU emulators
|
- name: Set up QEMU emulators
|
||||||
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
|
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
|
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||||
|
|
||||||
########## ACRs ##########
|
########## ACRs ##########
|
||||||
- name: Log in to Azure - production subscription
|
- name: Log in to Azure
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: bitwarden/gh-actions/azure-login@main
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
|
||||||
- name: Log in to ACR - production subscription
|
- name: Log in to ACR - production subscription
|
||||||
run: az acr login -n bitwardenprod
|
run: az acr login -n bitwardenprod
|
||||||
|
|
||||||
- name: Log in to Azure - CI subscription
|
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
|
||||||
with:
|
|
||||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
|
||||||
|
|
||||||
- name: Retrieve GitHub PAT secrets
|
- name: Retrieve GitHub PAT secrets
|
||||||
id: retrieve-secret-pat
|
id: retrieve-secret-pat
|
||||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||||
@@ -242,7 +237,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
id: build-artifacts
|
id: build-artifacts
|
||||||
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0
|
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile
|
file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile
|
||||||
@@ -257,7 +252,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
|
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
|
||||||
|
|
||||||
- name: Sign image with Cosign
|
- name: Sign image with Cosign
|
||||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
@@ -274,7 +269,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Scan Docker image
|
- name: Scan Docker image
|
||||||
id: container-scan
|
id: container-scan
|
||||||
uses: anchore/scan-action@abae793926ec39a78ab18002bc7fc45bbbd94342 # v6.0.0
|
uses: anchore/scan-action@2c901ab7378897c01b8efaa2d0c9bf519cc64b9e # v6.2.0
|
||||||
with:
|
with:
|
||||||
image: ${{ steps.image-tags.outputs.primary_tag }}
|
image: ${{ steps.image-tags.outputs.primary_tag }}
|
||||||
fail-build: false
|
fail-build: false
|
||||||
@@ -287,10 +282,16 @@ jobs:
|
|||||||
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
|
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
|
||||||
ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
|
ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
|
||||||
|
|
||||||
|
- name: Log out from Azure
|
||||||
|
uses: bitwarden/gh-actions/azure-logout@main
|
||||||
|
|
||||||
upload:
|
upload:
|
||||||
name: Upload
|
name: Upload
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs: build-artifacts
|
needs: build-artifacts
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
actions: read
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
@@ -298,12 +299,14 @@ jobs:
|
|||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||||
|
|
||||||
- name: Log in to Azure - production subscription
|
- name: Log in to Azure
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: bitwarden/gh-actions/azure-login@main
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
|
||||||
- name: Log in to ACR - production subscription
|
- name: Log in to ACR - production subscription
|
||||||
run: az acr login -n $_AZ_REGISTRY --only-show-errors
|
run: az acr login -n $_AZ_REGISTRY --only-show-errors
|
||||||
@@ -350,6 +353,9 @@ jobs:
|
|||||||
cd docker-stub/US; zip -r ../../docker-stub-US.zip *; cd ../..
|
cd docker-stub/US; zip -r ../../docker-stub-US.zip *; cd ../..
|
||||||
cd docker-stub/EU; zip -r ../../docker-stub-EU.zip *; cd ../..
|
cd docker-stub/EU; zip -r ../../docker-stub-EU.zip *; cd ../..
|
||||||
|
|
||||||
|
- name: Log out from Azure
|
||||||
|
uses: bitwarden/gh-actions/azure-logout@main
|
||||||
|
|
||||||
- name: Upload Docker stub US artifact
|
- name: Upload Docker stub US artifact
|
||||||
if: |
|
if: |
|
||||||
github.event_name != 'pull_request'
|
github.event_name != 'pull_request'
|
||||||
@@ -370,62 +376,23 @@ jobs:
|
|||||||
path: docker-stub-EU.zip
|
path: docker-stub-EU.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Build Public API Swagger
|
- name: Build Swagger files
|
||||||
run: |
|
run: |
|
||||||
cd ./src/Api
|
cd ./dev
|
||||||
echo "Restore tools"
|
pwsh ./generate_openapi_files.ps1
|
||||||
dotnet tool restore
|
|
||||||
echo "Publish"
|
|
||||||
dotnet publish -c "Release" -o obj/build-output/publish
|
|
||||||
|
|
||||||
dotnet swagger tofile --output ../../swagger.json --host https://api.bitwarden.com \
|
|
||||||
./obj/build-output/publish/Api.dll public
|
|
||||||
cd ../..
|
|
||||||
env:
|
|
||||||
ASPNETCORE_ENVIRONMENT: Production
|
|
||||||
swaggerGen: "True"
|
|
||||||
DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX: 2
|
|
||||||
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
|
|
||||||
|
|
||||||
- name: Upload Public API Swagger artifact
|
- name: Upload Public API Swagger artifact
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: swagger.json
|
name: swagger.json
|
||||||
path: swagger.json
|
path: api.public.json
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Build Internal API Swagger
|
|
||||||
run: |
|
|
||||||
cd ./src/Api
|
|
||||||
echo "Restore API tools"
|
|
||||||
dotnet tool restore
|
|
||||||
echo "Publish API"
|
|
||||||
dotnet publish -c "Release" -o obj/build-output/publish
|
|
||||||
|
|
||||||
dotnet swagger tofile --output ../../internal.json --host https://api.bitwarden.com \
|
|
||||||
./obj/build-output/publish/Api.dll internal
|
|
||||||
|
|
||||||
cd ../Identity
|
|
||||||
|
|
||||||
echo "Restore Identity tools"
|
|
||||||
dotnet tool restore
|
|
||||||
echo "Publish Identity"
|
|
||||||
dotnet publish -c "Release" -o obj/build-output/publish
|
|
||||||
|
|
||||||
dotnet swagger tofile --output ../../identity.json --host https://identity.bitwarden.com \
|
|
||||||
./obj/build-output/publish/Identity.dll v1
|
|
||||||
cd ../..
|
|
||||||
env:
|
|
||||||
ASPNETCORE_ENVIRONMENT: Development
|
|
||||||
swaggerGen: "True"
|
|
||||||
DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX: 2
|
|
||||||
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
|
|
||||||
|
|
||||||
- name: Upload Internal API Swagger artifact
|
- name: Upload Internal API Swagger artifact
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: internal.json
|
name: internal.json
|
||||||
path: internal.json
|
path: api.json
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Identity Swagger artifact
|
- name: Upload Identity Swagger artifact
|
||||||
@@ -458,7 +425,7 @@ jobs:
|
|||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
@@ -496,11 +463,15 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- build-artifacts
|
- build-artifacts
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Log in to Azure - CI subscription
|
- name: Log in to Azure
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: bitwarden/gh-actions/azure-login@main
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
|
||||||
- name: Retrieve GitHub PAT secrets
|
- name: Retrieve GitHub PAT secrets
|
||||||
id: retrieve-secret-pat
|
id: retrieve-secret-pat
|
||||||
@@ -509,8 +480,11 @@ jobs:
|
|||||||
keyvault: "bitwarden-ci"
|
keyvault: "bitwarden-ci"
|
||||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
|
||||||
|
- name: Log out from Azure
|
||||||
|
uses: bitwarden/gh-actions/azure-logout@main
|
||||||
|
|
||||||
- name: Trigger self-host build
|
- name: Trigger self-host build
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
script: |
|
script: |
|
||||||
@@ -530,11 +504,15 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- build-artifacts
|
- build-artifacts
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Log in to Azure - CI subscription
|
- name: Log in to Azure
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: bitwarden/gh-actions/azure-login@main
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
|
||||||
- name: Retrieve GitHub PAT secrets
|
- name: Retrieve GitHub PAT secrets
|
||||||
id: retrieve-secret-pat
|
id: retrieve-secret-pat
|
||||||
@@ -543,8 +521,11 @@ jobs:
|
|||||||
keyvault: "bitwarden-ci"
|
keyvault: "bitwarden-ci"
|
||||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
|
||||||
|
- name: Log out from Azure
|
||||||
|
uses: bitwarden/gh-actions/azure-logout@main
|
||||||
|
|
||||||
- name: Trigger k8s deploy
|
- name: Trigger k8s deploy
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
script: |
|
script: |
|
||||||
@@ -572,7 +553,9 @@ jobs:
|
|||||||
project: server
|
project: server
|
||||||
pull_request_number: ${{ github.event.number || 0 }}
|
pull_request_number: ${{ github.event.number || 0 }}
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
permissions: read-all
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
check-failures:
|
check-failures:
|
||||||
name: Check for failures
|
name: Check for failures
|
||||||
@@ -585,6 +568,8 @@ jobs:
|
|||||||
- build-mssqlmigratorutility
|
- build-mssqlmigratorutility
|
||||||
- self-host-build
|
- self-host-build
|
||||||
- trigger-k8s-deploy
|
- trigger-k8s-deploy
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Check if any job failed
|
- name: Check if any job failed
|
||||||
if: |
|
if: |
|
||||||
@@ -593,11 +578,12 @@ jobs:
|
|||||||
&& contains(needs.*.result, 'failure')
|
&& contains(needs.*.result, 'failure')
|
||||||
run: exit 1
|
run: exit 1
|
||||||
|
|
||||||
- name: Log in to Azure - CI subscription
|
- name: Log in to Azure
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: bitwarden/gh-actions/azure-login@main
|
||||||
if: failure()
|
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
@@ -607,6 +593,9 @@ jobs:
|
|||||||
keyvault: "bitwarden-ci"
|
keyvault: "bitwarden-ci"
|
||||||
secrets: "devops-alerts-slack-webhook-url"
|
secrets: "devops-alerts-slack-webhook-url"
|
||||||
|
|
||||||
|
- name: Log out from Azure
|
||||||
|
uses: bitwarden/gh-actions/azure-logout@main
|
||||||
|
|
||||||
- name: Notify Slack on failure
|
- name: Notify Slack on failure
|
||||||
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
|
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
|
||||||
if: failure()
|
if: failure()
|
||||||
|
|||||||
8
.github/workflows/build_target.yml
vendored
8
.github/workflows/build_target.yml
vendored
@@ -14,6 +14,8 @@ jobs:
|
|||||||
check-run:
|
check-run:
|
||||||
name: Check PR run
|
name: Check PR run
|
||||||
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
|
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
run-workflow:
|
run-workflow:
|
||||||
name: Run Build on PR Target
|
name: Run Build on PR Target
|
||||||
@@ -21,3 +23,9 @@ jobs:
|
|||||||
if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
|
if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
|
||||||
uses: ./.github/workflows/build.yml
|
uses: ./.github/workflows/build.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
id-token: write
|
||||||
|
security-events: write
|
||||||
|
|||||||
13
.github/workflows/cleanup-after-pr.yml
vendored
13
.github/workflows/cleanup-after-pr.yml
vendored
@@ -11,11 +11,15 @@ jobs:
|
|||||||
build-docker:
|
build-docker:
|
||||||
name: Remove branch-specific Docker images
|
name: Remove branch-specific Docker images
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Log in to Azure - production subscription
|
- name: Log in to Azure
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: bitwarden/gh-actions/azure-login@main
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
|
||||||
- name: Log in to Azure ACR
|
- name: Log in to Azure ACR
|
||||||
run: az acr login -n $_AZ_REGISTRY --only-show-errors
|
run: az acr login -n $_AZ_REGISTRY --only-show-errors
|
||||||
@@ -62,3 +66,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Log out of Docker
|
- name: Log out of Docker
|
||||||
run: docker logout
|
run: docker logout
|
||||||
|
|
||||||
|
- name: Log out from Azure
|
||||||
|
uses: bitwarden/gh-actions/azure-logout@main
|
||||||
|
|||||||
14
.github/workflows/cleanup-rc-branch.yml
vendored
14
.github/workflows/cleanup-rc-branch.yml
vendored
@@ -9,11 +9,16 @@ jobs:
|
|||||||
delete-rc:
|
delete-rc:
|
||||||
name: Delete RC Branch
|
name: Delete RC Branch
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Login to Azure - CI Subscription
|
- name: Log in to Azure
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: bitwarden/gh-actions/azure-login@main
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
|
||||||
- name: Retrieve bot secrets
|
- name: Retrieve bot secrets
|
||||||
id: retrieve-bot-secrets
|
id: retrieve-bot-secrets
|
||||||
@@ -22,6 +27,9 @@ jobs:
|
|||||||
keyvault: bitwarden-ci
|
keyvault: bitwarden-ci
|
||||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
|
||||||
|
- name: Log out from Azure
|
||||||
|
uses: bitwarden/gh-actions/azure-logout@main
|
||||||
|
|
||||||
- name: Checkout main
|
- name: Checkout main
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
|
|||||||
39
.github/workflows/code-references.yml
vendored
39
.github/workflows/code-references.yml
vendored
@@ -1,25 +1,24 @@
|
|||||||
name: Collect code references
|
name: Collect code references
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-ld-secret:
|
check-secret-access:
|
||||||
name: Check for LD secret
|
name: Check for secret access
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
outputs:
|
outputs:
|
||||||
available: ${{ steps.check-ld-secret.outputs.available }}
|
available: ${{ steps.check-secret-access.outputs.available }}
|
||||||
permissions:
|
permissions: {}
|
||||||
contents: read
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check
|
- name: Check
|
||||||
id: check-ld-secret
|
id: check-secret-access
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ secrets.LD_ACCESS_TOKEN }}" != '' ]; then
|
if [ "${{ secrets.AZURE_CLIENT_ID }}" != '' ]; then
|
||||||
echo "available=true" >> $GITHUB_OUTPUT;
|
echo "available=true" >> $GITHUB_OUTPUT;
|
||||||
else
|
else
|
||||||
echo "available=false" >> $GITHUB_OUTPUT;
|
echo "available=false" >> $GITHUB_OUTPUT;
|
||||||
@@ -28,21 +27,39 @@ jobs:
|
|||||||
refs:
|
refs:
|
||||||
name: Code reference collection
|
name: Code reference collection
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs: check-ld-secret
|
needs: check-secret-access
|
||||||
if: ${{ needs.check-ld-secret.outputs.available == 'true' }}
|
if: ${{ needs.check-secret-access.outputs.available == 'true' }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository
|
- name: Check out repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
|
- name: Log in to Azure
|
||||||
|
uses: bitwarden/gh-actions/azure-login@main
|
||||||
|
with:
|
||||||
|
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
|
||||||
|
- name: Get Azure Key Vault secrets
|
||||||
|
id: get-kv-secrets
|
||||||
|
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||||
|
with:
|
||||||
|
keyvault: gh-server
|
||||||
|
secrets: "LD-ACCESS-TOKEN"
|
||||||
|
|
||||||
|
- name: Log out from Azure
|
||||||
|
uses: bitwarden/gh-actions/azure-logout@main
|
||||||
|
|
||||||
- name: Collect
|
- name: Collect
|
||||||
id: collect
|
id: collect
|
||||||
uses: launchdarkly/find-code-references@e3e9da201b87ada54eb4c550c14fb783385c5c8a # v2.13.0
|
uses: launchdarkly/find-code-references@e3e9da201b87ada54eb4c550c14fb783385c5c8a # v2.13.0
|
||||||
with:
|
with:
|
||||||
accessToken: ${{ secrets.LD_ACCESS_TOKEN }}
|
accessToken: ${{ steps.get-kv-secrets.outputs.LD-ACCESS-TOKEN }}
|
||||||
projKey: default
|
projKey: default
|
||||||
allowTags: true
|
allowTags: true
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/ephemeral-environment.yml
vendored
4
.github/workflows/ephemeral-environment.yml
vendored
@@ -4,6 +4,10 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
types: [labeled]
|
types: [labeled]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup-ephemeral-environment:
|
setup-ephemeral-environment:
|
||||||
name: Setup Ephemeral Environment
|
name: Setup Ephemeral Environment
|
||||||
|
|||||||
112
.github/workflows/load-test.yml
vendored
Normal file
112
.github/workflows/load-test.yml
vendored
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
name: Load test
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * 1" # Run every Monday at 00:00
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
test-id:
|
||||||
|
type: string
|
||||||
|
description: "Identifier label for Datadog metrics"
|
||||||
|
default: "server-load-test"
|
||||||
|
k6-test-path:
|
||||||
|
type: string
|
||||||
|
description: "Path to load test files"
|
||||||
|
default: "perf/load/*.js"
|
||||||
|
k6-flags:
|
||||||
|
type: string
|
||||||
|
description: "Additional k6 flags"
|
||||||
|
api-env-url:
|
||||||
|
type: string
|
||||||
|
description: "URL of the API environment"
|
||||||
|
default: "https://api.qa.bitwarden.pw"
|
||||||
|
identity-env-url:
|
||||||
|
type: string
|
||||||
|
description: "URL of the Identity environment"
|
||||||
|
default: "https://identity.qa.bitwarden.pw"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Secret configuration
|
||||||
|
AZURE_KEY_VAULT_NAME: gh-server
|
||||||
|
AZURE_KEY_VAULT_SECRETS: DD-API-KEY, K6-CLIENT-ID, K6-AUTH-USER-EMAIL, K6-AUTH-USER-PASSWORD-HASH
|
||||||
|
# Specify defaults for scheduled runs
|
||||||
|
TEST_ID: ${{ inputs.test-id || 'server-load-test' }}
|
||||||
|
K6_TEST_PATH: ${{ inputs.k6-test-path || 'perf/load/*.js' }}
|
||||||
|
API_ENV_URL: ${{ inputs.api-env-url || 'https://api.qa.bitwarden.pw' }}
|
||||||
|
IDENTITY_ENV_URL: ${{ inputs.identity-env-url || 'https://identity.qa.bitwarden.pw' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-tests:
|
||||||
|
name: Run load tests
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Log in to Azure
|
||||||
|
uses: bitwarden/gh-actions/azure-login@main
|
||||||
|
with:
|
||||||
|
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
|
||||||
|
- name: Get Azure Key Vault secrets
|
||||||
|
id: get-kv-secrets
|
||||||
|
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||||
|
with:
|
||||||
|
keyvault: ${{ env.AZURE_KEY_VAULT_NAME }}
|
||||||
|
secrets: ${{ env.AZURE_KEY_VAULT_SECRETS }}
|
||||||
|
|
||||||
|
- name: Log out of Azure
|
||||||
|
uses: bitwarden/gh-actions/azure-logout@main
|
||||||
|
|
||||||
|
# Datadog agent for collecting OTEL metrics from k6
|
||||||
|
- name: Start Datadog agent
|
||||||
|
run: |
|
||||||
|
docker run --detach \
|
||||||
|
--name datadog-agent \
|
||||||
|
-p 4317:4317 \
|
||||||
|
-p 5555:5555 \
|
||||||
|
-e DD_SITE=us3.datadoghq.com \
|
||||||
|
-e DD_API_KEY=${{ steps.get-kv-secrets.outputs.DD-API-KEY }} \
|
||||||
|
-e DD_DOGSTATSD_NON_LOCAL_TRAFFIC=1 \
|
||||||
|
-e DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_ENDPOINT=0.0.0.0:4317 \
|
||||||
|
-e DD_HEALTH_PORT=5555 \
|
||||||
|
-e HOST_PROC=/proc \
|
||||||
|
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
--volume /sys/fs/cgroup/:/host/sys/fs/cgroup:ro \
|
||||||
|
--health-cmd "curl -f http://localhost:5555/health || exit 1" \
|
||||||
|
--health-interval 10s \
|
||||||
|
--health-timeout 5s \
|
||||||
|
--health-retries 10 \
|
||||||
|
--health-start-period 30s \
|
||||||
|
--pid host \
|
||||||
|
datadog/agent:7-full@sha256:7ea933dec3b8baa8c19683b1c3f6f801dbf3291f748d9ed59234accdaac4e479
|
||||||
|
|
||||||
|
- name: Check out repo
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Set up k6
|
||||||
|
uses: grafana/setup-k6-action@ffe7d7290dfa715e48c2ccc924d068444c94bde2 # v1.1.0
|
||||||
|
|
||||||
|
- name: Run k6 tests
|
||||||
|
uses: grafana/run-k6-action@c6b79182b9b666aa4f630f4a6be9158ead62536e # v1.2.0
|
||||||
|
continue-on-error: false
|
||||||
|
env:
|
||||||
|
K6_OTEL_METRIC_PREFIX: k6_
|
||||||
|
K6_OTEL_GRPC_EXPORTER_INSECURE: true
|
||||||
|
# Load test specific environment variables
|
||||||
|
API_URL: ${{ env.API_ENV_URL }}
|
||||||
|
IDENTITY_URL: ${{ env.IDENTITY_ENV_URL }}
|
||||||
|
CLIENT_ID: ${{ steps.get-kv-secrets.outputs.K6-CLIENT-ID }}
|
||||||
|
AUTH_USER_EMAIL: ${{ steps.get-kv-secrets.outputs.K6-AUTH-USER-EMAIL }}
|
||||||
|
AUTH_USER_PASSWORD_HASH: ${{ steps.get-kv-secrets.outputs.K6-AUTH-USER-PASSWORD-HASH }}
|
||||||
|
with:
|
||||||
|
flags: >-
|
||||||
|
--tag test-id=${{ env.TEST_ID }}
|
||||||
|
-o experimental-opentelemetry
|
||||||
|
${{ inputs.k6-flags }}
|
||||||
|
path: ${{ env.K6_TEST_PATH }}
|
||||||
19
.github/workflows/publish.yml
vendored
19
.github/workflows/publish.yml
vendored
@@ -26,6 +26,9 @@ jobs:
|
|||||||
setup:
|
setup:
|
||||||
name: Setup
|
name: Setup
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
deployments: write
|
||||||
outputs:
|
outputs:
|
||||||
branch-name: ${{ steps.branch.outputs.branch-name }}
|
branch-name: ${{ steps.branch.outputs.branch-name }}
|
||||||
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||||
@@ -63,6 +66,9 @@ jobs:
|
|||||||
name: Publish Docker images
|
name: Publish Docker images
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs: setup
|
needs: setup
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
env:
|
env:
|
||||||
_RELEASE_VERSION: ${{ needs.setup.outputs.release-version }}
|
_RELEASE_VERSION: ${{ needs.setup.outputs.release-version }}
|
||||||
_BRANCH_NAME: ${{ needs.setup.outputs.branch-name }}
|
_BRANCH_NAME: ${{ needs.setup.outputs.branch-name }}
|
||||||
@@ -109,10 +115,12 @@ jobs:
|
|||||||
echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT
|
echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
########## ACR PROD ##########
|
########## ACR PROD ##########
|
||||||
- name: Log in to Azure - production subscription
|
- name: Log in to Azure
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: bitwarden/gh-actions/azure-login@main
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
|
||||||
- name: Log in to Azure ACR
|
- name: Log in to Azure ACR
|
||||||
run: az acr login -n $_AZ_REGISTRY --only-show-errors
|
run: az acr login -n $_AZ_REGISTRY --only-show-errors
|
||||||
@@ -152,12 +160,17 @@ jobs:
|
|||||||
- name: Log out of Docker
|
- name: Log out of Docker
|
||||||
run: docker logout
|
run: docker logout
|
||||||
|
|
||||||
|
- name: Log out from Azure
|
||||||
|
uses: bitwarden/gh-actions/azure-logout@main
|
||||||
|
|
||||||
update-deployment:
|
update-deployment:
|
||||||
name: Update Deployment Status
|
name: Update Deployment Status
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- setup
|
- setup
|
||||||
- publish-docker
|
- publish-docker
|
||||||
|
permissions:
|
||||||
|
deployments: write
|
||||||
if: ${{ always() && inputs.publish_type != 'Dry Run' }}
|
if: ${{ always() && inputs.publish_type != 'Dry Run' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check if any job failed
|
- name: Check if any job failed
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -86,7 +86,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||||
uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0
|
uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1.16.0
|
||||||
with:
|
with:
|
||||||
artifacts: "docker-stub-US.zip,
|
artifacts: "docker-stub-US.zip,
|
||||||
docker-stub-EU.zip,
|
docker-stub-EU.zip,
|
||||||
|
|||||||
67
.github/workflows/repository-management.yml
vendored
67
.github/workflows/repository-management.yml
vendored
@@ -22,7 +22,9 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
permissions: {}
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup:
|
setup:
|
||||||
@@ -54,7 +56,27 @@ jobs:
|
|||||||
- setup
|
- setup
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.set-final-version-output.outputs.version }}
|
version: ${{ steps.set-final-version-output.outputs.version }}
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Log in to Azure
|
||||||
|
uses: bitwarden/gh-actions/azure-login@main
|
||||||
|
with:
|
||||||
|
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
|
||||||
|
- name: Get Azure Key Vault secrets
|
||||||
|
id: get-kv-secrets
|
||||||
|
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||||
|
with:
|
||||||
|
keyvault: gh-org-bitwarden
|
||||||
|
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
|
||||||
|
|
||||||
|
- name: Log out from Azure
|
||||||
|
uses: bitwarden/gh-actions/azure-logout@main
|
||||||
|
|
||||||
- name: Validate version input format
|
- name: Validate version input format
|
||||||
if: ${{ inputs.version_number_override != '' }}
|
if: ${{ inputs.version_number_override != '' }}
|
||||||
uses: bitwarden/gh-actions/version-check@main
|
uses: bitwarden/gh-actions/version-check@main
|
||||||
@@ -62,11 +84,11 @@ jobs:
|
|||||||
version: ${{ inputs.version_number_override }}
|
version: ${{ inputs.version_number_override }}
|
||||||
|
|
||||||
- name: Generate GH App token
|
- name: Generate GH App token
|
||||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
|
||||||
id: app-token
|
id: app-token
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||||
|
|
||||||
- name: Check out branch
|
- name: Check out branch
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
@@ -158,13 +180,33 @@ jobs:
|
|||||||
- setup
|
- setup
|
||||||
- bump_version
|
- bump_version
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Log in to Azure
|
||||||
|
uses: bitwarden/gh-actions/azure-login@main
|
||||||
|
with:
|
||||||
|
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
|
||||||
|
- name: Get Azure Key Vault secrets
|
||||||
|
id: get-kv-secrets
|
||||||
|
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||||
|
with:
|
||||||
|
keyvault: gh-org-bitwarden
|
||||||
|
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
|
||||||
|
|
||||||
|
- name: Log out from Azure
|
||||||
|
uses: bitwarden/gh-actions/azure-logout@main
|
||||||
|
|
||||||
- name: Generate GH App token
|
- name: Generate GH App token
|
||||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
|
||||||
id: app-token
|
id: app-token
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||||
|
|
||||||
- name: Check out target ref
|
- name: Check out target ref
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
@@ -188,8 +230,13 @@ jobs:
|
|||||||
git switch --quiet --create $BRANCH_NAME
|
git switch --quiet --create $BRANCH_NAME
|
||||||
git push --quiet --set-upstream origin $BRANCH_NAME
|
git push --quiet --set-upstream origin $BRANCH_NAME
|
||||||
|
|
||||||
move_future_db_scripts:
|
move_edd_db_scripts:
|
||||||
name: Move finalization database scripts
|
name: Move EDD database scripts
|
||||||
needs: cut_branch
|
needs: cut_branch
|
||||||
uses: ./.github/workflows/_move_finalization_db_scripts.yml
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: write
|
||||||
|
id-token: write
|
||||||
|
pull-requests: write
|
||||||
|
uses: ./.github/workflows/_move_edd_db_scripts.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|||||||
109
.github/workflows/review-code.yml
vendored
Normal file
109
.github/workflows/review-code.yml
vendored
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
name: Review code
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
review:
|
||||||
|
name: Review
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out repo
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Check for Vault team changes
|
||||||
|
id: check_changes
|
||||||
|
run: |
|
||||||
|
# Ensure we have the base branch
|
||||||
|
git fetch origin ${{ github.base_ref }}
|
||||||
|
|
||||||
|
echo "Comparing changes between origin/${{ github.base_ref }} and HEAD"
|
||||||
|
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
|
||||||
|
|
||||||
|
if [ -z "$CHANGED_FILES" ]; then
|
||||||
|
echo "Zero files changed"
|
||||||
|
echo "vault_team_changes=false" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Handle variations in spacing and multiple teams
|
||||||
|
VAULT_PATTERNS=$(grep -E "@bitwarden/team-vault-dev(\s|$)" .github/CODEOWNERS 2>/dev/null | awk '{print $1}')
|
||||||
|
|
||||||
|
if [ -z "$VAULT_PATTERNS" ]; then
|
||||||
|
echo "⚠️ No patterns found for @bitwarden/team-vault-dev in CODEOWNERS"
|
||||||
|
echo "vault_team_changes=false" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
vault_team_changes=false
|
||||||
|
for pattern in $VAULT_PATTERNS; do
|
||||||
|
echo "Checking pattern: $pattern"
|
||||||
|
|
||||||
|
# Handle **/directory patterns
|
||||||
|
if [[ "$pattern" == "**/"* ]]; then
|
||||||
|
# Remove the **/ prefix
|
||||||
|
dir_pattern="${pattern#\*\*/}"
|
||||||
|
# Check if any file contains this directory in its path
|
||||||
|
if echo "$CHANGED_FILES" | grep -qE "(^|/)${dir_pattern}(/|$)"; then
|
||||||
|
vault_team_changes=true
|
||||||
|
echo "✅ Found files matching pattern: $pattern"
|
||||||
|
echo "$CHANGED_FILES" | grep -E "(^|/)${dir_pattern}(/|$)" | sed 's/^/ - /'
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Handle other patterns (shouldn't happen based on your CODEOWNERS)
|
||||||
|
if echo "$CHANGED_FILES" | grep -q "$pattern"; then
|
||||||
|
vault_team_changes=true
|
||||||
|
echo "✅ Found files matching pattern: $pattern"
|
||||||
|
echo "$CHANGED_FILES" | grep "$pattern" | sed 's/^/ - /'
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "vault_team_changes=$vault_team_changes" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
if [ "$vault_team_changes" = "true" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "✅ Vault team changes detected - proceeding with review"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "❌ No Vault team changes detected - skipping review"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Review with Claude Code
|
||||||
|
if: steps.check_changes.outputs.vault_team_changes == 'true'
|
||||||
|
uses: anthropics/claude-code-action@a5528eec7426a4f0c9c1ac96018daa53ebd05bc4 # v1.0.7
|
||||||
|
with:
|
||||||
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
track_progress: true
|
||||||
|
prompt: |
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
TITLE: ${{ github.event.pull_request.title }}
|
||||||
|
BODY: ${{ github.event.pull_request.body }}
|
||||||
|
AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||||
|
|
||||||
|
Please review this pull request with a focus on:
|
||||||
|
- Code quality and best practices
|
||||||
|
- Potential bugs or issues
|
||||||
|
- Security implications
|
||||||
|
- Performance considerations
|
||||||
|
|
||||||
|
Note: The PR branch is already checked out in the current working directory.
|
||||||
|
|
||||||
|
Provide detailed feedback using inline comments for specific issues.
|
||||||
|
|
||||||
|
claude_args: |
|
||||||
|
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
|
||||||
83
.github/workflows/scan.yml
vendored
83
.github/workflows/scan.yml
vendored
@@ -16,83 +16,40 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- "main"
|
- "main"
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-run:
|
check-run:
|
||||||
name: Check PR run
|
name: Check PR run
|
||||||
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
|
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
sast:
|
sast:
|
||||||
name: SAST scan
|
name: Checkmarx
|
||||||
runs-on: ubuntu-22.04
|
uses: bitwarden/gh-actions/.github/workflows/_checkmarx.yml@main
|
||||||
needs: check-run
|
needs: check-run
|
||||||
|
secrets:
|
||||||
|
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
security-events: write
|
security-events: write
|
||||||
|
id-token: write
|
||||||
steps:
|
|
||||||
- name: Check out repo
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
|
|
||||||
- name: Scan with Checkmarx
|
|
||||||
uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41
|
|
||||||
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@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
|
|
||||||
with:
|
|
||||||
sarif_file: cx_result.sarif
|
|
||||||
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
|
|
||||||
ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
|
|
||||||
|
|
||||||
quality:
|
quality:
|
||||||
name: Quality scan
|
name: Sonar
|
||||||
runs-on: ubuntu-22.04
|
uses: bitwarden/gh-actions/.github/workflows/_sonar.yml@main
|
||||||
needs: check-run
|
needs: check-run
|
||||||
|
secrets:
|
||||||
|
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
id-token: write
|
||||||
steps:
|
with:
|
||||||
- name: Set up JDK 17
|
sonar-config: "dotnet"
|
||||||
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
|
|
||||||
with:
|
|
||||||
java-version: 17
|
|
||||||
distribution: "zulu"
|
|
||||||
|
|
||||||
- name: Check out repo
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
|
|
||||||
- name: Set up .NET
|
|
||||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
|
||||||
|
|
||||||
- name: Install SonarCloud scanner
|
|
||||||
run: dotnet tool install dotnet-sonarscanner -g
|
|
||||||
|
|
||||||
- name: Scan with SonarCloud
|
|
||||||
env:
|
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
||||||
run: |
|
|
||||||
dotnet-sonarscanner begin /k:"${{ github.repository_owner }}_${{ github.event.repository.name }}" \
|
|
||||||
/d:sonar.test.inclusions=test/,bitwarden_license/test/ \
|
|
||||||
/d:sonar.exclusions=test/,bitwarden_license/test/ \
|
|
||||||
/o:"${{ github.repository_owner }}" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" \
|
|
||||||
/d:sonar.host.url="https://sonarcloud.io" ${{ contains(github.event_name, 'pull_request') && format('/d:sonar.pullrequest.key={0}', github.event.pull_request.number) || '' }}
|
|
||||||
dotnet build
|
|
||||||
dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
|
|
||||||
|
|||||||
30
.github/workflows/test-database.yml
vendored
30
.github/workflows/test-database.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
|||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||||
|
|
||||||
- name: Restore tools
|
- name: Restore tools
|
||||||
run: dotnet tool restore
|
run: dotnet tool restore
|
||||||
@@ -154,7 +154,7 @@ jobs:
|
|||||||
run: 'docker logs $(docker ps --quiet --filter "name=mssql")'
|
run: 'docker logs $(docker ps --quiet --filter "name=mssql")'
|
||||||
|
|
||||||
- name: Report test results
|
- name: Report test results
|
||||||
uses: dorny/test-reporter@6e6a65b7a0bd2c9197df7d0ae36ac5cee784230c # v2.0.0
|
uses: dorny/test-reporter@890a17cecf52a379fc869ab770a71657660be727 # v2.1.0
|
||||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
||||||
with:
|
with:
|
||||||
name: Test Results
|
name: Test Results
|
||||||
@@ -163,7 +163,7 @@ jobs:
|
|||||||
fail-on-error: true
|
fail-on-error: true
|
||||||
|
|
||||||
- name: Upload to codecov.io
|
- name: Upload to codecov.io
|
||||||
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
|
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||||
|
|
||||||
- name: Docker Compose down
|
- name: Docker Compose down
|
||||||
if: always()
|
if: always()
|
||||||
@@ -179,7 +179,7 @@ jobs:
|
|||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
@@ -229,11 +229,27 @@ jobs:
|
|||||||
- name: Validate XML
|
- name: Validate XML
|
||||||
run: |
|
run: |
|
||||||
if grep -q "<Operations>" "report.xml"; then
|
if grep -q "<Operations>" "report.xml"; then
|
||||||
echo
|
echo "ERROR: Migration files are not in sync with the SQL project"
|
||||||
echo "Migration files are not in sync with the files in the Sql project. Review to make sure that any stored procedures / other db changes match with the stored procedures in the Sql project."
|
echo ""
|
||||||
|
echo "Check these locations:"
|
||||||
|
echo " - Migration scripts: util/Migrator/DbScripts/"
|
||||||
|
echo " - SQL project files: src/Sql/"
|
||||||
|
echo " - Download 'report.xml' artifact for full details"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Show actual SQL differences - exclude database setup commands
|
||||||
|
if [ -s "diff.sql" ]; then
|
||||||
|
echo "Key SQL differences:"
|
||||||
|
# Show meaningful schema differences, filtering out database setup noise
|
||||||
|
grep -E "^(CREATE|DROP|ALTER)" diff.sql | grep -v "ALTER DATABASE" | grep -v "DatabaseName" | head -5
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Common causes: naming differences (underscores, case), missing objects, or definition mismatches"
|
||||||
|
|
||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
echo "Report looks good"
|
echo "SUCCESS: Database validation passed"
|
||||||
fi
|
fi
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
run: dotnet test ./bitwarden_license/test --configuration Debug --logger "trx;LogFileName=bw-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
|
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
|
- name: Report test results
|
||||||
uses: dorny/test-reporter@6e6a65b7a0bd2c9197df7d0ae36ac5cee784230c # v2.0.0
|
uses: dorny/test-reporter@890a17cecf52a379fc869ab770a71657660be727 # v2.1.0
|
||||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
||||||
with:
|
with:
|
||||||
name: Test Results
|
name: Test Results
|
||||||
@@ -58,4 +58,4 @@ jobs:
|
|||||||
fail-on-error: true
|
fail-on-error: true
|
||||||
|
|
||||||
- name: Upload to codecov.io
|
- name: Upload to codecov.io
|
||||||
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
|
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||||
|
|||||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -129,7 +129,7 @@ publish/
|
|||||||
# Publish Web Output
|
# Publish Web Output
|
||||||
*.[Pp]ublish.xml
|
*.[Pp]ublish.xml
|
||||||
*.azurePubxml
|
*.azurePubxml
|
||||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||||
# but database connection strings (with potential passwords) will be unencrypted
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
*.pubxml
|
*.pubxml
|
||||||
*.publishproj
|
*.publishproj
|
||||||
@@ -214,6 +214,9 @@ bitwarden_license/src/Sso/wwwroot/assets
|
|||||||
.idea/*
|
.idea/*
|
||||||
**/**.swp
|
**/**.swp
|
||||||
.mono
|
.mono
|
||||||
|
src/Core/MailTemplates/Mjml/out
|
||||||
|
NativeMethods.g.cs
|
||||||
|
util/RustSdk/rust/target
|
||||||
|
|
||||||
src/Admin/Admin.zip
|
src/Admin/Admin.zip
|
||||||
src/Api/Api.zip
|
src/Api/Api.zip
|
||||||
@@ -225,5 +228,8 @@ src/Notifications/Notifications.zip
|
|||||||
bitwarden_license/src/Portal/Portal.zip
|
bitwarden_license/src/Portal/Portal.zip
|
||||||
bitwarden_license/src/Sso/Sso.zip
|
bitwarden_license/src/Sso/Sso.zip
|
||||||
**/src/**/flags.json
|
**/src/**/flags.json
|
||||||
NativeMethods.g.cs
|
|
||||||
util/RustSdk/rust/target
|
# Generated swagger specs
|
||||||
|
/identity.json
|
||||||
|
/api.json
|
||||||
|
/api.public.json
|
||||||
|
|||||||
72
CLAUDE.md
Normal file
72
CLAUDE.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Bitwarden Server - Claude Code Configuration
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
- **NEVER** edit: `/bin/`, `/obj/`, `/.git/`, `/.vs/`, `/packages/` which are generated files
|
||||||
|
- **NEVER** use code regions: If complexity suggests regions, refactor for better readability
|
||||||
|
- **NEVER** compromise zero-knowledge principles: User vault data must remain encrypted and inaccessible to Bitwarden
|
||||||
|
- **NEVER** log or expose sensitive data: No PII, passwords, keys, or vault data in logs or error messages
|
||||||
|
- **ALWAYS** use secure communication channels: Enforce confidentiality, integrity, and authenticity
|
||||||
|
- **ALWAYS** encrypt sensitive data: All vault data must be encrypted at rest, in transit, and in use
|
||||||
|
- **ALWAYS** prioritize cryptographic integrity and data protection
|
||||||
|
- **ALWAYS** add unit tests (with mocking) for any new feature development
|
||||||
|
|
||||||
|
## Project Context
|
||||||
|
|
||||||
|
- **Architecture**: Feature and team-based organization
|
||||||
|
- **Framework**: .NET 8.0, ASP.NET Core
|
||||||
|
- **Database**: SQL Server primary, EF Core supports PostgreSQL, MySQL/MariaDB, SQLite
|
||||||
|
- **Testing**: xUnit, NSubstitute
|
||||||
|
- **Container**: Docker, Docker Compose, Kubernetes/Helm deployable
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
- **Source Code**: `/src/` - Services and core infrastructure
|
||||||
|
- **Tests**: `/test/` - Test logic aligning with the source structure, albeit with a `.Test` suffix
|
||||||
|
- **Utilities**: `/util/` - Migration tools, seeders, and setup scripts
|
||||||
|
- **Dev Tools**: `/dev/` - Local development helpers
|
||||||
|
- **Configuration**: `appsettings.{Environment}.json`, `/dev/secrets.json` for local development
|
||||||
|
|
||||||
|
## Security Requirements
|
||||||
|
|
||||||
|
- **Compliance**: SOC 2 Type II, SOC 3, HIPAA, ISO 27001, GDPR, CCPA
|
||||||
|
- **Principles**: Zero-knowledge, end-to-end encryption, secure defaults
|
||||||
|
- **Validation**: Input sanitization, parameterized queries, rate limiting
|
||||||
|
- **Logging**: Structured logs, no PII/sensitive data in logs
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
- **Build**: `dotnet build`
|
||||||
|
- **Test**: `dotnet test`
|
||||||
|
- **Run locally**: `dotnet run --project src/Api`
|
||||||
|
- **Database update**: `pwsh dev/migrate.ps1`
|
||||||
|
- **Generate OpenAPI**: `pwsh dev/generate_openapi_files.ps1`
|
||||||
|
|
||||||
|
## Code Review Checklist
|
||||||
|
|
||||||
|
- Security impact assessed
|
||||||
|
- xUnit tests added / updated
|
||||||
|
- Performance impact considered
|
||||||
|
- Error handling implemented
|
||||||
|
- Breaking changes documented
|
||||||
|
- CI passes: build, test, lint
|
||||||
|
- Feature flags considered for new features
|
||||||
|
- CODEOWNERS file respected
|
||||||
|
|
||||||
|
### Key Architectural Decisions
|
||||||
|
|
||||||
|
- Use .NET nullable reference types (ADR 0024)
|
||||||
|
- TryAdd dependency injection pattern (ADR 0026)
|
||||||
|
- Authorization patterns (ADR 0022)
|
||||||
|
- OpenTelemetry for observability (ADR 0020)
|
||||||
|
- Log to standard output (ADR 0021)
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Server architecture](https://contributing.bitwarden.com/architecture/server/)
|
||||||
|
- [Architectural Decision Records (ADRs)](https://contributing.bitwarden.com/architecture/adr/)
|
||||||
|
- [Contributing guidelines](https://contributing.bitwarden.com/contributing/)
|
||||||
|
- [Setup guide](https://contributing.bitwarden.com/getting-started/server/guide/)
|
||||||
|
- [Code style](https://contributing.bitwarden.com/contributing/code-style/)
|
||||||
|
- [Bitwarden security whitepaper](https://bitwarden.com/help/bitwarden-security-white-paper/)
|
||||||
|
- [Bitwarden security definitions](https://contributing.bitwarden.com/architecture/security/definitions)
|
||||||
@@ -3,62 +3,40 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
|
||||||
<Version>2025.6.2</Version>
|
<Version>2025.10.0</Version>
|
||||||
|
|
||||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||||
<!-- Treat it as a test project if the project hasn't set their own value and it follows our test project conventions -->
|
|
||||||
<IsTestProject Condition="'$(IsTestProject)' == '' and ($(MSBuildProjectName.EndsWith('.Test')) or $(MSBuildProjectName.EndsWith('.IntegrationTest')))">true</IsTestProject>
|
<IsTestProject Condition="'$(IsTestProject)' == '' and ($(MSBuildProjectName.EndsWith('.Test')) or $(MSBuildProjectName.EndsWith('.IntegrationTest')))">true</IsTestProject>
|
||||||
<Nullable Condition="'$(Nullable)' == '' and '$(IsTestProject)' == 'true'">annotations</Nullable>
|
<Nullable Condition="'$(Nullable)' == '' and '$(IsTestProject)' == 'true'">annotations</Nullable>
|
||||||
<!-- Uncomment the below line when we are ready to enable nullable repo wide -->
|
<Nullable Condition="'$(Nullable)' == '' and '$(IsTestProject)' != 'true'">enable</Nullable>
|
||||||
<!-- <Nullable Condition="'$(Nullable)' == '' and '$(IsTestProject)' != 'true'">enable</Nullable> -->
|
|
||||||
<TreatWarningsAsErrors Condition="'$(TreatWarningsAsErrors)' == ''">true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors Condition="'$(TreatWarningsAsErrors)' == ''">true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!--
|
|
||||||
This section is for packages that we use multiple times throughout the solution
|
|
||||||
It gives us a single place to manage the version to ensure we are using the same version
|
|
||||||
across the solution.
|
|
||||||
-->
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!--
|
|
||||||
NuGet: https://www.nuget.org/packages/Microsoft.NET.Test.Sdk
|
|
||||||
-->
|
|
||||||
<MicrosoftNetTestSdkVersion>17.8.0</MicrosoftNetTestSdkVersion>
|
<MicrosoftNetTestSdkVersion>17.8.0</MicrosoftNetTestSdkVersion>
|
||||||
<!--
|
|
||||||
NuGet: https://www.nuget.org/packages/xunit
|
|
||||||
-->
|
|
||||||
<XUnitVersion>2.6.6</XUnitVersion>
|
<XUnitVersion>2.6.6</XUnitVersion>
|
||||||
<!--
|
|
||||||
NuGet: https://www.nuget.org/packages/xunit.runner.visualstudio
|
|
||||||
-->
|
|
||||||
<XUnitRunnerVisualStudioVersion>2.5.6</XUnitRunnerVisualStudioVersion>
|
<XUnitRunnerVisualStudioVersion>2.5.6</XUnitRunnerVisualStudioVersion>
|
||||||
<!--
|
|
||||||
NuGet: https://www.nuget.org/packages/coverlet.collector
|
|
||||||
-->
|
|
||||||
<CoverletCollectorVersion>6.0.0</CoverletCollectorVersion>
|
<CoverletCollectorVersion>6.0.0</CoverletCollectorVersion>
|
||||||
<!--
|
|
||||||
NuGet: https://www.nuget.org/packages/NSubstitute
|
|
||||||
-->
|
|
||||||
<NSubstituteVersion>5.1.0</NSubstituteVersion>
|
<NSubstituteVersion>5.1.0</NSubstituteVersion>
|
||||||
<!--
|
|
||||||
NuGet: https://www.nuget.org/packages/AutoFixture.Xunit2
|
|
||||||
-->
|
|
||||||
<AutoFixtureXUnit2Version>4.18.1</AutoFixtureXUnit2Version>
|
<AutoFixtureXUnit2Version>4.18.1</AutoFixtureXUnit2Version>
|
||||||
<!--
|
|
||||||
NuGet: https://www.nuget.org/packages/AutoFixture.AutoNSubstitute
|
|
||||||
-->
|
|
||||||
<AutoFixtureAutoNSubstituteVersion>4.18.1</AutoFixtureAutoNSubstituteVersion>
|
<AutoFixtureAutoNSubstituteVersion>4.18.1</AutoFixtureAutoNSubstituteVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!--
|
|
||||||
This section is for getting & setting the gitHash value, which can easily be accessed
|
|
||||||
via the Core.Utilities.AssemblyHelpers class.
|
|
||||||
-->
|
|
||||||
<Target Name="SetSourceRevisionId" BeforeTargets="CoreGenerateAssemblyInfo">
|
<Target Name="SetSourceRevisionId" BeforeTargets="CoreGenerateAssemblyInfo">
|
||||||
<Exec Command="git describe --long --always --dirty --exclude=* --abbrev=8" ConsoleToMSBuild="True" IgnoreExitCode="False">
|
<Exec Command="git describe --long --always --dirty --exclude=* --abbrev=8" ConsoleToMSBuild="True" IgnoreExitCode="False">
|
||||||
<Output PropertyName="SourceRevisionId" TaskParameter="ConsoleOutput"/>
|
<Output PropertyName="SourceRevisionId" TaskParameter="ConsoleOutput" />
|
||||||
</Exec>
|
</Exec>
|
||||||
</Target>
|
</Target>
|
||||||
<Target Name="WriteRevision" AfterTargets="SetSourceRevisionId">
|
<Target Name="WriteRevision" AfterTargets="SetSourceRevisionId">
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ EndProject
|
|||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbSeederUtility", "util\DbSeederUtility\DbSeederUtility.csproj", "{17A89266-260A-4A03-81AE-C0468C6EE06E}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbSeederUtility", "util\DbSeederUtility\DbSeederUtility.csproj", "{17A89266-260A-4A03-81AE-C0468C6EE06E}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RustSdk", "util\RustSdk\RustSdk.csproj", "{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RustSdk", "util\RustSdk\RustSdk.csproj", "{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}"
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedWeb.Test", "test\SharedWeb.Test\SharedWeb.Test.csproj", "{AD59537D-5259-4B7A-948F-0CF58E80B359}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -343,6 +344,10 @@ Global
|
|||||||
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Release|Any CPU.Build.0 = Release|Any CPU
|
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -398,6 +403,7 @@ Global
|
|||||||
{9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
{9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||||
{17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
{17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||||
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||||
|
{AD59537D-5259-4B7A-948F-0CF58E80B359} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using Bit.Core;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
@@ -134,25 +136,13 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }]
|
Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }]
|
||||||
};
|
};
|
||||||
|
|
||||||
var setNonUSBusinessUseToReverseCharge = _featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||||
|
|
||||||
if (setNonUSBusinessUseToReverseCharge)
|
|
||||||
{
|
|
||||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
|
||||||
}
|
|
||||||
else if (customer.HasRecognizedTaxLocation())
|
|
||||||
{
|
|
||||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
|
|
||||||
{
|
|
||||||
Enabled = customer.Address.Country == "US" ||
|
|
||||||
customer.TaxIds.Any()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||||
|
|
||||||
organization.GatewaySubscriptionId = subscription.Id;
|
organization.GatewaySubscriptionId = subscription.Id;
|
||||||
organization.Status = OrganizationStatusType.Created;
|
organization.Status = OrganizationStatusType.Created;
|
||||||
|
organization.Enabled = true;
|
||||||
|
|
||||||
await _providerBillingService.ScaleSeats(provider, organization.PlanType, -organization.Seats ?? 0);
|
await _providerBillingService.ScaleSeats(provider, organization.PlanType, -organization.Seats ?? 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
@@ -9,7 +12,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Payment.Models;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Providers.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@@ -87,7 +90,7 @@ public class ProviderService : IProviderService
|
|||||||
_providerClientOrganizationSignUpCommand = providerClientOrganizationSignUpCommand;
|
_providerClientOrganizationSignUpCommand = providerClientOrganizationSignUpCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource = null)
|
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress)
|
||||||
{
|
{
|
||||||
var owner = await _userService.GetUserByIdAsync(ownerUserId);
|
var owner = await _userService.GetUserByIdAsync(ownerUserId);
|
||||||
if (owner == null)
|
if (owner == null)
|
||||||
@@ -112,24 +115,7 @@ public class ProviderService : IProviderService
|
|||||||
throw new BadRequestException("Invalid owner.");
|
throw new BadRequestException("Invalid owner.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (taxInfo == null || string.IsNullOrEmpty(taxInfo.BillingAddressCountry) || string.IsNullOrEmpty(taxInfo.BillingAddressPostalCode))
|
var customer = await _providerBillingService.SetupCustomer(provider, paymentMethod, billingAddress);
|
||||||
{
|
|
||||||
throw new BadRequestException("Both address and postal code are required to set up your provider.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var requireProviderPaymentMethodDuringSetup =
|
|
||||||
_featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup);
|
|
||||||
|
|
||||||
if (requireProviderPaymentMethodDuringSetup && tokenizedPaymentSource is not
|
|
||||||
{
|
|
||||||
Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal,
|
|
||||||
Token: not null and not ""
|
|
||||||
})
|
|
||||||
{
|
|
||||||
throw new BadRequestException("A payment method is required to set up your provider.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var customer = await _providerBillingService.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
|
|
||||||
provider.GatewayCustomerId = customer.Id;
|
provider.GatewayCustomerId = customer.Id;
|
||||||
var subscription = await _providerBillingService.SetupSubscription(provider);
|
var subscription = await _providerBillingService.SetupSubscription(provider);
|
||||||
provider.GatewaySubscriptionId = subscription.Id;
|
provider.GatewaySubscriptionId = subscription.Id;
|
||||||
@@ -149,7 +135,15 @@ public class ProviderService : IProviderService
|
|||||||
throw new ArgumentException("Cannot create provider this way.");
|
throw new ArgumentException("Cannot create provider this way.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var existingProvider = await _providerRepository.GetByIdAsync(provider.Id);
|
||||||
|
var enabledStatusChanged = existingProvider != null && existingProvider.Enabled != provider.Enabled;
|
||||||
|
|
||||||
await _providerRepository.ReplaceAsync(provider);
|
await _providerRepository.ReplaceAsync(provider);
|
||||||
|
|
||||||
|
if (enabledStatusChanged && (provider.Type == ProviderType.Msp || provider.Type == ProviderType.BusinessUnit))
|
||||||
|
{
|
||||||
|
await UpdateClientOrganizationsEnabledStatusAsync(provider.Id, provider.Enabled);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<ProviderUser>> InviteUserAsync(ProviderUserInvite<string> invite)
|
public async Task<List<ProviderUser>> InviteUserAsync(ProviderUserInvite<string> invite)
|
||||||
@@ -725,4 +719,20 @@ public class ProviderService : IProviderService
|
|||||||
throw new BadRequestException($"Unsupported provider type {providerType}.");
|
throw new BadRequestException($"Unsupported provider type {providerType}.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task UpdateClientOrganizationsEnabledStatusAsync(Guid providerId, bool enabled)
|
||||||
|
{
|
||||||
|
var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId);
|
||||||
|
|
||||||
|
foreach (var providerOrganization in providerOrganizations)
|
||||||
|
{
|
||||||
|
var organization = await _organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);
|
||||||
|
if (organization != null && organization.Enabled != enabled)
|
||||||
|
{
|
||||||
|
organization.Enabled = enabled;
|
||||||
|
await _organizationRepository.ReplaceAsync(organization);
|
||||||
|
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.Globalization;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using System.Globalization;
|
||||||
using Bit.Core.Billing.Providers.Entities;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
using CsvHelper.Configuration.Attributes;
|
using CsvHelper.Configuration.Attributes;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Bit.Core.Billing.Providers.Models;
|
||||||
|
using Bit.Core.Billing.Providers.Queries;
|
||||||
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Stripe;
|
||||||
|
using Stripe.Tax;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.Billing.Providers.Queries;
|
||||||
|
|
||||||
|
using static Bit.Core.Constants;
|
||||||
|
using static StripeConstants;
|
||||||
|
using SuspensionWarning = ProviderWarnings.SuspensionWarning;
|
||||||
|
using TaxIdWarning = ProviderWarnings.TaxIdWarning;
|
||||||
|
|
||||||
|
public class GetProviderWarningsQuery(
|
||||||
|
ICurrentContext currentContext,
|
||||||
|
IStripeAdapter stripeAdapter,
|
||||||
|
ISubscriberService subscriberService) : IGetProviderWarningsQuery
|
||||||
|
{
|
||||||
|
public async Task<ProviderWarnings?> Run(Provider provider)
|
||||||
|
{
|
||||||
|
var warnings = new ProviderWarnings();
|
||||||
|
|
||||||
|
var subscription =
|
||||||
|
await subscriberService.GetSubscription(provider,
|
||||||
|
new SubscriptionGetOptions { Expand = ["customer.tax_ids"] });
|
||||||
|
|
||||||
|
if (subscription == null)
|
||||||
|
{
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
warnings.Suspension = GetSuspensionWarning(provider, subscription);
|
||||||
|
|
||||||
|
warnings.TaxId = await GetTaxIdWarningAsync(provider, subscription.Customer);
|
||||||
|
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SuspensionWarning? GetSuspensionWarning(
|
||||||
|
Provider provider,
|
||||||
|
Subscription subscription)
|
||||||
|
{
|
||||||
|
if (provider.Enabled)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return subscription.Status switch
|
||||||
|
{
|
||||||
|
SubscriptionStatus.Unpaid => currentContext.ProviderProviderAdmin(provider.Id)
|
||||||
|
? new SuspensionWarning { Resolution = "add_payment_method", SubscriptionCancelsAt = subscription.CancelAt }
|
||||||
|
: new SuspensionWarning { Resolution = "contact_administrator" },
|
||||||
|
_ => new SuspensionWarning { Resolution = "contact_support" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<TaxIdWarning?> GetTaxIdWarningAsync(
|
||||||
|
Provider provider,
|
||||||
|
Customer customer)
|
||||||
|
{
|
||||||
|
if (customer.Address?.Country == CountryAbbreviations.UnitedStates)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentContext.ProviderProviderAdmin(provider.Id))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Potentially DRY this out with the GetOrganizationWarningsQuery
|
||||||
|
|
||||||
|
// Get active and scheduled registrations
|
||||||
|
var registrations = (await Task.WhenAll(
|
||||||
|
stripeAdapter.TaxRegistrationsListAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Active }),
|
||||||
|
stripeAdapter.TaxRegistrationsListAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Scheduled })))
|
||||||
|
.SelectMany(registrations => registrations.Data);
|
||||||
|
|
||||||
|
// Find the matching registration for the customer
|
||||||
|
var registration = registrations.FirstOrDefault(registration => registration.Country == customer.Address?.Country);
|
||||||
|
|
||||||
|
// If we're not registered in their country, we don't need a warning
|
||||||
|
if (registration == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var taxId = customer.TaxIds.FirstOrDefault();
|
||||||
|
|
||||||
|
return taxId switch
|
||||||
|
{
|
||||||
|
// Customer's tax ID is missing
|
||||||
|
null => new TaxIdWarning { Type = "tax_id_missing" },
|
||||||
|
// Not sure if this case is valid, but Stripe says this property is nullable
|
||||||
|
not null when taxId.Verification == null => null,
|
||||||
|
// Customer's tax ID is pending verification
|
||||||
|
not null when taxId.Verification.Status == TaxIdVerificationStatus.Pending => new TaxIdWarning { Type = "tax_id_pending_verification" },
|
||||||
|
// Customer's tax ID failed verification
|
||||||
|
not null when taxId.Verification.Status == TaxIdVerificationStatus.Unverified => new TaxIdWarning { Type = "tax_id_failed_verification" },
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.Globalization;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using System.Globalization;
|
||||||
using Bit.Commercial.Core.Billing.Providers.Models;
|
using Bit.Commercial.Core.Billing.Providers.Models;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
@@ -11,6 +14,7 @@ using Bit.Core.Billing.Constants;
|
|||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
|
using Bit.Core.Billing.Payment.Models;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Providers.Entities;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
using Bit.Core.Billing.Providers.Models;
|
using Bit.Core.Billing.Providers.Models;
|
||||||
@@ -18,10 +22,8 @@ using Bit.Core.Billing.Providers.Repositories;
|
|||||||
using Bit.Core.Billing.Providers.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Billing.Tax.Models;
|
using Bit.Core.Billing.Tax.Models;
|
||||||
using Bit.Core.Billing.Tax.Services;
|
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Business;
|
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
@@ -35,10 +37,12 @@ using Subscription = Stripe.Subscription;
|
|||||||
|
|
||||||
namespace Bit.Commercial.Core.Billing.Providers.Services;
|
namespace Bit.Commercial.Core.Billing.Providers.Services;
|
||||||
|
|
||||||
|
using static Constants;
|
||||||
|
using static StripeConstants;
|
||||||
|
|
||||||
public class ProviderBillingService(
|
public class ProviderBillingService(
|
||||||
IBraintreeGateway braintreeGateway,
|
IBraintreeGateway braintreeGateway,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IFeatureService featureService,
|
|
||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
ILogger<ProviderBillingService> logger,
|
ILogger<ProviderBillingService> logger,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@@ -49,8 +53,7 @@ public class ProviderBillingService(
|
|||||||
IProviderUserRepository providerUserRepository,
|
IProviderUserRepository providerUserRepository,
|
||||||
ISetupIntentCache setupIntentCache,
|
ISetupIntentCache setupIntentCache,
|
||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
ISubscriberService subscriberService,
|
ISubscriberService subscriberService)
|
||||||
ITaxService taxService)
|
|
||||||
: IProviderBillingService
|
: IProviderBillingService
|
||||||
{
|
{
|
||||||
public async Task AddExistingOrganization(
|
public async Task AddExistingOrganization(
|
||||||
@@ -59,10 +62,7 @@ public class ProviderBillingService(
|
|||||||
string key)
|
string key)
|
||||||
{
|
{
|
||||||
await stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
|
await stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
|
||||||
new SubscriptionUpdateOptions
|
new SubscriptionUpdateOptions { CancelAtPeriodEnd = false });
|
||||||
{
|
|
||||||
CancelAtPeriodEnd = false
|
|
||||||
});
|
|
||||||
|
|
||||||
var subscription =
|
var subscription =
|
||||||
await stripeAdapter.SubscriptionCancelAsync(organization.GatewaySubscriptionId,
|
await stripeAdapter.SubscriptionCancelAsync(organization.GatewaySubscriptionId,
|
||||||
@@ -81,7 +81,7 @@ public class ProviderBillingService(
|
|||||||
|
|
||||||
var wasTrialing = subscription.TrialEnd.HasValue && subscription.TrialEnd.Value > now;
|
var wasTrialing = subscription.TrialEnd.HasValue && subscription.TrialEnd.Value > now;
|
||||||
|
|
||||||
if (!wasTrialing && subscription.LatestInvoice.Status == StripeConstants.InvoiceStatus.Draft)
|
if (!wasTrialing && subscription.LatestInvoice.Status == InvoiceStatus.Draft)
|
||||||
{
|
{
|
||||||
await stripeAdapter.InvoiceFinalizeInvoiceAsync(subscription.LatestInvoiceId,
|
await stripeAdapter.InvoiceFinalizeInvoiceAsync(subscription.LatestInvoiceId,
|
||||||
new InvoiceFinalizeOptions { AutoAdvance = true });
|
new InvoiceFinalizeOptions { AutoAdvance = true });
|
||||||
@@ -182,16 +182,8 @@ public class ProviderBillingService(
|
|||||||
{
|
{
|
||||||
Items =
|
Items =
|
||||||
[
|
[
|
||||||
new SubscriptionItemOptions
|
new SubscriptionItemOptions { Price = newPriceId, Quantity = oldSubscriptionItem!.Quantity },
|
||||||
{
|
new SubscriptionItemOptions { Id = oldSubscriptionItem.Id, Deleted = true }
|
||||||
Price = newPriceId,
|
|
||||||
Quantity = oldSubscriptionItem!.Quantity
|
|
||||||
},
|
|
||||||
new SubscriptionItemOptions
|
|
||||||
{
|
|
||||||
Id = oldSubscriptionItem.Id,
|
|
||||||
Deleted = true
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -200,7 +192,8 @@ public class ProviderBillingService(
|
|||||||
// Refactor later to ?ChangeClientPlanCommand? (ProviderPlanId, ProviderId, OrganizationId)
|
// Refactor later to ?ChangeClientPlanCommand? (ProviderPlanId, ProviderId, OrganizationId)
|
||||||
// 1. Retrieve PlanType and PlanName for ProviderPlan
|
// 1. Retrieve PlanType and PlanName for ProviderPlan
|
||||||
// 2. Assign PlanType & PlanName to Organization
|
// 2. Assign PlanType & PlanName to Organization
|
||||||
var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerPlan.ProviderId);
|
var providerOrganizations =
|
||||||
|
await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerPlan.ProviderId);
|
||||||
|
|
||||||
var newPlan = await pricingClient.GetPlanOrThrow(newPlanType);
|
var newPlan = await pricingClient.GetPlanOrThrow(newPlanType);
|
||||||
|
|
||||||
@@ -211,6 +204,7 @@ public class ProviderBillingService(
|
|||||||
{
|
{
|
||||||
throw new ConflictException($"Organization '{providerOrganization.Id}' not found.");
|
throw new ConflictException($"Organization '{providerOrganization.Id}' not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
organization.PlanType = newPlanType;
|
organization.PlanType = newPlanType;
|
||||||
organization.Plan = newPlan.Name;
|
organization.Plan = newPlan.Name;
|
||||||
await organizationRepository.ReplaceAsync(organization);
|
await organizationRepository.ReplaceAsync(organization);
|
||||||
@@ -226,15 +220,15 @@ public class ProviderBillingService(
|
|||||||
|
|
||||||
if (!string.IsNullOrEmpty(organization.GatewayCustomerId))
|
if (!string.IsNullOrEmpty(organization.GatewayCustomerId))
|
||||||
{
|
{
|
||||||
logger.LogWarning("Client organization ({ID}) already has a populated {FieldName}", organization.Id, nameof(organization.GatewayCustomerId));
|
logger.LogWarning("Client organization ({ID}) already has a populated {FieldName}", organization.Id,
|
||||||
|
nameof(organization.GatewayCustomerId));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var providerCustomer = await subscriberService.GetCustomerOrThrow(provider, new CustomerGetOptions
|
var providerCustomer =
|
||||||
{
|
await subscriberService.GetCustomerOrThrow(provider,
|
||||||
Expand = ["tax", "tax_ids"]
|
new CustomerGetOptions { Expand = ["tax", "tax_ids"] });
|
||||||
});
|
|
||||||
|
|
||||||
var providerTaxId = providerCustomer.TaxIds.FirstOrDefault();
|
var providerTaxId = providerCustomer.TaxIds.FirstOrDefault();
|
||||||
|
|
||||||
@@ -267,25 +261,18 @@ public class ProviderBillingService(
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
Metadata = new Dictionary<string, string>
|
Metadata = new Dictionary<string, string> { { "region", globalSettings.BaseServiceUri.CloudRegion } },
|
||||||
{
|
TaxIdData = providerTaxId == null
|
||||||
{ "region", globalSettings.BaseServiceUri.CloudRegion }
|
? null
|
||||||
},
|
:
|
||||||
TaxIdData = providerTaxId == null ? null :
|
[
|
||||||
[
|
new CustomerTaxIdDataOptions { Type = providerTaxId.Type, Value = providerTaxId.Value }
|
||||||
new CustomerTaxIdDataOptions
|
]
|
||||||
{
|
|
||||||
Type = providerTaxId.Type,
|
|
||||||
Value = providerTaxId.Value
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
if (providerCustomer.Address is not { Country: CountryAbbreviations.UnitedStates })
|
||||||
|
|
||||||
if (setNonUSBusinessUseToReverseCharge && providerCustomer.Address is not { Country: "US" })
|
|
||||||
{
|
{
|
||||||
customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse;
|
customerCreateOptions.TaxExempt = TaxExempt.Reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
var customer = await stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
var customer = await stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
||||||
@@ -347,9 +334,9 @@ public class ProviderBillingService(
|
|||||||
.Where(pair => pair.subscription is
|
.Where(pair => pair.subscription is
|
||||||
{
|
{
|
||||||
Status:
|
Status:
|
||||||
StripeConstants.SubscriptionStatus.Active or
|
SubscriptionStatus.Active or
|
||||||
StripeConstants.SubscriptionStatus.Trialing or
|
SubscriptionStatus.Trialing or
|
||||||
StripeConstants.SubscriptionStatus.PastDue
|
SubscriptionStatus.PastDue
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
if (active.Count == 0)
|
if (active.Count == 0)
|
||||||
@@ -474,35 +461,27 @@ public class ProviderBillingService(
|
|||||||
// Below the limit to above the limit
|
// Below the limit to above the limit
|
||||||
(currentlyAssignedSeatTotal <= seatMinimum && newlyAssignedSeatTotal > seatMinimum) ||
|
(currentlyAssignedSeatTotal <= seatMinimum && newlyAssignedSeatTotal > seatMinimum) ||
|
||||||
// Above the limit to further above the limit
|
// Above the limit to further above the limit
|
||||||
(currentlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > currentlyAssignedSeatTotal);
|
(currentlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > seatMinimum &&
|
||||||
|
newlyAssignedSeatTotal > currentlyAssignedSeatTotal);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Customer> SetupCustomer(
|
public async Task<Customer> SetupCustomer(
|
||||||
Provider provider,
|
Provider provider,
|
||||||
TaxInfo taxInfo,
|
TokenizedPaymentMethod paymentMethod,
|
||||||
TokenizedPaymentSource tokenizedPaymentSource = null)
|
BillingAddress billingAddress)
|
||||||
{
|
{
|
||||||
if (taxInfo is not
|
|
||||||
{
|
|
||||||
BillingAddressCountry: not null and not "",
|
|
||||||
BillingAddressPostalCode: not null and not ""
|
|
||||||
})
|
|
||||||
{
|
|
||||||
logger.LogError("Cannot create customer for provider ({ProviderID}) without both a country and postal code", provider.Id);
|
|
||||||
throw new BillingException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = new CustomerCreateOptions
|
var options = new CustomerCreateOptions
|
||||||
{
|
{
|
||||||
Address = new AddressOptions
|
Address = new AddressOptions
|
||||||
{
|
{
|
||||||
Country = taxInfo.BillingAddressCountry,
|
Country = billingAddress.Country,
|
||||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
PostalCode = billingAddress.PostalCode,
|
||||||
Line1 = taxInfo.BillingAddressLine1,
|
Line1 = billingAddress.Line1,
|
||||||
Line2 = taxInfo.BillingAddressLine2,
|
Line2 = billingAddress.Line2,
|
||||||
City = taxInfo.BillingAddressCity,
|
City = billingAddress.City,
|
||||||
State = taxInfo.BillingAddressState
|
State = billingAddress.State
|
||||||
},
|
},
|
||||||
|
Coupon = !string.IsNullOrEmpty(provider.DiscountId) ? provider.DiscountId : null,
|
||||||
Description = provider.DisplayBusinessName(),
|
Description = provider.DisplayBusinessName(),
|
||||||
Email = provider.BillingEmail,
|
Email = provider.BillingEmail,
|
||||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||||
@@ -518,112 +497,71 @@ public class ProviderBillingService(
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
Metadata = new Dictionary<string, string>
|
Metadata = new Dictionary<string, string> { { "region", globalSettings.BaseServiceUri.CloudRegion } },
|
||||||
{
|
TaxExempt = billingAddress.Country != CountryAbbreviations.UnitedStates ? TaxExempt.Reverse : TaxExempt.None
|
||||||
{ "region", globalSettings.BaseServiceUri.CloudRegion }
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
if (billingAddress.TaxId != null)
|
||||||
|
|
||||||
if (setNonUSBusinessUseToReverseCharge && taxInfo.BillingAddressCountry != "US")
|
|
||||||
{
|
{
|
||||||
options.TaxExempt = StripeConstants.TaxExempt.Reverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(taxInfo.TaxIdNumber))
|
|
||||||
{
|
|
||||||
var taxIdType = taxService.GetStripeTaxCode(
|
|
||||||
taxInfo.BillingAddressCountry,
|
|
||||||
taxInfo.TaxIdNumber);
|
|
||||||
|
|
||||||
if (taxIdType == null)
|
|
||||||
{
|
|
||||||
logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.",
|
|
||||||
taxInfo.BillingAddressCountry,
|
|
||||||
taxInfo.TaxIdNumber);
|
|
||||||
|
|
||||||
throw new BadRequestException("billingTaxIdTypeInferenceError");
|
|
||||||
}
|
|
||||||
|
|
||||||
options.TaxIdData =
|
options.TaxIdData =
|
||||||
[
|
[
|
||||||
new CustomerTaxIdDataOptions { Type = taxIdType, Value = taxInfo.TaxIdNumber }
|
new CustomerTaxIdDataOptions { Type = billingAddress.TaxId.Code, Value = billingAddress.TaxId.Value }
|
||||||
];
|
];
|
||||||
|
|
||||||
if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)
|
if (billingAddress.TaxId.Code == TaxIdType.SpanishNIF)
|
||||||
{
|
{
|
||||||
options.TaxIdData.Add(new CustomerTaxIdDataOptions
|
options.TaxIdData.Add(new CustomerTaxIdDataOptions
|
||||||
{
|
{
|
||||||
Type = StripeConstants.TaxIdType.EUVAT,
|
Type = TaxIdType.EUVAT,
|
||||||
Value = $"ES{taxInfo.TaxIdNumber}"
|
Value = $"ES{billingAddress.TaxId.Value}"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(provider.DiscountId))
|
|
||||||
{
|
|
||||||
options.Coupon = provider.DiscountId;
|
|
||||||
}
|
|
||||||
|
|
||||||
var requireProviderPaymentMethodDuringSetup =
|
|
||||||
featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup);
|
|
||||||
|
|
||||||
var braintreeCustomerId = "";
|
var braintreeCustomerId = "";
|
||||||
|
|
||||||
if (requireProviderPaymentMethodDuringSetup)
|
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||||
|
switch (paymentMethod.Type)
|
||||||
{
|
{
|
||||||
if (tokenizedPaymentSource is not
|
case TokenizablePaymentMethodType.BankAccount:
|
||||||
{
|
{
|
||||||
Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal,
|
var setupIntent =
|
||||||
Token: not null and not ""
|
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions
|
||||||
})
|
|
||||||
{
|
|
||||||
logger.LogError("Cannot create customer for provider ({ProviderID}) without a payment method", provider.Id);
|
|
||||||
throw new BillingException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var (type, token) = tokenizedPaymentSource;
|
|
||||||
|
|
||||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case PaymentMethodType.BankAccount:
|
|
||||||
{
|
|
||||||
var setupIntent =
|
|
||||||
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = token }))
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
if (setupIntent == null)
|
|
||||||
{
|
{
|
||||||
logger.LogError("Cannot create customer for provider ({ProviderID}) without a setup intent for their bank account", provider.Id);
|
PaymentMethod = paymentMethod.Token
|
||||||
throw new BillingException();
|
}))
|
||||||
}
|
.FirstOrDefault();
|
||||||
|
|
||||||
await setupIntentCache.Set(provider.Id, setupIntent.Id);
|
if (setupIntent == null)
|
||||||
break;
|
|
||||||
}
|
|
||||||
case PaymentMethodType.Card:
|
|
||||||
{
|
{
|
||||||
options.PaymentMethod = token;
|
logger.LogError(
|
||||||
options.InvoiceSettings.DefaultPaymentMethod = token;
|
"Cannot create customer for provider ({ProviderID}) without a setup intent for their bank account",
|
||||||
break;
|
provider.Id);
|
||||||
|
throw new BillingException();
|
||||||
}
|
}
|
||||||
case PaymentMethodType.PayPal:
|
|
||||||
{
|
await setupIntentCache.Set(provider.Id, setupIntent.Id);
|
||||||
braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(provider, token);
|
break;
|
||||||
options.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId;
|
}
|
||||||
break;
|
case TokenizablePaymentMethodType.Card:
|
||||||
}
|
{
|
||||||
}
|
options.PaymentMethod = paymentMethod.Token;
|
||||||
|
options.InvoiceSettings.DefaultPaymentMethod = paymentMethod.Token;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TokenizablePaymentMethodType.PayPal:
|
||||||
|
{
|
||||||
|
braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(provider, paymentMethod.Token);
|
||||||
|
options.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await stripeAdapter.CustomerCreateAsync(options);
|
return await stripeAdapter.CustomerCreateAsync(options);
|
||||||
}
|
}
|
||||||
catch (StripeException stripeException) when (stripeException.StripeError?.Code ==
|
catch (StripeException stripeException) when (stripeException.StripeError?.Code == ErrorCodes.TaxIdInvalid)
|
||||||
StripeConstants.ErrorCodes.TaxIdInvalid)
|
|
||||||
{
|
{
|
||||||
await Revert();
|
await Revert();
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
@@ -637,25 +575,22 @@ public class ProviderBillingService(
|
|||||||
|
|
||||||
async Task Revert()
|
async Task Revert()
|
||||||
{
|
{
|
||||||
if (requireProviderPaymentMethodDuringSetup && tokenizedPaymentSource != null)
|
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||||
|
switch (paymentMethod.Type)
|
||||||
{
|
{
|
||||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
case TokenizablePaymentMethodType.BankAccount:
|
||||||
switch (tokenizedPaymentSource.Type)
|
{
|
||||||
{
|
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(provider.Id);
|
||||||
case PaymentMethodType.BankAccount:
|
await stripeAdapter.SetupIntentCancel(setupIntentId,
|
||||||
{
|
new SetupIntentCancelOptions { CancellationReason = "abandoned" });
|
||||||
var setupIntentId = await setupIntentCache.Get(provider.Id);
|
await setupIntentCache.RemoveSetupIntentForSubscriber(provider.Id);
|
||||||
await stripeAdapter.SetupIntentCancel(setupIntentId,
|
break;
|
||||||
new SetupIntentCancelOptions { CancellationReason = "abandoned" });
|
}
|
||||||
await setupIntentCache.Remove(provider.Id);
|
case TokenizablePaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
|
||||||
break;
|
{
|
||||||
}
|
await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);
|
||||||
case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
|
break;
|
||||||
{
|
}
|
||||||
await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -670,9 +605,10 @@ public class ProviderBillingService(
|
|||||||
|
|
||||||
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
||||||
|
|
||||||
if (providerPlans == null || providerPlans.Count == 0)
|
if (providerPlans.Count == 0)
|
||||||
{
|
{
|
||||||
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured plans", provider.Id);
|
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured plans",
|
||||||
|
provider.Id);
|
||||||
|
|
||||||
throw new BillingException();
|
throw new BillingException();
|
||||||
}
|
}
|
||||||
@@ -685,7 +621,9 @@ public class ProviderBillingService(
|
|||||||
|
|
||||||
if (!providerPlan.IsConfigured())
|
if (!providerPlan.IsConfigured())
|
||||||
{
|
{
|
||||||
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured {ProviderName} plan", provider.Id, plan.Name);
|
logger.LogError(
|
||||||
|
"Cannot start subscription for provider ({ProviderID}) that has no configured {ProviderName} plan",
|
||||||
|
provider.Id, plan.Name);
|
||||||
throw new BillingException();
|
throw new BillingException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -698,23 +636,17 @@ public class ProviderBillingService(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var requireProviderPaymentMethodDuringSetup =
|
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(provider.Id);
|
||||||
featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup);
|
|
||||||
|
|
||||||
var setupIntentId = await setupIntentCache.Get(provider.Id);
|
|
||||||
|
|
||||||
var setupIntent = !string.IsNullOrEmpty(setupIntentId)
|
var setupIntent = !string.IsNullOrEmpty(setupIntentId)
|
||||||
? await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions
|
? await stripeAdapter.SetupIntentGet(setupIntentId,
|
||||||
{
|
new SetupIntentGetOptions { Expand = ["payment_method"] })
|
||||||
Expand = ["payment_method"]
|
|
||||||
})
|
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var usePaymentMethod =
|
var usePaymentMethod =
|
||||||
requireProviderPaymentMethodDuringSetup &&
|
!string.IsNullOrEmpty(customer.InvoiceSettings?.DefaultPaymentMethodId) ||
|
||||||
(!string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) ||
|
customer.Metadata?.ContainsKey(BraintreeCustomerIdKey) == true ||
|
||||||
customer.Metadata.ContainsKey(BraintreeCustomerIdKey) ||
|
setupIntent?.IsUnverifiedBankAccount() == true;
|
||||||
setupIntent.IsUnverifiedBankAccount());
|
|
||||||
|
|
||||||
int? trialPeriodDays = provider.Type switch
|
int? trialPeriodDays = provider.Type switch
|
||||||
{
|
{
|
||||||
@@ -725,35 +657,20 @@ public class ProviderBillingService(
|
|||||||
|
|
||||||
var subscriptionCreateOptions = new SubscriptionCreateOptions
|
var subscriptionCreateOptions = new SubscriptionCreateOptions
|
||||||
{
|
{
|
||||||
CollectionMethod = usePaymentMethod ?
|
CollectionMethod =
|
||||||
StripeConstants.CollectionMethod.ChargeAutomatically : StripeConstants.CollectionMethod.SendInvoice,
|
usePaymentMethod
|
||||||
|
? CollectionMethod.ChargeAutomatically
|
||||||
|
: CollectionMethod.SendInvoice,
|
||||||
Customer = customer.Id,
|
Customer = customer.Id,
|
||||||
DaysUntilDue = usePaymentMethod ? null : 30,
|
DaysUntilDue = usePaymentMethod ? null : 30,
|
||||||
Items = subscriptionItemOptionsList,
|
Items = subscriptionItemOptionsList,
|
||||||
Metadata = new Dictionary<string, string>
|
Metadata = new Dictionary<string, string> { { "providerId", provider.Id.ToString() } },
|
||||||
{
|
|
||||||
{ "providerId", provider.Id.ToString() }
|
|
||||||
},
|
|
||||||
OffSession = true,
|
OffSession = true,
|
||||||
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
|
ProrationBehavior = ProrationBehavior.CreateProrations,
|
||||||
TrialPeriodDays = trialPeriodDays
|
TrialPeriodDays = trialPeriodDays,
|
||||||
|
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
||||||
};
|
};
|
||||||
|
|
||||||
var setNonUSBusinessUseToReverseCharge =
|
|
||||||
featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
|
||||||
|
|
||||||
if (setNonUSBusinessUseToReverseCharge)
|
|
||||||
{
|
|
||||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
|
||||||
}
|
|
||||||
else if (customer.HasRecognizedTaxLocation())
|
|
||||||
{
|
|
||||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
|
|
||||||
{
|
|
||||||
Enabled = customer.Address.Country == "US" ||
|
|
||||||
customer.TaxIds.Any()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -761,7 +678,7 @@ public class ProviderBillingService(
|
|||||||
|
|
||||||
if (subscription is
|
if (subscription is
|
||||||
{
|
{
|
||||||
Status: StripeConstants.SubscriptionStatus.Active or StripeConstants.SubscriptionStatus.Trialing
|
Status: SubscriptionStatus.Active or SubscriptionStatus.Trialing
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
return subscription;
|
return subscription;
|
||||||
@@ -775,9 +692,11 @@ public class ProviderBillingService(
|
|||||||
|
|
||||||
throw new BillingException();
|
throw new BillingException();
|
||||||
}
|
}
|
||||||
catch (StripeException stripeException) when (stripeException.StripeError?.Code == StripeConstants.ErrorCodes.CustomerTaxLocationInvalid)
|
catch (StripeException stripeException) when (stripeException.StripeError?.Code ==
|
||||||
|
ErrorCodes.CustomerTaxLocationInvalid)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Your location wasn't recognized. Please ensure your country and postal code are valid.");
|
throw new BadRequestException(
|
||||||
|
"Your location wasn't recognized. Please ensure your country and postal code are valid.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -791,7 +710,7 @@ public class ProviderBillingService(
|
|||||||
subscriberService.UpdateTaxInformation(provider, taxInformation));
|
subscriberService.UpdateTaxInformation(provider, taxInformation));
|
||||||
|
|
||||||
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
|
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
|
||||||
new SubscriptionUpdateOptions { CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically });
|
new SubscriptionUpdateOptions { CollectionMethod = CollectionMethod.ChargeAutomatically });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command)
|
public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command)
|
||||||
@@ -891,13 +810,9 @@ public class ProviderBillingService(
|
|||||||
|
|
||||||
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId, new SubscriptionUpdateOptions
|
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId, new SubscriptionUpdateOptions
|
||||||
{
|
{
|
||||||
Items = [
|
Items =
|
||||||
new SubscriptionItemOptions
|
[
|
||||||
{
|
new SubscriptionItemOptions { Id = item.Id, Price = priceId, Quantity = newlySubscribedSeats }
|
||||||
Id = item.Id,
|
|
||||||
Price = priceId,
|
|
||||||
Quantity = newlySubscribedSeats
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -920,7 +835,8 @@ public class ProviderBillingService(
|
|||||||
var plan = await pricingClient.GetPlanOrThrow(planType);
|
var plan = await pricingClient.GetPlanOrThrow(planType);
|
||||||
|
|
||||||
return providerOrganizations
|
return providerOrganizations
|
||||||
.Where(providerOrganization => providerOrganization.Plan == plan.Name && providerOrganization.Status == OrganizationStatusType.Managed)
|
.Where(providerOrganization => providerOrganization.Plan == plan.Name &&
|
||||||
|
providerOrganization.Status == OrganizationStatusType.Managed)
|
||||||
.Sum(providerOrganization => providerOrganization.Seats ?? 0);
|
.Sum(providerOrganization => providerOrganization.Seats ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
<PackageReference Include="CsvHelper" Version="33.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Bit.Core.SecretsManager.Commands.Porting;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Bit.Core.SecretsManager.Commands.Porting;
|
||||||
using Bit.Core.SecretsManager.Commands.Porting.Interfaces;
|
using Bit.Core.SecretsManager.Commands.Porting.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Entities;
|
using Bit.Core.SecretsManager.Entities;
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
using Bit.Core.Context;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Bit.Core.Auth.Identity;
|
||||||
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Identity;
|
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
|
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Entities;
|
using Bit.Core.SecretsManager.Entities;
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
using Bit.Core.Repositories;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
|
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Entities;
|
using Bit.Core.SecretsManager.Entities;
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;
|
namespace Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;
|
||||||
|
|
||||||
@@ -10,15 +16,21 @@ public class CreateServiceAccountCommand : ICreateServiceAccountCommand
|
|||||||
private readonly IAccessPolicyRepository _accessPolicyRepository;
|
private readonly IAccessPolicyRepository _accessPolicyRepository;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||||
|
private readonly IEventService _eventService;
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
|
||||||
public CreateServiceAccountCommand(
|
public CreateServiceAccountCommand(
|
||||||
IAccessPolicyRepository accessPolicyRepository,
|
IAccessPolicyRepository accessPolicyRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IServiceAccountRepository serviceAccountRepository)
|
IServiceAccountRepository serviceAccountRepository,
|
||||||
|
IEventService eventService,
|
||||||
|
ICurrentContext currentContext)
|
||||||
{
|
{
|
||||||
_accessPolicyRepository = accessPolicyRepository;
|
_accessPolicyRepository = accessPolicyRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_serviceAccountRepository = serviceAccountRepository;
|
_serviceAccountRepository = serviceAccountRepository;
|
||||||
|
_eventService = eventService;
|
||||||
|
_currentContext = currentContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount, Guid userId)
|
public async Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount, Guid userId)
|
||||||
@@ -35,6 +47,7 @@ public class CreateServiceAccountCommand : ICreateServiceAccountCommand
|
|||||||
Write = true,
|
Write = true,
|
||||||
};
|
};
|
||||||
await _accessPolicyRepository.CreateManyAsync(new List<BaseAccessPolicy> { accessPolicy });
|
await _accessPolicyRepository.CreateManyAsync(new List<BaseAccessPolicy> { accessPolicy });
|
||||||
|
await _eventService.LogServiceAccountPeopleEventAsync(user.Id, accessPolicy, EventType.ServiceAccount_UserAdded, _currentContext.IdentityClientType);
|
||||||
return createdServiceAccount;
|
return createdServiceAccount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.Security.Claims;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using System.Security.Claims;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.SecretsManager.Queries.Interfaces;
|
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using Bit.Commercial.Core.AdminConsole.Providers;
|
using Bit.Commercial.Core.AdminConsole.Providers;
|
||||||
using Bit.Commercial.Core.AdminConsole.Services;
|
using Bit.Commercial.Core.AdminConsole.Services;
|
||||||
|
using Bit.Commercial.Core.Billing.Providers.Queries;
|
||||||
using Bit.Commercial.Core.Billing.Providers.Services;
|
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
|
using Bit.Core.Billing.Providers.Queries;
|
||||||
using Bit.Core.Billing.Providers.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
@@ -17,5 +19,6 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<IRemoveOrganizationFromProviderCommand, RemoveOrganizationFromProviderCommand>();
|
services.AddScoped<IRemoveOrganizationFromProviderCommand, RemoveOrganizationFromProviderCommand>();
|
||||||
services.AddTransient<IProviderBillingService, ProviderBillingService>();
|
services.AddTransient<IProviderBillingService, ProviderBillingService>();
|
||||||
services.AddTransient<IBusinessUnitConverter, BusinessUnitConverter>();
|
services.AddTransient<IBusinessUnitConverter, BusinessUnitConverter>();
|
||||||
|
services.AddTransient<IGetProviderWarningsQuery, GetProviderWarningsQuery>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<ProjectPermissionDetails>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
|
public async Task<IEnumerable<ProjectPermissionDetails>> GetManyByOrganizationIdAsync(
|
||||||
|
Guid organizationId,
|
||||||
|
Guid userId,
|
||||||
|
AccessClientType accessType)
|
||||||
{
|
{
|
||||||
using var scope = ServiceScopeFactory.CreateScope();
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
var dbContext = GetDatabaseContext(scope);
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
|||||||
@@ -45,6 +45,19 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyTrashedSecretsByIds(IEnumerable<Guid> ids)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var secrets = await dbContext.Secret
|
||||||
|
.Where(c => ids.Contains(c.Id) && c.DeletedDate != null)
|
||||||
|
.Include(c => c.Projects)
|
||||||
|
.ToListAsync();
|
||||||
|
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByOrganizationIdAsync(
|
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByOrganizationIdAsync(
|
||||||
Guid organizationId, Guid userId, AccessClientType accessType)
|
Guid organizationId, Guid userId, AccessClientType accessType)
|
||||||
{
|
{
|
||||||
@@ -66,10 +79,14 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
|||||||
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
|
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
|
public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(
|
||||||
|
Guid organizationId,
|
||||||
|
Guid userId,
|
||||||
|
AccessClientType accessType)
|
||||||
{
|
{
|
||||||
using var scope = ServiceScopeFactory.CreateScope();
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
var dbContext = GetDatabaseContext(scope);
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
|
||||||
var query = dbContext.Secret
|
var query = dbContext.Secret
|
||||||
.Include(c => c.Projects)
|
.Include(c => c.Projects)
|
||||||
.Where(c => c.OrganizationId == organizationId && c.DeletedDate == null)
|
.Where(c => c.OrganizationId == organizationId && c.DeletedDate == null)
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;
|
using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Scim.Models;
|
using Bit.Scim.Models;
|
||||||
using Bit.Scim.Users.Interfaces;
|
using Bit.Scim.Users.Interfaces;
|
||||||
using Bit.Scim.Utilities;
|
using Bit.Scim.Utilities;
|
||||||
@@ -19,29 +21,28 @@ namespace Bit.Scim.Controllers.v2;
|
|||||||
public class UsersController : Controller
|
public class UsersController : Controller
|
||||||
{
|
{
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IOrganizationService _organizationService;
|
|
||||||
private readonly IGetUsersListQuery _getUsersListQuery;
|
private readonly IGetUsersListQuery _getUsersListQuery;
|
||||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||||
private readonly IPatchUserCommand _patchUserCommand;
|
private readonly IPatchUserCommand _patchUserCommand;
|
||||||
private readonly IPostUserCommand _postUserCommand;
|
private readonly IPostUserCommand _postUserCommand;
|
||||||
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
||||||
|
private readonly IRevokeOrganizationUserCommand _revokeOrganizationUserCommand;
|
||||||
|
|
||||||
public UsersController(
|
public UsersController(IOrganizationUserRepository organizationUserRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
|
||||||
IOrganizationService organizationService,
|
|
||||||
IGetUsersListQuery getUsersListQuery,
|
IGetUsersListQuery getUsersListQuery,
|
||||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||||
IPatchUserCommand patchUserCommand,
|
IPatchUserCommand patchUserCommand,
|
||||||
IPostUserCommand postUserCommand,
|
IPostUserCommand postUserCommand,
|
||||||
IRestoreOrganizationUserCommand restoreOrganizationUserCommand)
|
IRestoreOrganizationUserCommand restoreOrganizationUserCommand,
|
||||||
|
IRevokeOrganizationUserCommand revokeOrganizationUserCommand)
|
||||||
{
|
{
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_organizationService = organizationService;
|
|
||||||
_getUsersListQuery = getUsersListQuery;
|
_getUsersListQuery = getUsersListQuery;
|
||||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||||
_patchUserCommand = patchUserCommand;
|
_patchUserCommand = patchUserCommand;
|
||||||
_postUserCommand = postUserCommand;
|
_postUserCommand = postUserCommand;
|
||||||
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
||||||
|
_revokeOrganizationUserCommand = revokeOrganizationUserCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
@@ -98,7 +99,7 @@ public class UsersController : Controller
|
|||||||
}
|
}
|
||||||
else if (!model.Active && orgUser.Status != OrganizationUserStatusType.Revoked)
|
else if (!model.Active && orgUser.Status != OrganizationUserStatusType.Revoked)
|
||||||
{
|
{
|
||||||
await _organizationService.RevokeUserAsync(orgUser, EventSystemUser.SCIM);
|
await _revokeOrganizationUserCommand.RevokeUserAsync(orgUser, EventSystemUser.SCIM);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Have to get full details object for response model
|
// Have to get full details object for response model
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
###############################################
|
###############################################
|
||||||
# Build stage #
|
# Build stage #
|
||||||
###############################################
|
###############################################
|
||||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build
|
||||||
|
|
||||||
# Docker buildx supplies the value for this arg
|
# Docker buildx supplies the value for this arg
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
@@ -9,11 +9,11 @@ ARG TARGETPLATFORM
|
|||||||
# Determine proper runtime value for .NET
|
# Determine proper runtime value for .NET
|
||||||
# We put the value in a file to be read by later layers.
|
# We put the value in a file to be read by later layers.
|
||||||
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||||
RID=linux-x64 ; \
|
RID=linux-musl-x64 ; \
|
||||||
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||||
RID=linux-arm64 ; \
|
RID=linux-musl-arm64 ; \
|
||||||
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
|
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
|
||||||
RID=linux-arm ; \
|
RID=linux-musl-arm ; \
|
||||||
fi \
|
fi \
|
||||||
&& echo "RID=$RID" > /tmp/rid.txt
|
&& echo "RID=$RID" > /tmp/rid.txt
|
||||||
|
|
||||||
@@ -37,21 +37,21 @@ RUN . /tmp/rid.txt && dotnet publish \
|
|||||||
###############################################
|
###############################################
|
||||||
# App stage #
|
# App stage #
|
||||||
###############################################
|
###############################################
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
LABEL com.bitwarden.product="bitwarden"
|
LABEL com.bitwarden.product="bitwarden"
|
||||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||||
ENV ASPNETCORE_URLS=http://+:5000
|
ENV ASPNETCORE_URLS=http://+:5000
|
||||||
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
|
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
|
||||||
|
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apk add --no-cache curl \
|
||||||
&& apt-get install -y --no-install-recommends \
|
krb5 \
|
||||||
gosu \
|
icu-libs \
|
||||||
curl \
|
shadow \
|
||||||
krb5-user \
|
&& apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Copy app from the build stage
|
# Copy app from the build stage
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Scim.Groups.Interfaces;
|
using Bit.Scim.Groups.Interfaces;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.Text.Json;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Bit.Scim.Utilities;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Bit.Scim.Utilities;
|
||||||
|
|
||||||
namespace Bit.Scim.Models;
|
namespace Bit.Scim.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
namespace Bit.Scim.Models;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models;
|
||||||
|
|
||||||
public abstract class BaseScimModel
|
public abstract class BaseScimModel
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Bit.Scim.Utilities;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Bit.Scim.Utilities;
|
||||||
|
|
||||||
namespace Bit.Scim.Models;
|
namespace Bit.Scim.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Bit.Scim.Utilities;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Bit.Scim.Utilities;
|
||||||
|
|
||||||
namespace Bit.Scim.Models;
|
namespace Bit.Scim.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Scim.Models;
|
namespace Bit.Scim.Models;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
|
||||||
namespace Bit.Scim.Models;
|
namespace Bit.Scim.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Bit.Scim.Utilities;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Bit.Scim.Utilities;
|
||||||
|
|
||||||
namespace Bit.Scim.Models;
|
namespace Bit.Scim.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.Text.Json;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Bit.Scim.Models;
|
namespace Bit.Scim.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
using Bit.Core.AdminConsole.Enums;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Models.Business;
|
using Bit.Core.AdminConsole.Models.Business;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using OrganizationUserInvite = Bit.Core.Models.Business.OrganizationUserInvite;
|
|
||||||
|
|
||||||
namespace Bit.Scim.Models;
|
namespace Bit.Scim.Models;
|
||||||
|
|
||||||
@@ -44,7 +47,7 @@ public class ScimUserRequestModel : BaseScimUserModel
|
|||||||
return new InviteOrganizationUsersRequest(
|
return new InviteOrganizationUsersRequest(
|
||||||
invites:
|
invites:
|
||||||
[
|
[
|
||||||
new Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite(
|
new OrganizationUserInviteCommandModel(
|
||||||
email: email,
|
email: email,
|
||||||
externalId: ExternalIdForInvite()
|
externalId: ExternalIdForInvite()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using Bit.Core.Utilities;
|
|||||||
using Bit.Scim.Context;
|
using Bit.Scim.Context;
|
||||||
using Bit.Scim.Utilities;
|
using Bit.Scim.Utilities;
|
||||||
using Bit.SharedWeb.Utilities;
|
using Bit.SharedWeb.Utilities;
|
||||||
using IdentityModel;
|
using Duende.IdentityModel;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Scim.Users.Interfaces;
|
using Bit.Scim.Users.Interfaces;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Scim.Models;
|
using Bit.Scim.Models;
|
||||||
|
|
||||||
namespace Bit.Scim.Users.Interfaces;
|
namespace Bit.Scim.Users.Interfaces;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Scim.Models;
|
using Bit.Scim.Models;
|
||||||
using Bit.Scim.Users.Interfaces;
|
using Bit.Scim.Users.Interfaces;
|
||||||
|
|
||||||
@@ -11,20 +11,19 @@ namespace Bit.Scim.Users;
|
|||||||
public class PatchUserCommand : IPatchUserCommand
|
public class PatchUserCommand : IPatchUserCommand
|
||||||
{
|
{
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IOrganizationService _organizationService;
|
|
||||||
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
||||||
private readonly ILogger<PatchUserCommand> _logger;
|
private readonly ILogger<PatchUserCommand> _logger;
|
||||||
|
private readonly IRevokeOrganizationUserCommand _revokeOrganizationUserCommand;
|
||||||
|
|
||||||
public PatchUserCommand(
|
public PatchUserCommand(IOrganizationUserRepository organizationUserRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
|
||||||
IOrganizationService organizationService,
|
|
||||||
IRestoreOrganizationUserCommand restoreOrganizationUserCommand,
|
IRestoreOrganizationUserCommand restoreOrganizationUserCommand,
|
||||||
ILogger<PatchUserCommand> logger)
|
ILogger<PatchUserCommand> logger,
|
||||||
|
IRevokeOrganizationUserCommand revokeOrganizationUserCommand)
|
||||||
{
|
{
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_organizationService = organizationService;
|
|
||||||
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_revokeOrganizationUserCommand = revokeOrganizationUserCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PatchUserAsync(Guid organizationId, Guid id, ScimPatchModel model)
|
public async Task PatchUserAsync(Guid organizationId, Guid id, ScimPatchModel model)
|
||||||
@@ -80,7 +79,7 @@ public class PatchUserCommand : IPatchUserCommand
|
|||||||
}
|
}
|
||||||
else if (!active && orgUser.Status != OrganizationUserStatusType.Revoked)
|
else if (!active && orgUser.Status != OrganizationUserStatusType.Revoked)
|
||||||
{
|
{
|
||||||
await _organizationService.RevokeUserAsync(orgUser, EventSystemUser.SCIM);
|
await _revokeOrganizationUserCommand.RevokeUserAsync(orgUser, EventSystemUser.SCIM);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using System.Text.Encodings.Web;
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Scim.Context;
|
using Bit.Scim.Context;
|
||||||
using IdentityModel;
|
using Duende.IdentityModel;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/bin/sh
|
||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ then
|
|||||||
mkdir -p /etc/bitwarden/ca-certificates
|
mkdir -p /etc/bitwarden/ca-certificates
|
||||||
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
|
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
|
||||||
|
|
||||||
if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then
|
if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then
|
||||||
chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos
|
chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -46,13 +46,13 @@ else
|
|||||||
gosu_cmd=""
|
gosu_cmd=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then
|
if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then
|
||||||
cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf
|
cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf
|
||||||
$gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab
|
$gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $globalSettings__selfHosted == "true" ]]; then
|
if [ "$globalSettings__selfHosted" = "true" ]; then
|
||||||
if [[ -z $globalSettings__identityServer__certificateLocation ]]; then
|
if [ -z "$globalSettings__identityServer__certificateLocation" ]; then
|
||||||
export globalSettings__identityServer__certificateLocation=/etc/bitwarden/identity/identity.pfx
|
export globalSettings__identityServer__certificateLocation=/etc/bitwarden/identity/identity.pfx
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
using System.Security.Claims;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using System.Security.Claims;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
@@ -19,10 +23,10 @@ using Bit.Core.Tokens;
|
|||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Sso.Models;
|
using Bit.Sso.Models;
|
||||||
using Bit.Sso.Utilities;
|
using Bit.Sso.Utilities;
|
||||||
|
using Duende.IdentityModel;
|
||||||
using Duende.IdentityServer;
|
using Duende.IdentityServer;
|
||||||
using Duende.IdentityServer.Services;
|
using Duende.IdentityServer.Services;
|
||||||
using Duende.IdentityServer.Stores;
|
using Duende.IdentityServer.Stores;
|
||||||
using IdentityModel;
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@@ -104,36 +108,32 @@ public class AccountController : Controller
|
|||||||
// Validate domain_hint provided
|
// Validate domain_hint provided
|
||||||
if (string.IsNullOrWhiteSpace(domainHint))
|
if (string.IsNullOrWhiteSpace(domainHint))
|
||||||
{
|
{
|
||||||
return InvalidJson("NoOrganizationIdentifierProvidedError");
|
_logger.LogError(new ArgumentException("domainHint is required."), "domainHint not specified.");
|
||||||
|
return InvalidJson("SsoInvalidIdentifierError");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate organization exists from domain_hint
|
// Validate organization exists from domain_hint
|
||||||
var organization = await _organizationRepository.GetByIdentifierAsync(domainHint);
|
var organization = await _organizationRepository.GetByIdentifierAsync(domainHint);
|
||||||
if (organization == null)
|
if (organization is not { UseSso: true })
|
||||||
{
|
{
|
||||||
return InvalidJson("OrganizationNotFoundByIdentifierError");
|
_logger.LogError("Organization not configured to use SSO.");
|
||||||
}
|
return InvalidJson("SsoInvalidIdentifierError");
|
||||||
if (!organization.UseSso)
|
|
||||||
{
|
|
||||||
return InvalidJson("SsoNotAllowedForOrganizationError");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate SsoConfig exists and is Enabled
|
// Validate SsoConfig exists and is Enabled
|
||||||
var ssoConfig = await _ssoConfigRepository.GetByIdentifierAsync(domainHint);
|
var ssoConfig = await _ssoConfigRepository.GetByIdentifierAsync(domainHint);
|
||||||
if (ssoConfig == null)
|
if (ssoConfig is not { Enabled: true })
|
||||||
{
|
{
|
||||||
return InvalidJson("SsoConfigurationNotFoundForOrganizationError");
|
_logger.LogError("SsoConfig not enabled.");
|
||||||
}
|
return InvalidJson("SsoInvalidIdentifierError");
|
||||||
if (!ssoConfig.Enabled)
|
|
||||||
{
|
|
||||||
return InvalidJson("SsoNotEnabledForOrganizationError");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Authentication Scheme exists and is loaded (cache)
|
// Validate Authentication Scheme exists and is loaded (cache)
|
||||||
var scheme = await _schemeProvider.GetSchemeAsync(organization.Id.ToString());
|
var scheme = await _schemeProvider.GetSchemeAsync(organization.Id.ToString());
|
||||||
if (scheme == null || !(scheme is IDynamicAuthenticationScheme dynamicScheme))
|
if (scheme is not IDynamicAuthenticationScheme dynamicScheme)
|
||||||
{
|
{
|
||||||
return InvalidJson("NoSchemeOrHandlerForSsoConfigurationFoundError");
|
_logger.LogError("Invalid authentication scheme for organization.");
|
||||||
|
return InvalidJson("SsoInvalidIdentifierError");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run scheme validation
|
// Run scheme validation
|
||||||
@@ -143,13 +143,8 @@ public class AccountController : Controller
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
var translatedException = _i18nService.GetLocalizedHtmlString(ex.Message);
|
_logger.LogError(ex, "An error occurred while validating SSO dynamic scheme.");
|
||||||
var errorKey = "InvalidSchemeConfigurationError";
|
return InvalidJson("SsoInvalidIdentifierError");
|
||||||
if (!translatedException.ResourceNotFound)
|
|
||||||
{
|
|
||||||
errorKey = ex.Message;
|
|
||||||
}
|
|
||||||
return InvalidJson(errorKey, translatedException.ResourceNotFound ? ex : null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var tokenable = new SsoTokenable(organization, _globalSettings.Sso.SsoTokenLifetimeInSeconds);
|
var tokenable = new SsoTokenable(organization, _globalSettings.Sso.SsoTokenLifetimeInSeconds);
|
||||||
@@ -159,7 +154,8 @@ public class AccountController : Controller
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return InvalidJson("PreValidationError", ex);
|
_logger.LogError(ex, "An error occurred during SSO prevalidation.");
|
||||||
|
return InvalidJson("SsoInvalidIdentifierError");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,18 +247,23 @@ public class AccountController : Controller
|
|||||||
var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
|
var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
|
||||||
_logger.LogDebug("External claims: {@claims}", externalClaims);
|
_logger.LogDebug("External claims: {@claims}", externalClaims);
|
||||||
|
|
||||||
// Lookup our user and external provider info
|
// See if the user has logged in with this SSO provider before and has already been provisioned.
|
||||||
|
// This is signified by the user existing in the User table and the SSOUser table for the SSO provider they're using.
|
||||||
var (user, provider, providerUserId, claims, ssoConfigData) = await FindUserFromExternalProviderAsync(result);
|
var (user, provider, providerUserId, claims, ssoConfigData) = await FindUserFromExternalProviderAsync(result);
|
||||||
|
|
||||||
|
// The user has not authenticated with this SSO provider before.
|
||||||
|
// They could have an existing Bitwarden account in the User table though.
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
// This might be where you might initiate a custom workflow for user registration
|
// If we're manually linking to SSO, the user's external identifier will be passed as query string parameter.
|
||||||
// in this sample we don't show how that would be done, as our sample implementation
|
|
||||||
// simply auto-provisions new external user
|
|
||||||
var userIdentifier = result.Properties.Items.Keys.Contains("user_identifier") ?
|
var userIdentifier = result.Properties.Items.Keys.Contains("user_identifier") ?
|
||||||
result.Properties.Items["user_identifier"] : null;
|
result.Properties.Items["user_identifier"] : null;
|
||||||
user = await AutoProvisionUserAsync(provider, providerUserId, claims, userIdentifier, ssoConfigData);
|
user = await AutoProvisionUserAsync(provider, providerUserId, claims, userIdentifier, ssoConfigData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Either the user already authenticated with the SSO provider, or we've just provisioned them.
|
||||||
|
// Either way, we have associated the SSO login with a Bitwarden user.
|
||||||
|
// We will now sign the Bitwarden user in.
|
||||||
if (user != null)
|
if (user != null)
|
||||||
{
|
{
|
||||||
// This allows us to collect any additional claims or properties
|
// This allows us to collect any additional claims or properties
|
||||||
@@ -342,6 +343,10 @@ public class AccountController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to map the external identity to a Bitwarden user, through the SsoUser table, which holds the `externalId`.
|
||||||
|
/// The claims on the external identity are used to determine an `externalId`, and that is used to find the appropriate `SsoUser` and `User` records.
|
||||||
|
/// </summary>
|
||||||
private async Task<(User user, string provider, string providerUserId, IEnumerable<Claim> claims, SsoConfigurationData config)>
|
private async Task<(User user, string provider, string providerUserId, IEnumerable<Claim> claims, SsoConfigurationData config)>
|
||||||
FindUserFromExternalProviderAsync(AuthenticateResult result)
|
FindUserFromExternalProviderAsync(AuthenticateResult result)
|
||||||
{
|
{
|
||||||
@@ -399,6 +404,23 @@ public class AccountController : Controller
|
|||||||
return (user, provider, providerUserId, claims, ssoConfigData);
|
return (user, provider, providerUserId, claims, ssoConfigData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provision an SSO-linked Bitwarden user.
|
||||||
|
/// This handles three different scenarios:
|
||||||
|
/// 1. Creating an SsoUser link for an existing User and OrganizationUser
|
||||||
|
/// - User is a member of the organization, but hasn't authenticated with the org's SSO provider before.
|
||||||
|
/// 2. Creating a new User and a new OrganizationUser, then establishing an SsoUser link
|
||||||
|
/// - User is joining the organization through JIT provisioning, without a pending invitation
|
||||||
|
/// 3. Creating a new User for an existing OrganizationUser (created by invitation), then establishing an SsoUser link
|
||||||
|
/// - User is signing in with a pending invitation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="provider">The external identity provider.</param>
|
||||||
|
/// <param name="providerUserId">The external identity provider's user identifier.</param>
|
||||||
|
/// <param name="claims">The claims from the external IdP.</param>
|
||||||
|
/// <param name="userIdentifier">The user identifier used for manual SSO linking.</param>
|
||||||
|
/// <param name="config">The SSO configuration for the organization.</param>
|
||||||
|
/// <returns>The User to sign in.</returns>
|
||||||
|
/// <exception cref="Exception">An exception if the user cannot be provisioned as requested.</exception>
|
||||||
private async Task<User> AutoProvisionUserAsync(string provider, string providerUserId,
|
private async Task<User> AutoProvisionUserAsync(string provider, string providerUserId,
|
||||||
IEnumerable<Claim> claims, string userIdentifier, SsoConfigurationData config)
|
IEnumerable<Claim> claims, string userIdentifier, SsoConfigurationData config)
|
||||||
{
|
{
|
||||||
@@ -426,50 +448,15 @@ public class AccountController : Controller
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var split = userIdentifier.Split(",");
|
existingUser = await GetUserFromManualLinkingData(userIdentifier);
|
||||||
if (split.Length < 2)
|
|
||||||
{
|
|
||||||
throw new Exception(_i18nService.T("InvalidUserIdentifier"));
|
|
||||||
}
|
|
||||||
var userId = split[0];
|
|
||||||
var token = split[1];
|
|
||||||
|
|
||||||
var tokenOptions = new TokenOptions();
|
|
||||||
|
|
||||||
var claimedUser = await _userService.GetUserByIdAsync(userId);
|
|
||||||
if (claimedUser != null)
|
|
||||||
{
|
|
||||||
var tokenIsValid = await _userManager.VerifyUserTokenAsync(
|
|
||||||
claimedUser, tokenOptions.PasswordResetTokenProvider, TokenPurposes.LinkSso, token);
|
|
||||||
if (tokenIsValid)
|
|
||||||
{
|
|
||||||
existingUser = claimedUser;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new Exception(_i18nService.T("UserIdAndTokenMismatch"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OrganizationUser orgUser = null;
|
// Try to find the OrganizationUser if it exists.
|
||||||
var organization = await _organizationRepository.GetByIdAsync(orgId);
|
var (organization, orgUser) = await FindOrganizationUser(existingUser, email, orgId);
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
throw new Exception(_i18nService.T("CouldNotFindOrganization", orgId));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to find OrgUser via existing User Id (accepted/confirmed user)
|
//----------------------------------------------------
|
||||||
if (existingUser != null)
|
// Scenario 1: We've found the user in the User table
|
||||||
{
|
//----------------------------------------------------
|
||||||
var orgUsersByUserId = await _organizationUserRepository.GetManyByUserAsync(existingUser.Id);
|
|
||||||
orgUser = orgUsersByUserId.SingleOrDefault(u => u.OrganizationId == orgId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no Org User found by Existing User Id - search all organization users via email
|
|
||||||
orgUser ??= await _organizationUserRepository.GetByOrganizationEmailAsync(orgId, email);
|
|
||||||
|
|
||||||
// All Existing User flows handled below
|
|
||||||
if (existingUser != null)
|
if (existingUser != null)
|
||||||
{
|
{
|
||||||
if (existingUser.UsesKeyConnector &&
|
if (existingUser.UsesKeyConnector &&
|
||||||
@@ -478,20 +465,22 @@ public class AccountController : Controller
|
|||||||
throw new Exception(_i18nService.T("UserAlreadyExistsKeyConnector"));
|
throw new Exception(_i18nService.T("UserAlreadyExistsKeyConnector"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the user already exists in Bitwarden, we require that the user already be in the org,
|
||||||
|
// and that they are either Accepted or Confirmed.
|
||||||
if (orgUser == null)
|
if (orgUser == null)
|
||||||
{
|
{
|
||||||
// Org User is not created - no invite has been sent
|
// Org User is not created - no invite has been sent
|
||||||
throw new Exception(_i18nService.T("UserAlreadyExistsInviteProcess"));
|
throw new Exception(_i18nService.T("UserAlreadyExistsInviteProcess"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orgUser.Status == OrganizationUserStatusType.Invited)
|
EnsureOrgUserStatusAllowed(orgUser.Status, organization.DisplayName(),
|
||||||
{
|
allowedStatuses: [OrganizationUserStatusType.Accepted, OrganizationUserStatusType.Confirmed]);
|
||||||
// Org User is invited - they must manually accept the invite via email and authenticate with MP
|
|
||||||
// This allows us to enroll them in MP reset if required
|
|
||||||
throw new Exception(_i18nService.T("AcceptInviteBeforeUsingSSO", organization.DisplayName()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accepted or Confirmed - create SSO link and return;
|
|
||||||
|
// Since we're in the auto-provisioning logic, this means that the user exists, but they have not
|
||||||
|
// authenticated with the org's SSO provider before now (otherwise we wouldn't be auto-provisioning them).
|
||||||
|
// We've verified that the user is Accepted or Confnirmed, so we can create an SsoUser link and proceed
|
||||||
|
// with authentication.
|
||||||
await CreateSsoUserRecord(providerUserId, existingUser.Id, orgId, orgUser);
|
await CreateSsoUserRecord(providerUserId, existingUser.Id, orgId, orgUser);
|
||||||
return existingUser;
|
return existingUser;
|
||||||
}
|
}
|
||||||
@@ -534,7 +523,9 @@ public class AccountController : Controller
|
|||||||
emailVerified = organizationDomain?.VerifiedDate.HasValue ?? false;
|
emailVerified = organizationDomain?.VerifiedDate.HasValue ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create user record - all existing user flows are handled above
|
//--------------------------------------------------
|
||||||
|
// Scenarios 2 and 3: We need to register a new user
|
||||||
|
//--------------------------------------------------
|
||||||
var user = new User
|
var user = new User
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
@@ -560,7 +551,11 @@ public class AccountController : Controller
|
|||||||
await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
|
await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create Org User if null or else update existing Org User
|
//-----------------------------------------------------------------
|
||||||
|
// Scenario 2: We also need to create an OrganizationUser
|
||||||
|
// This means that an invitation was not sent for this user and we
|
||||||
|
// need to establish their invited status now.
|
||||||
|
//-----------------------------------------------------------------
|
||||||
if (orgUser == null)
|
if (orgUser == null)
|
||||||
{
|
{
|
||||||
orgUser = new OrganizationUser
|
orgUser = new OrganizationUser
|
||||||
@@ -572,18 +567,107 @@ public class AccountController : Controller
|
|||||||
};
|
};
|
||||||
await _organizationUserRepository.CreateAsync(orgUser);
|
await _organizationUserRepository.CreateAsync(orgUser);
|
||||||
}
|
}
|
||||||
|
//-----------------------------------------------------------------
|
||||||
|
// Scenario 3: There is already an existing OrganizationUser
|
||||||
|
// That was established through an invitation. We just need to
|
||||||
|
// update the UserId now that we have created a User record.
|
||||||
|
//-----------------------------------------------------------------
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
orgUser.UserId = user.Id;
|
orgUser.UserId = user.Id;
|
||||||
await _organizationUserRepository.ReplaceAsync(orgUser);
|
await _organizationUserRepository.ReplaceAsync(orgUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create sso user record
|
// Create the SsoUser record to link the user to the SSO provider.
|
||||||
await CreateSsoUserRecord(providerUserId, user.Id, orgId, orgUser);
|
await CreateSsoUserRecord(providerUserId, user.Id, orgId, orgUser);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<User> GetUserFromManualLinkingData(string userIdentifier)
|
||||||
|
{
|
||||||
|
User user = null;
|
||||||
|
var split = userIdentifier.Split(",");
|
||||||
|
if (split.Length < 2)
|
||||||
|
{
|
||||||
|
throw new Exception(_i18nService.T("InvalidUserIdentifier"));
|
||||||
|
}
|
||||||
|
var userId = split[0];
|
||||||
|
var token = split[1];
|
||||||
|
|
||||||
|
var tokenOptions = new TokenOptions();
|
||||||
|
|
||||||
|
var claimedUser = await _userService.GetUserByIdAsync(userId);
|
||||||
|
if (claimedUser != null)
|
||||||
|
{
|
||||||
|
var tokenIsValid = await _userManager.VerifyUserTokenAsync(
|
||||||
|
claimedUser, tokenOptions.PasswordResetTokenProvider, TokenPurposes.LinkSso, token);
|
||||||
|
if (tokenIsValid)
|
||||||
|
{
|
||||||
|
user = claimedUser;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception(_i18nService.T("UserIdAndTokenMismatch"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(Organization, OrganizationUser)> FindOrganizationUser(User existingUser, string email, Guid orgId)
|
||||||
|
{
|
||||||
|
OrganizationUser orgUser = null;
|
||||||
|
var organization = await _organizationRepository.GetByIdAsync(orgId);
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
throw new Exception(_i18nService.T("CouldNotFindOrganization", orgId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find OrgUser via existing User Id.
|
||||||
|
// This covers any OrganizationUser state after they have accepted an invite.
|
||||||
|
if (existingUser != null)
|
||||||
|
{
|
||||||
|
var orgUsersByUserId = await _organizationUserRepository.GetManyByUserAsync(existingUser.Id);
|
||||||
|
orgUser = orgUsersByUserId.SingleOrDefault(u => u.OrganizationId == orgId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no Org User found by Existing User Id - search all the organization's users via email.
|
||||||
|
// This covers users who are Invited but haven't accepted their invite yet.
|
||||||
|
orgUser ??= await _organizationUserRepository.GetByOrganizationEmailAsync(orgId, email);
|
||||||
|
|
||||||
|
return (organization, orgUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureOrgUserStatusAllowed(
|
||||||
|
OrganizationUserStatusType status,
|
||||||
|
string organizationDisplayName,
|
||||||
|
params OrganizationUserStatusType[] allowedStatuses)
|
||||||
|
{
|
||||||
|
// if this status is one of the allowed ones, just return
|
||||||
|
if (allowedStatuses.Contains(status))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise throw the appropriate exception
|
||||||
|
switch (status)
|
||||||
|
{
|
||||||
|
case OrganizationUserStatusType.Invited:
|
||||||
|
// Org User is invited – must accept via email first
|
||||||
|
throw new Exception(
|
||||||
|
_i18nService.T("AcceptInviteBeforeUsingSSO", organizationDisplayName));
|
||||||
|
case OrganizationUserStatusType.Revoked:
|
||||||
|
// Revoked users may not be (auto)‑provisioned
|
||||||
|
throw new Exception(
|
||||||
|
_i18nService.T("OrganizationUserAccessRevoked", organizationDisplayName));
|
||||||
|
default:
|
||||||
|
// anything else is “unknown”
|
||||||
|
throw new Exception(
|
||||||
|
_i18nService.T("OrganizationUserUnknownStatus", organizationDisplayName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private IActionResult InvalidJson(string errorMessageKey, Exception ex = null)
|
private IActionResult InvalidJson(string errorMessageKey, Exception ex = null)
|
||||||
{
|
{
|
||||||
Response.StatusCode = ex == null ? 400 : 500;
|
Response.StatusCode = ex == null ? 400 : 500;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.Diagnostics;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
using Bit.Sso.Models;
|
using Bit.Sso.Models;
|
||||||
using Duende.IdentityServer.Services;
|
using Duende.IdentityServer.Services;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
###############################################
|
###############################################
|
||||||
# Build stage #
|
# Build stage #
|
||||||
###############################################
|
###############################################
|
||||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build
|
||||||
|
|
||||||
# Docker buildx supplies the value for this arg
|
# Docker buildx supplies the value for this arg
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
@@ -9,11 +9,11 @@ ARG TARGETPLATFORM
|
|||||||
# Determine proper runtime value for .NET
|
# Determine proper runtime value for .NET
|
||||||
# We put the value in a file to be read by later layers.
|
# We put the value in a file to be read by later layers.
|
||||||
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||||
RID=linux-x64 ; \
|
RID=linux-musl-x64 ; \
|
||||||
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||||
RID=linux-arm64 ; \
|
RID=linux-musl-arm64 ; \
|
||||||
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
|
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
|
||||||
RID=linux-arm ; \
|
RID=linux-musl-arm ; \
|
||||||
fi \
|
fi \
|
||||||
&& echo "RID=$RID" > /tmp/rid.txt
|
&& echo "RID=$RID" > /tmp/rid.txt
|
||||||
|
|
||||||
@@ -37,21 +37,21 @@ RUN . /tmp/rid.txt && dotnet publish \
|
|||||||
###############################################
|
###############################################
|
||||||
# App stage #
|
# App stage #
|
||||||
###############################################
|
###############################################
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
LABEL com.bitwarden.product="bitwarden"
|
LABEL com.bitwarden.product="bitwarden"
|
||||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||||
ENV ASPNETCORE_URLS=http://+:5000
|
ENV ASPNETCORE_URLS=http://+:5000
|
||||||
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
|
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
|
||||||
|
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apk add --no-cache curl \
|
||||||
&& apt-get install -y --no-install-recommends \
|
krb5 \
|
||||||
gosu \
|
icu-libs \
|
||||||
curl \
|
shadow \
|
||||||
krb5-user \
|
&& apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Copy app from the build stage
|
# Copy app from the build stage
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Duende.IdentityServer.Models;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Duende.IdentityServer.Models;
|
||||||
|
|
||||||
namespace Bit.Sso.Models;
|
namespace Bit.Sso.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
namespace Bit.Sso.Models;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.Sso.Models;
|
||||||
|
|
||||||
public class RedirectViewModel
|
public class RedirectViewModel
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.Security.Cryptography.X509Certificates;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
namespace Bit.Sso.Models;
|
namespace Bit.Sso.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<!-- This is a transitive dependency to Sustainsys.Saml2.AspNetCore2 -->
|
<!-- This is a transitive dependency to Sustainsys.Saml2.AspNetCore2 -->
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
|
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
|
||||||
|
|
||||||
<PackageReference Include="Sustainsys.Saml2.AspNetCore2" Version="2.10.0" />
|
<PackageReference Include="Sustainsys.Saml2.AspNetCore2" Version="2.11.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.Security.Claims;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Bit.Sso.Utilities;
|
namespace Bit.Sso.Utilities;
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
using System.Security.Cryptography.X509Certificates;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
|
using Bit.Core.Auth.IdentityServer;
|
||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Sso.Models;
|
using Bit.Sso.Models;
|
||||||
using Bit.Sso.Utilities;
|
using Bit.Sso.Utilities;
|
||||||
|
using Duende.IdentityModel;
|
||||||
using Duende.IdentityServer;
|
using Duende.IdentityServer;
|
||||||
using Duende.IdentityServer.Infrastructure;
|
using Duende.IdentityServer.Infrastructure;
|
||||||
using IdentityModel;
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
@@ -413,7 +417,7 @@ public class DynamicAuthenticationSchemeProvider : AuthenticationSchemeProvider
|
|||||||
SPOptions = spOptions,
|
SPOptions = spOptions,
|
||||||
SignInScheme = AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme,
|
SignInScheme = AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme,
|
||||||
SignOutScheme = IdentityServerConstants.DefaultCookieAuthenticationScheme,
|
SignOutScheme = IdentityServerConstants.DefaultCookieAuthenticationScheme,
|
||||||
CookieManager = new IdentityServer.DistributedCacheCookieManager(),
|
CookieManager = new DistributedCacheCookieManager(),
|
||||||
};
|
};
|
||||||
options.IdentityProviders.Add(idp);
|
options.IdentityProviders.Add(idp);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.Collections.Concurrent;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Bit.Sso.Utilities;
|
namespace Bit.Sso.Utilities;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||||
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
||||||
|
|
||||||
namespace Bit.Sso.Utilities;
|
namespace Bit.Sso.Utilities;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.IO.Compression;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using System.IO.Compression;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using Sustainsys.Saml2;
|
using Sustainsys.Saml2;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Bit.Core.Business.Sso;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Bit.Core.Business.Sso;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.SharedWeb.Utilities;
|
using Bit.SharedWeb.Utilities;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/bin/sh
|
||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ then
|
|||||||
mkdir -p /etc/bitwarden/ca-certificates
|
mkdir -p /etc/bitwarden/ca-certificates
|
||||||
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
|
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
|
||||||
|
|
||||||
if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then
|
if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then
|
||||||
chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos
|
chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -46,13 +46,13 @@ else
|
|||||||
gosu_cmd=""
|
gosu_cmd=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then
|
if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then
|
||||||
cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf
|
cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf
|
||||||
$gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab
|
$gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $globalSettings__selfHosted == "true" ]]; then
|
if [ "$globalSettings__selfHosted" = "true" ]; then
|
||||||
if [[ -z $globalSettings__identityServer__certificateLocation ]]; then
|
if [ -z "$globalSettings__identityServer__certificateLocation" ]; then
|
||||||
export globalSettings__identityServer__certificateLocation=/etc/bitwarden/identity/identity.pfx
|
export globalSettings__identityServer__certificateLocation=/etc/bitwarden/identity/identity.pfx
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
182
bitwarden_license/src/Sso/package-lock.json
generated
182
bitwarden_license/src/Sso/package-lock.json
generated
@@ -17,9 +17,9 @@
|
|||||||
"css-loader": "7.1.2",
|
"css-loader": "7.1.2",
|
||||||
"expose-loader": "5.0.1",
|
"expose-loader": "5.0.1",
|
||||||
"mini-css-extract-plugin": "2.9.2",
|
"mini-css-extract-plugin": "2.9.2",
|
||||||
"sass": "1.88.0",
|
"sass": "1.91.0",
|
||||||
"sass-loader": "16.0.5",
|
"sass-loader": "16.0.5",
|
||||||
"webpack": "5.99.8",
|
"webpack": "5.101.3",
|
||||||
"webpack-cli": "5.1.4"
|
"webpack-cli": "5.1.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -34,18 +34,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.8",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/set-array": "^1.2.1",
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
|
||||||
"@jridgewell/trace-mapping": "^0.3.24"
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/resolve-uri": {
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
@@ -58,20 +54,10 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/set-array": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/source-map": {
|
"node_modules/@jridgewell/source-map": {
|
||||||
"version": "0.3.6",
|
"version": "0.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
|
||||||
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
|
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -80,16 +66,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.25",
|
"version": "0.3.30",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
|
||||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
"integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -441,9 +427,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -455,13 +441,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.15.21",
|
"version": "24.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz",
|
||||||
"integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
|
"integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~7.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@webassemblyjs/ast": {
|
"node_modules/@webassemblyjs/ast": {
|
||||||
@@ -687,9 +673,9 @@
|
|||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.1",
|
"version": "8.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -699,6 +685,19 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/acorn-import-phases": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"acorn": "^8.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "8.17.1",
|
"version": "8.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||||
@@ -781,9 +780,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.24.5",
|
"version": "4.25.4",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz",
|
||||||
"integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
|
"integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -801,8 +800,8 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001716",
|
"caniuse-lite": "^1.0.30001737",
|
||||||
"electron-to-chromium": "^1.5.149",
|
"electron-to-chromium": "^1.5.211",
|
||||||
"node-releases": "^2.0.19",
|
"node-releases": "^2.0.19",
|
||||||
"update-browserslist-db": "^1.1.3"
|
"update-browserslist-db": "^1.1.3"
|
||||||
},
|
},
|
||||||
@@ -821,9 +820,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001718",
|
"version": "1.0.30001741",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz",
|
||||||
"integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
|
"integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -975,16 +974,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.155",
|
"version": "1.5.215",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.215.tgz",
|
||||||
"integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==",
|
"integrity": "sha512-TIvGp57UpeNetj/wV/xpFNpWGb0b/ROw372lHPx5Aafx02gjTBtWnEEcaSX3W2dLM3OSdGGyHX/cHl01JQsLaQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.18.1",
|
"version": "5.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||||
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
|
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1107,9 +1106,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.0.6",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||||
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
|
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1241,9 +1240,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/immutable": {
|
"node_modules/immutable": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz",
|
||||||
"integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==",
|
"integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -1528,9 +1527,9 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.19",
|
"version": "2.0.20",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.20.tgz",
|
||||||
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
"integrity": "sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -1635,9 +1634,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.3",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1655,7 +1654,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.8",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -1860,9 +1859,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.88.0",
|
"version": "1.91.0",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.91.0.tgz",
|
||||||
"integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==",
|
"integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2061,24 +2060,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
"version": "2.2.2",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
|
||||||
"integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
|
"integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/terser": {
|
"node_modules/terser": {
|
||||||
"version": "5.39.2",
|
"version": "5.44.0",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
|
||||||
"integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==",
|
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
"acorn": "^8.14.0",
|
"acorn": "^8.15.0",
|
||||||
"commander": "^2.20.0",
|
"commander": "^2.20.0",
|
||||||
"source-map-support": "~0.5.20"
|
"source-map-support": "~0.5.20"
|
||||||
},
|
},
|
||||||
@@ -2139,9 +2142,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.21.0",
|
"version": "7.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -2198,22 +2201,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack": {
|
"node_modules/webpack": {
|
||||||
"version": "5.99.8",
|
"version": "5.101.3",
|
||||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz",
|
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz",
|
||||||
"integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
|
"integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/eslint-scope": "^3.7.7",
|
"@types/eslint-scope": "^3.7.7",
|
||||||
"@types/estree": "^1.0.6",
|
"@types/estree": "^1.0.8",
|
||||||
"@types/json-schema": "^7.0.15",
|
"@types/json-schema": "^7.0.15",
|
||||||
"@webassemblyjs/ast": "^1.14.1",
|
"@webassemblyjs/ast": "^1.14.1",
|
||||||
"@webassemblyjs/wasm-edit": "^1.14.1",
|
"@webassemblyjs/wasm-edit": "^1.14.1",
|
||||||
"@webassemblyjs/wasm-parser": "^1.14.1",
|
"@webassemblyjs/wasm-parser": "^1.14.1",
|
||||||
"acorn": "^8.14.0",
|
"acorn": "^8.15.0",
|
||||||
|
"acorn-import-phases": "^1.0.3",
|
||||||
"browserslist": "^4.24.0",
|
"browserslist": "^4.24.0",
|
||||||
"chrome-trace-event": "^1.0.2",
|
"chrome-trace-event": "^1.0.2",
|
||||||
"enhanced-resolve": "^5.17.1",
|
"enhanced-resolve": "^5.17.3",
|
||||||
"es-module-lexer": "^1.2.1",
|
"es-module-lexer": "^1.2.1",
|
||||||
"eslint-scope": "5.1.1",
|
"eslint-scope": "5.1.1",
|
||||||
"events": "^3.2.0",
|
"events": "^3.2.0",
|
||||||
@@ -2227,7 +2231,7 @@
|
|||||||
"tapable": "^2.1.1",
|
"tapable": "^2.1.1",
|
||||||
"terser-webpack-plugin": "^5.3.11",
|
"terser-webpack-plugin": "^5.3.11",
|
||||||
"watchpack": "^2.4.1",
|
"watchpack": "^2.4.1",
|
||||||
"webpack-sources": "^3.2.3"
|
"webpack-sources": "^3.3.3"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"webpack": "bin/webpack.js"
|
"webpack": "bin/webpack.js"
|
||||||
@@ -2317,9 +2321,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack-sources": {
|
"node_modules/webpack-sources": {
|
||||||
"version": "3.2.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
|
||||||
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
|
"integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -16,9 +16,9 @@
|
|||||||
"css-loader": "7.1.2",
|
"css-loader": "7.1.2",
|
||||||
"expose-loader": "5.0.1",
|
"expose-loader": "5.0.1",
|
||||||
"mini-css-extract-plugin": "2.9.2",
|
"mini-css-extract-plugin": "2.9.2",
|
||||||
"sass": "1.88.0",
|
"sass": "1.91.0",
|
||||||
"sass-loader": "16.0.5",
|
"sass-loader": "16.0.5",
|
||||||
"webpack": "5.99.8",
|
"webpack": "5.101.3",
|
||||||
"webpack-cli": "5.1.4"
|
"webpack-cli": "5.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Bit.Commercial.Core.AdminConsole.Providers;
|
using Bit.Commercial.Core.AdminConsole.Providers;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
@@ -263,7 +262,8 @@ public class RemoveOrganizationFromProviderCommandTests
|
|||||||
org =>
|
org =>
|
||||||
org.BillingEmail == "a@example.com" &&
|
org.BillingEmail == "a@example.com" &&
|
||||||
org.GatewaySubscriptionId == "subscription_id" &&
|
org.GatewaySubscriptionId == "subscription_id" &&
|
||||||
org.Status == OrganizationStatusType.Created));
|
org.Status == OrganizationStatusType.Created &&
|
||||||
|
org.Enabled == true)); // Verify organization is enabled when new subscription is created
|
||||||
|
|
||||||
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
|
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
|
||||||
.DeleteAsync(providerOrganization);
|
.DeleteAsync(providerOrganization);
|
||||||
@@ -331,9 +331,6 @@ public class RemoveOrganizationFromProviderCommandTests
|
|||||||
Id = "subscription_id"
|
Id = "subscription_id"
|
||||||
});
|
});
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
|
||||||
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
|
|
||||||
|
|
||||||
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
||||||
|
|
||||||
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
||||||
@@ -354,7 +351,8 @@ public class RemoveOrganizationFromProviderCommandTests
|
|||||||
org =>
|
org =>
|
||||||
org.BillingEmail == "a@example.com" &&
|
org.BillingEmail == "a@example.com" &&
|
||||||
org.GatewaySubscriptionId == "subscription_id" &&
|
org.GatewaySubscriptionId == "subscription_id" &&
|
||||||
org.Status == OrganizationStatusType.Created));
|
org.Status == OrganizationStatusType.Created &&
|
||||||
|
org.Enabled == true)); // Verify organization is enabled when new subscription is created
|
||||||
|
|
||||||
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
|
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
|
||||||
.DeleteAsync(providerOrganization);
|
.DeleteAsync(providerOrganization);
|
||||||
@@ -390,4 +388,62 @@ public class RemoveOrganizationFromProviderCommandTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RemoveOrganizationFromProvider_DisabledOrganization_ConsolidatedBilling_EnablesOrganization(
|
||||||
|
Provider provider,
|
||||||
|
ProviderOrganization providerOrganization,
|
||||||
|
Organization organization,
|
||||||
|
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange: Set up a disabled organization that meets the criteria for consolidated billing
|
||||||
|
provider.Status = ProviderStatusType.Billable;
|
||||||
|
providerOrganization.ProviderId = provider.Id;
|
||||||
|
organization.Status = OrganizationStatusType.Managed;
|
||||||
|
organization.PlanType = PlanType.TeamsMonthly;
|
||||||
|
organization.Enabled = false; // Start with a disabled organization
|
||||||
|
|
||||||
|
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(
|
||||||
|
providerOrganization.OrganizationId,
|
||||||
|
[],
|
||||||
|
includeProvider: false)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
|
||||||
|
organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns([
|
||||||
|
"owner@example.com"
|
||||||
|
]);
|
||||||
|
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
|
||||||
|
stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Any<CustomerUpdateOptions>())
|
||||||
|
.Returns(new Customer
|
||||||
|
{
|
||||||
|
Id = "customer_id",
|
||||||
|
Address = new Address
|
||||||
|
{
|
||||||
|
Country = "US"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
|
||||||
|
{
|
||||||
|
Id = "new_subscription_id"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
||||||
|
|
||||||
|
// Assert: Verify the disabled organization is now enabled
|
||||||
|
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(
|
||||||
|
org =>
|
||||||
|
org.Enabled == true && // The previously disabled organization should now be enabled
|
||||||
|
org.Status == OrganizationStatusType.Created &&
|
||||||
|
org.GatewaySubscriptionId == "new_subscription_id"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
using Bit.Commercial.Core.AdminConsole.Services;
|
using Bit.Commercial.Core.AdminConsole.Services;
|
||||||
using Bit.Commercial.Core.Test.AdminConsole.AutoFixture;
|
using Bit.Commercial.Core.Test.AdminConsole.AutoFixture;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Business.Provider;
|
using Bit.Core.AdminConsole.Models.Business.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Payment.Models;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Providers.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@@ -41,7 +41,7 @@ public class ProviderServiceTests
|
|||||||
public async Task CompleteSetupAsync_UserIdIsInvalid_Throws(SutProvider<ProviderService> sutProvider)
|
public async Task CompleteSetupAsync_UserIdIsInvalid_Throws(SutProvider<ProviderService> sutProvider)
|
||||||
{
|
{
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.CompleteSetupAsync(default, default, default, default, null));
|
() => sutProvider.Sut.CompleteSetupAsync(default, default, default, default, null, null));
|
||||||
Assert.Contains("Invalid owner.", exception.Message);
|
Assert.Contains("Invalid owner.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,85 +53,12 @@ public class ProviderServiceTests
|
|||||||
userService.GetUserByIdAsync(user.Id).Returns(user);
|
userService.GetUserByIdAsync(user.Id).Returns(user);
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default, null));
|
() => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default, null, null));
|
||||||
Assert.Contains("Invalid token.", exception.Message);
|
Assert.Contains("Invalid token.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task CompleteSetupAsync_InvalidTaxInfo_ThrowsBadRequestException(
|
public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TokenizedPaymentMethod tokenizedPaymentMethod, BillingAddress billingAddress,
|
||||||
User user,
|
|
||||||
Provider provider,
|
|
||||||
string key,
|
|
||||||
TaxInfo taxInfo,
|
|
||||||
TokenizedPaymentSource tokenizedPaymentSource,
|
|
||||||
[ProviderUser] ProviderUser providerUser,
|
|
||||||
SutProvider<ProviderService> sutProvider)
|
|
||||||
{
|
|
||||||
providerUser.ProviderId = provider.Id;
|
|
||||||
providerUser.UserId = user.Id;
|
|
||||||
var userService = sutProvider.GetDependency<IUserService>();
|
|
||||||
userService.GetUserByIdAsync(user.Id).Returns(user);
|
|
||||||
|
|
||||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
|
||||||
providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);
|
|
||||||
|
|
||||||
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
|
|
||||||
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
|
||||||
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
|
|
||||||
.Returns(protector);
|
|
||||||
|
|
||||||
sutProvider.Create();
|
|
||||||
|
|
||||||
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
|
||||||
|
|
||||||
taxInfo.BillingAddressCountry = null;
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
|
||||||
sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource));
|
|
||||||
|
|
||||||
Assert.Equal("Both address and postal code are required to set up your provider.", exception.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task CompleteSetupAsync_InvalidTokenizedPaymentSource_ThrowsBadRequestException(
|
|
||||||
User user,
|
|
||||||
Provider provider,
|
|
||||||
string key,
|
|
||||||
TaxInfo taxInfo,
|
|
||||||
TokenizedPaymentSource tokenizedPaymentSource,
|
|
||||||
[ProviderUser] ProviderUser providerUser,
|
|
||||||
SutProvider<ProviderService> sutProvider)
|
|
||||||
{
|
|
||||||
providerUser.ProviderId = provider.Id;
|
|
||||||
providerUser.UserId = user.Id;
|
|
||||||
var userService = sutProvider.GetDependency<IUserService>();
|
|
||||||
userService.GetUserByIdAsync(user.Id).Returns(user);
|
|
||||||
|
|
||||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
|
||||||
providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);
|
|
||||||
|
|
||||||
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
|
|
||||||
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
|
||||||
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
|
|
||||||
.Returns(protector);
|
|
||||||
|
|
||||||
sutProvider.Create();
|
|
||||||
|
|
||||||
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
|
||||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
|
||||||
|
|
||||||
tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay };
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
|
||||||
sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource));
|
|
||||||
|
|
||||||
Assert.Equal("A payment method is required to set up your provider.", exception.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource,
|
|
||||||
[ProviderUser] ProviderUser providerUser,
|
[ProviderUser] ProviderUser providerUser,
|
||||||
SutProvider<ProviderService> sutProvider)
|
SutProvider<ProviderService> sutProvider)
|
||||||
{
|
{
|
||||||
@@ -151,7 +78,7 @@ public class ProviderServiceTests
|
|||||||
var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();
|
var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();
|
||||||
|
|
||||||
var customer = new Customer { Id = "customer_id" };
|
var customer = new Customer { Id = "customer_id" };
|
||||||
providerBillingService.SetupCustomer(provider, taxInfo, tokenizedPaymentSource).Returns(customer);
|
providerBillingService.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress).Returns(customer);
|
||||||
|
|
||||||
var subscription = new Subscription { Id = "subscription_id" };
|
var subscription = new Subscription { Id = "subscription_id" };
|
||||||
providerBillingService.SetupSubscription(provider).Returns(subscription);
|
providerBillingService.SetupSubscription(provider).Returns(subscription);
|
||||||
@@ -160,7 +87,7 @@ public class ProviderServiceTests
|
|||||||
|
|
||||||
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||||
|
|
||||||
await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource);
|
await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, tokenizedPaymentMethod, billingAddress);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IProviderRepository>().Received().UpsertAsync(Arg.Is<Provider>(
|
await sutProvider.GetDependency<IProviderRepository>().Received().UpsertAsync(Arg.Is<Provider>(
|
||||||
p =>
|
p =>
|
||||||
@@ -188,6 +115,262 @@ public class ProviderServiceTests
|
|||||||
await sutProvider.Sut.UpdateAsync(provider);
|
await sutProvider.Sut.UpdateAsync(provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task UpdateAsync_ExistingProviderIsNull_DoesNotCallUpdateClientOrganizationsEnabledStatus(
|
||||||
|
Provider provider, SutProvider<ProviderService> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
|
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns((Provider)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateAsync(provider);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await providerRepository.Received(1).ReplaceAsync(provider);
|
||||||
|
await providerOrganizationRepository.DidNotReceive().GetManyDetailsByProviderAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task UpdateAsync_EnabledStatusNotChanged_DoesNotCallUpdateClientOrganizationsEnabledStatus(
|
||||||
|
Provider provider, Provider existingProvider, SutProvider<ProviderService> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
|
|
||||||
|
existingProvider.Id = provider.Id;
|
||||||
|
existingProvider.Enabled = provider.Enabled; // Same enabled status
|
||||||
|
provider.Type = ProviderType.Msp; // Set to a type that would trigger update if status changed
|
||||||
|
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(existingProvider);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateAsync(provider);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await providerRepository.Received(1).ReplaceAsync(provider);
|
||||||
|
await providerOrganizationRepository.DidNotReceive().GetManyDetailsByProviderAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task UpdateAsync_EnabledStatusChangedButProviderTypeIsReseller_DoesNotCallUpdateClientOrganizationsEnabledStatus(
|
||||||
|
Provider provider, Provider existingProvider, SutProvider<ProviderService> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
|
|
||||||
|
existingProvider.Id = provider.Id;
|
||||||
|
existingProvider.Enabled = !provider.Enabled; // Different enabled status
|
||||||
|
provider.Type = ProviderType.Reseller; // Type that should not trigger update
|
||||||
|
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(existingProvider);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateAsync(provider);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await providerRepository.Received(1).ReplaceAsync(provider);
|
||||||
|
await providerOrganizationRepository.DidNotReceive().GetManyDetailsByProviderAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task UpdateAsync_EnabledStatusChangedAndProviderTypeIsMsp_CallsUpdateClientOrganizationsEnabledStatus(
|
||||||
|
Provider provider, Provider existingProvider, SutProvider<ProviderService> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
|
||||||
|
|
||||||
|
existingProvider.Id = provider.Id;
|
||||||
|
existingProvider.Enabled = !provider.Enabled; // Different enabled status
|
||||||
|
provider.Type = ProviderType.Msp; // Type that should trigger update
|
||||||
|
|
||||||
|
// Create test provider organization details
|
||||||
|
var providerOrganizationDetails = new List<ProviderOrganizationOrganizationDetails>
|
||||||
|
{
|
||||||
|
new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() },
|
||||||
|
new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create test organizations with different enabled status than what we're setting
|
||||||
|
var organizations = providerOrganizationDetails.Select(po =>
|
||||||
|
{
|
||||||
|
var org = new Organization { Id = po.OrganizationId, Enabled = !provider.Enabled };
|
||||||
|
return org;
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(existingProvider);
|
||||||
|
providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id).Returns(providerOrganizationDetails);
|
||||||
|
|
||||||
|
foreach (var org in organizations)
|
||||||
|
{
|
||||||
|
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateAsync(provider);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await providerRepository.Received(1).ReplaceAsync(provider);
|
||||||
|
await providerOrganizationRepository.Received(1).GetManyDetailsByProviderAsync(provider.Id);
|
||||||
|
|
||||||
|
foreach (var org in organizations)
|
||||||
|
{
|
||||||
|
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(o =>
|
||||||
|
o.Id == org.Id && o.Enabled == provider.Enabled));
|
||||||
|
await applicationCacheService.Received(1).UpsertOrganizationAbilityAsync(Arg.Is<Organization>(o =>
|
||||||
|
o.Id == org.Id && o.Enabled == provider.Enabled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task UpdateAsync_EnabledStatusChangedAndProviderTypeIsBusinessUnit_CallsUpdateClientOrganizationsEnabledStatus(
|
||||||
|
Provider provider, Provider existingProvider, SutProvider<ProviderService> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
|
||||||
|
|
||||||
|
existingProvider.Id = provider.Id;
|
||||||
|
existingProvider.Enabled = !provider.Enabled; // Different enabled status
|
||||||
|
provider.Type = ProviderType.BusinessUnit; // Type that should trigger update
|
||||||
|
|
||||||
|
// Create test provider organization details
|
||||||
|
var providerOrganizationDetails = new List<ProviderOrganizationOrganizationDetails>
|
||||||
|
{
|
||||||
|
new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() },
|
||||||
|
new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create test organizations with different enabled status than what we're setting
|
||||||
|
var organizations = providerOrganizationDetails.Select(po =>
|
||||||
|
{
|
||||||
|
var org = new Organization { Id = po.OrganizationId, Enabled = !provider.Enabled };
|
||||||
|
return org;
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(existingProvider);
|
||||||
|
providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id).Returns(providerOrganizationDetails);
|
||||||
|
|
||||||
|
foreach (var org in organizations)
|
||||||
|
{
|
||||||
|
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateAsync(provider);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await providerRepository.Received(1).ReplaceAsync(provider);
|
||||||
|
await providerOrganizationRepository.Received(1).GetManyDetailsByProviderAsync(provider.Id);
|
||||||
|
|
||||||
|
foreach (var org in organizations)
|
||||||
|
{
|
||||||
|
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(o =>
|
||||||
|
o.Id == org.Id && o.Enabled == provider.Enabled));
|
||||||
|
await applicationCacheService.Received(1).UpsertOrganizationAbilityAsync(Arg.Is<Organization>(o =>
|
||||||
|
o.Id == org.Id && o.Enabled == provider.Enabled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task UpdateAsync_OrganizationEnabledStatusAlreadyMatches_DoesNotUpdateOrganization(
|
||||||
|
Provider provider, Provider existingProvider, SutProvider<ProviderService> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
|
||||||
|
|
||||||
|
existingProvider.Id = provider.Id;
|
||||||
|
existingProvider.Enabled = !provider.Enabled; // Different enabled status
|
||||||
|
provider.Type = ProviderType.Msp; // Type that should trigger update
|
||||||
|
|
||||||
|
// Create test provider organization details
|
||||||
|
var providerOrganizationDetails = new List<ProviderOrganizationOrganizationDetails>
|
||||||
|
{
|
||||||
|
new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() },
|
||||||
|
new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create test organizations with SAME enabled status as what we're setting
|
||||||
|
var organizations = providerOrganizationDetails.Select(po =>
|
||||||
|
{
|
||||||
|
var org = new Organization { Id = po.OrganizationId, Enabled = provider.Enabled };
|
||||||
|
return org;
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(existingProvider);
|
||||||
|
providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id).Returns(providerOrganizationDetails);
|
||||||
|
|
||||||
|
foreach (var org in organizations)
|
||||||
|
{
|
||||||
|
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateAsync(provider);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await providerRepository.Received(1).ReplaceAsync(provider);
|
||||||
|
await providerOrganizationRepository.Received(1).GetManyDetailsByProviderAsync(provider.Id);
|
||||||
|
|
||||||
|
// Organizations should not be updated since their enabled status already matches
|
||||||
|
foreach (var org in organizations)
|
||||||
|
{
|
||||||
|
await organizationRepository.DidNotReceive().ReplaceAsync(Arg.Any<Organization>());
|
||||||
|
await applicationCacheService.DidNotReceive().UpsertOrganizationAbilityAsync(Arg.Any<Organization>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task UpdateAsync_OrganizationIsNull_SkipsNullOrganization(
|
||||||
|
Provider provider, Provider existingProvider, SutProvider<ProviderService> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
|
||||||
|
|
||||||
|
existingProvider.Id = provider.Id;
|
||||||
|
existingProvider.Enabled = !provider.Enabled; // Different enabled status
|
||||||
|
provider.Type = ProviderType.Msp; // Type that should trigger update
|
||||||
|
|
||||||
|
// Create test provider organization details
|
||||||
|
var providerOrganizationDetails = new List<ProviderOrganizationOrganizationDetails>
|
||||||
|
{
|
||||||
|
new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() },
|
||||||
|
new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() }
|
||||||
|
};
|
||||||
|
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(existingProvider);
|
||||||
|
providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id).Returns(providerOrganizationDetails);
|
||||||
|
|
||||||
|
// Return null for all organizations
|
||||||
|
organizationRepository.GetByIdAsync(Arg.Any<Guid>()).Returns((Organization)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateAsync(provider);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await providerRepository.Received(1).ReplaceAsync(provider);
|
||||||
|
await providerOrganizationRepository.Received(1).GetManyDetailsByProviderAsync(provider.Id);
|
||||||
|
|
||||||
|
// No organizations should be updated since they're all null
|
||||||
|
await organizationRepository.DidNotReceive().ReplaceAsync(Arg.Any<Organization>());
|
||||||
|
await applicationCacheService.DidNotReceive().UpsertOrganizationAbilityAsync(Arg.Any<Organization>());
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task InviteUserAsync_ProviderIdIsInvalid_Throws(ProviderUserInvite<string> invite, SutProvider<ProviderService> sutProvider)
|
public async Task InviteUserAsync_ProviderIdIsInvalid_Throws(ProviderUserInvite<string> invite, SutProvider<ProviderService> sutProvider)
|
||||||
{
|
{
|
||||||
@@ -937,7 +1120,7 @@ public class ProviderServiceTests
|
|||||||
private static SubscriptionUpdateOptions SubscriptionUpdateRequest(string expectedPlanId, Subscription subscriptionItem) =>
|
private static SubscriptionUpdateOptions SubscriptionUpdateRequest(string expectedPlanId, Subscription subscriptionItem) =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Items = new List<Stripe.SubscriptionItemOptions>
|
Items = new List<SubscriptionItemOptions>
|
||||||
{
|
{
|
||||||
new() { Id = subscriptionItem.Id, Price = expectedPlanId },
|
new() { Id = subscriptionItem.Id, Price = expectedPlanId },
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,556 @@
|
|||||||
|
using Bit.Commercial.Core.Billing.Providers.Queries;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using NSubstitute.ReturnsExtensions;
|
||||||
|
using Stripe;
|
||||||
|
using Stripe.Tax;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.Test.Billing.Providers.Queries;
|
||||||
|
|
||||||
|
using static StripeConstants;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class GetProviderWarningsQueryTests
|
||||||
|
{
|
||||||
|
private static readonly string[] _requiredExpansions = ["customer.tax_ids"];
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_NoSubscription_NoWarnings(
|
||||||
|
Provider provider,
|
||||||
|
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.ReturnsNull();
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(provider);
|
||||||
|
|
||||||
|
Assert.True(response is
|
||||||
|
{
|
||||||
|
Suspension: null,
|
||||||
|
TaxId: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_ProviderEnabled_NoSuspensionWarning(
|
||||||
|
Provider provider,
|
||||||
|
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Enabled = true;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = SubscriptionStatus.Unpaid,
|
||||||
|
Customer = new Customer
|
||||||
|
{
|
||||||
|
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||||
|
Address = new Address { Country = "CA" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||||
|
.Returns(new StripeList<Registration> { Data = [] });
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(provider);
|
||||||
|
|
||||||
|
Assert.Null(response!.Suspension);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_Has_SuspensionWarning_AddPaymentMethod(
|
||||||
|
Provider provider,
|
||||||
|
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Enabled = false;
|
||||||
|
var cancelAt = DateTime.UtcNow.AddDays(7);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = SubscriptionStatus.Unpaid,
|
||||||
|
CancelAt = cancelAt,
|
||||||
|
Customer = new Customer
|
||||||
|
{
|
||||||
|
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||||
|
Address = new Address { Country = "CA" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||||
|
.Returns(new StripeList<Registration> { Data = [] });
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(provider);
|
||||||
|
|
||||||
|
Assert.True(response is
|
||||||
|
{
|
||||||
|
Suspension.Resolution: "add_payment_method"
|
||||||
|
});
|
||||||
|
Assert.Equal(cancelAt, response.Suspension.SubscriptionCancelsAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_Has_SuspensionWarning_ContactAdministrator(
|
||||||
|
Provider provider,
|
||||||
|
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Enabled = false;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = SubscriptionStatus.Unpaid,
|
||||||
|
Customer = new Customer
|
||||||
|
{
|
||||||
|
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||||
|
Address = new Address { Country = "CA" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(false);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||||
|
.Returns(new StripeList<Registration> { Data = [] });
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(provider);
|
||||||
|
|
||||||
|
Assert.True(response is
|
||||||
|
{
|
||||||
|
Suspension.Resolution: "contact_administrator"
|
||||||
|
});
|
||||||
|
Assert.Null(response.Suspension.SubscriptionCancelsAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_Has_SuspensionWarning_ContactSupport(
|
||||||
|
Provider provider,
|
||||||
|
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Enabled = false;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = SubscriptionStatus.Canceled,
|
||||||
|
Customer = new Customer
|
||||||
|
{
|
||||||
|
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||||
|
Address = new Address { Country = "CA" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||||
|
.Returns(new StripeList<Registration> { Data = [] });
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(provider);
|
||||||
|
|
||||||
|
Assert.True(response is
|
||||||
|
{
|
||||||
|
Suspension.Resolution: "contact_support"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_NotProviderAdmin_NoTaxIdWarning(
|
||||||
|
Provider provider,
|
||||||
|
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Enabled = true;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = SubscriptionStatus.Active,
|
||||||
|
Customer = new Customer
|
||||||
|
{
|
||||||
|
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||||
|
Address = new Address { Country = "CA" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(false);
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(provider);
|
||||||
|
|
||||||
|
Assert.Null(response!.TaxId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_NoTaxRegistrationForCountry_NoTaxIdWarning(
|
||||||
|
Provider provider,
|
||||||
|
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Enabled = true;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = SubscriptionStatus.Active,
|
||||||
|
Customer = new Customer
|
||||||
|
{
|
||||||
|
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||||
|
Address = new Address { Country = "CA" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||||
|
.Returns(new StripeList<Registration>
|
||||||
|
{
|
||||||
|
Data = [new Registration { Country = "GB" }]
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(provider);
|
||||||
|
|
||||||
|
Assert.Null(response!.TaxId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_Has_TaxIdMissingWarning(
|
||||||
|
Provider provider,
|
||||||
|
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Enabled = true;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = SubscriptionStatus.Active,
|
||||||
|
Customer = new Customer
|
||||||
|
{
|
||||||
|
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||||
|
Address = new Address { Country = "CA" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||||
|
.Returns(new StripeList<Registration>
|
||||||
|
{
|
||||||
|
Data = [new Registration { Country = "CA" }]
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(provider);
|
||||||
|
|
||||||
|
Assert.True(response is
|
||||||
|
{
|
||||||
|
TaxId.Type: "tax_id_missing"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_TaxIdVerificationIsNull_NoTaxIdWarning(
|
||||||
|
Provider provider,
|
||||||
|
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Enabled = true;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = SubscriptionStatus.Active,
|
||||||
|
Customer = new Customer
|
||||||
|
{
|
||||||
|
TaxIds = new StripeList<TaxId>
|
||||||
|
{
|
||||||
|
Data = [new TaxId { Verification = null }]
|
||||||
|
},
|
||||||
|
Address = new Address { Country = "CA" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||||
|
.Returns(new StripeList<Registration>
|
||||||
|
{
|
||||||
|
Data = [new Registration { Country = "CA" }]
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(provider);
|
||||||
|
|
||||||
|
Assert.Null(response!.TaxId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_Has_TaxIdPendingVerificationWarning(
|
||||||
|
Provider provider,
|
||||||
|
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Enabled = true;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = SubscriptionStatus.Active,
|
||||||
|
Customer = new Customer
|
||||||
|
{
|
||||||
|
TaxIds = new StripeList<TaxId>
|
||||||
|
{
|
||||||
|
Data = [new TaxId
|
||||||
|
{
|
||||||
|
Verification = new TaxIdVerification
|
||||||
|
{
|
||||||
|
Status = TaxIdVerificationStatus.Pending
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
Address = new Address { Country = "CA" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||||
|
.Returns(new StripeList<Registration>
|
||||||
|
{
|
||||||
|
Data = [new Registration { Country = "CA" }]
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(provider);
|
||||||
|
|
||||||
|
Assert.True(response is
|
||||||
|
{
|
||||||
|
TaxId.Type: "tax_id_pending_verification"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_Has_TaxIdFailedVerificationWarning(
|
||||||
|
Provider provider,
|
||||||
|
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Enabled = true;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = SubscriptionStatus.Active,
|
||||||
|
Customer = new Customer
|
||||||
|
{
|
||||||
|
TaxIds = new StripeList<TaxId>
|
||||||
|
{
|
||||||
|
Data = [new TaxId
|
||||||
|
{
|
||||||
|
Verification = new TaxIdVerification
|
||||||
|
{
|
||||||
|
Status = TaxIdVerificationStatus.Unverified
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
Address = new Address { Country = "CA" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||||
|
.Returns(new StripeList<Registration>
|
||||||
|
{
|
||||||
|
Data = [new Registration { Country = "CA" }]
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(provider);
|
||||||
|
|
||||||
|
Assert.True(response is
|
||||||
|
{
|
||||||
|
TaxId.Type: "tax_id_failed_verification"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_TaxIdVerified_NoTaxIdWarning(
|
||||||
|
Provider provider,
|
||||||
|
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Enabled = true;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = SubscriptionStatus.Active,
|
||||||
|
Customer = new Customer
|
||||||
|
{
|
||||||
|
TaxIds = new StripeList<TaxId>
|
||||||
|
{
|
||||||
|
Data = [new TaxId
|
||||||
|
{
|
||||||
|
Verification = new TaxIdVerification
|
||||||
|
{
|
||||||
|
Status = TaxIdVerificationStatus.Verified
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
Address = new Address { Country = "CA" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||||
|
.Returns(new StripeList<Registration>
|
||||||
|
{
|
||||||
|
Data = [new Registration { Country = "CA" }]
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(provider);
|
||||||
|
|
||||||
|
Assert.Null(response!.TaxId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_MultipleRegistrations_MatchesCorrectCountry(
|
||||||
|
Provider provider,
|
||||||
|
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Enabled = true;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = SubscriptionStatus.Active,
|
||||||
|
Customer = new Customer
|
||||||
|
{
|
||||||
|
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||||
|
Address = new Address { Country = "DE" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Is<RegistrationListOptions>(opt => opt.Status == TaxRegistrationStatus.Active))
|
||||||
|
.Returns(new StripeList<Registration>
|
||||||
|
{
|
||||||
|
Data = [
|
||||||
|
new Registration { Country = "US" },
|
||||||
|
new Registration { Country = "DE" },
|
||||||
|
new Registration { Country = "FR" }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Is<RegistrationListOptions>(opt => opt.Status == TaxRegistrationStatus.Scheduled))
|
||||||
|
.Returns(new StripeList<Registration> { Data = [] });
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(provider);
|
||||||
|
|
||||||
|
Assert.True(response is
|
||||||
|
{
|
||||||
|
TaxId.Type: "tax_id_missing"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_CombinesBothWarningTypes(
|
||||||
|
Provider provider,
|
||||||
|
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Enabled = false;
|
||||||
|
var cancelAt = DateTime.UtcNow.AddDays(5);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = SubscriptionStatus.Unpaid,
|
||||||
|
CancelAt = cancelAt,
|
||||||
|
Customer = new Customer
|
||||||
|
{
|
||||||
|
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||||
|
Address = new Address { Country = "CA" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||||
|
.Returns(new StripeList<Registration>
|
||||||
|
{
|
||||||
|
Data = [new Registration { Country = "CA" }]
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(provider);
|
||||||
|
|
||||||
|
Assert.True(response is
|
||||||
|
{
|
||||||
|
Suspension.Resolution: "add_payment_method",
|
||||||
|
TaxId.Type: "tax_id_missing"
|
||||||
|
});
|
||||||
|
Assert.Equal(cancelAt, response.Suspension.SubscriptionCancelsAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_USCustomer_NoTaxIdWarning(
|
||||||
|
Provider provider,
|
||||||
|
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Enabled = true;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = SubscriptionStatus.Active,
|
||||||
|
Customer = new Customer
|
||||||
|
{
|
||||||
|
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||||
|
Address = new Address { Country = "US" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||||
|
.Returns(new StripeList<Registration>
|
||||||
|
{
|
||||||
|
Data = [new Registration { Country = "US" }]
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(provider);
|
||||||
|
|
||||||
|
Assert.Null(response!.TaxId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ using Bit.Core.Billing.Enums;
|
|||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Providers.Entities;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
using Bit.Core.Billing.Providers.Repositories;
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Net;
|
|
||||||
using Bit.Commercial.Core.Billing.Providers.Models;
|
using Bit.Commercial.Core.Billing.Providers.Models;
|
||||||
using Bit.Commercial.Core.Billing.Providers.Services;
|
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
@@ -11,17 +9,16 @@ using Bit.Core.AdminConsole.Repositories;
|
|||||||
using Bit.Core.Billing.Caches;
|
using Bit.Core.Billing.Caches;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Payment.Models;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Providers.Entities;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
using Bit.Core.Billing.Providers.Models;
|
using Bit.Core.Billing.Providers.Models;
|
||||||
using Bit.Core.Billing.Providers.Repositories;
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Billing.Tax.Services;
|
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Business;
|
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
@@ -352,9 +349,6 @@ public class ProviderBillingServiceTests
|
|||||||
CloudRegion = "US"
|
CloudRegion = "US"
|
||||||
});
|
});
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
|
||||||
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IStripeAdapter>().CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(
|
sutProvider.GetDependency<IStripeAdapter>().CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(
|
||||||
options =>
|
options =>
|
||||||
options.Address.Country == providerCustomer.Address.Country &&
|
options.Address.Country == providerCustomer.Address.Country &&
|
||||||
@@ -898,208 +892,97 @@ public class ProviderBillingServiceTests
|
|||||||
#region SetupCustomer
|
#region SetupCustomer
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task SetupCustomer_MissingCountry_ContactSupport(
|
public async Task SetupCustomer_NullPaymentMethod_ThrowsNullReferenceException(
|
||||||
SutProvider<ProviderBillingService> sutProvider,
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
Provider provider,
|
Provider provider,
|
||||||
TaxInfo taxInfo)
|
BillingAddress billingAddress)
|
||||||
{
|
{
|
||||||
taxInfo.BillingAddressCountry = null;
|
await Assert.ThrowsAsync<NullReferenceException>(() =>
|
||||||
|
sutProvider.Sut.SetupCustomer(provider, null, billingAddress));
|
||||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupCustomer(provider, taxInfo));
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IStripeAdapter>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.CustomerGetAsync(Arg.Any<string>(), Arg.Any<CustomerGetOptions>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task SetupCustomer_MissingPostalCode_ContactSupport(
|
|
||||||
SutProvider<ProviderBillingService> sutProvider,
|
|
||||||
Provider provider,
|
|
||||||
TaxInfo taxInfo)
|
|
||||||
{
|
|
||||||
taxInfo.BillingAddressCountry = null;
|
|
||||||
|
|
||||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupCustomer(provider, taxInfo));
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IStripeAdapter>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.CustomerGetAsync(Arg.Any<string>(), Arg.Any<CustomerGetOptions>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task SetupCustomer_NoPaymentMethod_Success(
|
|
||||||
SutProvider<ProviderBillingService> sutProvider,
|
|
||||||
Provider provider,
|
|
||||||
TaxInfo taxInfo)
|
|
||||||
{
|
|
||||||
provider.Name = "MSP";
|
|
||||||
|
|
||||||
sutProvider.GetDependency<ITaxService>()
|
|
||||||
.GetStripeTaxCode(Arg.Is<string>(
|
|
||||||
p => p == taxInfo.BillingAddressCountry),
|
|
||||||
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
|
||||||
.Returns(taxInfo.TaxIdType);
|
|
||||||
|
|
||||||
taxInfo.BillingAddressCountry = "AD";
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
|
||||||
|
|
||||||
var expected = new Customer
|
|
||||||
{
|
|
||||||
Id = "customer_id",
|
|
||||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
|
||||||
};
|
|
||||||
|
|
||||||
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
|
||||||
o.Address.Country == taxInfo.BillingAddressCountry &&
|
|
||||||
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
|
||||||
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
|
||||||
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
|
||||||
o.Address.City == taxInfo.BillingAddressCity &&
|
|
||||||
o.Address.State == taxInfo.BillingAddressState &&
|
|
||||||
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
|
|
||||||
o.Email == provider.BillingEmail &&
|
|
||||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
|
|
||||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
|
|
||||||
o.Metadata["region"] == "" &&
|
|
||||||
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
|
|
||||||
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
|
|
||||||
.Returns(expected);
|
|
||||||
|
|
||||||
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo);
|
|
||||||
|
|
||||||
Assert.Equivalent(expected, actual);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task SetupCustomer_InvalidRequiredPaymentMethod_ThrowsBillingException(
|
|
||||||
SutProvider<ProviderBillingService> sutProvider,
|
|
||||||
Provider provider,
|
|
||||||
TaxInfo taxInfo,
|
|
||||||
TokenizedPaymentSource tokenizedPaymentSource)
|
|
||||||
{
|
|
||||||
provider.Name = "MSP";
|
|
||||||
|
|
||||||
sutProvider.GetDependency<ITaxService>()
|
|
||||||
.GetStripeTaxCode(Arg.Is<string>(
|
|
||||||
p => p == taxInfo.BillingAddressCountry),
|
|
||||||
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
|
||||||
.Returns(taxInfo.TaxIdType);
|
|
||||||
|
|
||||||
taxInfo.BillingAddressCountry = "AD";
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
|
||||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
|
||||||
|
|
||||||
tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay };
|
|
||||||
|
|
||||||
await ThrowsBillingExceptionAsync(() =>
|
|
||||||
sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task SetupCustomer_WithBankAccount_Error_Reverts(
|
public async Task SetupCustomer_WithBankAccount_Error_Reverts(
|
||||||
SutProvider<ProviderBillingService> sutProvider,
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
Provider provider,
|
Provider provider,
|
||||||
TaxInfo taxInfo)
|
BillingAddress billingAddress)
|
||||||
{
|
{
|
||||||
provider.Name = "MSP";
|
provider.Name = "MSP";
|
||||||
|
billingAddress.Country = "AD";
|
||||||
sutProvider.GetDependency<ITaxService>()
|
billingAddress.TaxId = new TaxID("es_nif", "12345678Z");
|
||||||
.GetStripeTaxCode(Arg.Is<string>(
|
|
||||||
p => p == taxInfo.BillingAddressCountry),
|
|
||||||
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
|
||||||
.Returns(taxInfo.TaxIdType);
|
|
||||||
|
|
||||||
taxInfo.BillingAddressCountry = "AD";
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.BankAccount, Token = "token" };
|
||||||
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.BankAccount, "token");
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
|
||||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
|
||||||
|
|
||||||
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options =>
|
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options =>
|
||||||
options.PaymentMethod == tokenizedPaymentSource.Token)).Returns([
|
options.PaymentMethod == tokenizedPaymentMethod.Token)).Returns([
|
||||||
new SetupIntent { Id = "setup_intent_id" }
|
new SetupIntent { Id = "setup_intent_id" }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||||
o.Address.Country == taxInfo.BillingAddressCountry &&
|
o.Address.Country == billingAddress.Country &&
|
||||||
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
o.Address.PostalCode == billingAddress.PostalCode &&
|
||||||
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
o.Address.Line1 == billingAddress.Line1 &&
|
||||||
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
o.Address.Line2 == billingAddress.Line2 &&
|
||||||
o.Address.City == taxInfo.BillingAddressCity &&
|
o.Address.City == billingAddress.City &&
|
||||||
o.Address.State == taxInfo.BillingAddressState &&
|
o.Address.State == billingAddress.State &&
|
||||||
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
|
o.Description == provider.DisplayBusinessName() &&
|
||||||
o.Email == provider.BillingEmail &&
|
o.Email == provider.BillingEmail &&
|
||||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
|
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&
|
||||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
|
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&
|
||||||
o.Metadata["region"] == "" &&
|
o.Metadata["region"] == "" &&
|
||||||
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
|
o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&
|
||||||
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
|
o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))
|
||||||
.Throws<StripeException>();
|
.Throws<StripeException>();
|
||||||
|
|
||||||
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns("setup_intent_id");
|
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns("setup_intent_id");
|
||||||
|
|
||||||
await Assert.ThrowsAsync<StripeException>(() =>
|
await Assert.ThrowsAsync<StripeException>(() =>
|
||||||
sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
|
sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress));
|
||||||
|
|
||||||
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Set(provider.Id, "setup_intent_id");
|
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Set(provider.Id, "setup_intent_id");
|
||||||
|
|
||||||
await stripeAdapter.Received(1).SetupIntentCancel("setup_intent_id", Arg.Is<SetupIntentCancelOptions>(options =>
|
await stripeAdapter.Received(1).SetupIntentCancel("setup_intent_id", Arg.Is<SetupIntentCancelOptions>(options =>
|
||||||
options.CancellationReason == "abandoned"));
|
options.CancellationReason == "abandoned"));
|
||||||
|
|
||||||
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Remove(provider.Id);
|
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).RemoveSetupIntentForSubscriber(provider.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task SetupCustomer_WithPayPal_Error_Reverts(
|
public async Task SetupCustomer_WithPayPal_Error_Reverts(
|
||||||
SutProvider<ProviderBillingService> sutProvider,
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
Provider provider,
|
Provider provider,
|
||||||
TaxInfo taxInfo)
|
BillingAddress billingAddress)
|
||||||
{
|
{
|
||||||
provider.Name = "MSP";
|
provider.Name = "MSP";
|
||||||
|
billingAddress.Country = "AD";
|
||||||
sutProvider.GetDependency<ITaxService>()
|
billingAddress.TaxId = new TaxID("es_nif", "12345678Z");
|
||||||
.GetStripeTaxCode(Arg.Is<string>(
|
|
||||||
p => p == taxInfo.BillingAddressCountry),
|
|
||||||
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
|
||||||
.Returns(taxInfo.TaxIdType);
|
|
||||||
|
|
||||||
taxInfo.BillingAddressCountry = "AD";
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.PayPal, Token = "token" };
|
||||||
|
|
||||||
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.PayPal, "token");
|
sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentMethod.Token)
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
|
||||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentSource.Token)
|
|
||||||
.Returns("braintree_customer_id");
|
.Returns("braintree_customer_id");
|
||||||
|
|
||||||
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||||
o.Address.Country == taxInfo.BillingAddressCountry &&
|
o.Address.Country == billingAddress.Country &&
|
||||||
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
o.Address.PostalCode == billingAddress.PostalCode &&
|
||||||
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
o.Address.Line1 == billingAddress.Line1 &&
|
||||||
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
o.Address.Line2 == billingAddress.Line2 &&
|
||||||
o.Address.City == taxInfo.BillingAddressCity &&
|
o.Address.City == billingAddress.City &&
|
||||||
o.Address.State == taxInfo.BillingAddressState &&
|
o.Address.State == billingAddress.State &&
|
||||||
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
|
o.Description == provider.DisplayBusinessName() &&
|
||||||
o.Email == provider.BillingEmail &&
|
o.Email == provider.BillingEmail &&
|
||||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
|
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&
|
||||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
|
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&
|
||||||
o.Metadata["region"] == "" &&
|
o.Metadata["region"] == "" &&
|
||||||
o.Metadata["btCustomerId"] == "braintree_customer_id" &&
|
o.Metadata["btCustomerId"] == "braintree_customer_id" &&
|
||||||
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
|
o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&
|
||||||
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
|
o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))
|
||||||
.Throws<StripeException>();
|
.Throws<StripeException>();
|
||||||
|
|
||||||
await Assert.ThrowsAsync<StripeException>(() =>
|
await Assert.ThrowsAsync<StripeException>(() =>
|
||||||
sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
|
sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress));
|
||||||
|
|
||||||
await sutProvider.GetDependency<IBraintreeGateway>().Customer.Received(1).DeleteAsync("braintree_customer_id");
|
await sutProvider.GetDependency<IBraintreeGateway>().Customer.Received(1).DeleteAsync("braintree_customer_id");
|
||||||
}
|
}
|
||||||
@@ -1108,17 +991,11 @@ public class ProviderBillingServiceTests
|
|||||||
public async Task SetupCustomer_WithBankAccount_Success(
|
public async Task SetupCustomer_WithBankAccount_Success(
|
||||||
SutProvider<ProviderBillingService> sutProvider,
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
Provider provider,
|
Provider provider,
|
||||||
TaxInfo taxInfo)
|
BillingAddress billingAddress)
|
||||||
{
|
{
|
||||||
provider.Name = "MSP";
|
provider.Name = "MSP";
|
||||||
|
billingAddress.Country = "AD";
|
||||||
sutProvider.GetDependency<ITaxService>()
|
billingAddress.TaxId = new TaxID("es_nif", "12345678Z");
|
||||||
.GetStripeTaxCode(Arg.Is<string>(
|
|
||||||
p => p == taxInfo.BillingAddressCountry),
|
|
||||||
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
|
||||||
.Returns(taxInfo.TaxIdType);
|
|
||||||
|
|
||||||
taxInfo.BillingAddressCountry = "AD";
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
|
||||||
@@ -1128,33 +1005,30 @@ public class ProviderBillingServiceTests
|
|||||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
||||||
};
|
};
|
||||||
|
|
||||||
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.BankAccount, "token");
|
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.BankAccount, Token = "token" };
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
|
||||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
|
||||||
|
|
||||||
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options =>
|
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options =>
|
||||||
options.PaymentMethod == tokenizedPaymentSource.Token)).Returns([
|
options.PaymentMethod == tokenizedPaymentMethod.Token)).Returns([
|
||||||
new SetupIntent { Id = "setup_intent_id" }
|
new SetupIntent { Id = "setup_intent_id" }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||||
o.Address.Country == taxInfo.BillingAddressCountry &&
|
o.Address.Country == billingAddress.Country &&
|
||||||
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
o.Address.PostalCode == billingAddress.PostalCode &&
|
||||||
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
o.Address.Line1 == billingAddress.Line1 &&
|
||||||
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
o.Address.Line2 == billingAddress.Line2 &&
|
||||||
o.Address.City == taxInfo.BillingAddressCity &&
|
o.Address.City == billingAddress.City &&
|
||||||
o.Address.State == taxInfo.BillingAddressState &&
|
o.Address.State == billingAddress.State &&
|
||||||
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
|
o.Description == provider.DisplayBusinessName() &&
|
||||||
o.Email == provider.BillingEmail &&
|
o.Email == provider.BillingEmail &&
|
||||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
|
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&
|
||||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
|
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&
|
||||||
o.Metadata["region"] == "" &&
|
o.Metadata["region"] == "" &&
|
||||||
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
|
o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&
|
||||||
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
|
o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))
|
||||||
.Returns(expected);
|
.Returns(expected);
|
||||||
|
|
||||||
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
|
var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress);
|
||||||
|
|
||||||
Assert.Equivalent(expected, actual);
|
Assert.Equivalent(expected, actual);
|
||||||
|
|
||||||
@@ -1165,17 +1039,11 @@ public class ProviderBillingServiceTests
|
|||||||
public async Task SetupCustomer_WithPayPal_Success(
|
public async Task SetupCustomer_WithPayPal_Success(
|
||||||
SutProvider<ProviderBillingService> sutProvider,
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
Provider provider,
|
Provider provider,
|
||||||
TaxInfo taxInfo)
|
BillingAddress billingAddress)
|
||||||
{
|
{
|
||||||
provider.Name = "MSP";
|
provider.Name = "MSP";
|
||||||
|
billingAddress.Country = "AD";
|
||||||
sutProvider.GetDependency<ITaxService>()
|
billingAddress.TaxId = new TaxID("es_nif", "12345678Z");
|
||||||
.GetStripeTaxCode(Arg.Is<string>(
|
|
||||||
p => p == taxInfo.BillingAddressCountry),
|
|
||||||
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
|
||||||
.Returns(taxInfo.TaxIdType);
|
|
||||||
|
|
||||||
taxInfo.BillingAddressCountry = "AD";
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
|
||||||
@@ -1185,32 +1053,29 @@ public class ProviderBillingServiceTests
|
|||||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
||||||
};
|
};
|
||||||
|
|
||||||
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.PayPal, "token");
|
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.PayPal, Token = "token" };
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentMethod.Token)
|
||||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentSource.Token)
|
|
||||||
.Returns("braintree_customer_id");
|
.Returns("braintree_customer_id");
|
||||||
|
|
||||||
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||||
o.Address.Country == taxInfo.BillingAddressCountry &&
|
o.Address.Country == billingAddress.Country &&
|
||||||
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
o.Address.PostalCode == billingAddress.PostalCode &&
|
||||||
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
o.Address.Line1 == billingAddress.Line1 &&
|
||||||
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
o.Address.Line2 == billingAddress.Line2 &&
|
||||||
o.Address.City == taxInfo.BillingAddressCity &&
|
o.Address.City == billingAddress.City &&
|
||||||
o.Address.State == taxInfo.BillingAddressState &&
|
o.Address.State == billingAddress.State &&
|
||||||
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
|
o.Description == provider.DisplayBusinessName() &&
|
||||||
o.Email == provider.BillingEmail &&
|
o.Email == provider.BillingEmail &&
|
||||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
|
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&
|
||||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
|
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&
|
||||||
o.Metadata["region"] == "" &&
|
o.Metadata["region"] == "" &&
|
||||||
o.Metadata["btCustomerId"] == "braintree_customer_id" &&
|
o.Metadata["btCustomerId"] == "braintree_customer_id" &&
|
||||||
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
|
o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&
|
||||||
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
|
o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))
|
||||||
.Returns(expected);
|
.Returns(expected);
|
||||||
|
|
||||||
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
|
var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress);
|
||||||
|
|
||||||
Assert.Equivalent(expected, actual);
|
Assert.Equivalent(expected, actual);
|
||||||
}
|
}
|
||||||
@@ -1219,17 +1084,11 @@ public class ProviderBillingServiceTests
|
|||||||
public async Task SetupCustomer_WithCard_Success(
|
public async Task SetupCustomer_WithCard_Success(
|
||||||
SutProvider<ProviderBillingService> sutProvider,
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
Provider provider,
|
Provider provider,
|
||||||
TaxInfo taxInfo)
|
BillingAddress billingAddress)
|
||||||
{
|
{
|
||||||
provider.Name = "MSP";
|
provider.Name = "MSP";
|
||||||
|
billingAddress.Country = "AD";
|
||||||
sutProvider.GetDependency<ITaxService>()
|
billingAddress.TaxId = new TaxID("es_nif", "12345678Z");
|
||||||
.GetStripeTaxCode(Arg.Is<string>(
|
|
||||||
p => p == taxInfo.BillingAddressCountry),
|
|
||||||
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
|
||||||
.Returns(taxInfo.TaxIdType);
|
|
||||||
|
|
||||||
taxInfo.BillingAddressCountry = "AD";
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
|
||||||
@@ -1239,30 +1098,26 @@ public class ProviderBillingServiceTests
|
|||||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
||||||
};
|
};
|
||||||
|
|
||||||
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token");
|
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = "token" };
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
|
||||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
|
||||||
|
|
||||||
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||||
o.Address.Country == taxInfo.BillingAddressCountry &&
|
o.Address.Country == billingAddress.Country &&
|
||||||
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
o.Address.PostalCode == billingAddress.PostalCode &&
|
||||||
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
o.Address.Line1 == billingAddress.Line1 &&
|
||||||
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
o.Address.Line2 == billingAddress.Line2 &&
|
||||||
o.Address.City == taxInfo.BillingAddressCity &&
|
o.Address.City == billingAddress.City &&
|
||||||
o.Address.State == taxInfo.BillingAddressState &&
|
o.Address.State == billingAddress.State &&
|
||||||
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
|
o.Description == provider.DisplayBusinessName() &&
|
||||||
o.Email == provider.BillingEmail &&
|
o.Email == provider.BillingEmail &&
|
||||||
o.PaymentMethod == tokenizedPaymentSource.Token &&
|
o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentMethod.Token &&
|
||||||
o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentSource.Token &&
|
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&
|
||||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
|
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&
|
||||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
|
|
||||||
o.Metadata["region"] == "" &&
|
o.Metadata["region"] == "" &&
|
||||||
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
|
o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&
|
||||||
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
|
o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))
|
||||||
.Returns(expected);
|
.Returns(expected);
|
||||||
|
|
||||||
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
|
var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress);
|
||||||
|
|
||||||
Assert.Equivalent(expected, actual);
|
Assert.Equivalent(expected, actual);
|
||||||
}
|
}
|
||||||
@@ -1271,17 +1126,11 @@ public class ProviderBillingServiceTests
|
|||||||
public async Task SetupCustomer_WithCard_ReverseCharge_Success(
|
public async Task SetupCustomer_WithCard_ReverseCharge_Success(
|
||||||
SutProvider<ProviderBillingService> sutProvider,
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
Provider provider,
|
Provider provider,
|
||||||
TaxInfo taxInfo)
|
BillingAddress billingAddress)
|
||||||
{
|
{
|
||||||
provider.Name = "MSP";
|
provider.Name = "MSP";
|
||||||
|
billingAddress.Country = "FR"; // Non-US country to trigger reverse charge
|
||||||
sutProvider.GetDependency<ITaxService>()
|
billingAddress.TaxId = new TaxID("fr_siren", "123456789");
|
||||||
.GetStripeTaxCode(Arg.Is<string>(
|
|
||||||
p => p == taxInfo.BillingAddressCountry),
|
|
||||||
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
|
||||||
.Returns(taxInfo.TaxIdType);
|
|
||||||
|
|
||||||
taxInfo.BillingAddressCountry = "AD";
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
|
||||||
@@ -1291,59 +1140,51 @@ public class ProviderBillingServiceTests
|
|||||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
||||||
};
|
};
|
||||||
|
|
||||||
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token");
|
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = "token" };
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
|
||||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
|
||||||
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
|
|
||||||
|
|
||||||
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||||
o.Address.Country == taxInfo.BillingAddressCountry &&
|
o.Address.Country == billingAddress.Country &&
|
||||||
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
o.Address.PostalCode == billingAddress.PostalCode &&
|
||||||
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
o.Address.Line1 == billingAddress.Line1 &&
|
||||||
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
o.Address.Line2 == billingAddress.Line2 &&
|
||||||
o.Address.City == taxInfo.BillingAddressCity &&
|
o.Address.City == billingAddress.City &&
|
||||||
o.Address.State == taxInfo.BillingAddressState &&
|
o.Address.State == billingAddress.State &&
|
||||||
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
|
o.Description == provider.DisplayBusinessName() &&
|
||||||
o.Email == provider.BillingEmail &&
|
o.Email == provider.BillingEmail &&
|
||||||
o.PaymentMethod == tokenizedPaymentSource.Token &&
|
o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentMethod.Token &&
|
||||||
o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentSource.Token &&
|
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&
|
||||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
|
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&
|
||||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
|
|
||||||
o.Metadata["region"] == "" &&
|
o.Metadata["region"] == "" &&
|
||||||
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
|
o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&
|
||||||
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber &&
|
o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value &&
|
||||||
o.TaxExempt == StripeConstants.TaxExempt.Reverse))
|
o.TaxExempt == StripeConstants.TaxExempt.Reverse))
|
||||||
.Returns(expected);
|
.Returns(expected);
|
||||||
|
|
||||||
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
|
var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress);
|
||||||
|
|
||||||
Assert.Equivalent(expected, actual);
|
Assert.Equivalent(expected, actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid(
|
public async Task SetupCustomer_WithInvalidTaxId_ThrowsBadRequestException(
|
||||||
SutProvider<ProviderBillingService> sutProvider,
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
Provider provider,
|
Provider provider,
|
||||||
TaxInfo taxInfo)
|
BillingAddress billingAddress)
|
||||||
{
|
{
|
||||||
provider.Name = "MSP";
|
provider.Name = "MSP";
|
||||||
|
billingAddress.Country = "AD";
|
||||||
|
billingAddress.TaxId = new TaxID("es_nif", "invalid_tax_id");
|
||||||
|
|
||||||
taxInfo.BillingAddressCountry = "AD";
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = "token" };
|
||||||
|
|
||||||
sutProvider.GetDependency<ITaxService>()
|
stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>())
|
||||||
.GetStripeTaxCode(Arg.Is<string>(
|
.Throws(new StripeException("Invalid tax ID") { StripeError = new StripeError { Code = "tax_id_invalid" } });
|
||||||
p => p == taxInfo.BillingAddressCountry),
|
|
||||||
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
|
||||||
.Returns((string)null);
|
|
||||||
|
|
||||||
var actual = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
var actual = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||||
await sutProvider.Sut.SetupCustomer(provider, taxInfo));
|
await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress));
|
||||||
|
|
||||||
Assert.IsType<BadRequestException>(actual);
|
Assert.Equal("Your tax ID wasn't recognized for your selected country. Please ensure your country and tax ID are valid.", actual.Message);
|
||||||
Assert.Equal("billingTaxIdTypeInferenceError", actual.Message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -1616,8 +1457,6 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
|
||||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||||
sub =>
|
sub =>
|
||||||
@@ -1694,12 +1533,10 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
|
||||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
|
||||||
|
|
||||||
const string setupIntentId = "seti_123";
|
const string setupIntentId = "seti_123";
|
||||||
|
|
||||||
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns(setupIntentId);
|
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntentId);
|
||||||
|
|
||||||
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntentId, Arg.Is<SetupIntentGetOptions>(options =>
|
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntentId, Arg.Is<SetupIntentGetOptions>(options =>
|
||||||
options.Expand.Contains("payment_method"))).Returns(new SetupIntent
|
options.Expand.Contains("payment_method"))).Returns(new SetupIntent
|
||||||
@@ -1797,8 +1634,6 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
|
||||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||||
sub =>
|
sub =>
|
||||||
@@ -1877,11 +1712,6 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
|
||||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
|
||||||
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||||
sub =>
|
sub =>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Bit.Commercial.Core.Billing.Providers.Services;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@@ -295,7 +295,7 @@ public class ProjectServiceAccountsAccessPoliciesAuthorizationHandlerTests
|
|||||||
{
|
{
|
||||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
|
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.OrganizationId)
|
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default!, resource.OrganizationId)
|
||||||
.ReturnsForAnyArgs((accessClientType, userId));
|
.ReturnsForAnyArgs((accessClientType, userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ public class ServiceAccountGrantedPoliciesAuthorizationHandlerTests
|
|||||||
{
|
{
|
||||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
|
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.OrganizationId)
|
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default!, resource.OrganizationId)
|
||||||
.ReturnsForAnyArgs((accessClientType, userId));
|
.ReturnsForAnyArgs((accessClientType, userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ public class BulkSecretAuthorizationHandlerTests
|
|||||||
{
|
{
|
||||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId)
|
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId)
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId)
|
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default!, organizationId)
|
||||||
.ReturnsForAnyArgs((accessClientType, userId));
|
.ReturnsForAnyArgs((accessClientType, userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Scim.Models;
|
using Bit.Scim.Models;
|
||||||
using Bit.Scim.Users;
|
using Bit.Scim.Users;
|
||||||
using Bit.Scim.Utilities;
|
using Bit.Scim.Utilities;
|
||||||
@@ -101,7 +101,7 @@ public class PatchUserCommandTests
|
|||||||
|
|
||||||
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
|
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationService>().Received(1).RevokeUserAsync(organizationUser, EventSystemUser.SCIM);
|
await sutProvider.GetDependency<IRevokeOrganizationUserCommand>().Received(1).RevokeUserAsync(organizationUser, EventSystemUser.SCIM);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@@ -129,7 +129,7 @@ public class PatchUserCommandTests
|
|||||||
|
|
||||||
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
|
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationService>().Received(1).RevokeUserAsync(organizationUser, EventSystemUser.SCIM);
|
await sutProvider.GetDependency<IRevokeOrganizationUserCommand>().Received(1).RevokeUserAsync(organizationUser, EventSystemUser.SCIM);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@@ -149,7 +149,7 @@ public class PatchUserCommandTests
|
|||||||
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
|
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IRestoreOrganizationUserCommand>().DidNotReceiveWithAnyArgs().RestoreUserAsync(default, EventSystemUser.SCIM);
|
await sutProvider.GetDependency<IRestoreOrganizationUserCommand>().DidNotReceiveWithAnyArgs().RestoreUserAsync(default, EventSystemUser.SCIM);
|
||||||
await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs().RevokeUserAsync(default, EventSystemUser.SCIM);
|
await sutProvider.GetDependency<IRevokeOrganizationUserCommand>().DidNotReceiveWithAnyArgs().RevokeUserAsync(default, EventSystemUser.SCIM);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ services:
|
|||||||
- ./.data/postgres/log:/var/log/postgresql
|
- ./.data/postgres/log:/var/log/postgresql
|
||||||
profiles:
|
profiles:
|
||||||
- postgres
|
- postgres
|
||||||
|
- ef
|
||||||
|
|
||||||
mysql:
|
mysql:
|
||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
@@ -69,6 +70,7 @@ services:
|
|||||||
- mysql_dev_data:/var/lib/mysql
|
- mysql_dev_data:/var/lib/mysql
|
||||||
profiles:
|
profiles:
|
||||||
- mysql
|
- mysql
|
||||||
|
- ef
|
||||||
|
|
||||||
mariadb:
|
mariadb:
|
||||||
image: mariadb:10
|
image: mariadb:10
|
||||||
@@ -76,13 +78,13 @@ services:
|
|||||||
- 4306:3306
|
- 4306:3306
|
||||||
environment:
|
environment:
|
||||||
MARIADB_USER: maria
|
MARIADB_USER: maria
|
||||||
MARIADB_PASSWORD: ${MARIADB_ROOT_PASSWORD}
|
|
||||||
MARIADB_DATABASE: vault_dev
|
MARIADB_DATABASE: vault_dev
|
||||||
MARIADB_RANDOM_ROOT_PASSWORD: "true"
|
MARIADB_RANDOM_ROOT_PASSWORD: "true"
|
||||||
volumes:
|
volumes:
|
||||||
- mariadb_dev_data:/var/lib/mysql
|
- mariadb_dev_data:/var/lib/mysql
|
||||||
profiles:
|
profiles:
|
||||||
- mariadb
|
- mariadb
|
||||||
|
- ef
|
||||||
|
|
||||||
idp:
|
idp:
|
||||||
image: kenchan0130/simplesamlphp:1.19.8
|
image: kenchan0130/simplesamlphp:1.19.8
|
||||||
@@ -99,7 +101,7 @@ services:
|
|||||||
- idp
|
- idp
|
||||||
|
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
image: rabbitmq:4.1.0-management
|
image: rabbitmq:4.1.3-management
|
||||||
container_name: rabbitmq
|
container_name: rabbitmq
|
||||||
ports:
|
ports:
|
||||||
- "5672:5672"
|
- "5672:5672"
|
||||||
@@ -153,5 +155,6 @@ volumes:
|
|||||||
mssql_dev_data:
|
mssql_dev_data:
|
||||||
postgres_dev_data:
|
postgres_dev_data:
|
||||||
mysql_dev_data:
|
mysql_dev_data:
|
||||||
|
mariadb_dev_data:
|
||||||
rabbitmq_data:
|
rabbitmq_data:
|
||||||
redis_data:
|
redis_data:
|
||||||
|
|||||||
28
dev/generate_openapi_files.ps1
Normal file
28
dev/generate_openapi_files.ps1
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
Set-Location "$PSScriptRoot/.."
|
||||||
|
|
||||||
|
$env:ASPNETCORE_ENVIRONMENT = "Development"
|
||||||
|
$env:swaggerGen = "True"
|
||||||
|
$env:DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX = "2"
|
||||||
|
$env:GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING = "placeholder"
|
||||||
|
|
||||||
|
dotnet tool restore
|
||||||
|
|
||||||
|
# Identity
|
||||||
|
Set-Location "./src/Identity"
|
||||||
|
dotnet build
|
||||||
|
dotnet swagger tofile --output "../../identity.json" --host "https://identity.bitwarden.com" "./bin/Debug/net8.0/Identity.dll" "v1"
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
|
||||||
|
# Api internal & public
|
||||||
|
Set-Location "../../src/Api"
|
||||||
|
dotnet build
|
||||||
|
dotnet swagger tofile --output "../../api.json" --host "https://api.bitwarden.com" "./bin/Debug/net8.0/Api.dll" "internal"
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
dotnet swagger tofile --output "../../api.public.json" --host "https://api.bitwarden.com" "./bin/Debug/net8.0/Api.dll" "public"
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
@@ -70,7 +70,7 @@ Foreach ($item in @(
|
|||||||
@($mysql, "MySQL", "MySqlMigrations", "mySql", 2),
|
@($mysql, "MySQL", "MySqlMigrations", "mySql", 2),
|
||||||
# MariaDB shares the MySQL connection string in the server config so they are mutually exclusive in that context.
|
# MariaDB shares the MySQL connection string in the server config so they are mutually exclusive in that context.
|
||||||
# However they can still be run independently for integration tests.
|
# However they can still be run independently for integration tests.
|
||||||
@($mariadb, "MariaDB", "MySqlMigrations", "mySql", 3)
|
@($mariadb, "MariaDB", "MySqlMigrations", "mySql", 4)
|
||||||
)) {
|
)) {
|
||||||
if (!$item[0] -and !$all) {
|
if (!$item[0] -and !$all) {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -33,6 +33,8 @@
|
|||||||
"id": "<your Installation Id>",
|
"id": "<your Installation Id>",
|
||||||
"key": "<your Installation Key>"
|
"key": "<your Installation Key>"
|
||||||
},
|
},
|
||||||
"licenseDirectory": "<full path to license directory>"
|
"licenseDirectory": "<full path to license directory>",
|
||||||
|
"enableNewDeviceVerification": true,
|
||||||
|
"enableEmailVerification": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "events-webhook-subscription"
|
"Name": "events-webhook-subscription"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "events-hec-subscription"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "events-datadog-subscription"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -64,6 +70,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "integration-hec-subscription",
|
||||||
|
"Rules": [
|
||||||
|
{
|
||||||
|
"Name": "hec-integration-filter",
|
||||||
|
"Properties": {
|
||||||
|
"FilterType": "Correlation",
|
||||||
|
"CorrelationFilter": {
|
||||||
|
"Label": "hec"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "integration-datadog-subscription",
|
||||||
|
"Rules": [
|
||||||
|
{
|
||||||
|
"Name": "datadog-integration-filter",
|
||||||
|
"Properties": {
|
||||||
|
"FilterType": "Correlation",
|
||||||
|
"CorrelationFilter": {
|
||||||
|
"Label": "datadog"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class StaticClientStoreTests
|
|||||||
[Benchmark]
|
[Benchmark]
|
||||||
public Client? TryGetValue()
|
public Client? TryGetValue()
|
||||||
{
|
{
|
||||||
return _store.ApiClients.TryGetValue(ClientId, out var client)
|
return _store.Clients.TryGetValue(ClientId, out var client)
|
||||||
? client
|
? client
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.15.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -9,12 +9,6 @@ const AUTH_USERNAME = __ENV.AUTH_USER_EMAIL;
|
|||||||
const AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH;
|
const AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH;
|
||||||
|
|
||||||
export const options = {
|
export const options = {
|
||||||
ext: {
|
|
||||||
loadimpact: {
|
|
||||||
projectID: 3639465,
|
|
||||||
name: "Config",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scenarios: {
|
scenarios: {
|
||||||
constant_load: {
|
constant_load: {
|
||||||
executor: "constant-arrival-rate",
|
executor: "constant-arrival-rate",
|
||||||
|
|||||||
@@ -10,12 +10,6 @@ const AUTH_CLIENT_ID = __ENV.AUTH_CLIENT_ID;
|
|||||||
const AUTH_CLIENT_SECRET = __ENV.AUTH_CLIENT_SECRET;
|
const AUTH_CLIENT_SECRET = __ENV.AUTH_CLIENT_SECRET;
|
||||||
|
|
||||||
export const options = {
|
export const options = {
|
||||||
ext: {
|
|
||||||
loadimpact: {
|
|
||||||
projectID: 3639465,
|
|
||||||
name: "Groups",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scenarios: {
|
scenarios: {
|
||||||
constant_load: {
|
constant_load: {
|
||||||
executor: "constant-arrival-rate",
|
executor: "constant-arrival-rate",
|
||||||
|
|||||||
@@ -6,12 +6,6 @@ const AUTH_USERNAME = __ENV.AUTH_USER_EMAIL;
|
|||||||
const AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH;
|
const AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH;
|
||||||
|
|
||||||
export const options = {
|
export const options = {
|
||||||
ext: {
|
|
||||||
loadimpact: {
|
|
||||||
projectID: 3639465,
|
|
||||||
name: "Login",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scenarios: {
|
scenarios: {
|
||||||
constant_load: {
|
constant_load: {
|
||||||
executor: "constant-arrival-rate",
|
executor: "constant-arrival-rate",
|
||||||
|
|||||||
@@ -9,12 +9,6 @@ const AUTH_USERNAME = __ENV.AUTH_USER_EMAIL;
|
|||||||
const AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH;
|
const AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH;
|
||||||
|
|
||||||
export const options = {
|
export const options = {
|
||||||
ext: {
|
|
||||||
loadimpact: {
|
|
||||||
projectID: 3639465,
|
|
||||||
name: "Sync",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scenarios: {
|
scenarios: {
|
||||||
constant_load: {
|
constant_load: {
|
||||||
executor: "constant-arrival-rate",
|
executor: "constant-arrival-rate",
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.Net;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using System.Net;
|
||||||
using Bit.Admin.AdminConsole.Models;
|
using Bit.Admin.AdminConsole.Models;
|
||||||
using Bit.Admin.Enums;
|
using Bit.Admin.Enums;
|
||||||
using Bit.Admin.Services;
|
using Bit.Admin.Services;
|
||||||
@@ -6,6 +9,7 @@ using Bit.Admin.Utilities;
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
@@ -29,7 +33,6 @@ namespace Bit.Admin.AdminConsole.Controllers;
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public class OrganizationsController : Controller
|
public class OrganizationsController : Controller
|
||||||
{
|
{
|
||||||
private readonly IOrganizationService _organizationService;
|
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
|
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
|
||||||
@@ -52,9 +55,9 @@ public class OrganizationsController : Controller
|
|||||||
private readonly IProviderBillingService _providerBillingService;
|
private readonly IProviderBillingService _providerBillingService;
|
||||||
private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand;
|
private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand;
|
||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
|
private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;
|
||||||
|
|
||||||
public OrganizationsController(
|
public OrganizationsController(
|
||||||
IOrganizationService organizationService,
|
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IOrganizationConnectionRepository organizationConnectionRepository,
|
IOrganizationConnectionRepository organizationConnectionRepository,
|
||||||
@@ -76,9 +79,9 @@ public class OrganizationsController : Controller
|
|||||||
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
|
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
|
||||||
IProviderBillingService providerBillingService,
|
IProviderBillingService providerBillingService,
|
||||||
IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand,
|
IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand,
|
||||||
IPricingClient pricingClient)
|
IPricingClient pricingClient,
|
||||||
|
IResendOrganizationInviteCommand resendOrganizationInviteCommand)
|
||||||
{
|
{
|
||||||
_organizationService = organizationService;
|
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_organizationConnectionRepository = organizationConnectionRepository;
|
_organizationConnectionRepository = organizationConnectionRepository;
|
||||||
@@ -101,6 +104,7 @@ public class OrganizationsController : Controller
|
|||||||
_providerBillingService = providerBillingService;
|
_providerBillingService = providerBillingService;
|
||||||
_organizationInitiateDeleteCommand = organizationInitiateDeleteCommand;
|
_organizationInitiateDeleteCommand = organizationInitiateDeleteCommand;
|
||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
|
_resendOrganizationInviteCommand = resendOrganizationInviteCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequirePermission(Permission.Org_List_View)]
|
[RequirePermission(Permission.Org_List_View)]
|
||||||
@@ -392,7 +396,7 @@ public class OrganizationsController : Controller
|
|||||||
var organizationUsers = await _organizationUserRepository.GetManyByOrganizationAsync(id, OrganizationUserType.Owner);
|
var organizationUsers = await _organizationUserRepository.GetManyByOrganizationAsync(id, OrganizationUserType.Owner);
|
||||||
foreach (var organizationUser in organizationUsers)
|
foreach (var organizationUser in organizationUsers)
|
||||||
{
|
{
|
||||||
await _organizationService.ResendInviteAsync(id, null, organizationUser.Id, true);
|
await _resendOrganizationInviteCommand.ResendInviteAsync(id, null, organizationUser.Id, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Json(null);
|
return Json(null);
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using Bit.Admin.AdminConsole.Models;
|
using Bit.Admin.AdminConsole.Models;
|
||||||
using Bit.Admin.Enums;
|
using Bit.Admin.Enums;
|
||||||
|
using Bit.Admin.Services;
|
||||||
using Bit.Admin.Utilities;
|
using Bit.Admin.Utilities;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||||
@@ -18,6 +21,7 @@ using Bit.Core.Billing.Providers.Entities;
|
|||||||
using Bit.Core.Billing.Providers.Models;
|
using Bit.Core.Billing.Providers.Models;
|
||||||
using Bit.Core.Billing.Providers.Repositories;
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
using Bit.Core.Billing.Providers.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@@ -34,26 +38,26 @@ namespace Bit.Admin.AdminConsole.Controllers;
|
|||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public class ProvidersController : Controller
|
public class ProvidersController : Controller
|
||||||
{
|
{
|
||||||
|
private readonly string _stripeUrl;
|
||||||
|
private readonly string _braintreeMerchantUrl;
|
||||||
|
private readonly string _braintreeMerchantId;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IResellerClientOrganizationSignUpCommand _resellerClientOrganizationSignUpCommand;
|
private readonly IResellerClientOrganizationSignUpCommand _resellerClientOrganizationSignUpCommand;
|
||||||
private readonly IProviderRepository _providerRepository;
|
private readonly IProviderRepository _providerRepository;
|
||||||
private readonly IProviderUserRepository _providerUserRepository;
|
private readonly IProviderUserRepository _providerUserRepository;
|
||||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||||
|
private readonly IProviderService _providerService;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
private readonly IProviderService _providerService;
|
|
||||||
private readonly ICreateProviderCommand _createProviderCommand;
|
private readonly ICreateProviderCommand _createProviderCommand;
|
||||||
private readonly IFeatureService _featureService;
|
|
||||||
private readonly IProviderPlanRepository _providerPlanRepository;
|
private readonly IProviderPlanRepository _providerPlanRepository;
|
||||||
private readonly IProviderBillingService _providerBillingService;
|
private readonly IProviderBillingService _providerBillingService;
|
||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
private readonly IStripeAdapter _stripeAdapter;
|
private readonly IStripeAdapter _stripeAdapter;
|
||||||
private readonly string _stripeUrl;
|
private readonly IAccessControlService _accessControlService;
|
||||||
private readonly string _braintreeMerchantUrl;
|
private readonly ISubscriberService _subscriberService;
|
||||||
private readonly string _braintreeMerchantId;
|
|
||||||
|
|
||||||
public ProvidersController(
|
public ProvidersController(IOrganizationRepository organizationRepository,
|
||||||
IOrganizationRepository organizationRepository,
|
|
||||||
IResellerClientOrganizationSignUpCommand resellerClientOrganizationSignUpCommand,
|
IResellerClientOrganizationSignUpCommand resellerClientOrganizationSignUpCommand,
|
||||||
IProviderRepository providerRepository,
|
IProviderRepository providerRepository,
|
||||||
IProviderUserRepository providerUserRepository,
|
IProviderUserRepository providerUserRepository,
|
||||||
@@ -62,12 +66,13 @@ public class ProvidersController : Controller
|
|||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
ICreateProviderCommand createProviderCommand,
|
ICreateProviderCommand createProviderCommand,
|
||||||
IFeatureService featureService,
|
|
||||||
IProviderPlanRepository providerPlanRepository,
|
IProviderPlanRepository providerPlanRepository,
|
||||||
IProviderBillingService providerBillingService,
|
IProviderBillingService providerBillingService,
|
||||||
IWebHostEnvironment webHostEnvironment,
|
IWebHostEnvironment webHostEnvironment,
|
||||||
IPricingClient pricingClient,
|
IPricingClient pricingClient,
|
||||||
IStripeAdapter stripeAdapter)
|
IStripeAdapter stripeAdapter,
|
||||||
|
IAccessControlService accessControlService,
|
||||||
|
ISubscriberService subscriberService)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_resellerClientOrganizationSignUpCommand = resellerClientOrganizationSignUpCommand;
|
_resellerClientOrganizationSignUpCommand = resellerClientOrganizationSignUpCommand;
|
||||||
@@ -78,14 +83,15 @@ public class ProvidersController : Controller
|
|||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
_createProviderCommand = createProviderCommand;
|
_createProviderCommand = createProviderCommand;
|
||||||
_featureService = featureService;
|
|
||||||
_providerPlanRepository = providerPlanRepository;
|
_providerPlanRepository = providerPlanRepository;
|
||||||
_providerBillingService = providerBillingService;
|
_providerBillingService = providerBillingService;
|
||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
_stripeAdapter = stripeAdapter;
|
_stripeAdapter = stripeAdapter;
|
||||||
|
_accessControlService = accessControlService;
|
||||||
_stripeUrl = webHostEnvironment.GetStripeUrl();
|
_stripeUrl = webHostEnvironment.GetStripeUrl();
|
||||||
_braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl();
|
_braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl();
|
||||||
_braintreeMerchantId = globalSettings.Braintree.MerchantId;
|
_braintreeMerchantId = globalSettings.Braintree.MerchantId;
|
||||||
|
_subscriberService = subscriberService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequirePermission(Permission.Provider_List_View)]
|
[RequirePermission(Permission.Provider_List_View)]
|
||||||
@@ -288,9 +294,31 @@ public class ProvidersController : Controller
|
|||||||
return View(oldModel);
|
return View(oldModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var originalProviderStatus = provider.Enabled;
|
||||||
|
|
||||||
model.ToProvider(provider);
|
model.ToProvider(provider);
|
||||||
|
|
||||||
await _providerRepository.ReplaceAsync(provider);
|
// validate the stripe ids to prevent saving a bad one
|
||||||
|
if (provider.IsBillable())
|
||||||
|
{
|
||||||
|
if (!await _subscriberService.IsValidGatewayCustomerIdAsync(provider))
|
||||||
|
{
|
||||||
|
var oldModel = await GetEditModel(id);
|
||||||
|
ModelState.AddModelError(nameof(model.GatewayCustomerId), $"Invalid Gateway Customer Id: {model.GatewayCustomerId}");
|
||||||
|
return View(oldModel);
|
||||||
|
}
|
||||||
|
if (!await _subscriberService.IsValidGatewaySubscriptionIdAsync(provider))
|
||||||
|
{
|
||||||
|
var oldModel = await GetEditModel(id);
|
||||||
|
ModelState.AddModelError(nameof(model.GatewaySubscriptionId), $"Invalid Gateway Subscription Id: {model.GatewaySubscriptionId}");
|
||||||
|
return View(oldModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.Enabled = _accessControlService.UserHasPermission(Permission.Provider_CheckEnabledBox)
|
||||||
|
? model.Enabled : originalProviderStatus;
|
||||||
|
|
||||||
|
await _providerService.UpdateAsync(provider);
|
||||||
await _applicationCacheService.UpsertProviderAbilityAsync(provider);
|
await _applicationCacheService.UpsertProviderAbilityAsync(provider);
|
||||||
|
|
||||||
if (!provider.IsBillable())
|
if (!provider.IsBillable())
|
||||||
@@ -311,21 +339,17 @@ public class ProvidersController : Controller
|
|||||||
]);
|
]);
|
||||||
await _providerBillingService.UpdateSeatMinimums(updateMspSeatMinimumsCommand);
|
await _providerBillingService.UpdateSeatMinimums(updateMspSeatMinimumsCommand);
|
||||||
|
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically))
|
var customer = await _stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId);
|
||||||
|
if (model.PayByInvoice != customer.ApprovedToPayByInvoice())
|
||||||
{
|
{
|
||||||
var customer = await _stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId);
|
var approvedToPayByInvoice = model.PayByInvoice ? "1" : "0";
|
||||||
|
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||||
if (model.PayByInvoice != customer.ApprovedToPayByInvoice())
|
|
||||||
{
|
{
|
||||||
var approvedToPayByInvoice = model.PayByInvoice ? "1" : "0";
|
Metadata = new Dictionary<string, string>
|
||||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
|
||||||
{
|
{
|
||||||
Metadata = new Dictionary<string, string>
|
[StripeConstants.MetadataKeys.InvoiceApproved] = approvedToPayByInvoice
|
||||||
{
|
}
|
||||||
[StripeConstants.MetadataKeys.InvoiceApproved] = approvedToPayByInvoice
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ProviderType.BusinessUnit:
|
case ProviderType.BusinessUnit:
|
||||||
@@ -370,10 +394,7 @@ public class ProvidersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
||||||
|
var payByInvoice = ((await _subscriberService.GetCustomer(provider))?.ApprovedToPayByInvoice() ?? false);
|
||||||
var payByInvoice =
|
|
||||||
_featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically) &&
|
|
||||||
(await _stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId)).ApprovedToPayByInvoice();
|
|
||||||
|
|
||||||
return new ProviderEditModel(
|
return new ProviderEditModel(
|
||||||
provider, users, providerOrganizations,
|
provider, users, providerOrganizations,
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.SharedWeb.Utilities;
|
using Bit.SharedWeb.Utilities;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.SharedWeb.Utilities;
|
using Bit.SharedWeb.Utilities;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user