mirror of
https://github.com/bitwarden/server.git
synced 2025-02-07 00:21:32 +01:00
Merge branch 'main' into ac/ac-1682/ef-migrations
# Conflicts: # src/Sql/dbo/Stored Procedures/Organization_EnableCollectionEnhancements.sql # util/SqliteMigrations/SqliteMigrations.csproj
This commit is contained in:
commit
39e336eddd
13
.checkmarx/config.yml
Normal file
13
.checkmarx/config.yml
Normal file
@ -0,0 +1,13 @@
|
||||
version: 1
|
||||
|
||||
# Checkmarx configuration file
|
||||
#
|
||||
# https://checkmarx.com/resource/documents/en/34965-68549-configuring-projects-using-config-as-code-files.html
|
||||
checkmarx:
|
||||
scan:
|
||||
configs:
|
||||
sast:
|
||||
# Exclude test directory
|
||||
filter: "!test"
|
||||
kics:
|
||||
filter: "!dev,!.devcontainer"
|
@ -7,7 +7,7 @@
|
||||
"commands": ["swagger"]
|
||||
},
|
||||
"dotnet-ef": {
|
||||
"version": "7.0.14",
|
||||
"version": "8.0.1",
|
||||
"commands": ["dotnet-ef"]
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ version: '3'
|
||||
|
||||
services:
|
||||
bitwarden_server:
|
||||
image: mcr.microsoft.com/devcontainers/dotnet:dev-6.0
|
||||
image: mcr.microsoft.com/devcontainers/dotnet:8.0
|
||||
volumes:
|
||||
- ../../:/workspace:cached
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
|
2
.github/codecov.yml
vendored
Normal file
2
.github/codecov.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
ignore:
|
||||
- "test" # Tests
|
59
.github/renovate.json
vendored
59
.github/renovate.json
vendored
@ -1,17 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base",
|
||||
":combinePatchMinorReleases",
|
||||
":dependencyDashboard",
|
||||
":maintainLockFilesWeekly",
|
||||
":pinAllExceptPeerDependencies",
|
||||
":prConcurrentLimit10",
|
||||
":rebaseStalePrs",
|
||||
":separateMajorReleases",
|
||||
"group:monorepos",
|
||||
"schedule:weekends"
|
||||
],
|
||||
"extends": ["github>bitwarden/renovate-config"],
|
||||
"enabledManagers": [
|
||||
"dockerfile",
|
||||
"docker-compose",
|
||||
@ -19,8 +8,6 @@
|
||||
"npm",
|
||||
"nuget"
|
||||
],
|
||||
"commitMessagePrefix": "[deps]:",
|
||||
"commitMessageTopic": "{{depName}}",
|
||||
"packageRules": [
|
||||
{
|
||||
"groupName": "dockerfile minor",
|
||||
@ -112,27 +99,13 @@
|
||||
"groupName": "Microsoft.Extensions.Logging",
|
||||
"description": "Group Microsoft.Extensions.Logging to exclude them from the dotnet monorepo preset"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["CommandDotNet", "dbup-sqlserver", "YamlDotNet"],
|
||||
"description": "DevOps owned dependencies",
|
||||
"commitMessagePrefix": "[deps] DevOps:",
|
||||
"reviewers": ["team:team-devops"]
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer",
|
||||
"Microsoft.AspNetCore.Http",
|
||||
"Microsoft.Data.SqlClient"
|
||||
],
|
||||
"description": "Platform owned dependencies",
|
||||
"commitMessagePrefix": "[deps] Platform:",
|
||||
"reviewers": ["team:team-platform-dev"]
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"Dapper",
|
||||
"dbup-sqlserver",
|
||||
"dotnet-ef",
|
||||
"linq2db.EntityFrameworkCore",
|
||||
"Microsoft.Data.SqlClient",
|
||||
"Microsoft.EntityFrameworkCore.Design",
|
||||
"Microsoft.EntityFrameworkCore.InMemory",
|
||||
"Microsoft.EntityFrameworkCore.Relational",
|
||||
@ -141,9 +114,24 @@
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL",
|
||||
"Pomelo.EntityFrameworkCore.MySql"
|
||||
],
|
||||
"description": "Secrets Manager owned dependencies",
|
||||
"commitMessagePrefix": "[deps] SM:",
|
||||
"reviewers": ["team:team-secrets-manager-dev"]
|
||||
"description": "DbOps owned dependencies",
|
||||
"commitMessagePrefix": "[deps] DbOps:",
|
||||
"reviewers": ["team:dept-dbops"]
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["CommandDotNet", "YamlDotNet"],
|
||||
"description": "DevOps owned dependencies",
|
||||
"commitMessagePrefix": "[deps] DevOps:",
|
||||
"reviewers": ["team:dept-devops"]
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer",
|
||||
"Microsoft.AspNetCore.Http"
|
||||
],
|
||||
"description": "Platform owned dependencies",
|
||||
"commitMessagePrefix": "[deps] Platform:",
|
||||
"reviewers": ["team:team-platform-dev"]
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["EntityFrameworkCore", "^dotnet-ef"],
|
||||
@ -203,10 +191,5 @@
|
||||
"reviewers": ["team:team-vault-dev"]
|
||||
}
|
||||
],
|
||||
"force": {
|
||||
"constraints": {
|
||||
"dotnet": "6.0.100"
|
||||
}
|
||||
},
|
||||
"ignoreDeps": ["dotnet-sdk"]
|
||||
}
|
||||
|
7
.github/test/on-master-event.json
vendored
7
.github/test/on-master-event.json
vendored
@ -1,7 +0,0 @@
|
||||
{
|
||||
"release": {
|
||||
"head": {
|
||||
"ref": "master"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
---
|
||||
|
||||
name: _move_finalization_db_scripts
|
||||
run-name: Move finalization db scripts
|
||||
run-name: Move finalization database scripts
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@ -11,7 +10,6 @@ permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
|
||||
setup:
|
||||
name: Setup
|
||||
runs-on: ubuntu-22.04
|
||||
@ -19,7 +17,7 @@ jobs:
|
||||
migration_filename_prefix: ${{ steps.prefix.outputs.prefix }}
|
||||
copy_finalization_scripts: ${{ steps.check-finalization-scripts-existence.outputs.copy_finalization_scripts }}
|
||||
steps:
|
||||
- name: Login to Azure
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@de95379fe4dadc2defb305917eaa7e5dde727294 # v1.5.1
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
@ -31,7 +29,7 @@ jobs:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||
|
||||
- name: Checkout Branch
|
||||
- name: Check out branch
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
@ -40,7 +38,7 @@ jobs:
|
||||
id: prefix
|
||||
run: echo "prefix=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check if any files in db finalization
|
||||
- name: Check if any files in DB finalization directory
|
||||
id: check-finalization-scripts-existence
|
||||
run: |
|
||||
if [ -f util/Migrator/DbScripts_finalization/* ]; then
|
||||
@ -50,7 +48,7 @@ jobs:
|
||||
fi
|
||||
|
||||
move-finalization-db-scripts:
|
||||
name: Move finalization db scripts
|
||||
name: Move finalization database scripts
|
||||
runs-on: ubuntu-22.04
|
||||
needs: setup
|
||||
if: ${{ needs.setup.outputs.copy_finalization_scripts == 'true' }}
|
||||
@ -95,12 +93,12 @@ jobs:
|
||||
done
|
||||
echo "moved_files=$moved_files" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Azure - Prod Subscription
|
||||
- name: Log in to Azure - production subscription
|
||||
uses: Azure/login@de95379fe4dadc2defb305917eaa7e5dde727294 # v1.5.1
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve Secrets
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
@ -140,7 +138,7 @@ jobs:
|
||||
BRANCH: ${{ steps.branch_name.outputs.branch_name }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
MOVED_FILES: ${{ steps.move-files.outputs.moved_files }}
|
||||
TITLE: "Move finalization db scripts"
|
||||
TITLE: "Move finalization database scripts"
|
||||
run: |
|
||||
PR_URL=$(gh pr create --title "$TITLE" \
|
||||
--base "main" \
|
||||
|
@ -6,8 +6,8 @@ on:
|
||||
- labeled
|
||||
jobs:
|
||||
close-issue:
|
||||
name: 'Close issue with automatic response'
|
||||
runs-on: ubuntu-20.04
|
||||
name: Close issue with automatic response
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
@ -24,7 +24,7 @@ jobs:
|
||||
This issue will now be closed. Thanks!
|
||||
# Intended behavior
|
||||
- if: github.event.label.name == 'intended-behavior'
|
||||
name: Intended behaviour
|
||||
name: Intended behavior
|
||||
uses: peter-evans/close-issue@1373cadf1f0c96c1420bc000cfba2273ea307fd1 # v2.2.0
|
||||
with:
|
||||
comment: |
|
||||
|
128
.github/workflows/build.yml
vendored
128
.github/workflows/build.yml
vendored
@ -2,93 +2,35 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- "l10n_master"
|
||||
- "gh-pages"
|
||||
paths-ignore:
|
||||
- ".github/workflows/**"
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "rc"
|
||||
- "hotfix-rc"
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
||||
|
||||
jobs:
|
||||
cloc:
|
||||
name: CLOC
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
|
||||
- name: Install cloc
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install cloc
|
||||
|
||||
- name: Print lines of code
|
||||
run: cloc --include-lang C#,SQL,Razor,"Bourne Shell",PowerShell,HTML,CSS,Sass,JavaScript,TypeScript --vcs git
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
|
||||
- name: Set up dotnet
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
|
||||
- name: Verify Format
|
||||
- name: Verify format
|
||||
run: dotnet format --verify-no-changes
|
||||
|
||||
testing:
|
||||
name: Testing
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
|
||||
- name: Set up dotnet
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
dotnet --info
|
||||
nuget help | grep Version
|
||||
echo "GitHub ref: $GITHUB_REF"
|
||||
echo "GitHub event: $GITHUB_EVENT"
|
||||
|
||||
- name: Remove SQL proj
|
||||
run: dotnet sln bitwarden-server.sln remove src/Sql/Sql.sqlproj
|
||||
|
||||
- name: Test OSS solution
|
||||
run: dotnet test ./test --configuration Release --logger "trx;LogFileName=oss-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
|
||||
|
||||
- name: Test Bitwarden solution
|
||||
run: dotnet test ./bitwarden_license/test --configuration Release --logger "trx;LogFileName=bw-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226 # v1.6.0
|
||||
if: always()
|
||||
with:
|
||||
name: Test Results
|
||||
path: "**/*-test-results.trx"
|
||||
reporter: dotnet-trx
|
||||
fail-on-error: true
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
build-artifacts:
|
||||
name: Build artifacts
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- testing
|
||||
- lint
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@ -125,10 +67,10 @@ jobs:
|
||||
base_path: ./bitwarden_license/src
|
||||
node: true
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
|
||||
- name: Set up dotnet
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
|
||||
- name: Set up Node
|
||||
@ -228,7 +170,7 @@ jobs:
|
||||
base_path: ./bitwarden_license/src
|
||||
dotnet: true
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
|
||||
- name: Check Branch to Publish
|
||||
@ -245,7 +187,7 @@ jobs:
|
||||
fi
|
||||
|
||||
########## ACRs ##########
|
||||
- name: Login to Azure - PROD Subscription
|
||||
- name: Log in to Azure - production subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
@ -253,7 +195,7 @@ jobs:
|
||||
- name: Login to PROD ACR
|
||||
run: az acr login -n bitwardenprod
|
||||
|
||||
- name: Login to Azure - CI Subscription
|
||||
- name: Log in to Azure - CI subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
@ -269,13 +211,20 @@ jobs:
|
||||
- name: Generate Docker image tag
|
||||
id: tag
|
||||
run: |
|
||||
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") # slash safe branch name
|
||||
if [[ $(grep "pull" <<< "${GITHUB_REF}") ]]; then
|
||||
IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s#/#-#g")
|
||||
else
|
||||
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
|
||||
fi
|
||||
|
||||
if [[ "$IMAGE_TAG" == "main" ]]; then
|
||||
IMAGE_TAG=dev
|
||||
fi
|
||||
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup project name
|
||||
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
||||
echo "### :mega: Docker Image Tag: $IMAGE_TAG" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Set up project name
|
||||
id: setup
|
||||
run: |
|
||||
PROJECT_NAME=$(echo "${{ matrix.project_name }}" | awk '{print tolower($0)}')
|
||||
@ -303,7 +252,7 @@ jobs:
|
||||
with:
|
||||
name: ${{ matrix.project_name }}.zip
|
||||
|
||||
- name: Setup build artifact
|
||||
- name: Set up build artifact
|
||||
if: ${{ matrix.dotnet }}
|
||||
run: |
|
||||
mkdir -p ${{ matrix.base_path}}/${{ matrix.project_name }}/obj/build-output/publish
|
||||
@ -326,13 +275,13 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: build-docker
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
|
||||
- name: Set up dotnet
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
|
||||
- name: Login to Azure - PROD Subscription
|
||||
- name: Log in to Azure - production subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
@ -445,7 +394,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
build-mssqlmigratorutility:
|
||||
name: Build MsSqlMigratorUtility
|
||||
name: Build MSSQL migrator utility
|
||||
runs-on: ubuntu-22.04
|
||||
needs: lint
|
||||
defaults:
|
||||
@ -460,10 +409,10 @@ jobs:
|
||||
- linux-x64
|
||||
- win-x64
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
|
||||
- name: Set up dotnet
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
|
||||
- name: Print environment
|
||||
@ -478,7 +427,7 @@ jobs:
|
||||
dotnet publish -c "Release" -o obj/build-output/publish -r ${{ matrix.target }} -p:PublishSingleFile=true \
|
||||
-p:IncludeNativeLibrariesForSelfExtract=true --self-contained true
|
||||
|
||||
- name: Upload project artifact Windows
|
||||
- name: Upload project artifact for Windows
|
||||
if: ${{ contains(matrix.target, 'win') == true }}
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
@ -499,7 +448,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: build-docker
|
||||
steps:
|
||||
- name: Login to Azure - CI Subscription
|
||||
- name: Log in to Azure - CI subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
@ -532,7 +481,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: build-docker
|
||||
steps:
|
||||
- name: Login to Azure - CI Subscription
|
||||
- name: Log in to Azure - CI subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
@ -565,9 +514,7 @@ jobs:
|
||||
if: always()
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- cloc
|
||||
- lint
|
||||
- testing
|
||||
- build-artifacts
|
||||
- build-docker
|
||||
- upload
|
||||
@ -581,7 +528,6 @@ jobs:
|
||||
|| github.ref == 'refs/heads/rc'
|
||||
|| github.ref == 'refs/heads/hotfix-rc'
|
||||
env:
|
||||
CLOC_STATUS: ${{ needs.cloc.result }}
|
||||
LINT_STATUS: ${{ needs.lint.result }}
|
||||
TESTING_STATUS: ${{ needs.testing.result }}
|
||||
BUILD_ARTIFACTS_STATUS: ${{ needs.build-artifacts.result }}
|
||||
@ -591,9 +537,7 @@ jobs:
|
||||
TRIGGER_SELF_HOST_BUILD_STATUS: ${{ needs.self-host-build.result }}
|
||||
TRIGGER_K8S_DEPLOY_STATUS: ${{ needs.trigger-k8s-deploy.result }}
|
||||
run: |
|
||||
if [ "$CLOC_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$LINT_STATUS" = "failure" ]; then
|
||||
if [ "$LINT_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$TESTING_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
@ -611,7 +555,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Login to Azure - CI subscription
|
||||
- name: Log in to Azure - CI subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
if: failure()
|
||||
with:
|
||||
|
22
.github/workflows/cleanup-after-pr.yml
vendored
22
.github/workflows/cleanup-after-pr.yml
vendored
@ -1,5 +1,5 @@
|
||||
---
|
||||
name: Clean After PR
|
||||
name: Container registry cleanup
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@ -7,32 +7,30 @@ on:
|
||||
|
||||
jobs:
|
||||
build-docker:
|
||||
name: Remove feature branch docker images
|
||||
runs-on: ubuntu-20.04
|
||||
name: Remove branch-specific Docker images
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
|
||||
########## ACR ##########
|
||||
- name: Login to Azure - QA Subscription
|
||||
- name: Log in to Azure - QA Subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
|
||||
|
||||
- name: Login to Azure ACR
|
||||
- name: Log in to Azure ACR
|
||||
run: az acr login -n bitwardenqa
|
||||
|
||||
- name: Login to Azure - PROD Subscription
|
||||
- name: Log in to Azure - production subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
|
||||
- name: Login to Azure ACR
|
||||
- name: Log in to Azure ACR
|
||||
run: az acr login -n bitwardenprod
|
||||
|
||||
########## Remove Docker images ##########
|
||||
- name: Remove the docker image from ACR
|
||||
- name: Remove the Docker image from ACR
|
||||
env:
|
||||
REF: ${{ github.event.pull_request.head.ref }}
|
||||
REGISTRIES: |
|
||||
registries:
|
||||
- bitwardenprod
|
||||
@ -59,7 +57,7 @@ jobs:
|
||||
for REGISTRY in $( echo "${{ env.REGISTRIES }}" | yq e ".registries[]" - )
|
||||
do
|
||||
SERVICE_NAME=$(echo $SERVICE | awk '{print tolower($0)}')
|
||||
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") # slash safe branch name
|
||||
IMAGE_TAG=$(echo "${REF}" | sed "s#/#-#g") # slash safe branch name
|
||||
|
||||
echo "[*] Checking if remote exists: $REGISTRY.azurecr.io/$SERVICE_NAME:$IMAGE_TAG"
|
||||
TAG_EXISTS=$(
|
||||
|
42
.github/workflows/code-references.yml
vendored
Normal file
42
.github/workflows/code-references.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
---
|
||||
name: Collect code references
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- "renovate/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
refs:
|
||||
name: Code reference collection
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Collect
|
||||
id: collect
|
||||
uses: launchdarkly/find-code-references-in-pull-request@2e9333c88539377cfbe818c265ba8b9ebced3c91 # v1.1.0
|
||||
with:
|
||||
project-key: default
|
||||
environment-key: dev
|
||||
access-token: ${{ secrets.LD_ACCESS_TOKEN }}
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Add label
|
||||
if: steps.collect.outputs.any-changed == 'true'
|
||||
run: gh pr edit $PR_NUMBER --add-label feature-flag
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
- name: Remove label
|
||||
if: steps.collect.outputs.any-changed == 'false'
|
||||
run: gh pr edit $PR_NUMBER --remove-label feature-flag
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
12
.github/workflows/container-registry-purge.yml
vendored
12
.github/workflows/container-registry-purge.yml
vendored
@ -1,18 +1,18 @@
|
||||
---
|
||||
name: Container Registry Purge
|
||||
name: Container registry purge
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * SUN'
|
||||
- cron: "0 0 * * SUN"
|
||||
workflow_dispatch:
|
||||
inputs: {}
|
||||
|
||||
jobs:
|
||||
purge:
|
||||
name: Purge old images
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Login to Azure
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
@ -68,7 +68,7 @@ jobs:
|
||||
check-failures:
|
||||
name: Check for failures
|
||||
if: always()
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- purge
|
||||
steps:
|
||||
@ -84,7 +84,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Login to Azure - CI subscription
|
||||
- name: Log in to Azure - CI subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
if: failure()
|
||||
with:
|
||||
|
95
.github/workflows/database.yml
vendored
95
.github/workflows/database.yml
vendored
@ -1,95 +0,0 @@
|
||||
---
|
||||
name: Validate Database
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- 'l10n_master'
|
||||
- 'gh-pages'
|
||||
paths:
|
||||
- 'src/Sql/**'
|
||||
- 'util/Migrator/**'
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'rc'
|
||||
paths:
|
||||
- 'src/Sql/**'
|
||||
- 'util/Migrator/**'
|
||||
workflow_dispatch:
|
||||
inputs: {}
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Validate
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
|
||||
- name: Set up dotnet
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
dotnet --info
|
||||
nuget help | grep Version
|
||||
echo "GitHub ref: $GITHUB_REF"
|
||||
echo "GitHub event: $GITHUB_EVENT"
|
||||
|
||||
- name: Build DACPAC
|
||||
run: dotnet build src/Sql --configuration Release --verbosity minimal --output .
|
||||
shell: pwsh
|
||||
|
||||
- name: Upload DACPAC
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: sql.dacpac
|
||||
path: Sql.dacpac
|
||||
|
||||
- name: Docker Compose up
|
||||
working-directory: "dev"
|
||||
run: |
|
||||
cp .env.example .env
|
||||
docker compose --profile mssql up -d
|
||||
shell: pwsh
|
||||
|
||||
- name: Migrate
|
||||
working-directory: "dev"
|
||||
run: "pwsh ./migrate.ps1"
|
||||
shell: pwsh
|
||||
|
||||
- name: Diff sqlproj to migrations
|
||||
run: /usr/local/sqlpackage/sqlpackage /action:DeployReport /SourceFile:"Sql.dacpac" /TargetConnectionString:"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;" /OutputPath:"report.xml" /p:IgnoreColumnOrder=True /p:IgnoreComments=True
|
||||
shell: pwsh
|
||||
|
||||
- name: Generate SQL file
|
||||
run: /usr/local/sqlpackage/sqlpackage /action:Script /SourceFile:"Sql.dacpac" /TargetConnectionString:"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;" /OutputPath:"diff.sql" /p:IgnoreColumnOrder=True /p:IgnoreComments=True
|
||||
shell: pwsh
|
||||
|
||||
- name: Upload Report
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: report.xml
|
||||
path: |
|
||||
report.xml
|
||||
diff.sql
|
||||
|
||||
- name: Validate XML
|
||||
run: |
|
||||
if grep -q "<Operations>" "report.xml"; then
|
||||
echo
|
||||
echo "Migrations are out of sync with sqlproj!"
|
||||
exit 1
|
||||
else
|
||||
echo "Report looks good"
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Docker compose down
|
||||
if: ${{ always() }}
|
||||
working-directory: "dev"
|
||||
run: docker compose down
|
||||
shell: pwsh
|
19
.github/workflows/enforce-labels.yml
vendored
19
.github/workflows/enforce-labels.yml
vendored
@ -2,15 +2,18 @@
|
||||
name: Enforce PR labels
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
pull_request:
|
||||
types: [labeled, unlabeled, opened, edited, synchronize]
|
||||
|
||||
types: [labeled, unlabeled, opened, reopened, synchronize]
|
||||
jobs:
|
||||
enforce-label:
|
||||
name: EnforceLabel
|
||||
runs-on: ubuntu-20.04
|
||||
if: ${{ contains(github.event.*.labels.*.name, 'hold') || contains(github.event.*.labels.*.name, 'needs-qa') || contains(github.event.*.labels.*.name, 'DB-migrations-changed') }}
|
||||
name: Enforce label
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Enforce Label
|
||||
uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024 # 2.2.2
|
||||
with:
|
||||
BANNED_LABELS: "hold,DB-migrations-changed,needs-qa"
|
||||
- name: Check for label
|
||||
run: |
|
||||
echo "PRs with the hold or needs-qa labels cannot be merged"
|
||||
echo "### :x: PRs with the hold or needs-qa labels cannot be merged" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
|
117
.github/workflows/infrastructure-tests.yml
vendored
117
.github/workflows/infrastructure-tests.yml
vendored
@ -1,117 +0,0 @@
|
||||
---
|
||||
name: Run Database Infrastructure Tests
|
||||
on:
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- 'l10n_master'
|
||||
- 'gh-pages'
|
||||
paths:
|
||||
- '.github/workflows/infrastructure-tests.yml' # This file
|
||||
- 'src/Sql/**' # SQL Server Database Changes
|
||||
- 'util/Migrator/**' # New SQL Server Migrations
|
||||
- 'util/MySqlMigrations/**' # Changes to MySQL
|
||||
- 'util/PostgresMigrations/**' # Changes to Postgres
|
||||
- 'util/SqliteMigrations/**' # Changes to Sqlite
|
||||
- 'src/Infrastructure.Dapper/**' # Changes to SQL Server Dapper Repository Layer
|
||||
- 'src/Infrastructure.EntityFramework/**' # Changes to Entity Framework Repository Layer
|
||||
- 'test/Infrastructure.IntegrationTest/**' # Any changes to the tests
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'rc'
|
||||
paths:
|
||||
- '.github/workflows/infrastructure-tests.yml' # This file
|
||||
- 'src/Sql/**' # SQL Server Database Changes
|
||||
- 'util/Migrator/**' # New SQL Server Migrations
|
||||
- 'util/MySqlMigrations/**' # Changes to MySQL
|
||||
- 'util/PostgresMigrations/**' # Changes to Postgres
|
||||
- 'util/SqliteMigrations/**' # Changes to Sqlite
|
||||
- 'src/Infrastructure.Dapper/**' # Changes to SQL Server Dapper Repository Layer
|
||||
- 'src/Infrastructure.EntityFramework/**' # Changes to Entity Framework Repository Layer
|
||||
- 'test/Infrastructure.IntegrationTest/**' # Any changes to the tests
|
||||
workflow_dispatch:
|
||||
inputs: {}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: 'Run Infrastructure.IntegrationTest'
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
|
||||
- name: Set up dotnet
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
- name: Restore Tools
|
||||
run: dotnet tool restore
|
||||
|
||||
- name: Compose Databases
|
||||
working-directory: 'dev'
|
||||
# We could think about not using profiles and pulling images directly to cover multiple versions
|
||||
run: |
|
||||
cp .env.example .env
|
||||
docker compose --profile mssql --profile postgres --profile mysql up -d
|
||||
shell: pwsh
|
||||
|
||||
# I've seen the SQL Server container not be ready for commands right after starting up and just needing a bit longer to be ready
|
||||
- name: Sleep
|
||||
run: sleep 15s
|
||||
|
||||
- name: Migrate SQL Server
|
||||
working-directory: 'dev'
|
||||
run: "pwsh ./migrate.ps1"
|
||||
shell: pwsh
|
||||
|
||||
- name: Migrate MySQL
|
||||
working-directory: 'util/MySqlMigrations'
|
||||
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:MySql:ConnectionString="$CONN_STR"'
|
||||
env:
|
||||
CONN_STR: "server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev;Allow User Variables=true"
|
||||
|
||||
- name: Migrate Postgres
|
||||
working-directory: 'util/PostgresMigrations'
|
||||
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:PostgreSql:ConnectionString="$CONN_STR"'
|
||||
env:
|
||||
CONN_STR: "Host=localhost;Username=postgres;Password=SET_A_PASSWORD_HERE_123;Database=vault_dev"
|
||||
|
||||
- name: Migrate Sqlite
|
||||
working-directory: 'util/SqliteMigrations'
|
||||
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:Sqlite:ConnectionString="$CONN_STR"'
|
||||
env:
|
||||
CONN_STR: "Data Source=${{ runner.temp }}/test.db"
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: 'test/Infrastructure.IntegrationTest'
|
||||
env:
|
||||
# Default Postgres:
|
||||
BW_TEST_DATABASES__0__TYPE: "Postgres"
|
||||
BW_TEST_DATABASES__0__CONNECTIONSTRING: "Host=localhost;Username=postgres;Password=SET_A_PASSWORD_HERE_123;Database=vault_dev"
|
||||
# Default MySql
|
||||
BW_TEST_DATABASES__1__TYPE: "MySql"
|
||||
BW_TEST_DATABASES__1__CONNECTIONSTRING: "server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev"
|
||||
# Default Dapper SqlServer
|
||||
BW_TEST_DATABASES__2__TYPE: "SqlServer"
|
||||
BW_TEST_DATABASES__2__CONNECTIONSTRING: "Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;"
|
||||
# Default Sqlite
|
||||
BW_TEST_DATABASES__3__TYPE: "Sqlite"
|
||||
BW_TEST_DATABASES__3__CONNECTIONSTRING: "Data Source=${{ runner.temp }}/test.db"
|
||||
run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx"
|
||||
shell: pwsh
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226 # v1.6.0
|
||||
if: always()
|
||||
with:
|
||||
name: Test Results
|
||||
path: "**/*-test-results.trx"
|
||||
reporter: dotnet-trx
|
||||
fail-on-error: true
|
||||
|
||||
- name: Docker compose down
|
||||
if: always()
|
||||
working-directory: "dev"
|
||||
run: docker compose down
|
||||
shell: pwsh
|
7
.github/workflows/protect-files.yml
vendored
7
.github/workflows/protect-files.yml
vendored
@ -2,8 +2,7 @@
|
||||
# Starts a matrix job to check for modified files, then sets output based on the results.
|
||||
# The input decides if the label job is ran, adding a label to the PR.
|
||||
---
|
||||
|
||||
name: Protect Files
|
||||
name: Protect files
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@ -17,7 +16,7 @@ on:
|
||||
jobs:
|
||||
changed-files:
|
||||
name: Check for file changes
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
changes: ${{steps.check-changes.outputs.changes_detected}}
|
||||
|
||||
@ -29,7 +28,7 @@ jobs:
|
||||
path: util/Migrator/DbScripts
|
||||
label: "DB-migrations-changed"
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
34
.github/workflows/release.yml
vendored
34
.github/workflows/release.yml
vendored
@ -16,7 +16,7 @@ on:
|
||||
- Dry Run
|
||||
|
||||
env:
|
||||
_AZ_REGISTRY: 'bitwardenprod.azurecr.io'
|
||||
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
@ -36,10 +36,10 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout repo
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
|
||||
- name: Check Release Version
|
||||
- name: Check release version
|
||||
id: version
|
||||
uses: bitwarden/gh-actions/release-version-check@main
|
||||
with:
|
||||
@ -87,7 +87,7 @@ jobs:
|
||||
task: "deploy"
|
||||
description: "Deploy from ${{ needs.setup.outputs.branch-name }} branch"
|
||||
|
||||
- name: Download latest Release ${{ matrix.name }} asset
|
||||
- name: Download latest release ${{ matrix.name }} asset
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
@ -96,7 +96,7 @@ jobs:
|
||||
branch: ${{ needs.setup.outputs.branch-name }}
|
||||
artifacts: ${{ matrix.name }}.zip
|
||||
|
||||
- name: Dry Run - Download latest Release ${{ matrix.name }} asset
|
||||
- name: Dry run - Download latest release ${{ matrix.name }} asset
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
@ -105,7 +105,7 @@ jobs:
|
||||
branch: main
|
||||
artifacts: ${{ matrix.name }}.zip
|
||||
|
||||
- name: Login to Azure - CI subscription
|
||||
- name: Log in to Azure - CI subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
@ -130,12 +130,12 @@ jobs:
|
||||
echo "::add-mask::$publish_profile"
|
||||
echo "publish-profile=$publish_profile" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Azure
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
|
||||
- name: Deploy App
|
||||
- name: Deploy app
|
||||
uses: azure/webapps-deploy@4bca689e4c7129e55923ea9c45401b22dc6aa96f # v2.2.11
|
||||
with:
|
||||
app-name: ${{ steps.retrieve-secrets.outputs.webapp-name }}
|
||||
@ -156,7 +156,7 @@ jobs:
|
||||
fi
|
||||
az webapp start -n $WEBAPP_NAME -g $RESOURCE_GROUP -s staging
|
||||
|
||||
- name: Update ${{ matrix.name }} deployment status to Success
|
||||
- name: Update ${{ matrix.name }} deployment status to success
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }}
|
||||
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
|
||||
with:
|
||||
@ -164,7 +164,7 @@ jobs:
|
||||
state: "success"
|
||||
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
|
||||
- name: Update ${{ matrix.name }} deployment status to Failure
|
||||
- name: Update ${{ matrix.name }} deployment status to failure
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }}
|
||||
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
|
||||
with:
|
||||
@ -210,10 +210,10 @@ jobs:
|
||||
echo "GitHub event: $GITHUB_EVENT"
|
||||
echo "Github Release Option: $RELEASE_OPTION"
|
||||
|
||||
- name: Checkout repo
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
|
||||
- name: Setup project name
|
||||
- name: Set up project name
|
||||
id: setup
|
||||
run: |
|
||||
PROJECT_NAME=$(echo "${{ matrix.project_name }}" | awk '{print tolower($0)}')
|
||||
@ -222,12 +222,12 @@ jobs:
|
||||
echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
########## ACR PROD ##########
|
||||
- name: Login to Azure - PROD Subscription
|
||||
- name: Log in to Azure - production subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
|
||||
- name: Login to Azure ACR
|
||||
- name: Log in to Azure ACR
|
||||
run: az acr login -n $_AZ_REGISTRY --only-show-errors
|
||||
|
||||
- name: Pull latest project image
|
||||
@ -266,13 +266,13 @@ jobs:
|
||||
run: docker logout
|
||||
|
||||
release:
|
||||
name: Create GitHub Release
|
||||
name: Create GitHub release
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- setup
|
||||
- deploy
|
||||
steps:
|
||||
- name: Download latest Release Docker Stubs
|
||||
- name: Download latest release Docker stubs
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
@ -285,7 +285,7 @@ jobs:
|
||||
docker-stub-EU-sha256.txt,
|
||||
swagger.json"
|
||||
|
||||
- name: Dry Run - Download latest Release Docker Stubs
|
||||
- name: Dry Run - Download latest release Docker stubs
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
|
14
.github/workflows/stale-bot.yml
vendored
14
.github/workflows/stale-bot.yml
vendored
@ -1,20 +1,20 @@
|
||||
---
|
||||
name: 'Close stale issues and PRs'
|
||||
name: Staleness
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule: # Run once a day at 5.23am (arbitrary but should avoid peak loads on the hour)
|
||||
- cron: '23 5 * * *'
|
||||
- cron: "23 5 * * *"
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
name: 'Check for stale issues and PRs'
|
||||
runs-on: ubuntu-20.04
|
||||
name: Check for stale issues and PRs
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: 'Run stale action'
|
||||
- name: Check
|
||||
uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
|
||||
with:
|
||||
stale-issue-label: 'needs-reply'
|
||||
stale-pr-label: 'needs-changes'
|
||||
stale-issue-label: "needs-reply"
|
||||
stale-pr-label: "needs-changes"
|
||||
days-before-stale: -1 # Do not apply the stale labels automatically, this is a manual process
|
||||
days-before-issue-close: 14 # Close issue if no further activity after X days
|
||||
days-before-pr-close: 21 # Close PR if no further activity after X days
|
||||
|
10
.github/workflows/stop-staging-slots.yml
vendored
10
.github/workflows/stop-staging-slots.yml
vendored
@ -1,5 +1,5 @@
|
||||
---
|
||||
name: Stop Staging Slots
|
||||
name: Stop staging slots
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@ -7,8 +7,8 @@ on:
|
||||
|
||||
jobs:
|
||||
stop-slots:
|
||||
name: Stop Slots
|
||||
runs-on: ubuntu-20.04
|
||||
name: Stop slots
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@ -28,7 +28,7 @@ jobs:
|
||||
echo "NAME_LOWER: $NAME_LOWER"
|
||||
echo "name_lower=$NAME_LOWER" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Azure - CI Subscription
|
||||
- name: Log in to Azure - CI subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
@ -46,7 +46,7 @@ jobs:
|
||||
echo "::add-mask::$webapp_name"
|
||||
echo "webapp-name=$webapp_name" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Azure
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
|
185
.github/workflows/test-database.yml
vendored
Normal file
185
.github/workflows/test-database.yml
vendored
Normal file
@ -0,0 +1,185 @@
|
||||
---
|
||||
name: Database testing
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "rc"
|
||||
- "hotfix-rc"
|
||||
paths:
|
||||
- ".github/workflows/infrastructure-tests.yml" # This file
|
||||
- "src/Sql/**" # SQL Server Database Changes
|
||||
- "util/Migrator/**" # New SQL Server Migrations
|
||||
- "util/MySqlMigrations/**" # Changes to MySQL
|
||||
- "util/PostgresMigrations/**" # Changes to Postgres
|
||||
- "util/SqliteMigrations/**" # Changes to Sqlite
|
||||
- "src/Infrastructure.Dapper/**" # Changes to SQL Server Dapper Repository Layer
|
||||
- "src/Infrastructure.EntityFramework/**" # Changes to Entity Framework Repository Layer
|
||||
- "test/Infrastructure.IntegrationTest/**" # Any changes to the tests
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/infrastructure-tests.yml" # This file
|
||||
- "src/Sql/**" # SQL Server Database Changes
|
||||
- "util/Migrator/**" # New SQL Server Migrations
|
||||
- "util/MySqlMigrations/**" # Changes to MySQL
|
||||
- "util/PostgresMigrations/**" # Changes to Postgres
|
||||
- "util/SqliteMigrations/**" # Changes to Sqlite
|
||||
- "src/Infrastructure.Dapper/**" # Changes to SQL Server Dapper Repository Layer
|
||||
- "src/Infrastructure.EntityFramework/**" # Changes to Entity Framework Repository Layer
|
||||
- "test/Infrastructure.IntegrationTest/**" # Any changes to the tests
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
|
||||
- name: Restore tools
|
||||
run: dotnet tool restore
|
||||
|
||||
- name: Docker Compose databases
|
||||
working-directory: "dev"
|
||||
# We could think about not using profiles and pulling images directly to cover multiple versions
|
||||
run: |
|
||||
cp .env.example .env
|
||||
docker compose --profile mssql --profile postgres --profile mysql up -d
|
||||
shell: pwsh
|
||||
|
||||
# I've seen the SQL Server container not be ready for commands right after starting up and just needing a bit longer to be ready
|
||||
- name: Sleep
|
||||
run: sleep 15s
|
||||
|
||||
- name: Migrate SQL Server
|
||||
working-directory: "dev"
|
||||
run: "./migrate.ps1"
|
||||
shell: pwsh
|
||||
|
||||
- name: Migrate MySQL
|
||||
working-directory: "util/MySqlMigrations"
|
||||
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:MySql:ConnectionString="$CONN_STR"'
|
||||
env:
|
||||
CONN_STR: "server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev;Allow User Variables=true"
|
||||
|
||||
- name: Migrate Postgres
|
||||
working-directory: "util/PostgresMigrations"
|
||||
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:PostgreSql:ConnectionString="$CONN_STR"'
|
||||
env:
|
||||
CONN_STR: "Host=localhost;Username=postgres;Password=SET_A_PASSWORD_HERE_123;Database=vault_dev"
|
||||
|
||||
- name: Migrate SQLite
|
||||
working-directory: "util/SqliteMigrations"
|
||||
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:Sqlite:ConnectionString="$CONN_STR"'
|
||||
env:
|
||||
CONN_STR: "Data Source=${{ runner.temp }}/test.db"
|
||||
|
||||
- name: Run tests
|
||||
working-directory: "test/Infrastructure.IntegrationTest"
|
||||
env:
|
||||
# Default Postgres:
|
||||
BW_TEST_DATABASES__0__TYPE: "Postgres"
|
||||
BW_TEST_DATABASES__0__CONNECTIONSTRING: "Host=localhost;Username=postgres;Password=SET_A_PASSWORD_HERE_123;Database=vault_dev"
|
||||
# Default MySql
|
||||
BW_TEST_DATABASES__1__TYPE: "MySql"
|
||||
BW_TEST_DATABASES__1__CONNECTIONSTRING: "server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev"
|
||||
# Default Dapper SqlServer
|
||||
BW_TEST_DATABASES__2__TYPE: "SqlServer"
|
||||
BW_TEST_DATABASES__2__CONNECTIONSTRING: "Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;"
|
||||
# Default Sqlite
|
||||
BW_TEST_DATABASES__3__TYPE: "Sqlite"
|
||||
BW_TEST_DATABASES__3__CONNECTIONSTRING: "Data Source=${{ runner.temp }}/test.db"
|
||||
run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx"
|
||||
shell: pwsh
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226 # v1.6.0
|
||||
if: always()
|
||||
with:
|
||||
name: Test Results
|
||||
path: "**/*-test-results.trx"
|
||||
reporter: dotnet-trx
|
||||
fail-on-error: true
|
||||
|
||||
- name: Docker Compose down
|
||||
if: always()
|
||||
working-directory: "dev"
|
||||
run: docker compose down
|
||||
shell: pwsh
|
||||
|
||||
validate:
|
||||
name: Run validation
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
dotnet --info
|
||||
nuget help | grep Version
|
||||
echo "GitHub ref: $GITHUB_REF"
|
||||
echo "GitHub event: $GITHUB_EVENT"
|
||||
|
||||
- name: Build DACPAC
|
||||
run: dotnet build src/Sql --configuration Release --verbosity minimal --output .
|
||||
shell: pwsh
|
||||
|
||||
- name: Upload DACPAC
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: sql.dacpac
|
||||
path: Sql.dacpac
|
||||
|
||||
- name: Docker Compose up
|
||||
working-directory: "dev"
|
||||
run: |
|
||||
cp .env.example .env
|
||||
docker compose --profile mssql up -d
|
||||
shell: pwsh
|
||||
|
||||
- name: Migrate
|
||||
working-directory: "dev"
|
||||
run: "./migrate.ps1"
|
||||
shell: pwsh
|
||||
|
||||
- name: Diff .sqlproj to migrations
|
||||
run: /usr/local/sqlpackage/sqlpackage /action:DeployReport /SourceFile:"Sql.dacpac" /TargetConnectionString:"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;" /OutputPath:"report.xml" /p:IgnoreColumnOrder=True /p:IgnoreComments=True
|
||||
shell: pwsh
|
||||
|
||||
- name: Generate SQL file
|
||||
run: /usr/local/sqlpackage/sqlpackage /action:Script /SourceFile:"Sql.dacpac" /TargetConnectionString:"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;" /OutputPath:"diff.sql" /p:IgnoreColumnOrder=True /p:IgnoreComments=True
|
||||
shell: pwsh
|
||||
|
||||
- name: Report validation results
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: report.xml
|
||||
path: |
|
||||
report.xml
|
||||
diff.sql
|
||||
|
||||
- name: Validate XML
|
||||
run: |
|
||||
if grep -q "<Operations>" "report.xml"; then
|
||||
echo
|
||||
echo "Migrations are out of sync with sqlproj!"
|
||||
exit 1
|
||||
else
|
||||
echo "Report looks good"
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Docker Compose down
|
||||
if: ${{ always() }}
|
||||
working-directory: "dev"
|
||||
run: docker compose down
|
||||
shell: pwsh
|
58
.github/workflows/test.yml
vendored
Normal file
58
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
---
|
||||
name: Testing
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "rc"
|
||||
- "hotfix-rc"
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
||||
|
||||
jobs:
|
||||
testing:
|
||||
name: Run tests
|
||||
if: ${{ startsWith(github.head_ref, 'version_bump_') == false }}
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
dotnet --info
|
||||
nuget help | grep Version
|
||||
echo "GitHub ref: $GITHUB_REF"
|
||||
echo "GitHub event: $GITHUB_EVENT"
|
||||
|
||||
- name: Remove SQL project
|
||||
run: dotnet sln bitwarden-server.sln remove src/Sql/Sql.sqlproj
|
||||
|
||||
- name: Test OSS solution
|
||||
run: dotnet test ./test --configuration Debug --logger "trx;LogFileName=oss-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
|
||||
|
||||
- name: Test Bitwarden solution
|
||||
run: dotnet test ./bitwarden_license/test --configuration Debug --logger "trx;LogFileName=bw-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226 # v1.6.0
|
||||
if: always()
|
||||
with:
|
||||
name: Test Results
|
||||
path: "**/*-test-results.trx"
|
||||
reporter: dotnet-trx
|
||||
fail-on-error: true
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
57
.github/workflows/version-bump.yml
vendored
57
.github/workflows/version-bump.yml
vendored
@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Version Bump
|
||||
run-name: Version Bump - v${{ inputs.version_number }}
|
||||
name: Bump version
|
||||
run-name: Bump version to ${{ inputs.version_number }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@ -16,10 +16,10 @@ on:
|
||||
|
||||
jobs:
|
||||
bump_version:
|
||||
name: "Bump Version to v${{ inputs.version_number }}"
|
||||
name: Bump
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Login to Azure - CI Subscription
|
||||
- name: Log in to Azure - CI subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
@ -33,11 +33,20 @@ jobs:
|
||||
github-gpg-private-key-passphrase,
|
||||
github-pat-bitwarden-devops-bot-repo-scope"
|
||||
|
||||
- name: Checkout Branch
|
||||
- name: Check out branch
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
with:
|
||||
ref: main
|
||||
repository: bitwarden/server
|
||||
|
||||
- name: Check if RC branch exists
|
||||
if: ${{ inputs.cut_rc_branch == true }}
|
||||
run: |
|
||||
remote_rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
|
||||
if [[ "${remote_rc_branch_check}" -gt 0 ]]; then
|
||||
echo "Remote RC branch exists."
|
||||
echo "Please delete current RC branch before running again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0
|
||||
@ -47,7 +56,7 @@ jobs:
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
|
||||
- name: Create Version Branch
|
||||
- name: Create version branch
|
||||
id: create-branch
|
||||
run: |
|
||||
NAME=version_bump_${{ github.ref_name }}_${{ inputs.version_number }}
|
||||
@ -78,13 +87,13 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Bump Version - Props
|
||||
- name: Bump version props
|
||||
uses: bitwarden/gh-actions/version-bump@main
|
||||
with:
|
||||
version: ${{ inputs.version_number }}
|
||||
file_path: "Directory.Build.props"
|
||||
|
||||
- name: Setup git
|
||||
- name: Set up Git
|
||||
run: |
|
||||
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||
git config --local user.name "bitwarden-devops-bot"
|
||||
@ -109,7 +118,7 @@ jobs:
|
||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
||||
run: git push -u origin $PR_BRANCH
|
||||
|
||||
- name: Create Version PR
|
||||
- name: Create version PR
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
id: create-pr
|
||||
env:
|
||||
@ -152,28 +161,36 @@ jobs:
|
||||
if: ${{ inputs.cut_rc_branch == true }}
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout Branch
|
||||
- name: Check out branch
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Check if RC branch exists
|
||||
- name: Install xmllint
|
||||
run: sudo apt install -y libxml2-utils
|
||||
|
||||
- name: Verify version has been updated
|
||||
env:
|
||||
NEW_VERSION: ${{ inputs.version_number }}
|
||||
run: |
|
||||
remote_rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
|
||||
if [[ "${remote_rc_branch_check}" -gt 0 ]]; then
|
||||
echo "Remote RC branch exists."
|
||||
echo "Please delete current RC branch before running again."
|
||||
exit 1
|
||||
fi
|
||||
# Wait for version to change.
|
||||
while : ; do
|
||||
echo "Waiting for version to be updated..."
|
||||
git pull --force
|
||||
CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
|
||||
|
||||
# If the versions don't match we continue the loop, otherwise we break out of the loop.
|
||||
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break
|
||||
sleep 10
|
||||
done
|
||||
|
||||
- name: Cut RC branch
|
||||
run: |
|
||||
git switch --quiet --create rc
|
||||
git push --quiet --set-upstream origin rc
|
||||
|
||||
|
||||
move-future-db-scripts:
|
||||
name: Move future DB scripts
|
||||
name: Move finalization database scripts
|
||||
needs: cut_rc
|
||||
uses: ./.github/workflows/_move_finalization_db_scripts.yml
|
||||
secrets: inherit
|
||||
|
11
.github/workflows/workflow-linter.yml
vendored
11
.github/workflows/workflow-linter.yml
vendored
@ -1,11 +0,0 @@
|
||||
---
|
||||
name: Workflow Linter
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/**
|
||||
|
||||
jobs:
|
||||
call-workflow:
|
||||
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@main
|
28
.vscode/launch.json
vendored
28
.vscode/launch.json
vendored
@ -252,7 +252,7 @@
|
||||
"requireExactSource": true,
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/Identity/bin/Debug/net6.0/Identity.dll",
|
||||
"program": "${workspaceFolder}/src/Identity/bin/Debug/net8.0/Identity.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/Identity",
|
||||
"stopAtEntry": false,
|
||||
@ -271,7 +271,7 @@
|
||||
"requireExactSource": true,
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/Api/bin/Debug/net6.0/Api.dll",
|
||||
"program": "${workspaceFolder}/src/Api/bin/Debug/net8.0/Api.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/Api",
|
||||
"stopAtEntry": false,
|
||||
@ -290,7 +290,7 @@
|
||||
"requireExactSource": true,
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/Billing/bin/Debug/net6.0/Billing.dll",
|
||||
"program": "${workspaceFolder}/src/Billing/bin/Debug/net8.0/Billing.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/Billing",
|
||||
"stopAtEntry": false,
|
||||
@ -310,7 +310,7 @@
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"OS-COMMENT4": "If you have changed target frameworks, make sure to update the program path.",
|
||||
"program": "${workspaceFolder}/src/Admin/bin/Debug/net6.0/Admin.dll",
|
||||
"program": "${workspaceFolder}/src/Admin/bin/Debug/net8.0/Admin.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/Admin",
|
||||
"stopAtEntry": false,
|
||||
@ -330,7 +330,7 @@
|
||||
"requireExactSource": true,
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/bitwarden_license/src/Sso/bin/Debug/net6.0/Sso.dll",
|
||||
"program": "${workspaceFolder}/bitwarden_license/src/Sso/bin/Debug/net8.0/Sso.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/bitwarden_license/src/Sso",
|
||||
"stopAtEntry": false,
|
||||
@ -349,7 +349,7 @@
|
||||
"requireExactSource": true,
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/EventsProcessor/bin/Debug/net6.0/EventsProcessor.dll",
|
||||
"program": "${workspaceFolder}/src/EventsProcessor/bin/Debug/net8.0/EventsProcessor.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/EventsProcessor",
|
||||
"stopAtEntry": false,
|
||||
@ -368,7 +368,7 @@
|
||||
"requireExactSource": true,
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/Icons/bin/Debug/net6.0/Icons.dll",
|
||||
"program": "${workspaceFolder}/src/Icons/bin/Debug/net8.0/Icons.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/Icons",
|
||||
"stopAtEntry": false,
|
||||
@ -387,7 +387,7 @@
|
||||
"requireExactSource": true,
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/Notifications/bin/Debug/net6.0/Notifications.dll",
|
||||
"program": "${workspaceFolder}/src/Notifications/bin/Debug/net8.0/Notifications.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/Notifications",
|
||||
"stopAtEntry": false,
|
||||
@ -406,7 +406,7 @@
|
||||
"requireExactSource": true,
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/Identity/bin/Debug/net6.0/Identity.dll",
|
||||
"program": "${workspaceFolder}/src/Identity/bin/Debug/net8.0/Identity.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/Identity",
|
||||
"stopAtEntry": false,
|
||||
@ -427,7 +427,7 @@
|
||||
"requireExactSource": true,
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/Api/bin/Debug/net6.0/Api.dll",
|
||||
"program": "${workspaceFolder}/src/Api/bin/Debug/net8.0/Api.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/Api",
|
||||
"stopAtEntry": false,
|
||||
@ -449,7 +449,7 @@
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"OS-COMMENT4": "If you have changed target frameworks, make sure to update the program path.",
|
||||
"program": "${workspaceFolder}/src/Admin/bin/Debug/net6.0/Admin.dll",
|
||||
"program": "${workspaceFolder}/src/Admin/bin/Debug/net8.0/Admin.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/Admin",
|
||||
"stopAtEntry": false,
|
||||
@ -471,7 +471,7 @@
|
||||
"requireExactSource": true,
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/bitwarden_license/src/Sso/bin/Debug/net6.0/Sso.dll",
|
||||
"program": "${workspaceFolder}/bitwarden_license/src/Sso/bin/Debug/net8.0/Sso.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/bitwarden_license/src/Sso",
|
||||
"stopAtEntry": false,
|
||||
@ -492,7 +492,7 @@
|
||||
"requireExactSource": true,
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/Notifications/bin/Debug/net6.0/Notifications.dll",
|
||||
"program": "${workspaceFolder}/src/Notifications/bin/Debug/net8.0/Notifications.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/Notifications",
|
||||
"stopAtEntry": false,
|
||||
@ -513,7 +513,7 @@
|
||||
"requireExactSource": true,
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/EventsProcessor/bin/Debug/net6.0/EventsProcessor.dll",
|
||||
"program": "${workspaceFolder}/src/EventsProcessor/bin/Debug/net8.0/EventsProcessor.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/EventsProcessor",
|
||||
"stopAtEntry": false,
|
||||
|
@ -1,8 +1,10 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Version>2024.1.0</Version>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<Version>2024.2.2</Version>
|
||||
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
@ -17,31 +19,31 @@
|
||||
<!--
|
||||
NuGet: https://www.nuget.org/packages/Microsoft.NET.Test.Sdk
|
||||
-->
|
||||
<MicrosoftNetTestSdkVersion>17.1.0</MicrosoftNetTestSdkVersion>
|
||||
<MicrosoftNetTestSdkVersion>17.8.0</MicrosoftNetTestSdkVersion>
|
||||
<!--
|
||||
NuGet: https://www.nuget.org/packages/xunit
|
||||
-->
|
||||
<XUnitVersion>2.4.1</XUnitVersion>
|
||||
<XUnitVersion>2.6.6</XUnitVersion>
|
||||
<!--
|
||||
NuGet: https://www.nuget.org/packages/xunit
|
||||
NuGet: https://www.nuget.org/packages/xunit.runner.visualstudio
|
||||
-->
|
||||
<XUnitRunnerVisualStudioVersion>2.4.3</XUnitRunnerVisualStudioVersion>
|
||||
<XUnitRunnerVisualStudioVersion>2.5.6</XUnitRunnerVisualStudioVersion>
|
||||
<!--
|
||||
NuGet: https://www.nuget.org/packages/coverlet.collector/
|
||||
NuGet: https://www.nuget.org/packages/coverlet.collector
|
||||
-->
|
||||
<CoverletCollectorVersion>3.1.2</CoverletCollectorVersion>
|
||||
<CoverletCollectorVersion>6.0.0</CoverletCollectorVersion>
|
||||
<!--
|
||||
NuGet: https://www.nuget.org/packages/NSubstitute/
|
||||
NuGet: https://www.nuget.org/packages/NSubstitute
|
||||
-->
|
||||
<NSubstituteVersion>4.3.0</NSubstituteVersion>
|
||||
<NSubstituteVersion>5.1.0</NSubstituteVersion>
|
||||
<!--
|
||||
NuGet: https://www.nuget.org/packages/AutoFixture.Xunit2/
|
||||
NuGet: https://www.nuget.org/packages/AutoFixture.Xunit2
|
||||
-->
|
||||
<AutoFixtureXUnit2Version>4.17.0</AutoFixtureXUnit2Version>
|
||||
<AutoFixtureXUnit2Version>4.18.1</AutoFixtureXUnit2Version>
|
||||
<!--
|
||||
NuGet: https://www.nuget.org/packages/AutoFixture.AutoNSubstitute/
|
||||
NuGet: https://www.nuget.org/packages/AutoFixture.AutoNSubstitute
|
||||
-->
|
||||
<AutoFixtureAutoNSubstituteVersion>4.17.0</AutoFixtureAutoNSubstituteVersion>
|
||||
<AutoFixtureAutoNSubstituteVersion>4.18.1</AutoFixtureAutoNSubstituteVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
|
@ -5,13 +5,13 @@ specifies another license. Bitwarden Licensed code is found only in the
|
||||
/bitwarden_license directory.
|
||||
|
||||
AGPL v3.0:
|
||||
https://github.com/bitwarden/server/blob/master/LICENSE_AGPL.txt
|
||||
https://github.com/bitwarden/server/blob/main/LICENSE_AGPL.txt
|
||||
|
||||
Bitwarden License v1.0:
|
||||
https://github.com/bitwarden/server/blob/master/LICENSE_BITWARDEN.txt
|
||||
https://github.com/bitwarden/server/blob/main/LICENSE_BITWARDEN.txt
|
||||
|
||||
No grant of any rights in the trademarks, service marks, or logos of Bitwarden is
|
||||
made (except as may be necessary to comply with the notice requirements as
|
||||
applicable), and use of any Bitwarden trademarks must comply with Bitwarden
|
||||
Trademark Guidelines
|
||||
<https://github.com/bitwarden/server/blob/master/TRADEMARK_GUIDELINES.md>.
|
||||
<https://github.com/bitwarden/server/blob/main/TRADEMARK_GUIDELINES.md>.
|
||||
|
@ -56,7 +56,7 @@ such Open Source Software only.
|
||||
logos of any Contributor (except as may be necessary to comply with the notice
|
||||
requirements in Section 2.3), and use of any Bitwarden trademarks must comply with
|
||||
Bitwarden Trademark Guidelines
|
||||
<https://github.com/bitwarden/server/blob/master/TRADEMARK_GUIDELINES.md>.
|
||||
<https://github.com/bitwarden/server/blob/main/TRADEMARK_GUIDELINES.md>.
|
||||
|
||||
3. TERMINATION
|
||||
|
||||
|
@ -8,7 +8,7 @@ As an open solution, Bitwarden publishes the source code for various modules und
|
||||
|
||||
# Bitwarden Software Licensing
|
||||
|
||||
We have two tiers of licensing for our software. The core products are offered under one of the GPL open source licenses: GPL 3 and A-GPL 3. A select number of features, primarily those designed for use by larger organizations rather than individuals and families, are licensed under a "Source Available" commercial license [here](https://github.com/bitwarden/server/blob/master/LICENSE_BITWARDEN.txt).
|
||||
We have two tiers of licensing for our software. The core products are offered under one of the GPL open source licenses: GPL 3 and A-GPL 3. A select number of features, primarily those designed for use by larger organizations rather than individuals and families, are licensed under a "Source Available" commercial license [here](https://github.com/bitwarden/server/blob/main/LICENSE_BITWARDEN.txt).
|
||||
|
||||
Our current software products have the following licenses:
|
||||
|
||||
@ -49,7 +49,7 @@ As detailed above, the Bitwarden password management clients for individual use,
|
||||
|
||||
***If I redistribute or provide services related to Bitwarden open source software can I use the "Bitwarden" name?***
|
||||
|
||||
Our licenses do not grant any rights in the trademarks, service marks, or logos of Bitwarden (except as may be necessary to comply with the notice requirements as applicable). The Bitwarden trademark is a trusted mark applied to products distributed by Bitwarden, Inc., owner of the Bitwarden trademarks and products. We have adopted and enforce strict rules governing use of our trademarks. Use of any Bitwarden trademarks must comply with Bitwarden [Trademark Guidelines](https://github.com/bitwarden/server/blob/master/TRADEMARK_GUIDELINES.md).
|
||||
Our licenses do not grant any rights in the trademarks, service marks, or logos of Bitwarden (except as may be necessary to comply with the notice requirements as applicable). The Bitwarden trademark is a trusted mark applied to products distributed by Bitwarden, Inc., owner of the Bitwarden trademarks and products. We have adopted and enforce strict rules governing use of our trademarks. Use of any Bitwarden trademarks must comply with Bitwarden [Trademark Guidelines](https://github.com/bitwarden/server/blob/main/TRADEMARK_GUIDELINES.md).
|
||||
|
||||
***Bitwarden Trademark Usage***
|
||||
|
||||
@ -61,10 +61,10 @@ You don't need permission to use our marks when truthfully referring to our prod
|
||||
|
||||
***How should I use the Bitwarden Trademarks when allowed?***
|
||||
|
||||
Use the Bitwarden Trademarks exactly as [shown](https://github.com/bitwarden/server/blob/master/TRADEMARK_GUIDELINES.md) and without modification. For example, do not abbreviate, hyphenate, or remove elements and separate them from surrounding text, images and other features. Always use the Bitwarden Trademarks as adjectives followed by a generic term, never as a noun or verb.
|
||||
Use the Bitwarden Trademarks exactly as [shown](https://github.com/bitwarden/server/blob/main/TRADEMARK_GUIDELINES.md) and without modification. For example, do not abbreviate, hyphenate, or remove elements and separate them from surrounding text, images and other features. Always use the Bitwarden Trademarks as adjectives followed by a generic term, never as a noun or verb.
|
||||
|
||||
Use the Bitwarden Trademarks only to reference one of our products or services, but never in a way that implies sponsorship or affiliation by Bitwarden. For example, do not use any part of the Bitwarden Trademarks as the name of your business, product or service name, application, domain name, publication or other offering – this can be confusing to others.
|
||||
|
||||
***Where can I find more information?***
|
||||
|
||||
For more information on how to use the Bitwarden Trademarks, including in connection with self-hosted options and open-source code, see our [Trademark Guidelines](https://github.com/bitwarden/server/blob/master/TRADEMARK_GUIDELINES.md) or [contacts us](https://bitwarden.com/contact/).
|
||||
For more information on how to use the Bitwarden Trademarks, including in connection with self-hosted options and open-source code, see our [Trademark Guidelines](https://github.com/bitwarden/server/blob/main/TRADEMARK_GUIDELINES.md) or [contacts us](https://bitwarden.com/contact/).
|
||||
|
@ -2,8 +2,8 @@
|
||||
<img src="https://github.com/bitwarden/brand/blob/main/screenshots/apps-combo-logo.png" alt="Bitwarden" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/bitwarden/server/actions/workflows/build.yml?query=branch:master" target="_blank">
|
||||
<img src="https://github.com/bitwarden/server/actions/workflows/build.yml/badge.svg?branch=master" alt="Github Workflow build on master" />
|
||||
<a href="https://github.com/bitwarden/server/actions/workflows/build.yml?query=branch:main" target="_blank">
|
||||
<img src="https://github.com/bitwarden/server/actions/workflows/build.yml/badge.svg?branch=main" alt="Github Workflow build on main" />
|
||||
</a>
|
||||
<a href="https://hub.docker.com/u/bitwarden/" target="_blank">
|
||||
<img src="https://img.shields.io/docker/pulls/bitwarden/api.svg" alt="DockerHub" />
|
||||
|
@ -1,3 +1,3 @@
|
||||
# Bitwarden Licensed Code
|
||||
|
||||
All source code under this directory is licensed under the [Bitwarden License Agreement](https://github.com/bitwarden/server/blob/master/LICENSE_BITWARDEN.txt).
|
||||
All source code under this directory is licensed under the [Bitwarden License Agreement](https://github.com/bitwarden/server/blob/main/LICENSE_BITWARDEN.txt).
|
||||
|
@ -0,0 +1,98 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Commercial.Core.AdminConsole.Providers;
|
||||
|
||||
public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProviderCommand
|
||||
{
|
||||
private readonly IEventService _eventService;
|
||||
private readonly ILogger<RemoveOrganizationFromProviderCommand> _logger;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly IStripeAdapter _stripeAdapter;
|
||||
|
||||
public RemoveOrganizationFromProviderCommand(
|
||||
IEventService eventService,
|
||||
ILogger<RemoveOrganizationFromProviderCommand> logger,
|
||||
IMailService mailService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationService organizationService,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IStripeAdapter stripeAdapter)
|
||||
{
|
||||
_eventService = eventService;
|
||||
_logger = logger;
|
||||
_mailService = mailService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationService = organizationService;
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_stripeAdapter = stripeAdapter;
|
||||
}
|
||||
|
||||
public async Task RemoveOrganizationFromProvider(
|
||||
Provider provider,
|
||||
ProviderOrganization providerOrganization,
|
||||
Organization organization)
|
||||
{
|
||||
if (provider == null ||
|
||||
providerOrganization == null ||
|
||||
organization == null ||
|
||||
providerOrganization.ProviderId != provider.Id)
|
||||
{
|
||||
throw new BadRequestException("Failed to remove organization. Please contact support.");
|
||||
}
|
||||
|
||||
if (!await _organizationService.HasConfirmedOwnersExceptAsync(
|
||||
providerOrganization.OrganizationId,
|
||||
Array.Empty<Guid>(),
|
||||
includeProvider: false))
|
||||
{
|
||||
throw new BadRequestException("Organization must have at least one confirmed owner.");
|
||||
}
|
||||
|
||||
var organizationOwnerEmails =
|
||||
(await _organizationRepository.GetOwnerEmailAddressesById(organization.Id)).ToList();
|
||||
|
||||
organization.BillingEmail = organizationOwnerEmails.MinBy(email => email);
|
||||
|
||||
await _organizationRepository.ReplaceAsync(organization);
|
||||
|
||||
var customerUpdateOptions = new CustomerUpdateOptions
|
||||
{
|
||||
Coupon = string.Empty,
|
||||
Email = organization.BillingEmail
|
||||
};
|
||||
|
||||
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, customerUpdateOptions);
|
||||
|
||||
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
|
||||
{
|
||||
CollectionMethod = "send_invoice",
|
||||
DaysUntilDue = 30
|
||||
};
|
||||
|
||||
await _stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, subscriptionUpdateOptions);
|
||||
|
||||
await _mailService.SendProviderUpdatePaymentMethod(
|
||||
organization.Id,
|
||||
organization.Name,
|
||||
provider.Name,
|
||||
organizationOwnerEmails);
|
||||
|
||||
await _providerOrganizationRepository.DeleteAsync(providerOrganization);
|
||||
|
||||
await _eventService.LogProviderOrganizationEventAsync(
|
||||
providerOrganization,
|
||||
EventType.ProviderOrganization_Removed);
|
||||
}
|
||||
}
|
@ -496,7 +496,7 @@ public class ProviderService : IProviderService
|
||||
{
|
||||
ThrowOnInvalidPlanType(organizationSignup.Plan);
|
||||
|
||||
var (organization, _) = await _organizationService.SignUpAsync(organizationSignup, true);
|
||||
var (organization, _, defaultCollection) = await _organizationService.SignUpAsync(organizationSignup, true);
|
||||
|
||||
var providerOrganization = new ProviderOrganization
|
||||
{
|
||||
@ -508,6 +508,21 @@ public class ProviderService : IProviderService
|
||||
await _providerOrganizationRepository.CreateAsync(providerOrganization);
|
||||
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Created);
|
||||
|
||||
// If using Flexible Collections, give the owner Can Manage access over the default collection
|
||||
// The orgUser is not available when the org is created so we have to do it here as part of the invite
|
||||
var defaultOwnerAccess = organization.FlexibleCollections && defaultCollection != null
|
||||
?
|
||||
[
|
||||
new CollectionAccessSelection
|
||||
{
|
||||
Id = defaultCollection.Id,
|
||||
HidePasswords = false,
|
||||
ReadOnly = false,
|
||||
Manage = true
|
||||
}
|
||||
]
|
||||
: Array.Empty<CollectionAccessSelection>();
|
||||
|
||||
await _organizationService.InviteUsersAsync(organization.Id, user.Id,
|
||||
new (OrganizationUserInvite, string)[]
|
||||
{
|
||||
@ -515,10 +530,13 @@ public class ProviderService : IProviderService
|
||||
new OrganizationUserInvite
|
||||
{
|
||||
Emails = new[] { clientOwnerEmail },
|
||||
AccessAll = true,
|
||||
|
||||
// If using Flexible Collections, AccessAll is deprecated and set to false.
|
||||
// If not using Flexible Collections, set AccessAll to true (previous behavior)
|
||||
AccessAll = !organization.FlexibleCollections,
|
||||
Type = OrganizationUserType.Owner,
|
||||
Permissions = null,
|
||||
Collections = Array.Empty<CollectionAccessSelection>(),
|
||||
Collections = defaultOwnerAccess,
|
||||
},
|
||||
null
|
||||
)
|
||||
@ -527,23 +545,6 @@ public class ProviderService : IProviderService
|
||||
return providerOrganization;
|
||||
}
|
||||
|
||||
public async Task RemoveOrganizationAsync(Guid providerId, Guid providerOrganizationId, Guid removingUserId)
|
||||
{
|
||||
var providerOrganization = await _providerOrganizationRepository.GetByIdAsync(providerOrganizationId);
|
||||
if (providerOrganization == null || providerOrganization.ProviderId != providerId)
|
||||
{
|
||||
throw new BadRequestException("Invalid organization.");
|
||||
}
|
||||
|
||||
if (!await _organizationService.HasConfirmedOwnersExceptAsync(providerOrganization.OrganizationId, new Guid[] { }, includeProvider: false))
|
||||
{
|
||||
throw new BadRequestException("Organization needs to have at least one confirmed owner.");
|
||||
}
|
||||
|
||||
await _providerOrganizationRepository.DeleteAsync(providerOrganization);
|
||||
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);
|
||||
}
|
||||
|
||||
public async Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid ownerId)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||
|
@ -26,7 +26,7 @@ public class CountNewServiceAccountSlotsRequiredQuery : ICountNewServiceAccountS
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!organization.SmServiceAccounts.HasValue || serviceAccountsToAdd == 0 || organization.SecretsManagerBeta)
|
||||
if (!organization.SmServiceAccounts.HasValue || serviceAccountsToAdd == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
@ -12,5 +12,6 @@ public static class ServiceCollectionExtensions
|
||||
{
|
||||
services.AddScoped<IProviderService, ProviderService>();
|
||||
services.AddScoped<ICreateProviderCommand, CreateProviderCommand>();
|
||||
services.AddScoped<IRemoveOrganizationFromProviderCommand, RemoveOrganizationFromProviderCommand>();
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||
|
||||
LABEL com.bitwarden.product="bitwarden"
|
||||
|
||||
|
@ -209,6 +209,8 @@ public class AccountController : Controller
|
||||
returnUrl = "~/";
|
||||
}
|
||||
|
||||
// Clean the returnUrl
|
||||
returnUrl = CoreHelpers.ReplaceWhiteSpace(returnUrl, string.Empty);
|
||||
if (!Url.IsLocalUrl(returnUrl) && !_interaction.IsValidReturnUrl(returnUrl))
|
||||
{
|
||||
throw new Exception(_i18nService.T("InvalidReturnUrl"));
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||
|
||||
LABEL com.bitwarden.product="bitwarden"
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
<!-- This is a transitive dependency to Sustainsys.Saml2.AspNetCore2 -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.22" />
|
||||
|
||||
<PackageReference Include="Sustainsys.Saml2.AspNetCore2" Version="2.9.0" />
|
||||
<PackageReference Include="Sustainsys.Saml2.AspNetCore2" Version="2.9.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -65,7 +65,7 @@ public class Startup
|
||||
}
|
||||
|
||||
// Authentication
|
||||
services.AddDistributedIdentityServices(globalSettings);
|
||||
services.AddDistributedIdentityServices();
|
||||
services.AddAuthentication()
|
||||
.AddCookie(AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);
|
||||
services.AddSsoServices(globalSettings);
|
||||
|
@ -349,7 +349,9 @@ public class DynamicAuthenticationSchemeProvider : AuthenticationSchemeProvider
|
||||
}
|
||||
|
||||
var spEntityId = new Sustainsys.Saml2.Metadata.EntityId(
|
||||
SsoConfigurationData.BuildSaml2ModulePath(_globalSettings.BaseServiceUri.Sso));
|
||||
SsoConfigurationData.BuildSaml2ModulePath(
|
||||
_globalSettings.BaseServiceUri.Sso,
|
||||
config.SpUniqueEntityId ? name : null));
|
||||
bool? allowCreate = null;
|
||||
if (config.SpNameIdFormat != Saml2NameIdFormat.Transient)
|
||||
{
|
||||
@ -415,7 +417,7 @@ public class DynamicAuthenticationSchemeProvider : AuthenticationSchemeProvider
|
||||
};
|
||||
options.IdentityProviders.Add(idp);
|
||||
|
||||
return new DynamicAuthenticationScheme(name, name, typeof(Saml2BitHandler), options, SsoType.Saml2);
|
||||
return new DynamicAuthenticationScheme(name, name, typeof(Saml2Handler), options, SsoType.Saml2);
|
||||
}
|
||||
|
||||
private NameIdFormat GetNameIdFormat(Saml2NameIdFormat format)
|
||||
|
@ -1,205 +0,0 @@
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Sustainsys.Saml2.AspNetCore2;
|
||||
using Sustainsys.Saml2.WebSso;
|
||||
|
||||
namespace Bit.Sso.Utilities;
|
||||
|
||||
// Temporary handler for validating Saml2 requests
|
||||
// Most of this is taken from Sustainsys.Saml2.AspNetCore2.Saml2Handler
|
||||
// TODO: PM-3641 - Remove this handler once there is a proper solution
|
||||
public class Saml2BitHandler : IAuthenticationRequestHandler
|
||||
{
|
||||
private readonly Saml2Handler _saml2Handler;
|
||||
private string _scheme;
|
||||
|
||||
private readonly IOptionsMonitorCache<Saml2Options> _optionsCache;
|
||||
private Saml2Options _options;
|
||||
private HttpContext _context;
|
||||
private readonly IDataProtector _dataProtector;
|
||||
private readonly IOptionsFactory<Saml2Options> _optionsFactory;
|
||||
private bool _emitSameSiteNone;
|
||||
|
||||
public Saml2BitHandler(
|
||||
IOptionsMonitorCache<Saml2Options> optionsCache,
|
||||
IDataProtectionProvider dataProtectorProvider,
|
||||
IOptionsFactory<Saml2Options> optionsFactory)
|
||||
{
|
||||
if (dataProtectorProvider == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(dataProtectorProvider));
|
||||
}
|
||||
|
||||
_optionsFactory = optionsFactory;
|
||||
_optionsCache = optionsCache;
|
||||
|
||||
_saml2Handler = new Saml2Handler(optionsCache, dataProtectorProvider, optionsFactory);
|
||||
_dataProtector = dataProtectorProvider.CreateProtector(_saml2Handler.GetType().FullName);
|
||||
}
|
||||
|
||||
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
_options = _optionsCache.GetOrAdd(scheme.Name, () => _optionsFactory.Create(scheme.Name));
|
||||
_emitSameSiteNone = _options.Notifications.EmitSameSiteNone(context.Request.GetUserAgent());
|
||||
_scheme = scheme.Name;
|
||||
|
||||
return _saml2Handler.InitializeAsync(scheme, context);
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> HandleRequestAsync()
|
||||
{
|
||||
if (!_context.Request.Path.StartsWithSegments(_options.SPOptions.ModulePath, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var commandName = _context.Request.Path.Value.Substring(
|
||||
_options.SPOptions.ModulePath.Length).TrimStart('/');
|
||||
|
||||
var commandResult = CommandFactory.GetCommand(commandName).Run(
|
||||
_context.ToHttpRequestData(_options.CookieManager, _dataProtector.Unprotect), _options);
|
||||
|
||||
// Scheme is the organization ID since we use dynamic handlers for authentication schemes.
|
||||
// We need to compare this to the scheme returned in the RelayData to ensure this value hasn't been
|
||||
// tampered with
|
||||
if (commandResult.RelayData["scheme"] != _scheme)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
await commandResult.Apply(
|
||||
_context, _dataProtector, _options.CookieManager, _options.SignInScheme, _options.SignOutScheme, _emitSameSiteNone);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public Task<AuthenticateResult> AuthenticateAsync() => _saml2Handler.AuthenticateAsync();
|
||||
|
||||
public Task ChallengeAsync(AuthenticationProperties properties) => _saml2Handler.ChallengeAsync(properties);
|
||||
|
||||
public Task ForbidAsync(AuthenticationProperties properties) => _saml2Handler.ForbidAsync(properties);
|
||||
}
|
||||
|
||||
|
||||
static class HttpRequestExtensions
|
||||
{
|
||||
public static HttpRequestData ToHttpRequestData(
|
||||
this HttpContext httpContext,
|
||||
ICookieManager cookieManager,
|
||||
Func<byte[], byte[]> cookieDecryptor)
|
||||
{
|
||||
var request = httpContext.Request;
|
||||
|
||||
var uri = new Uri(
|
||||
request.Scheme
|
||||
+ "://"
|
||||
+ request.Host
|
||||
+ request.Path
|
||||
+ request.QueryString);
|
||||
|
||||
var pathBase = httpContext.Request.PathBase.Value;
|
||||
pathBase = string.IsNullOrEmpty(pathBase) ? "/" : pathBase;
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>> formData = null;
|
||||
if (httpContext.Request.Method == "POST" && httpContext.Request.HasFormContentType)
|
||||
{
|
||||
formData = request.Form.Select(
|
||||
f => new KeyValuePair<string, IEnumerable<string>>(f.Key, f.Value));
|
||||
}
|
||||
|
||||
return new HttpRequestData(
|
||||
httpContext.Request.Method,
|
||||
uri,
|
||||
pathBase,
|
||||
formData,
|
||||
cookieName => cookieManager.GetRequestCookie(httpContext, cookieName),
|
||||
cookieDecryptor,
|
||||
httpContext.User);
|
||||
}
|
||||
|
||||
public static string GetUserAgent(this HttpRequest request)
|
||||
{
|
||||
return request.Headers["user-agent"].FirstOrDefault() ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
static class CommandResultExtensions
|
||||
{
|
||||
public static async Task Apply(
|
||||
this CommandResult commandResult,
|
||||
HttpContext httpContext,
|
||||
IDataProtector dataProtector,
|
||||
ICookieManager cookieManager,
|
||||
string signInScheme,
|
||||
string signOutScheme,
|
||||
bool emitSameSiteNone)
|
||||
{
|
||||
httpContext.Response.StatusCode = (int)commandResult.HttpStatusCode;
|
||||
|
||||
if (commandResult.Location != null)
|
||||
{
|
||||
httpContext.Response.Headers["Location"] = commandResult.Location.OriginalString;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(commandResult.SetCookieName))
|
||||
{
|
||||
var cookieData = HttpRequestData.ConvertBinaryData(
|
||||
dataProtector.Protect(commandResult.GetSerializedRequestState()));
|
||||
|
||||
cookieManager.AppendResponseCookie(
|
||||
httpContext,
|
||||
commandResult.SetCookieName,
|
||||
cookieData,
|
||||
new CookieOptions()
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = commandResult.SetCookieSecureFlag,
|
||||
// We are expecting a different site to POST back to us,
|
||||
// so the ASP.Net Core default of Lax is not appropriate in this case
|
||||
SameSite = emitSameSiteNone ? SameSiteMode.None : (SameSiteMode)(-1),
|
||||
IsEssential = true
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var h in commandResult.Headers)
|
||||
{
|
||||
httpContext.Response.Headers.Add(h.Key, h.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(commandResult.ClearCookieName))
|
||||
{
|
||||
cookieManager.DeleteCookie(
|
||||
httpContext,
|
||||
commandResult.ClearCookieName,
|
||||
new CookieOptions
|
||||
{
|
||||
Secure = commandResult.SetCookieSecureFlag
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(commandResult.Content))
|
||||
{
|
||||
var buffer = Encoding.UTF8.GetBytes(commandResult.Content);
|
||||
httpContext.Response.ContentType = commandResult.ContentType;
|
||||
await httpContext.Response.Body.WriteAsync(buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
if (commandResult.Principal != null)
|
||||
{
|
||||
var authProps = new AuthenticationProperties(commandResult.RelayData)
|
||||
{
|
||||
RedirectUri = commandResult.Location.OriginalString
|
||||
};
|
||||
await httpContext.SignInAsync(signInScheme, commandResult.Principal, authProps);
|
||||
}
|
||||
|
||||
if (commandResult.TerminateLocalSession)
|
||||
{
|
||||
await httpContext.SignOutAsync(signOutScheme ?? signInScheme);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
using Bit.Commercial.Core.AdminConsole.Providers;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.AdminConsole.ProviderFeatures;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class RemoveOrganizationFromProviderCommandTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemoveOrganizationFromProvider_NoProvider_BadRequest(
|
||||
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
|
||||
{
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RemoveOrganizationFromProvider(null, null, null));
|
||||
|
||||
Assert.Equal("Failed to remove organization. Please contact support.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemoveOrganizationFromProvider_NoProviderOrganization_BadRequest(
|
||||
Provider provider,
|
||||
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
|
||||
{
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RemoveOrganizationFromProvider(provider, null, null));
|
||||
|
||||
Assert.Equal("Failed to remove organization. Please contact support.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemoveOrganizationFromProvider_NoOrganization_BadRequest(
|
||||
Provider provider,
|
||||
ProviderOrganization providerOrganization,
|
||||
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
|
||||
{
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RemoveOrganizationFromProvider(
|
||||
provider, providerOrganization, null));
|
||||
|
||||
Assert.Equal("Failed to remove organization. Please contact support.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemoveOrganizationFromProvider_MismatchedProviderOrganization_BadRequest(
|
||||
Provider provider,
|
||||
ProviderOrganization providerOrganization,
|
||||
Organization organization,
|
||||
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
|
||||
{
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization));
|
||||
|
||||
Assert.Equal("Failed to remove organization. Please contact support.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemoveOrganizationFromProvider_NoConfirmedOwners_BadRequest(
|
||||
Provider provider,
|
||||
ProviderOrganization providerOrganization,
|
||||
Organization organization,
|
||||
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
|
||||
{
|
||||
providerOrganization.ProviderId = provider.Id;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(
|
||||
providerOrganization.OrganizationId,
|
||||
Array.Empty<Guid>(),
|
||||
includeProvider: false)
|
||||
.Returns(false);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization));
|
||||
|
||||
Assert.Equal("Organization must have at least one confirmed owner.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemoveOrganizationFromProvider_MakesCorrectInvocations(
|
||||
Provider provider,
|
||||
ProviderOrganization providerOrganization,
|
||||
Organization organization,
|
||||
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
|
||||
{
|
||||
providerOrganization.ProviderId = provider.Id;
|
||||
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(
|
||||
providerOrganization.OrganizationId,
|
||||
Array.Empty<Guid>(),
|
||||
includeProvider: false)
|
||||
.Returns(true);
|
||||
|
||||
var organizationOwnerEmails = new List<string> { "a@gmail.com", "b@gmail.com" };
|
||||
|
||||
organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns(organizationOwnerEmails);
|
||||
|
||||
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
||||
|
||||
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(
|
||||
org => org.Id == organization.Id && org.BillingEmail == "a@gmail.com"));
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
await stripeAdapter.Received(1).CustomerUpdateAsync(
|
||||
organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
|
||||
options => options.Coupon == string.Empty && options.Email == "a@gmail.com"));
|
||||
|
||||
await stripeAdapter.Received(1).SubscriptionUpdateAsync(
|
||||
organization.GatewaySubscriptionId, Arg.Is<SubscriptionUpdateOptions>(
|
||||
options => options.CollectionMethod == "send_invoice" && options.DaysUntilDue == 30));
|
||||
|
||||
await sutProvider.GetDependency<IMailService>().Received(1).SendProviderUpdatePaymentMethod(
|
||||
organization.Id,
|
||||
organization.Name,
|
||||
provider.Name,
|
||||
Arg.Is<IEnumerable<string>>(emails => emails.Contains("a@gmail.com") && emails.Contains("b@gmail.com")));
|
||||
|
||||
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
|
||||
.DeleteAsync(providerOrganization);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogProviderOrganizationEventAsync(
|
||||
providerOrganization,
|
||||
EventType.ProviderOrganization_Removed);
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@ -513,7 +514,7 @@ public class ProviderServiceTests
|
||||
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogProviderOrganizationEventsAsync(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
[Theory, OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
||||
public async Task CreateOrganizationAsync_Success(Provider provider, OrganizationSignup organizationSignup,
|
||||
Organization organization, string clientOwnerEmail, User user, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
@ -522,7 +523,7 @@ public class ProviderServiceTests
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup, true)
|
||||
.Returns(Tuple.Create(organization, null as OrganizationUser));
|
||||
.Returns((organization, null as OrganizationUser, new Collection()));
|
||||
|
||||
var providerOrganization =
|
||||
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
|
||||
@ -538,70 +539,45 @@ public class ProviderServiceTests
|
||||
t.First().Item1.Emails.First() == clientOwnerEmail &&
|
||||
t.First().Item1.Type == OrganizationUserType.Owner &&
|
||||
t.First().Item1.AccessAll &&
|
||||
!t.First().Item1.Collections.Any() &&
|
||||
t.First().Item2 == null));
|
||||
}
|
||||
|
||||
[Theory, OrganizationCustomize(FlexibleCollections = true), BitAutoData]
|
||||
public async Task CreateOrganizationAsync_WithFlexibleCollections_SetsAccessAllToFalse
|
||||
(Provider provider, OrganizationSignup organizationSignup, Organization organization, string clientOwnerEmail,
|
||||
User user, SutProvider<ProviderService> sutProvider, Collection defaultCollection)
|
||||
{
|
||||
organizationSignup.Plan = PlanType.EnterpriseAnnually;
|
||||
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup, true)
|
||||
.Returns((organization, null as OrganizationUser, defaultCollection));
|
||||
|
||||
var providerOrganization =
|
||||
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
|
||||
|
||||
await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received().LogProviderOrganizationEventAsync(providerOrganization,
|
||||
EventType.ProviderOrganization_Created);
|
||||
await sutProvider.GetDependency<IOrganizationService>()
|
||||
.Received().InviteUsersAsync(organization.Id, user.Id, Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(
|
||||
t => t.Count() == 1 &&
|
||||
t.First().Item1.Emails.Count() == 1 &&
|
||||
t.First().Item1.Emails.First() == clientOwnerEmail &&
|
||||
t.First().Item1.Type == OrganizationUserType.Owner &&
|
||||
t.First().Item1.AccessAll == false &&
|
||||
t.First().Item1.Collections.Single().Id == defaultCollection.Id &&
|
||||
!t.First().Item1.Collections.Single().HidePasswords &&
|
||||
!t.First().Item1.Collections.Single().ReadOnly &&
|
||||
t.First().Item1.Collections.Single().Manage &&
|
||||
t.First().Item2 == null));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemoveOrganization_ProviderOrganizationIsInvalid_Throws(Provider provider,
|
||||
ProviderOrganization providerOrganization, User user, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganization.Id)
|
||||
.ReturnsNull();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RemoveOrganizationAsync(provider.Id, providerOrganization.Id, user.Id));
|
||||
Assert.Equal("Invalid organization.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemoveOrganization_ProviderOrganizationBelongsToWrongProvider_Throws(Provider provider,
|
||||
ProviderOrganization providerOrganization, User user, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganization.Id)
|
||||
.Returns(providerOrganization);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RemoveOrganizationAsync(provider.Id, providerOrganization.Id, user.Id));
|
||||
Assert.Equal("Invalid organization.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemoveOrganization_HasNoOwners_Throws(Provider provider,
|
||||
ProviderOrganization providerOrganization, User user, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
providerOrganization.ProviderId = provider.Id;
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganization.Id)
|
||||
.Returns(providerOrganization);
|
||||
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(default, default, default)
|
||||
.ReturnsForAnyArgs(false);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RemoveOrganizationAsync(provider.Id, providerOrganization.Id, user.Id));
|
||||
Assert.Equal("Organization needs to have at least one confirmed owner.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemoveOrganization_Success(Provider provider,
|
||||
ProviderOrganization providerOrganization, User user, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
providerOrganization.ProviderId = provider.Id;
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||
providerOrganizationRepository.GetByIdAsync(providerOrganization.Id).Returns(providerOrganization);
|
||||
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(default, default, default)
|
||||
.ReturnsForAnyArgs(true);
|
||||
|
||||
await sutProvider.Sut.RemoveOrganizationAsync(provider.Id, providerOrganization.Id, user.Id);
|
||||
await providerOrganizationRepository.Received().DeleteAsync(providerOrganization);
|
||||
await sutProvider.GetDependency<IEventService>().Received()
|
||||
.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AddOrganization_CreateAfterNov162023_PlanTypeDoesNotUpdated(Provider provider, Organization organization, string key,
|
||||
public async Task AddOrganization_CreateAfterNov62023_PlanTypeDoesNotUpdated(Provider provider, Organization organization, string key,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
provider.Type = ProviderType.Msp;
|
||||
@ -623,10 +599,10 @@ public class ProviderServiceTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AddOrganization_CreateBeforeNov162023_PlanTypeUpdated(Provider provider, Organization organization, string key,
|
||||
public async Task AddOrganization_CreateBeforeNov62023_PlanTypeUpdated(Provider provider, Organization organization, string key,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var newCreationDate = DateTime.UtcNow.AddMonths(-3);
|
||||
var newCreationDate = new DateTime(2023, 11, 5);
|
||||
BackdateProviderCreationDate(provider, newCreationDate);
|
||||
provider.Type = ProviderType.Msp;
|
||||
|
||||
|
@ -28,7 +28,6 @@ public class CountNewServiceAccountSlotsRequiredQueryTests
|
||||
{
|
||||
organization.UseSecretsManager = true;
|
||||
organization.SmServiceAccounts = organizationSmServiceAccounts;
|
||||
organization.SecretsManagerBeta = false;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
@ -62,7 +61,6 @@ public class CountNewServiceAccountSlotsRequiredQueryTests
|
||||
|
||||
organization.UseSecretsManager = true;
|
||||
organization.SmServiceAccounts = null;
|
||||
organization.SecretsManagerBeta = false;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
@ -80,27 +78,6 @@ public class CountNewServiceAccountSlotsRequiredQueryTests
|
||||
.GetServiceAccountCountByOrganizationIdAsync(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CountNewServiceAccountSlotsRequiredAsync_WithSecretsManagerBeta_ReturnsZero(
|
||||
int serviceAccountsToAdd,
|
||||
Organization organization,
|
||||
SutProvider<CountNewServiceAccountSlotsRequiredQuery> sutProvider)
|
||||
{
|
||||
organization.UseSecretsManager = true;
|
||||
organization.SecretsManagerBeta = true;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
|
||||
var result = await sutProvider.Sut.CountNewServiceAccountSlotsRequiredAsync(organization.Id, serviceAccountsToAdd);
|
||||
|
||||
Assert.Equal(0, result);
|
||||
|
||||
await sutProvider.GetDependency<IServiceAccountRepository>().DidNotReceiveWithAnyArgs()
|
||||
.GetServiceAccountCountByOrganizationIdAsync(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CountNewServiceAccountSlotsRequiredAsync_WithNonExistentOrganizationId_ThrowsNotFound(
|
||||
Guid organizationId, int serviceAccountsToAdd,
|
||||
|
@ -90,12 +90,12 @@ public class ScimApplicationFactory : WebApplicationFactoryBase<Startup>
|
||||
|
||||
public async Task<HttpContext> GroupsPostAsync(Guid organizationId, ScimGroupRequestModel model)
|
||||
{
|
||||
return await Server.PostAsync($"/v2/{organizationId}/groups", GetStringContent(model), httpContext => httpContext.Request.Headers.Add(HeaderNames.UserAgent, "Okta"));
|
||||
return await Server.PostAsync($"/v2/{organizationId}/groups", GetStringContent(model), httpContext => httpContext.Request.Headers.Append(HeaderNames.UserAgent, "Okta"));
|
||||
}
|
||||
|
||||
public async Task<HttpContext> GroupsPutAsync(Guid organizationId, Guid id, ScimGroupRequestModel model)
|
||||
{
|
||||
return await Server.PutAsync($"/v2/{organizationId}/groups/{id}", GetStringContent(model), httpContext => httpContext.Request.Headers.Add(HeaderNames.UserAgent, "Okta"));
|
||||
return await Server.PutAsync($"/v2/{organizationId}/groups/{id}", GetStringContent(model), httpContext => httpContext.Request.Headers.Append(HeaderNames.UserAgent, "Okta"));
|
||||
}
|
||||
|
||||
public async Task<HttpContext> GroupsPatchAsync(Guid organizationId, Guid id, ScimPatchModel model)
|
||||
|
@ -9,7 +9,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
|
||||
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "6.0.100",
|
||||
"version": "8.0.100",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
||||
|
@ -3,19 +3,15 @@ using Bit.Identity.IdentityServer;
|
||||
using Bit.Infrastructure.Dapper.Auth.Repositories;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace Bit.MicroBenchmarks.Identity.IdentityServer;
|
||||
|
||||
[MemoryDiagnoser]
|
||||
public class RedisPersistedGrantStoreTests
|
||||
public class PersistedGrantStoreTests
|
||||
{
|
||||
const string SQL = nameof(SQL);
|
||||
const string Redis = nameof(Redis);
|
||||
const string Cosmos = nameof(Cosmos);
|
||||
|
||||
private readonly IPersistedGrantStore _redisGrantStore;
|
||||
private readonly IPersistedGrantStore _sqlGrantStore;
|
||||
private readonly IPersistedGrantStore _cosmosGrantStore;
|
||||
private readonly PersistedGrant _updateGrant;
|
||||
@ -39,14 +35,8 @@ public class RedisPersistedGrantStoreTests
|
||||
// 15) "ClientId"
|
||||
// 16) "web"
|
||||
|
||||
public RedisPersistedGrantStoreTests()
|
||||
public PersistedGrantStoreTests()
|
||||
{
|
||||
_redisGrantStore = new RedisPersistedGrantStore(
|
||||
ConnectionMultiplexer.Connect("localhost"),
|
||||
NullLogger<RedisPersistedGrantStore>.Instance,
|
||||
new InMemoryPersistedGrantStore()
|
||||
);
|
||||
|
||||
var sqlConnectionString = "YOUR CONNECTION STRING HERE";
|
||||
_sqlGrantStore = new PersistedGrantStore(
|
||||
new GrantRepository(
|
||||
@ -78,17 +68,13 @@ public class RedisPersistedGrantStoreTests
|
||||
};
|
||||
}
|
||||
|
||||
[Params(Redis, SQL, Cosmos)]
|
||||
[Params(SQL, Cosmos)]
|
||||
public string StoreType { get; set; } = null!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
if (StoreType == Redis)
|
||||
{
|
||||
_grantStore = _redisGrantStore;
|
||||
}
|
||||
else if (StoreType == SQL)
|
||||
if (StoreType == SQL)
|
||||
{
|
||||
_grantStore = _sqlGrantStore;
|
||||
}
|
@ -2,13 +2,12 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.11" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -43,7 +43,7 @@ export const options = {
|
||||
},
|
||||
thresholds: {
|
||||
http_req_failed: ["rate<0.01"],
|
||||
http_req_duration: ["p(95)<200"],
|
||||
http_req_duration: ["p(95)<350"],
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -44,7 +44,7 @@ export const options = {
|
||||
},
|
||||
thresholds: {
|
||||
http_req_failed: ["rate<0.01"],
|
||||
http_req_duration: ["p(95)<300"],
|
||||
http_req_duration: ["p(95)<400"],
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<UserSecretsId>bitwarden-Admin</UserSecretsId>
|
||||
@ -24,8 +24,4 @@
|
||||
</When>
|
||||
</Choose>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.26.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -3,13 +3,5 @@
|
||||
public class AdminSettings
|
||||
{
|
||||
public virtual string Admins { get; set; }
|
||||
public virtual CloudflareSettings Cloudflare { get; set; }
|
||||
public int? DeleteTrashDaysAgo { get; set; }
|
||||
|
||||
public class CloudflareSettings
|
||||
{
|
||||
public string ZoneId { get; set; }
|
||||
public string AuthEmail { get; set; }
|
||||
public string AuthKey { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,94 +0,0 @@
|
||||
using Bit.Admin.Models;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Azure.Cosmos;
|
||||
using Microsoft.Azure.Cosmos.Linq;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Admin.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
[RequirePermission(Enums.Permission.Logs_View)]
|
||||
public class LogsController : Controller
|
||||
{
|
||||
private const string Database = "Diagnostics";
|
||||
private const string Container = "Logs";
|
||||
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public LogsController(GlobalSettings globalSettings)
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Index(string cursor = null, int count = 50,
|
||||
LogEventLevel? level = null, string project = null, DateTime? start = null, DateTime? end = null)
|
||||
{
|
||||
using (var client = new CosmosClient(_globalSettings.DocumentDb.Uri,
|
||||
_globalSettings.DocumentDb.Key))
|
||||
{
|
||||
var cosmosContainer = client.GetContainer(Database, Container);
|
||||
var query = cosmosContainer.GetItemLinqQueryable<LogModel>(
|
||||
requestOptions: new QueryRequestOptions()
|
||||
{
|
||||
MaxItemCount = count
|
||||
},
|
||||
continuationToken: cursor
|
||||
).AsQueryable();
|
||||
|
||||
if (level.HasValue)
|
||||
{
|
||||
query = query.Where(l => l.Level == level.Value.ToString());
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(project))
|
||||
{
|
||||
query = query.Where(l => l.Properties != null && l.Properties["Project"] == (object)project);
|
||||
}
|
||||
if (start.HasValue)
|
||||
{
|
||||
query = query.Where(l => l.Timestamp >= start.Value);
|
||||
}
|
||||
if (end.HasValue)
|
||||
{
|
||||
query = query.Where(l => l.Timestamp <= end.Value);
|
||||
}
|
||||
var feedIterator = query.OrderByDescending(l => l.Timestamp).ToFeedIterator();
|
||||
var response = await feedIterator.ReadNextAsync();
|
||||
|
||||
return View(new LogsModel
|
||||
{
|
||||
Level = level,
|
||||
Project = project,
|
||||
Start = start,
|
||||
End = end,
|
||||
Items = response.ToList(),
|
||||
Count = count,
|
||||
Cursor = cursor,
|
||||
NextCursor = response.ContinuationToken
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> View(Guid id)
|
||||
{
|
||||
using (var client = new CosmosClient(_globalSettings.DocumentDb.Uri,
|
||||
_globalSettings.DocumentDb.Key))
|
||||
{
|
||||
var cosmosContainer = client.GetContainer(Database, Container);
|
||||
var query = cosmosContainer.GetItemLinqQueryable<LogDetailsModel>()
|
||||
.AsQueryable()
|
||||
.Where(l => l.Id == id.ToString());
|
||||
|
||||
var response = await query.ToFeedIterator().ReadNextAsync();
|
||||
if (response == null || response.Count == 0)
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
return View(response.First());
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,9 @@ using Bit.Admin.Models;
|
||||
using Bit.Admin.Services;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -48,6 +50,9 @@ public class OrganizationsController : Controller
|
||||
private readonly ISecretRepository _secretRepository;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
|
||||
private readonly IRemovePaymentMethodCommand _removePaymentMethodCommand;
|
||||
|
||||
public OrganizationsController(
|
||||
IOrganizationService organizationService,
|
||||
@ -71,7 +76,10 @@ public class OrganizationsController : Controller
|
||||
ICurrentContext currentContext,
|
||||
ISecretRepository secretRepository,
|
||||
IProjectRepository projectRepository,
|
||||
IServiceAccountRepository serviceAccountRepository)
|
||||
IServiceAccountRepository serviceAccountRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
|
||||
IRemovePaymentMethodCommand removePaymentMethodCommand)
|
||||
{
|
||||
_organizationService = organizationService;
|
||||
_organizationRepository = organizationRepository;
|
||||
@ -95,6 +103,9 @@ public class OrganizationsController : Controller
|
||||
_secretRepository = secretRepository;
|
||||
_projectRepository = projectRepository;
|
||||
_serviceAccountRepository = serviceAccountRepository;
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
|
||||
_removePaymentMethodCommand = removePaymentMethodCommand;
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Org_List_View)]
|
||||
@ -202,7 +213,6 @@ public class OrganizationsController : Controller
|
||||
var organization = await GetOrganization(id, model);
|
||||
|
||||
if (organization.UseSecretsManager &&
|
||||
!organization.SecretsManagerBeta &&
|
||||
!StaticStore.GetPlan(organization.PlanType).SupportsSecretsManager)
|
||||
{
|
||||
throw new BadRequestException("Plan does not support Secrets Manager");
|
||||
@ -286,6 +296,38 @@ public class OrganizationsController : Controller
|
||||
|
||||
return Json(null);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[RequirePermission(Permission.Provider_Edit)]
|
||||
public async Task<IActionResult> UnlinkOrganizationFromProviderAsync(Guid id)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
if (organization is null)
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
var provider = await _providerRepository.GetByOrganizationIdAsync(id);
|
||||
if (provider is null)
|
||||
{
|
||||
return RedirectToAction("Edit", new { id });
|
||||
}
|
||||
|
||||
var providerOrganization = await _providerOrganizationRepository.GetByOrganizationId(id);
|
||||
if (providerOrganization is null)
|
||||
{
|
||||
return RedirectToAction("Edit", new { id });
|
||||
}
|
||||
|
||||
await _removeOrganizationFromProviderCommand.RemoveOrganizationFromProvider(
|
||||
provider,
|
||||
providerOrganization,
|
||||
organization);
|
||||
|
||||
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
|
||||
|
||||
return Json(null);
|
||||
}
|
||||
private async Task<Organization> GetOrganization(Guid id, OrganizationEditModel model)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
@ -320,7 +362,6 @@ public class OrganizationsController : Controller
|
||||
organization.UseTotp = model.UseTotp;
|
||||
organization.UsersGetPremium = model.UsersGetPremium;
|
||||
organization.UseSecretsManager = model.UseSecretsManager;
|
||||
organization.SecretsManagerBeta = model.SecretsManagerBeta;
|
||||
|
||||
//secrets
|
||||
organization.SmSeats = model.SmSeats;
|
||||
|
67
src/Admin/Controllers/ProviderOrganizationsController.cs
Normal file
67
src/Admin/Controllers/ProviderOrganizationsController.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using Bit.Admin.Enums;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Admin.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public class ProviderOrganizationsController : Controller
|
||||
{
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
|
||||
private readonly IRemovePaymentMethodCommand _removePaymentMethodCommand;
|
||||
|
||||
public ProviderOrganizationsController(IProviderRepository providerRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
|
||||
IRemovePaymentMethodCommand removePaymentMethodCommand)
|
||||
{
|
||||
_providerRepository = providerRepository;
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
|
||||
_removePaymentMethodCommand = removePaymentMethodCommand;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[RequirePermission(Permission.Provider_Edit)]
|
||||
public async Task<IActionResult> DeleteAsync(Guid providerId, Guid id)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||
if (provider is null)
|
||||
{
|
||||
return RedirectToAction("Index", "Providers");
|
||||
}
|
||||
|
||||
var providerOrganization = await _providerOrganizationRepository.GetByIdAsync(id);
|
||||
if (providerOrganization is null)
|
||||
{
|
||||
return RedirectToAction("View", "Providers", new { id = providerId });
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);
|
||||
if (organization == null)
|
||||
{
|
||||
return RedirectToAction("View", "Providers", new { id = providerId });
|
||||
}
|
||||
|
||||
await _removeOrganizationFromProviderCommand.RemoveOrganizationFromProvider(
|
||||
provider,
|
||||
providerOrganization,
|
||||
organization);
|
||||
|
||||
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
|
||||
|
||||
return Json(null);
|
||||
}
|
||||
}
|
@ -27,7 +27,7 @@ public class UsersController : Controller
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
private bool UseFlexibleCollections =>
|
||||
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections);
|
||||
|
||||
public UsersController(
|
||||
IUserRepository userRepository,
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||
|
||||
LABEL com.bitwarden.product="bitwarden"
|
||||
|
||||
|
@ -47,7 +47,5 @@ public enum Permission
|
||||
Tools_GenerateLicenseFile,
|
||||
Tools_ManageTaxRates,
|
||||
Tools_ManageStripeSubscriptions,
|
||||
Tools_CreateEditTransaction,
|
||||
|
||||
Logs_View
|
||||
Tools_CreateEditTransaction
|
||||
}
|
||||
|
@ -1,83 +0,0 @@
|
||||
using Amazon;
|
||||
using Amazon.SQS;
|
||||
using Amazon.SQS.Model;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bit.Admin.HostedServices;
|
||||
|
||||
public class AmazonSqsBlockIpHostedService : BlockIpHostedService
|
||||
{
|
||||
private AmazonSQSClient _client;
|
||||
|
||||
public AmazonSqsBlockIpHostedService(
|
||||
ILogger<AmazonSqsBlockIpHostedService> logger,
|
||||
IOptions<AdminSettings> adminSettings,
|
||||
GlobalSettings globalSettings)
|
||||
: base(logger, adminSettings, globalSettings)
|
||||
{ }
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
_client?.Dispose();
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_client = new AmazonSQSClient(_globalSettings.Amazon.AccessKeyId,
|
||||
_globalSettings.Amazon.AccessKeySecret, RegionEndpoint.GetBySystemName(_globalSettings.Amazon.Region));
|
||||
var blockIpQueue = await _client.GetQueueUrlAsync("block-ip", cancellationToken);
|
||||
var blockIpQueueUrl = blockIpQueue.QueueUrl;
|
||||
var unblockIpQueue = await _client.GetQueueUrlAsync("unblock-ip", cancellationToken);
|
||||
var unblockIpQueueUrl = unblockIpQueue.QueueUrl;
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var blockMessageResponse = await _client.ReceiveMessageAsync(new ReceiveMessageRequest
|
||||
{
|
||||
QueueUrl = blockIpQueueUrl,
|
||||
MaxNumberOfMessages = 10,
|
||||
WaitTimeSeconds = 15
|
||||
}, cancellationToken);
|
||||
if (blockMessageResponse.Messages.Any())
|
||||
{
|
||||
foreach (var message in blockMessageResponse.Messages)
|
||||
{
|
||||
try
|
||||
{
|
||||
await BlockIpAsync(message.Body, cancellationToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to block IP.");
|
||||
}
|
||||
await _client.DeleteMessageAsync(blockIpQueueUrl, message.ReceiptHandle, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
var unblockMessageResponse = await _client.ReceiveMessageAsync(new ReceiveMessageRequest
|
||||
{
|
||||
QueueUrl = unblockIpQueueUrl,
|
||||
MaxNumberOfMessages = 10,
|
||||
WaitTimeSeconds = 15
|
||||
}, cancellationToken);
|
||||
if (unblockMessageResponse.Messages.Any())
|
||||
{
|
||||
foreach (var message in unblockMessageResponse.Messages)
|
||||
{
|
||||
try
|
||||
{
|
||||
await UnblockIpAsync(message.Body, cancellationToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to unblock IP.");
|
||||
}
|
||||
await _client.DeleteMessageAsync(unblockIpQueueUrl, message.ReceiptHandle, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(15));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
using Azure.Storage.Queues;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bit.Admin.HostedServices;
|
||||
|
||||
public class AzureQueueBlockIpHostedService : BlockIpHostedService
|
||||
{
|
||||
private QueueClient _blockIpQueueClient;
|
||||
private QueueClient _unblockIpQueueClient;
|
||||
|
||||
public AzureQueueBlockIpHostedService(
|
||||
ILogger<AzureQueueBlockIpHostedService> logger,
|
||||
IOptions<AdminSettings> adminSettings,
|
||||
GlobalSettings globalSettings)
|
||||
: base(logger, adminSettings, globalSettings)
|
||||
{ }
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_blockIpQueueClient = new QueueClient(_globalSettings.Storage.ConnectionString, "blockip");
|
||||
_unblockIpQueueClient = new QueueClient(_globalSettings.Storage.ConnectionString, "unblockip");
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var blockMessages = await _blockIpQueueClient.ReceiveMessagesAsync(maxMessages: 32);
|
||||
if (blockMessages.Value?.Any() ?? false)
|
||||
{
|
||||
foreach (var message in blockMessages.Value)
|
||||
{
|
||||
try
|
||||
{
|
||||
await BlockIpAsync(message.MessageText, cancellationToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to block IP.");
|
||||
}
|
||||
await _blockIpQueueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt);
|
||||
}
|
||||
}
|
||||
|
||||
var unblockMessages = await _unblockIpQueueClient.ReceiveMessagesAsync(maxMessages: 32);
|
||||
if (unblockMessages.Value?.Any() ?? false)
|
||||
{
|
||||
foreach (var message in unblockMessages.Value)
|
||||
{
|
||||
try
|
||||
{
|
||||
await UnblockIpAsync(message.MessageText, cancellationToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to unblock IP.");
|
||||
}
|
||||
await _unblockIpQueueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt);
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(15));
|
||||
}
|
||||
}
|
||||
}
|
@ -67,14 +67,14 @@ public class AzureQueueMailHostedService : IHostedService
|
||||
|
||||
if (root.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var mailQueueMessage in root.ToObject<List<MailQueueMessage>>())
|
||||
foreach (var mailQueueMessage in root.Deserialize<List<MailQueueMessage>>())
|
||||
{
|
||||
await _mailService.SendEnqueuedMailMessageAsync(mailQueueMessage);
|
||||
}
|
||||
}
|
||||
else if (root.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
var mailQueueMessage = root.ToObject<MailQueueMessage>();
|
||||
var mailQueueMessage = root.Deserialize<MailQueueMessage>();
|
||||
await _mailService.SendEnqueuedMailMessageAsync(mailQueueMessage);
|
||||
}
|
||||
}
|
||||
|
@ -1,164 +0,0 @@
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bit.Admin.HostedServices;
|
||||
|
||||
public abstract class BlockIpHostedService : IHostedService, IDisposable
|
||||
{
|
||||
protected readonly ILogger<BlockIpHostedService> _logger;
|
||||
protected readonly GlobalSettings _globalSettings;
|
||||
private readonly AdminSettings _adminSettings;
|
||||
|
||||
private Task _executingTask;
|
||||
private CancellationTokenSource _cts;
|
||||
private HttpClient _httpClient = new HttpClient();
|
||||
|
||||
public BlockIpHostedService(
|
||||
ILogger<BlockIpHostedService> logger,
|
||||
IOptions<AdminSettings> adminSettings,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
_adminSettings = adminSettings?.Value;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_executingTask = ExecuteAsync(_cts.Token);
|
||||
return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_executingTask == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_cts.Cancel();
|
||||
await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken));
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
public virtual void Dispose()
|
||||
{ }
|
||||
|
||||
protected abstract Task ExecuteAsync(CancellationToken cancellationToken);
|
||||
|
||||
protected async Task BlockIpAsync(string message, CancellationToken cancellationToken)
|
||||
{
|
||||
var request = new HttpRequestMessage();
|
||||
request.Headers.Accept.Clear();
|
||||
request.Headers.Add("X-Auth-Email", _adminSettings.Cloudflare.AuthEmail);
|
||||
request.Headers.Add("X-Auth-Key", _adminSettings.Cloudflare.AuthKey);
|
||||
request.Method = HttpMethod.Post;
|
||||
request.RequestUri = new Uri("https://api.cloudflare.com/" +
|
||||
$"client/v4/zones/{_adminSettings.Cloudflare.ZoneId}/firewall/access_rules/rules");
|
||||
|
||||
request.Content = JsonContent.Create(new
|
||||
{
|
||||
mode = "block",
|
||||
configuration = new
|
||||
{
|
||||
target = "ip",
|
||||
value = message
|
||||
},
|
||||
notes = $"Rate limit abuse on {DateTime.UtcNow.ToString()}."
|
||||
});
|
||||
|
||||
var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var accessRuleResponse = await response.Content.ReadFromJsonAsync<AccessRuleResponse>(cancellationToken: cancellationToken);
|
||||
if (!accessRuleResponse.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Send `accessRuleResponse.Result?.Id` message to unblock queue
|
||||
}
|
||||
|
||||
protected async Task UnblockIpAsync(string message, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.Contains(".") || message.Contains(":"))
|
||||
{
|
||||
// IP address messages
|
||||
var request = new HttpRequestMessage();
|
||||
request.Headers.Accept.Clear();
|
||||
request.Headers.Add("X-Auth-Email", _adminSettings.Cloudflare.AuthEmail);
|
||||
request.Headers.Add("X-Auth-Key", _adminSettings.Cloudflare.AuthKey);
|
||||
request.Method = HttpMethod.Get;
|
||||
request.RequestUri = new Uri("https://api.cloudflare.com/" +
|
||||
$"client/v4/zones/{_adminSettings.Cloudflare.ZoneId}/firewall/access_rules/rules?" +
|
||||
$"configuration_target=ip&configuration_value={message}");
|
||||
|
||||
var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var listResponse = await response.Content.ReadFromJsonAsync<ListResponse>(cancellationToken: cancellationToken);
|
||||
if (!listResponse.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var rule in listResponse.Result)
|
||||
{
|
||||
await DeleteAccessRuleAsync(rule.Id, cancellationToken);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Rule Id messages
|
||||
await DeleteAccessRuleAsync(message, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task DeleteAccessRuleAsync(string ruleId, CancellationToken cancellationToken)
|
||||
{
|
||||
var request = new HttpRequestMessage();
|
||||
request.Headers.Accept.Clear();
|
||||
request.Headers.Add("X-Auth-Email", _adminSettings.Cloudflare.AuthEmail);
|
||||
request.Headers.Add("X-Auth-Key", _adminSettings.Cloudflare.AuthKey);
|
||||
request.Method = HttpMethod.Delete;
|
||||
request.RequestUri = new Uri("https://api.cloudflare.com/" +
|
||||
$"client/v4/zones/{_adminSettings.Cloudflare.ZoneId}/firewall/access_rules/rules/{ruleId}");
|
||||
await _httpClient.SendAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
public class ListResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public List<AccessRuleResultResponse> Result { get; set; }
|
||||
}
|
||||
|
||||
public class AccessRuleResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public AccessRuleResultResponse Result { get; set; }
|
||||
}
|
||||
|
||||
public class AccessRuleResultResponse
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public ConfigurationResponse Configuration { get; set; }
|
||||
|
||||
public class ConfigurationResponse
|
||||
{
|
||||
public string Target { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
using Microsoft.Azure.Documents;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class LogModel : Resource
|
||||
{
|
||||
public long EventIdHash { get; set; }
|
||||
public string Level { get; set; }
|
||||
public string Message { get; set; }
|
||||
public string MessageTruncated => Message.Length > 200 ? $"{Message.Substring(0, 200)}..." : Message;
|
||||
public string MessageTemplate { get; set; }
|
||||
public IDictionary<string, object> Properties { get; set; }
|
||||
public string Project => Properties?.ContainsKey("Project") ?? false ? Properties["Project"].ToString() : null;
|
||||
}
|
||||
|
||||
public class LogDetailsModel : LogModel
|
||||
{
|
||||
public JObject Exception { get; set; }
|
||||
|
||||
public string ExceptionToString(JObject e)
|
||||
{
|
||||
if (e == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var val = string.Empty;
|
||||
if (e["Message"] != null && e["Message"].ToObject<string>() != null)
|
||||
{
|
||||
val += "Message:\n";
|
||||
val += e["Message"] + "\n";
|
||||
}
|
||||
|
||||
if (e["StackTrace"] != null && e["StackTrace"].ToObject<string>() != null)
|
||||
{
|
||||
val += "\nStack Trace:\n";
|
||||
val += e["StackTrace"];
|
||||
}
|
||||
else if (e["StackTraceString"] != null && e["StackTraceString"].ToObject<string>() != null)
|
||||
{
|
||||
val += "\nStack Trace String:\n";
|
||||
val += e["StackTraceString"];
|
||||
}
|
||||
|
||||
if (e["InnerException"] != null && e["InnerException"].ToObject<JObject>() != null)
|
||||
{
|
||||
val += "\n\n=== Inner Exception ===\n\n";
|
||||
val += ExceptionToString(e["InnerException"].ToObject<JObject>());
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class LogsModel : CursorPagedModel<LogModel>
|
||||
{
|
||||
public LogEventLevel? Level { get; set; }
|
||||
public string Project { get; set; }
|
||||
public DateTime? Start { get; set; }
|
||||
public DateTime? End { get; set; }
|
||||
}
|
@ -70,7 +70,6 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
MaxAutoscaleSmSeats = org.MaxAutoscaleSmSeats;
|
||||
SmServiceAccounts = org.SmServiceAccounts;
|
||||
MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;
|
||||
SecretsManagerBeta = org.SecretsManagerBeta;
|
||||
}
|
||||
|
||||
public BillingInfo BillingInfo { get; set; }
|
||||
@ -150,8 +149,6 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
public int? SmServiceAccounts { get; set; }
|
||||
[Display(Name = "Max Autoscale Service Accounts")]
|
||||
public int? MaxAutoscaleSmServiceAccounts { get; set; }
|
||||
[Display(Name = "Secrets Manager Beta")]
|
||||
public bool SecretsManagerBeta { get; set; }
|
||||
|
||||
/**
|
||||
* Creates a Plan[] object for use in Javascript
|
||||
@ -210,7 +207,6 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
existingOrganization.MaxAutoscaleSmSeats = MaxAutoscaleSmSeats;
|
||||
existingOrganization.SmServiceAccounts = SmServiceAccounts;
|
||||
existingOrganization.MaxAutoscaleSmServiceAccounts = MaxAutoscaleSmServiceAccounts;
|
||||
existingOrganization.SecretsManagerBeta = SecretsManagerBeta;
|
||||
return existingOrganization;
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ using Stripe;
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Bit.Admin.Services;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
|
||||
#if !OSS
|
||||
using Bit.Commercial.Core.Utilities;
|
||||
@ -87,6 +88,7 @@ public class Startup
|
||||
services.AddBaseServices(globalSettings);
|
||||
services.AddDefaultServices(globalSettings);
|
||||
services.AddScoped<IAccessControlService, AccessControlService>();
|
||||
services.AddBillingCommands();
|
||||
|
||||
#if OSS
|
||||
services.AddOosServices();
|
||||
@ -116,14 +118,6 @@ public class Startup
|
||||
}
|
||||
else
|
||||
{
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.Storage.ConnectionString))
|
||||
{
|
||||
services.AddHostedService<HostedServices.AzureQueueBlockIpHostedService>();
|
||||
}
|
||||
else if (CoreHelpers.SettingHasValue(globalSettings.Amazon?.AccessKeySecret))
|
||||
{
|
||||
services.AddHostedService<HostedServices.AmazonSqsBlockIpHostedService>();
|
||||
}
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.Mail.ConnectionString))
|
||||
{
|
||||
services.AddHostedService<HostedServices.AzureQueueMailHostedService>();
|
||||
|
@ -47,8 +47,7 @@ public static class RolePermissionMapping
|
||||
Permission.Tools_PromoteAdmin,
|
||||
Permission.Tools_GenerateLicenseFile,
|
||||
Permission.Tools_ManageTaxRates,
|
||||
Permission.Tools_ManageStripeSubscriptions,
|
||||
Permission.Logs_View
|
||||
Permission.Tools_ManageStripeSubscriptions
|
||||
}
|
||||
},
|
||||
{ "admin", new List<Permission>
|
||||
@ -94,8 +93,7 @@ public static class RolePermissionMapping
|
||||
Permission.Tools_GenerateLicenseFile,
|
||||
Permission.Tools_ManageTaxRates,
|
||||
Permission.Tools_ManageStripeSubscriptions,
|
||||
Permission.Tools_CreateEditTransaction,
|
||||
Permission.Logs_View
|
||||
Permission.Tools_CreateEditTransaction
|
||||
}
|
||||
},
|
||||
{ "cs", new List<Permission>
|
||||
@ -123,8 +121,7 @@ public static class RolePermissionMapping
|
||||
Permission.Org_Billing_View,
|
||||
Permission.Org_Billing_LaunchGateway,
|
||||
Permission.Provider_List_View,
|
||||
Permission.Provider_View,
|
||||
Permission.Logs_View
|
||||
Permission.Provider_View
|
||||
}
|
||||
},
|
||||
{ "billing", new List<Permission>
|
||||
@ -163,8 +160,7 @@ public static class RolePermissionMapping
|
||||
Permission.Tools_GenerateLicenseFile,
|
||||
Permission.Tools_ManageTaxRates,
|
||||
Permission.Tools_ManageStripeSubscriptions,
|
||||
Permission.Tools_CreateEditTransaction,
|
||||
Permission.Logs_View
|
||||
Permission.Tools_CreateEditTransaction
|
||||
}
|
||||
},
|
||||
{ "sales", new List<Permission>
|
||||
@ -193,8 +189,7 @@ public static class RolePermissionMapping
|
||||
Permission.Provider_Create,
|
||||
Permission.Provider_Edit,
|
||||
Permission.Provider_View,
|
||||
Permission.Provider_ResendEmailInvite,
|
||||
Permission.Logs_View
|
||||
Permission.Provider_ResendEmailInvite
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -1,91 +0,0 @@
|
||||
@model LogsModel
|
||||
@{
|
||||
ViewData["Title"] = "Logs";
|
||||
}
|
||||
|
||||
<h1>Logs</h1>
|
||||
|
||||
<p>Current UTC time: @DateTime.UtcNow.ToString()</p>
|
||||
|
||||
<form class="form-inline mb-2" method="get">
|
||||
<label class="sr-only" asp-for="Level">Level</label>
|
||||
<select class="form-control mb-2 mr-2" asp-for="Level" name="level"
|
||||
asp-items="Html.GetEnumSelectList<Serilog.Events.LogEventLevel>()">
|
||||
<option value="">-- Level --</option>
|
||||
</select>
|
||||
<label class="sr-only" asp-for="Project">Project</label>
|
||||
<select class="form-control mb-2 mr-2" asp-for="Project" name="project">
|
||||
<option asp-selected="string.IsNullOrWhiteSpace(Model.Project)" value="">-- Project --</option>
|
||||
<option asp-selected="@(Model.Project == "Admin")" value="Admin">Admin</option>
|
||||
<option asp-selected="@(Model.Project == "Api")" value="Api">Api</option>
|
||||
<option asp-selected="@(Model.Project == "Billing")" value="Billing">Billing</option>
|
||||
<option asp-selected="@(Model.Project == "Events")" value="Events">Events</option>
|
||||
<option asp-selected="@(Model.Project == "Events Processor")" value="Events Processor">Events Processor</option>
|
||||
<option asp-selected="@(Model.Project == "Identity")" value="Identity">Identity</option>
|
||||
<option asp-selected="@(Model.Project == "Notifications")" value="Notifications">Notifications</option>
|
||||
<option asp-selected="@(Model.Project == "Icons")" value="Icons">Icons</option>
|
||||
<option asp-selected="@(Model.Project == "SSO")" value="SSO">SSO</option>
|
||||
<option asp-selected="@(Model.Project == "Scim")" value="Scim">SCIM</option>
|
||||
</select>
|
||||
<input class="form-control mb-2 mr-2" type="datetime-local" asp-for="Start" name="start" placeholder="Start Date">
|
||||
<input class="form-control mb-2 mr-2" type="datetime-local" asp-for="End" name="end" placeholder="End Date">
|
||||
<button type="submit" class="btn btn-primary mb-2" title="Search"><i class="fa fa-search"></i> Search</button>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50px;"> </th>
|
||||
<th style="width: 210px;">Timestamp</th>
|
||||
<th style="width: 105px;">Project</th>
|
||||
<th style="width: 125px;">Level</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if(!Model.Items.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="5">No results to list.</td>
|
||||
</tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach(var log in Model.Items)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<a asp-action="View" asp-route-id="@log.Id" title="View">
|
||||
<i class="fa fa-file-text-o fa-lg"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td>@log.Timestamp.ToString()</td>
|
||||
<td>@(string.IsNullOrWhiteSpace(log.Project) ? "-" : log.Project)</td>
|
||||
<td>@log.Level</td>
|
||||
<td>@log.MessageTruncated</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
@if(string.IsNullOrWhiteSpace(Model.NextCursor))
|
||||
{
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1">Next</a>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" asp-action="Index" asp-route-cursor="@Model.NextCursor"
|
||||
asp-route-count="@Model.Count" asp-route-project="@Model.Project"
|
||||
asp-route-level="@Model.Level">Next</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
@ -1,42 +0,0 @@
|
||||
@model LogDetailsModel
|
||||
@{
|
||||
ViewData["Title"] = "Log: " + Model.Id;
|
||||
}
|
||||
|
||||
<h1>Log <small>@Model.Id</small></h1>
|
||||
|
||||
<h2>Information</h2>
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4 col-lg-3">Id</dt>
|
||||
<dd class="col-sm-8 col-lg-9"><code>@Model.Id</code></dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Event Id Hash</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.EventIdHash</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Timestamp</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Timestamp.ToString()</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Level</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Level</dd>
|
||||
</dl>
|
||||
|
||||
<h2>Message</h2>
|
||||
<pre style="max-height: 500px;">@Model.Message</pre>
|
||||
|
||||
@if(Model.Exception != null)
|
||||
{
|
||||
<h2>Exception</h2>
|
||||
<pre style="max-height: 500px;">@Model.ExceptionToString(Model.Exception)</pre>
|
||||
}
|
||||
|
||||
@if(Model.Properties != null && Model.Properties.Count > 0)
|
||||
{
|
||||
<h2>Properties</h2>
|
||||
<dl class="row">
|
||||
@foreach(var prop in Model.Properties)
|
||||
{
|
||||
<dt class="col-sm-4 col-lg-3">@prop.Key</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(prop.Value?.ToString() ?? "-")</dd>
|
||||
}
|
||||
</dl>
|
||||
}
|
@ -8,6 +8,7 @@
|
||||
var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.Org_BillingInformation_View);
|
||||
var canInitiateTrial = AccessControlService.UserHasPermission(Permission.Org_InitiateTrial);
|
||||
var canDelete = AccessControlService.UserHasPermission(Permission.Org_Delete);
|
||||
var canUnlinkFromProvider = AccessControlService.UserHasPermission(Permission.Provider_Edit);
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
@ -81,7 +82,7 @@
|
||||
<div class="d-flex mt-4">
|
||||
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
||||
<div class="ml-auto d-flex">
|
||||
@if (canInitiateTrial)
|
||||
@if (canInitiateTrial && Model.Provider is null)
|
||||
{
|
||||
<button class="btn btn-secondary mr-2" type="button" id="teams-trial">
|
||||
Teams Trial
|
||||
@ -90,6 +91,15 @@
|
||||
Enterprise Trial
|
||||
</button>
|
||||
}
|
||||
@if (canUnlinkFromProvider && Model.Provider is not null)
|
||||
{
|
||||
<button
|
||||
class="btn btn-outline-danger mr-2"
|
||||
onclick="return unlinkProvider('@Model.Organization.Id');"
|
||||
>
|
||||
Unlink provider
|
||||
</button>
|
||||
}
|
||||
@if (canDelete)
|
||||
{
|
||||
<form asp-action="Delete" asp-route-id="@Model.Organization.Id"
|
||||
|
@ -1,7 +1,15 @@
|
||||
@using Bit.Core.AdminConsole.Enums.Provider
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using Bit.Admin.Enums
|
||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||
@model ProviderViewModel
|
||||
|
||||
@{
|
||||
var canUnlinkFromProvider = AccessControlService.UserHasPermission(Permission.Provider_Edit);
|
||||
}
|
||||
|
||||
@await Html.PartialAsync("_ProviderScripts")
|
||||
@await Html.PartialAsync("_ProviderOrganizationScripts")
|
||||
|
||||
<h2>Provider Organizations</h2>
|
||||
<div class="row">
|
||||
@ -32,26 +40,28 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var org in Model.ProviderOrganizations)
|
||||
@foreach (var providerOrganization in Model.ProviderOrganizations)
|
||||
{
|
||||
<tr>
|
||||
<td class="align-middle">
|
||||
<a asp-controller="Organizations" asp-action="Edit" asp-route-id="@org.OrganizationId">@org.OrganizationName</a>
|
||||
<a asp-controller="Organizations" asp-action="Edit" asp-route-id="@providerOrganization.OrganizationId">@providerOrganization.OrganizationName</a>
|
||||
</td>
|
||||
<td>
|
||||
@org.Status
|
||||
@providerOrganization.Status
|
||||
</td>
|
||||
<td>
|
||||
<div class="float-right">
|
||||
@if (org.Status == OrganizationStatusType.Pending)
|
||||
@if (canUnlinkFromProvider)
|
||||
{
|
||||
<a href="#" class="float-right" onclick="return resendOwnerInvite('@org.OrganizationId');">
|
||||
<i class="fa fa-envelope-o fa-lg" title="Resend Setup Invite"></i>
|
||||
<a href="#" class="text-danger float-right" onclick="return unlinkProvider('@Model.Provider.Id', '@providerOrganization.Id');">
|
||||
Unlink provider
|
||||
</a>
|
||||
}
|
||||
else
|
||||
@if (providerOrganization.Status == OrganizationStatusType.Pending)
|
||||
{
|
||||
<i class="fa fa-envelope-o fa-lg text-secondary"></i>
|
||||
<a href="#" class="float-right mr-3" onclick="return resendOwnerInvite('@providerOrganization.OrganizationId');">
|
||||
Resend invitation
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
|
@ -0,0 +1,21 @@
|
||||
<script>
|
||||
function unlinkProvider(providerId, id) {
|
||||
if (confirm('Are you sure you want to unlink this organization from its provider?')) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: `@Url.Action("Delete", "ProviderOrganizations")?providerId=${providerId}&id=${id}`,
|
||||
dataType: 'json',
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function (response) {
|
||||
alert("Successfully unlinked provider");
|
||||
window.location.href = `@Url.Action("Edit", "Providers")?id=${providerId}`;
|
||||
},
|
||||
error: function (response) {
|
||||
alert("Error!");
|
||||
}
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
</script>
|
@ -1,4 +1,4 @@
|
||||
@using Bit.Admin.Enums;
|
||||
@using Bit.Admin.Enums;
|
||||
|
||||
@inject SignInManager<IdentityUser> SignInManager
|
||||
@inject Bit.Core.Settings.GlobalSettings GlobalSettings
|
||||
@ -8,7 +8,6 @@
|
||||
var canViewUsers = AccessControlService.UserHasPermission(Permission.User_List_View);
|
||||
var canViewOrgs = AccessControlService.UserHasPermission(Permission.Org_List_View);
|
||||
var canViewProviders = AccessControlService.UserHasPermission(Permission.Provider_List_View);
|
||||
var canViewLogs = AccessControlService.UserHasPermission(Permission.Logs_View);
|
||||
var canChargeBraintree = AccessControlService.UserHasPermission(Permission.Tools_ChargeBrainTreeCustomer);
|
||||
var canCreateTransaction = AccessControlService.UserHasPermission(Permission.Tools_CreateEditTransaction);
|
||||
var canPromoteAdmin = AccessControlService.UserHasPermission(Permission.Tools_PromoteAdmin);
|
||||
@ -121,12 +120,6 @@
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
@if (canViewLogs)
|
||||
{
|
||||
<li class="nav-item" active-controller="Logs">
|
||||
<a class="nav-link" asp-controller="Logs" asp-action="Index">Logs</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
}
|
||||
@if (GlobalSettings.SelfHosted)
|
||||
|
@ -174,10 +174,6 @@
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseSecretsManager" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UseSecretsManager"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="SecretsManagerBeta" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="SecretsManagerBeta"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@ -217,7 +213,7 @@
|
||||
|
||||
@if (canViewPlan)
|
||||
{
|
||||
<div id="organization-secrets-configuration" hidden="@(!Model.UseSecretsManager || Model.SecretsManagerBeta)">
|
||||
<div id="organization-secrets-configuration" hidden="@(!Model.UseSecretsManager)">
|
||||
<h2>Secrets Manager Configuration</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
@ -276,20 +272,7 @@
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="BillingEmail"></label>
|
||||
@if (Model.Provider?.Type == ProviderType.Reseller)
|
||||
{
|
||||
<input type="email" class="form-control" asp-for="BillingEmail" readonly="readonly">
|
||||
}
|
||||
else
|
||||
{
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
asp-for="BillingEmail"
|
||||
readonly='@(!canEditBilling)'
|
||||
pattern="@(@"[^@\s]+@[^@\s]+\.[^@\s]+")"
|
||||
title="Email address must be in the format 'address@domain.com'.">
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
|
@ -46,24 +46,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// SM beta requires SM access
|
||||
document.getElementById('@(nameof(Model.SecretsManagerBeta))').checked = false;
|
||||
clearSecretsManagerConfiguration();
|
||||
});
|
||||
|
||||
document.getElementById('@(nameof(Model.SecretsManagerBeta))').addEventListener('change', (event) => {
|
||||
document.getElementById('organization-secrets-configuration').hidden = event.target.checked;
|
||||
|
||||
if (event.target.checked) {
|
||||
// SM beta requires SM access
|
||||
document.getElementById('@(nameof(Model.UseSecretsManager))').checked = true;
|
||||
// SM Beta orgs do not have subscription limits
|
||||
clearSecretsManagerConfiguration();
|
||||
return;
|
||||
}
|
||||
|
||||
setInitialSecretsManagerConfiguration();
|
||||
});
|
||||
})();
|
||||
|
||||
function togglePlanFeatures(planType) {
|
||||
@ -113,6 +97,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
function unlinkProvider(id) {
|
||||
if (confirm('Are you sure you want to unlink this organization from its provider?')) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: `@Url.Action("UnlinkOrganizationFromProvider", "Organizations")?id=${id}`,
|
||||
dataType: 'json',
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function (response) {
|
||||
alert("Successfully unlinked provider");
|
||||
window.location.href = `@Url.Action("Edit", "Organizations")?id=${id}`;
|
||||
},
|
||||
error: function (response) {
|
||||
alert("Error!");
|
||||
}
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/***
|
||||
* Set Secrets Manager values based on current usage (for migrating from SM beta or reinstating an old subscription)
|
||||
*/
|
||||
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"globalSettings": {
|
||||
"selfHosted": false,
|
||||
"siteName": "Bitwarden",
|
||||
@ -48,12 +48,7 @@
|
||||
}
|
||||
},
|
||||
"adminSettings": {
|
||||
"admins": "",
|
||||
"cloudflare": {
|
||||
"zoneId": "SECRET",
|
||||
"authEmail": "SECRET",
|
||||
"authKey": "SECRET"
|
||||
}
|
||||
"admins": ""
|
||||
},
|
||||
"braintree": {
|
||||
"production": false,
|
||||
|
@ -3,7 +3,6 @@ using Bit.Api.AdminConsole.Models.Response;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Groups;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
@ -27,10 +26,8 @@ public class GroupsController : Controller
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ICreateGroupCommand _createGroupCommand;
|
||||
private readonly IUpdateGroupCommand _updateGroupCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
private bool UseFlexibleCollections => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
|
||||
public GroupsController(
|
||||
IGroupRepository groupRepository,
|
||||
@ -40,8 +37,8 @@ public class GroupsController : Controller
|
||||
ICreateGroupCommand createGroupCommand,
|
||||
IUpdateGroupCommand updateGroupCommand,
|
||||
IDeleteGroupCommand deleteGroupCommand,
|
||||
IFeatureService featureService,
|
||||
IAuthorizationService authorizationService)
|
||||
IAuthorizationService authorizationService,
|
||||
IApplicationCacheService applicationCacheService)
|
||||
{
|
||||
_groupRepository = groupRepository;
|
||||
_groupService = groupService;
|
||||
@ -50,8 +47,8 @@ public class GroupsController : Controller
|
||||
_createGroupCommand = createGroupCommand;
|
||||
_updateGroupCommand = updateGroupCommand;
|
||||
_deleteGroupCommand = deleteGroupCommand;
|
||||
_featureService = featureService;
|
||||
_authorizationService = authorizationService;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -81,7 +78,7 @@ public class GroupsController : Controller
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<GroupDetailsResponseModel>> Get(Guid orgId)
|
||||
{
|
||||
if (UseFlexibleCollections)
|
||||
if (await FlexibleCollectionsIsEnabledAsync(orgId))
|
||||
{
|
||||
// New flexible collections logic
|
||||
return await Get_vNext(orgId);
|
||||
@ -128,7 +125,7 @@ public class GroupsController : Controller
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
|
||||
var group = model.ToGroup(orgIdGuid);
|
||||
await _createGroupCommand.CreateGroupAsync(group, organization, model.Collections?.Select(c => c.ToSelectionReadOnly()), model.Users);
|
||||
await _createGroupCommand.CreateGroupAsync(group, organization, model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), model.Users);
|
||||
|
||||
return new GroupResponseModel(group);
|
||||
}
|
||||
@ -146,7 +143,7 @@ public class GroupsController : Controller
|
||||
var orgIdGuid = new Guid(orgId);
|
||||
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
|
||||
|
||||
await _updateGroupCommand.UpdateGroupAsync(model.ToGroup(group), organization, model.Collections?.Select(c => c.ToSelectionReadOnly()), model.Users);
|
||||
await _updateGroupCommand.UpdateGroupAsync(model.ToGroup(group), organization, model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), model.Users);
|
||||
return new GroupResponseModel(group);
|
||||
}
|
||||
|
||||
@ -217,4 +214,10 @@ public class GroupsController : Controller
|
||||
var responses = groups.Select(g => new GroupDetailsResponseModel(g.Item1, g.Item2));
|
||||
return new ListResponseModel<GroupDetailsResponseModel>(responses);
|
||||
}
|
||||
|
||||
private async Task<bool> FlexibleCollectionsIsEnabledAsync(Guid organizationId)
|
||||
{
|
||||
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
|
||||
return organizationAbility?.FlexibleCollections ?? false;
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
@ -13,6 +12,7 @@ using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
@ -39,10 +39,8 @@ public class OrganizationUsersController : Controller
|
||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||
private readonly IUpdateOrganizationUserGroupsCommand _updateOrganizationUserGroupsCommand;
|
||||
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
private bool UseFlexibleCollections => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
|
||||
public OrganizationUsersController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -57,8 +55,8 @@ public class OrganizationUsersController : Controller
|
||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
||||
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand,
|
||||
IAcceptOrgUserCommand acceptOrgUserCommand,
|
||||
IFeatureService featureService,
|
||||
IAuthorizationService authorizationService)
|
||||
IAuthorizationService authorizationService,
|
||||
IApplicationCacheService applicationCacheService)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -72,8 +70,8 @@ public class OrganizationUsersController : Controller
|
||||
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
||||
_updateOrganizationUserGroupsCommand = updateOrganizationUserGroupsCommand;
|
||||
_acceptOrgUserCommand = acceptOrgUserCommand;
|
||||
_featureService = featureService;
|
||||
_authorizationService = authorizationService;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -86,6 +84,15 @@ public class OrganizationUsersController : Controller
|
||||
}
|
||||
|
||||
var response = new OrganizationUserDetailsResponseModel(organizationUser.Item1, organizationUser.Item2);
|
||||
if (await FlexibleCollectionsIsEnabledAsync(organizationUser.Item1.OrganizationId))
|
||||
{
|
||||
// Downgrade Custom users with no other permissions than 'Edit/Delete Assigned Collections' to User
|
||||
response.Type = GetFlexibleCollectionsUserType(response.Type, response.Permissions);
|
||||
|
||||
// Set 'Edit/Delete Assigned Collections' custom permissions to false
|
||||
response.Permissions.EditAssignedCollections = false;
|
||||
response.Permissions.DeleteAssignedCollections = false;
|
||||
}
|
||||
|
||||
if (includeGroups)
|
||||
{
|
||||
@ -98,9 +105,12 @@ public class OrganizationUsersController : Controller
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false)
|
||||
{
|
||||
var authorized = UseFlexibleCollections
|
||||
? (await _authorizationService.AuthorizeAsync(User, OrganizationUserOperations.ReadAll(orgId))).Succeeded
|
||||
: await _currentContext.ViewAllCollections(orgId) ||
|
||||
if (await FlexibleCollectionsIsEnabledAsync(orgId))
|
||||
{
|
||||
return await Get_vNext(orgId, includeGroups, includeCollections);
|
||||
}
|
||||
|
||||
var authorized = await _currentContext.ViewAllCollections(orgId) ||
|
||||
await _currentContext.ViewAssignedCollections(orgId) ||
|
||||
await _currentContext.ManageGroups(orgId) ||
|
||||
await _currentContext.ManageUsers(orgId);
|
||||
@ -311,7 +321,7 @@ public class OrganizationUsersController : Controller
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
await _organizationService.SaveUserAsync(model.ToOrganizationUser(organizationUser), userId.Value,
|
||||
model.Collections?.Select(c => c.ToSelectionReadOnly()), model.Groups);
|
||||
model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), model.Groups);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/groups")]
|
||||
@ -518,4 +528,71 @@ public class OrganizationUsersController : Controller
|
||||
return new ListResponseModel<OrganizationUserBulkResponseModel>(result.Select(r =>
|
||||
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));
|
||||
}
|
||||
|
||||
private async Task<bool> FlexibleCollectionsIsEnabledAsync(Guid organizationId)
|
||||
{
|
||||
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
|
||||
return organizationAbility?.FlexibleCollections ?? false;
|
||||
}
|
||||
|
||||
private async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get_vNext(Guid orgId,
|
||||
bool includeGroups = false, bool includeCollections = false)
|
||||
{
|
||||
var authorized = (await _authorizationService.AuthorizeAsync(
|
||||
User, OrganizationUserOperations.ReadAll(orgId))).Succeeded;
|
||||
if (!authorized)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organizationUsers = await _organizationUserRepository
|
||||
.GetManyDetailsByOrganizationAsync(orgId, includeGroups, includeCollections);
|
||||
var responseTasks = organizationUsers
|
||||
.Select(async o =>
|
||||
{
|
||||
var orgUser = new OrganizationUserUserDetailsResponseModel(o,
|
||||
await _userService.TwoFactorIsEnabledAsync(o));
|
||||
|
||||
// Downgrade Custom users with no other permissions than 'Edit/Delete Assigned Collections' to User
|
||||
orgUser.Type = GetFlexibleCollectionsUserType(orgUser.Type, orgUser.Permissions);
|
||||
|
||||
// Set 'Edit/Delete Assigned Collections' custom permissions to false
|
||||
orgUser.Permissions.EditAssignedCollections = false;
|
||||
orgUser.Permissions.DeleteAssignedCollections = false;
|
||||
|
||||
return orgUser;
|
||||
});
|
||||
var responses = await Task.WhenAll(responseTasks);
|
||||
|
||||
return new ListResponseModel<OrganizationUserUserDetailsResponseModel>(responses);
|
||||
}
|
||||
|
||||
private OrganizationUserType GetFlexibleCollectionsUserType(OrganizationUserType type, Permissions permissions)
|
||||
{
|
||||
// Downgrade Custom users with no other permissions than 'Edit/Delete Assigned Collections' to User
|
||||
if (type == OrganizationUserType.Custom)
|
||||
{
|
||||
if ((permissions.EditAssignedCollections || permissions.DeleteAssignedCollections) &&
|
||||
permissions is
|
||||
{
|
||||
AccessEventLogs: false,
|
||||
AccessImportExport: false,
|
||||
AccessReports: false,
|
||||
CreateNewCollections: false,
|
||||
EditAnyCollection: false,
|
||||
DeleteAnyCollection: false,
|
||||
ManageGroups: false,
|
||||
ManagePolicies: false,
|
||||
ManageSso: false,
|
||||
ManageUsers: false,
|
||||
ManageResetPassword: false,
|
||||
ManageScim: false
|
||||
})
|
||||
{
|
||||
return OrganizationUserType.User;
|
||||
}
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
@ -14,10 +14,14 @@ using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Queries;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -27,6 +31,9 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -40,7 +47,6 @@ public class OrganizationsController : Controller
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IPaymentService _paymentService;
|
||||
@ -51,7 +57,6 @@ public class OrganizationsController : Controller
|
||||
private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand;
|
||||
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
|
||||
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||
private readonly IUpdateOrganizationLicenseCommand _updateOrganizationLicenseCommand;
|
||||
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
@ -59,12 +64,16 @@ public class OrganizationsController : Controller
|
||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||
private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand;
|
||||
private readonly IAddSecretsManagerSubscriptionCommand _addSecretsManagerSubscriptionCommand;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly ICancelSubscriptionCommand _cancelSubscriptionCommand;
|
||||
private readonly IGetSubscriptionQuery _getSubscriptionQuery;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
|
||||
|
||||
public OrganizationsController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IPolicyRepository policyRepository,
|
||||
IProviderRepository providerRepository,
|
||||
IOrganizationService organizationService,
|
||||
IUserService userService,
|
||||
IPaymentService paymentService,
|
||||
@ -75,19 +84,22 @@ public class OrganizationsController : Controller
|
||||
IRotateOrganizationApiKeyCommand rotateOrganizationApiKeyCommand,
|
||||
ICreateOrganizationApiKeyCommand createOrganizationApiKeyCommand,
|
||||
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
||||
IUpdateOrganizationLicenseCommand updateOrganizationLicenseCommand,
|
||||
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
|
||||
IFeatureService featureService,
|
||||
GlobalSettings globalSettings,
|
||||
ILicensingService licensingService,
|
||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
||||
IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand,
|
||||
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand)
|
||||
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand,
|
||||
IPushNotificationService pushNotificationService,
|
||||
ICancelSubscriptionCommand cancelSubscriptionCommand,
|
||||
IGetSubscriptionQuery getSubscriptionQuery,
|
||||
IReferenceEventService referenceEventService,
|
||||
IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_policyRepository = policyRepository;
|
||||
_providerRepository = providerRepository;
|
||||
_organizationService = organizationService;
|
||||
_userService = userService;
|
||||
_paymentService = paymentService;
|
||||
@ -98,7 +110,6 @@ public class OrganizationsController : Controller
|
||||
_rotateOrganizationApiKeyCommand = rotateOrganizationApiKeyCommand;
|
||||
_createOrganizationApiKeyCommand = createOrganizationApiKeyCommand;
|
||||
_organizationApiKeyRepository = organizationApiKeyRepository;
|
||||
_updateOrganizationLicenseCommand = updateOrganizationLicenseCommand;
|
||||
_cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery;
|
||||
_featureService = featureService;
|
||||
_globalSettings = globalSettings;
|
||||
@ -106,6 +117,11 @@ public class OrganizationsController : Controller
|
||||
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
||||
_upgradeOrganizationPlanCommand = upgradeOrganizationPlanCommand;
|
||||
_addSecretsManagerSubscriptionCommand = addSecretsManagerSubscriptionCommand;
|
||||
_pushNotificationService = pushNotificationService;
|
||||
_cancelSubscriptionCommand = cancelSubscriptionCommand;
|
||||
_getSubscriptionQuery = getSubscriptionQuery;
|
||||
_referenceEventService = referenceEventService;
|
||||
_organizationEnableCollectionEnhancementsCommand = organizationEnableCollectionEnhancementsCommand;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -245,6 +261,21 @@ public class OrganizationsController : Controller
|
||||
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, data?.AutoEnrollEnabled ?? false);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/risks-subscription-failure")]
|
||||
public async Task<OrganizationRisksSubscriptionFailureResponseModel> RisksSubscriptionFailure(Guid id)
|
||||
{
|
||||
if (!await _currentContext.EditPaymentMethods(id))
|
||||
{
|
||||
return new OrganizationRisksSubscriptionFailureResponseModel(id, false);
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
|
||||
var risksSubscriptionFailure = await _paymentService.RisksSubscriptionFailure(organization);
|
||||
|
||||
return new OrganizationRisksSubscriptionFailureResponseModel(id, risksSubscriptionFailure);
|
||||
}
|
||||
|
||||
[HttpPost("")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<OrganizationResponseModel> Post([FromBody] OrganizationCreateRequestModel model)
|
||||
@ -435,15 +466,48 @@ public class OrganizationsController : Controller
|
||||
|
||||
[HttpPost("{id}/cancel")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PostCancel(string id)
|
||||
public async Task PostCancel(Guid id, [FromBody] SubscriptionCancellationRequestModel request)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.EditSubscription(orgIdGuid))
|
||||
if (!await _currentContext.EditSubscription(id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _organizationService.CancelSubscriptionAsync(orgIdGuid);
|
||||
var presentUserWithOffboardingSurvey =
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AC1607_PresentUsersWithOffboardingSurvey);
|
||||
|
||||
if (presentUserWithOffboardingSurvey)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var subscription = await _getSubscriptionQuery.GetSubscription(organization);
|
||||
|
||||
await _cancelSubscriptionCommand.CancelSubscription(subscription,
|
||||
new OffboardingSurveyResponse
|
||||
{
|
||||
UserId = _currentContext.UserId!.Value,
|
||||
Reason = request.Reason,
|
||||
Feedback = request.Feedback
|
||||
},
|
||||
organization.IsExpired());
|
||||
|
||||
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(
|
||||
ReferenceEventType.CancelSubscription,
|
||||
organization,
|
||||
_currentContext)
|
||||
{
|
||||
EndOfPeriod = organization.IsExpired()
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
await _organizationService.CancelSubscriptionAsync(id);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{id}/reinstate")]
|
||||
@ -775,7 +839,6 @@ public class OrganizationsController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("{id}/collection-management")]
|
||||
[RequireFeature(FeatureFlagKeys.FlexibleCollections)]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<OrganizationResponseModel> PutCollectionManagement(Guid id, [FromBody] OrganizationCollectionManagementUpdateRequestModel model)
|
||||
{
|
||||
@ -790,7 +853,12 @@ public class OrganizationsController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var v1Enabled = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1, _currentContext);
|
||||
if (!organization.FlexibleCollections)
|
||||
{
|
||||
throw new BadRequestException("Organization does not have collection enhancements enabled");
|
||||
}
|
||||
|
||||
var v1Enabled = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1);
|
||||
|
||||
if (!v1Enabled)
|
||||
{
|
||||
@ -802,6 +870,40 @@ public class OrganizationsController : Controller
|
||||
return new OrganizationResponseModel(organization);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Migrates user, collection, and group data to the new Flexible Collections permissions scheme,
|
||||
/// then sets organization.FlexibleCollections to true to enable these new features for the organization.
|
||||
/// This is irreversible.
|
||||
/// </summary>
|
||||
/// <param name="organizationId"></param>
|
||||
/// <exception cref="NotFoundException"></exception>
|
||||
[HttpPost("{id}/enable-collection-enhancements")]
|
||||
[RequireFeature(FeatureFlagKeys.FlexibleCollectionsMigration)]
|
||||
public async Task EnableCollectionEnhancements(Guid id)
|
||||
{
|
||||
if (!await _currentContext.OrganizationOwner(id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _organizationEnableCollectionEnhancementsCommand.EnableCollectionEnhancements(organization);
|
||||
|
||||
// Force a vault sync for all owners and admins of the organization so that changes show immediately
|
||||
// Custom users are intentionally not handled as they are likely to be less impacted and we want to limit simultaneous syncs
|
||||
var orgUsers = await _organizationUserRepository.GetManyByOrganizationAsync(id, null);
|
||||
await Task.WhenAll(orgUsers
|
||||
.Where(ou => ou.UserId.HasValue &&
|
||||
ou.Status == OrganizationUserStatusType.Confirmed &&
|
||||
ou.Type is OrganizationUserType.Admin or OrganizationUserType.Owner)
|
||||
.Select(ou => _pushNotificationService.PushSyncOrganizationsAsync(ou.UserId.Value)));
|
||||
}
|
||||
|
||||
private async Task TryGrantOwnerAccessToSecretsManagerAsync(Guid organizationId, Guid userId)
|
||||
{
|
||||
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
|
||||
|
@ -1,10 +1,13 @@
|
||||
using Bit.Api.AdminConsole.Models.Request.Providers;
|
||||
using Bit.Api.AdminConsole.Models.Response.Providers;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -16,22 +19,33 @@ namespace Bit.Api.AdminConsole.Controllers;
|
||||
[Authorize("Application")]
|
||||
public class ProviderOrganizationsController : Controller
|
||||
{
|
||||
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly IProviderService _providerService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IProviderService _providerService;
|
||||
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
|
||||
private readonly IRemovePaymentMethodCommand _removePaymentMethodCommand;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public ProviderOrganizationsController(
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IProviderRepository providerRepository,
|
||||
IProviderService providerService,
|
||||
IUserService userService,
|
||||
ICurrentContext currentContext)
|
||||
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
|
||||
IRemovePaymentMethodCommand removePaymentMethodCommand,
|
||||
IUserService userService)
|
||||
{
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_providerService = providerService;
|
||||
_userService = userService;
|
||||
_currentContext = currentContext;
|
||||
_organizationRepository = organizationRepository;
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_providerRepository = providerRepository;
|
||||
_providerService = providerService;
|
||||
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
|
||||
_removePaymentMethodCommand = removePaymentMethodCommand;
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@ -87,7 +101,17 @@ public class ProviderOrganizationsController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
await _providerService.RemoveOrganizationAsync(providerId, id, userId.Value);
|
||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||
|
||||
var providerOrganization = await _providerOrganizationRepository.GetByIdAsync(id);
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);
|
||||
|
||||
await _removeOrganizationFromProviderCommand.RemoveOrganizationFromProvider(
|
||||
provider,
|
||||
providerOrganization,
|
||||
organization);
|
||||
|
||||
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ public class OrganizationConnectionRequestModel<T> : OrganizationConnectionReque
|
||||
|
||||
try
|
||||
{
|
||||
ParsedConfig = model.Config.ToObject<T>(JsonHelpers.IgnoreCase);
|
||||
ParsedConfig = model.Config.Deserialize<T>(JsonHelpers.IgnoreCase);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
|
@ -46,6 +46,7 @@ public class OrganizationCreateRequestModel : IValidatableObject
|
||||
public int? AdditionalServiceAccounts { get; set; }
|
||||
[Required]
|
||||
public bool UseSecretsManager { get; set; }
|
||||
public bool IsFromSecretsManagerTrial { get; set; }
|
||||
|
||||
public virtual OrganizationSignup ToOrganizationSignup(User user)
|
||||
{
|
||||
@ -67,6 +68,7 @@ public class OrganizationCreateRequestModel : IValidatableObject
|
||||
AdditionalSmSeats = AdditionalSmSeats.GetValueOrDefault(),
|
||||
AdditionalServiceAccounts = AdditionalServiceAccounts.GetValueOrDefault(),
|
||||
UseSecretsManager = UseSecretsManager,
|
||||
IsFromSecretsManagerTrial = IsFromSecretsManagerTrial,
|
||||
TaxInfo = new TaxInfo
|
||||
{
|
||||
TaxIdNumber = TaxIdNumber,
|
||||
|
@ -110,7 +110,6 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
||||
CoreHelpers.ReadableBytesSize(organization.Storage.Value) : null;
|
||||
StorageGb = organization.Storage.HasValue ?
|
||||
Math.Round(organization.Storage.Value / 1073741824D, 2) : 0; // 1 GB
|
||||
SecretsManagerBeta = organization.SecretsManagerBeta;
|
||||
}
|
||||
|
||||
public OrganizationSubscriptionResponseModel(Organization organization, SubscriptionInfo subscription, bool hideSensitiveData)
|
||||
@ -127,8 +126,6 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
||||
Subscription.Items = null;
|
||||
UpcomingInvoice.Amount = null;
|
||||
}
|
||||
|
||||
SecretsManagerBeta = organization.SecretsManagerBeta;
|
||||
}
|
||||
|
||||
public OrganizationSubscriptionResponseModel(Organization organization, OrganizationLicense license) :
|
||||
@ -143,8 +140,6 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
||||
license.Expires?.AddDays(-Constants
|
||||
.OrganizationSelfHostSubscriptionGracePeriodDays);
|
||||
}
|
||||
|
||||
SecretsManagerBeta = organization.SecretsManagerBeta;
|
||||
}
|
||||
|
||||
public string StorageName { get; set; }
|
||||
@ -162,6 +157,4 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
||||
/// Date when a self-hosted organization expires (includes grace period).
|
||||
/// </summary>
|
||||
public DateTime? Expiration { get; set; }
|
||||
|
||||
public bool SecretsManagerBeta { get; set; }
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
|
||||
public class OrganizationRisksSubscriptionFailureResponseModel : ResponseModel
|
||||
{
|
||||
public Guid OrganizationId { get; }
|
||||
public bool RisksSubscriptionFailure { get; }
|
||||
|
||||
public OrganizationRisksSubscriptionFailureResponseModel(
|
||||
Guid organizationId,
|
||||
bool risksSubscriptionFailure) : base("organizationRisksSubscriptionFailure")
|
||||
{
|
||||
OrganizationId = organizationId;
|
||||
RisksSubscriptionFailure = risksSubscriptionFailure;
|
||||
}
|
||||
}
|
@ -69,6 +69,37 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
KeyConnectorEnabled = ssoConfigData.MemberDecryptionType == MemberDecryptionType.KeyConnector && !string.IsNullOrEmpty(ssoConfigData.KeyConnectorUrl);
|
||||
KeyConnectorUrl = ssoConfigData.KeyConnectorUrl;
|
||||
}
|
||||
|
||||
if (FlexibleCollections)
|
||||
{
|
||||
// Downgrade Custom users with no other permissions than 'Edit/Delete Assigned Collections' to User
|
||||
if (Type == OrganizationUserType.Custom)
|
||||
{
|
||||
if ((Permissions.EditAssignedCollections || Permissions.DeleteAssignedCollections) &&
|
||||
Permissions is
|
||||
{
|
||||
AccessEventLogs: false,
|
||||
AccessImportExport: false,
|
||||
AccessReports: false,
|
||||
CreateNewCollections: false,
|
||||
EditAnyCollection: false,
|
||||
DeleteAnyCollection: false,
|
||||
ManageGroups: false,
|
||||
ManagePolicies: false,
|
||||
ManageSso: false,
|
||||
ManageUsers: false,
|
||||
ManageResetPassword: false,
|
||||
ManageScim: false
|
||||
})
|
||||
{
|
||||
organization.Type = OrganizationUserType.User;
|
||||
}
|
||||
}
|
||||
|
||||
// Set 'Edit/Delete Assigned Collections' custom permissions to false
|
||||
Permissions.EditAssignedCollections = false;
|
||||
Permissions.DeleteAssignedCollections = false;
|
||||
}
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
|
@ -43,5 +43,8 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
|
||||
ProviderId = organization.ProviderId;
|
||||
ProviderName = organization.ProviderName;
|
||||
PlanProductType = StaticStore.GetPlan(organization.PlanType).Product;
|
||||
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
FlexibleCollections = organization.FlexibleCollections;
|
||||
}
|
||||
}
|
||||
|
@ -110,8 +110,8 @@ public class GroupsController : Controller
|
||||
public async Task<IActionResult> Post([FromBody] GroupCreateUpdateRequestModel model)
|
||||
{
|
||||
var group = model.ToGroup(_currentContext.OrganizationId.Value);
|
||||
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection());
|
||||
var organization = await _organizationRepository.GetByIdAsync(_currentContext.OrganizationId.Value);
|
||||
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection(organization.FlexibleCollections)).ToList();
|
||||
await _createGroupCommand.CreateGroupAsync(group, organization, associations);
|
||||
var response = new GroupResponseModel(group, associations);
|
||||
return new JsonResult(response);
|
||||
@ -139,8 +139,8 @@ public class GroupsController : Controller
|
||||
}
|
||||
|
||||
var updatedGroup = model.ToGroup(existingGroup);
|
||||
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection());
|
||||
var organization = await _organizationRepository.GetByIdAsync(_currentContext.OrganizationId.Value);
|
||||
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection(organization.FlexibleCollections)).ToList();
|
||||
await _updateGroupCommand.UpdateGroupAsync(updatedGroup, organization, associations);
|
||||
var response = new GroupResponseModel(updatedGroup, associations);
|
||||
return new JsonResult(response);
|
||||
|
@ -5,7 +5,6 @@ using Bit.Api.Models.Public.Response;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -23,6 +22,7 @@ public class MembersController : Controller
|
||||
private readonly IUserService _userService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IUpdateOrganizationUserGroupsCommand _updateOrganizationUserGroupsCommand;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
|
||||
public MembersController(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@ -30,7 +30,8 @@ public class MembersController : Controller
|
||||
IOrganizationService organizationService,
|
||||
IUserService userService,
|
||||
ICurrentContext currentContext,
|
||||
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand)
|
||||
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand,
|
||||
IApplicationCacheService applicationCacheService)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_groupRepository = groupRepository;
|
||||
@ -38,6 +39,7 @@ public class MembersController : Controller
|
||||
_userService = userService;
|
||||
_currentContext = currentContext;
|
||||
_updateOrganizationUserGroupsCommand = updateOrganizationUserGroupsCommand;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -59,8 +61,9 @@ public class MembersController : Controller
|
||||
{
|
||||
return new NotFoundResult();
|
||||
}
|
||||
var flexibleCollectionsIsEnabled = await FlexibleCollectionsIsEnabledAsync(orgUser.OrganizationId);
|
||||
var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser),
|
||||
userDetails.Item2);
|
||||
userDetails.Item2, flexibleCollectionsIsEnabled);
|
||||
return new JsonResult(response);
|
||||
}
|
||||
|
||||
@ -99,9 +102,10 @@ public class MembersController : Controller
|
||||
{
|
||||
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(
|
||||
_currentContext.OrganizationId.Value);
|
||||
var flexibleCollectionsIsEnabled = await FlexibleCollectionsIsEnabledAsync(_currentContext.OrganizationId.Value);
|
||||
// TODO: Get all CollectionUser associations for the organization and marry them up here for the response.
|
||||
var memberResponsesTasks = users.Select(async u => new MemberResponseModel(u,
|
||||
await _userService.TwoFactorIsEnabledAsync(u), null));
|
||||
await _userService.TwoFactorIsEnabledAsync(u), null, flexibleCollectionsIsEnabled));
|
||||
var memberResponses = await Task.WhenAll(memberResponsesTasks);
|
||||
var response = new ListResponseModel<MemberResponseModel>(memberResponses);
|
||||
return new JsonResult(response);
|
||||
@ -119,17 +123,11 @@ public class MembersController : Controller
|
||||
[ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
|
||||
public async Task<IActionResult> Post([FromBody] MemberCreateRequestModel model)
|
||||
{
|
||||
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection());
|
||||
var invite = new OrganizationUserInvite
|
||||
{
|
||||
Emails = new List<string> { model.Email },
|
||||
Type = model.Type.Value,
|
||||
AccessAll = model.AccessAll.Value,
|
||||
Collections = associations
|
||||
};
|
||||
var flexibleCollectionsIsEnabled = await FlexibleCollectionsIsEnabledAsync(_currentContext.OrganizationId.Value);
|
||||
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection(flexibleCollectionsIsEnabled)).ToList();
|
||||
var user = await _organizationService.InviteUserAsync(_currentContext.OrganizationId.Value, null,
|
||||
model.Email, model.Type.Value, model.AccessAll.Value, model.ExternalId, associations, model.Groups);
|
||||
var response = new MemberResponseModel(user, associations);
|
||||
var response = new MemberResponseModel(user, associations, flexibleCollectionsIsEnabled);
|
||||
return new JsonResult(response);
|
||||
}
|
||||
|
||||
@ -154,18 +152,19 @@ public class MembersController : Controller
|
||||
return new NotFoundResult();
|
||||
}
|
||||
var updatedUser = model.ToOrganizationUser(existingUser);
|
||||
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection());
|
||||
var flexibleCollectionsIsEnabled = await FlexibleCollectionsIsEnabledAsync(_currentContext.OrganizationId.Value);
|
||||
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection(flexibleCollectionsIsEnabled)).ToList();
|
||||
await _organizationService.SaveUserAsync(updatedUser, null, associations, model.Groups);
|
||||
MemberResponseModel response = null;
|
||||
if (existingUser.UserId.HasValue)
|
||||
{
|
||||
var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
||||
response = new MemberResponseModel(existingUserDetails,
|
||||
await _userService.TwoFactorIsEnabledAsync(existingUserDetails), associations);
|
||||
await _userService.TwoFactorIsEnabledAsync(existingUserDetails), associations, flexibleCollectionsIsEnabled);
|
||||
}
|
||||
else
|
||||
{
|
||||
response = new MemberResponseModel(updatedUser, associations);
|
||||
response = new MemberResponseModel(updatedUser, associations, flexibleCollectionsIsEnabled);
|
||||
}
|
||||
return new JsonResult(response);
|
||||
}
|
||||
@ -236,4 +235,10 @@ public class MembersController : Controller
|
||||
await _organizationService.ResendInviteAsync(_currentContext.OrganizationId.Value, null, id);
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
private async Task<bool> FlexibleCollectionsIsEnabledAsync(Guid organizationId)
|
||||
{
|
||||
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
|
||||
return organizationAbility?.FlexibleCollections ?? false;
|
||||
}
|
||||
}
|
||||
|
@ -20,4 +20,9 @@ public abstract class AssociationWithPermissionsBaseModel
|
||||
/// This prevents easy copy-and-paste of hidden items, however it may not completely prevent user access.
|
||||
/// </summary>
|
||||
public bool? HidePasswords { get; set; }
|
||||
/// <summary>
|
||||
/// When true, the manage permission allows a user to both edit the ciphers within a collection and edit the users/groups that are assigned to the collection.
|
||||
/// This field will not affect behavior until your organization is using the latest collection enhancements (Releasing Q1, 2024)
|
||||
/// </summary>
|
||||
public bool? Manage { get; set; }
|
||||
}
|
||||
|
@ -13,9 +13,9 @@ public abstract class GroupBaseModel
|
||||
public string Name { get; set; }
|
||||
/// <summary>
|
||||
/// Determines if this group can access all collections within the organization, or only the associated
|
||||
/// collections. If set to <c>true</c>, this option overrides any collection assignments.
|
||||
/// collections. If set to <c>true</c>, this option overrides any collection assignments. If your organization is using
|
||||
/// the latest collection enhancements, you will not be allowed to set this property to <c>true</c>.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool? AccessAll { get; set; }
|
||||
/// <summary>
|
||||
/// External identifier for reference or linking this group to another system, such as a user directory.
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Public.Models;
|
||||
@ -9,42 +10,43 @@ public abstract class MemberBaseModel
|
||||
{
|
||||
public MemberBaseModel() { }
|
||||
|
||||
public MemberBaseModel(OrganizationUser user)
|
||||
public MemberBaseModel(OrganizationUser user, bool flexibleCollectionsEnabled)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
Type = user.Type;
|
||||
Type = flexibleCollectionsEnabled ? GetFlexibleCollectionsUserType(user.Type, user.GetPermissions()) : user.Type;
|
||||
AccessAll = user.AccessAll;
|
||||
ExternalId = user.ExternalId;
|
||||
ResetPasswordEnrolled = user.ResetPasswordKey != null;
|
||||
}
|
||||
|
||||
public MemberBaseModel(OrganizationUserUserDetails user)
|
||||
public MemberBaseModel(OrganizationUserUserDetails user, bool flexibleCollectionsEnabled)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
Type = user.Type;
|
||||
Type = flexibleCollectionsEnabled ? GetFlexibleCollectionsUserType(user.Type, user.GetPermissions()) : user.Type;
|
||||
AccessAll = user.AccessAll;
|
||||
ExternalId = user.ExternalId;
|
||||
ResetPasswordEnrolled = user.ResetPasswordKey != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The member's type (or role) within the organization.
|
||||
/// The member's type (or role) within the organization. If your organization has is using the latest collection enhancements,
|
||||
/// you will not be allowed to assign the Manager role (OrganizationUserType = 3).
|
||||
/// </summary>
|
||||
[Required]
|
||||
public OrganizationUserType? Type { get; set; }
|
||||
/// <summary>
|
||||
/// Determines if this member can access all collections within the organization, or only the associated
|
||||
/// collections. If set to <c>true</c>, this option overrides any collection assignments.
|
||||
/// collections. If set to <c>true</c>, this option overrides any collection assignments. If your organization is using
|
||||
/// the latest collection enhancements, you will not be allowed to set this property to <c>true</c>.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool? AccessAll { get; set; }
|
||||
/// <summary>
|
||||
/// External identifier for reference or linking this member to another system, such as a user directory.
|
||||
@ -57,4 +59,34 @@ public abstract class MemberBaseModel
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool ResetPasswordEnrolled { get; set; }
|
||||
|
||||
// TODO: AC-2188 - Remove this method when the custom users with no other permissions than 'Edit/Delete Assigned Collections' are migrated
|
||||
private OrganizationUserType GetFlexibleCollectionsUserType(OrganizationUserType type, Permissions permissions)
|
||||
{
|
||||
// Downgrade Custom users with no other permissions than 'Edit/Delete Assigned Collections' to User
|
||||
if (type == OrganizationUserType.Custom)
|
||||
{
|
||||
if ((permissions.EditAssignedCollections || permissions.DeleteAssignedCollections) &&
|
||||
permissions is
|
||||
{
|
||||
AccessEventLogs: false,
|
||||
AccessImportExport: false,
|
||||
AccessReports: false,
|
||||
CreateNewCollections: false,
|
||||
EditAnyCollection: false,
|
||||
DeleteAnyCollection: false,
|
||||
ManageGroups: false,
|
||||
ManagePolicies: false,
|
||||
ManageSso: false,
|
||||
ManageUsers: false,
|
||||
ManageResetPassword: false,
|
||||
ManageScim: false
|
||||
})
|
||||
{
|
||||
return OrganizationUserType.User;
|
||||
}
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,27 @@
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Public.Models.Request;
|
||||
|
||||
public class AssociationWithPermissionsRequestModel : AssociationWithPermissionsBaseModel
|
||||
{
|
||||
public CollectionAccessSelection ToCollectionAccessSelection()
|
||||
public CollectionAccessSelection ToCollectionAccessSelection(bool migratedToFlexibleCollections)
|
||||
{
|
||||
return new CollectionAccessSelection
|
||||
var collectionAccessSelection = new CollectionAccessSelection
|
||||
{
|
||||
Id = Id.Value,
|
||||
ReadOnly = ReadOnly.Value,
|
||||
HidePasswords = HidePasswords.GetValueOrDefault()
|
||||
HidePasswords = HidePasswords.GetValueOrDefault(),
|
||||
Manage = Manage.GetValueOrDefault()
|
||||
};
|
||||
|
||||
// Throws if the org has not migrated to use FC but has passed in a Manage value in the request
|
||||
if (!migratedToFlexibleCollections && Manage.GetValueOrDefault())
|
||||
{
|
||||
throw new BadRequestException(
|
||||
"Your organization must be using the latest collection enhancements to use the Manage property.");
|
||||
}
|
||||
|
||||
return collectionAccessSelection;
|
||||
}
|
||||
}
|
||||
|
@ -13,5 +13,6 @@ public class AssociationWithPermissionsResponseModel : AssociationWithPermission
|
||||
Id = selection.Id;
|
||||
ReadOnly = selection.ReadOnly;
|
||||
HidePasswords = selection.HidePasswords;
|
||||
Manage = selection.Manage;
|
||||
}
|
||||
}
|
||||
|
@ -12,8 +12,9 @@ namespace Bit.Api.AdminConsole.Public.Models.Response;
|
||||
/// </summary>
|
||||
public class MemberResponseModel : MemberBaseModel, IResponseModel
|
||||
{
|
||||
public MemberResponseModel(OrganizationUser user, IEnumerable<CollectionAccessSelection> collections)
|
||||
: base(user)
|
||||
public MemberResponseModel(OrganizationUser user, IEnumerable<CollectionAccessSelection> collections,
|
||||
bool flexibleCollectionsEnabled)
|
||||
: base(user, flexibleCollectionsEnabled)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
@ -28,8 +29,8 @@ public class MemberResponseModel : MemberBaseModel, IResponseModel
|
||||
}
|
||||
|
||||
public MemberResponseModel(OrganizationUserUserDetails user, bool twoFactorEnabled,
|
||||
IEnumerable<CollectionAccessSelection> collections)
|
||||
: base(user)
|
||||
IEnumerable<CollectionAccessSelection> collections, bool flexibleCollectionsEnabled)
|
||||
: base(user, flexibleCollectionsEnabled)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
|
@ -21,6 +21,9 @@ using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Auth.UserFeatures.UserKey;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Auth.Utilities;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Queries;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -32,6 +35,8 @@ using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
@ -63,10 +68,13 @@ public class AccountsController : Controller
|
||||
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
|
||||
private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ICancelSubscriptionCommand _cancelSubscriptionCommand;
|
||||
private readonly IGetSubscriptionQuery _getSubscriptionQuery;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
|
||||
private bool UseFlexibleCollections =>
|
||||
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections);
|
||||
|
||||
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
|
||||
private readonly IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> _folderValidator;
|
||||
@ -95,6 +103,9 @@ public class AccountsController : Controller
|
||||
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,
|
||||
IRotateUserKeyCommand rotateUserKeyCommand,
|
||||
IFeatureService featureService,
|
||||
ICancelSubscriptionCommand cancelSubscriptionCommand,
|
||||
IGetSubscriptionQuery getSubscriptionQuery,
|
||||
IReferenceEventService referenceEventService,
|
||||
ICurrentContext currentContext,
|
||||
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
|
||||
IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,
|
||||
@ -121,6 +132,9 @@ public class AccountsController : Controller
|
||||
_setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;
|
||||
_rotateUserKeyCommand = rotateUserKeyCommand;
|
||||
_featureService = featureService;
|
||||
_cancelSubscriptionCommand = cancelSubscriptionCommand;
|
||||
_getSubscriptionQuery = getSubscriptionQuery;
|
||||
_referenceEventService = referenceEventService;
|
||||
_currentContext = currentContext;
|
||||
_cipherValidator = cipherValidator;
|
||||
_folderValidator = folderValidator;
|
||||
@ -425,7 +439,7 @@ public class AccountsController : Controller
|
||||
}
|
||||
|
||||
IdentityResult result;
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.KeyRotationImprovements, _currentContext))
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.KeyRotationImprovements))
|
||||
{
|
||||
var dataModel = new RotateUserKeyData
|
||||
{
|
||||
@ -809,16 +823,44 @@ public class AccountsController : Controller
|
||||
|
||||
[HttpPost("cancel-premium")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PostCancel()
|
||||
public async Task PostCancel([FromBody] SubscriptionCancellationRequestModel request)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var presentUserWithOffboardingSurvey =
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AC1607_PresentUsersWithOffboardingSurvey);
|
||||
|
||||
if (presentUserWithOffboardingSurvey)
|
||||
{
|
||||
var subscription = await _getSubscriptionQuery.GetSubscription(user);
|
||||
|
||||
await _cancelSubscriptionCommand.CancelSubscription(subscription,
|
||||
new OffboardingSurveyResponse
|
||||
{
|
||||
UserId = user.Id,
|
||||
Reason = request.Reason,
|
||||
Feedback = request.Feedback
|
||||
},
|
||||
user.IsExpired());
|
||||
|
||||
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(
|
||||
ReferenceEventType.CancelSubscription,
|
||||
user,
|
||||
_currentContext)
|
||||
{
|
||||
EndOfPeriod = user.IsExpired()
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
await _userService.CancelPremiumAsync(user);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("reinstate-premium")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
|
@ -13,7 +13,6 @@ using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -21,7 +20,6 @@ namespace Bit.Api.Auth.Controllers;
|
||||
|
||||
[Route("webauthn")]
|
||||
[Authorize("Web")]
|
||||
[RequireFeature(FeatureFlagKeys.PasswordlessLogin)]
|
||||
public class WebAuthnController : Controller
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
|
@ -66,6 +66,7 @@ public class SsoConfigurationDataRequest : IValidatableObject
|
||||
public string ExpectedReturnAcrValue { get; set; }
|
||||
|
||||
// SAML2 SP
|
||||
public bool? SpUniqueEntityId { get; set; }
|
||||
public Saml2NameIdFormat SpNameIdFormat { get; set; }
|
||||
public string SpOutboundSigningAlgorithm { get; set; }
|
||||
public Saml2SigningBehavior SpSigningBehavior { get; set; }
|
||||
@ -190,6 +191,7 @@ public class SsoConfigurationDataRequest : IValidatableObject
|
||||
IdpAllowUnsolicitedAuthnResponse = IdpAllowUnsolicitedAuthnResponse.GetValueOrDefault(),
|
||||
IdpDisableOutboundLogoutRequests = IdpDisableOutboundLogoutRequests.GetValueOrDefault(),
|
||||
IdpWantAuthnRequestsSigned = IdpWantAuthnRequestsSigned.GetValueOrDefault(),
|
||||
SpUniqueEntityId = SpUniqueEntityId.GetValueOrDefault(),
|
||||
SpNameIdFormat = SpNameIdFormat,
|
||||
SpOutboundSigningAlgorithm = SpOutboundSigningAlgorithm ?? SamlSigningAlgorithms.Sha256,
|
||||
SpSigningBehavior = SpSigningBehavior,
|
||||
|
@ -33,7 +33,8 @@ public class SsoUrls
|
||||
{
|
||||
CallbackPath = SsoConfigurationData.BuildCallbackPath(globalSettings.BaseServiceUri.Sso);
|
||||
SignedOutCallbackPath = SsoConfigurationData.BuildSignedOutCallbackPath(globalSettings.BaseServiceUri.Sso);
|
||||
SpEntityId = SsoConfigurationData.BuildSaml2ModulePath(globalSettings.BaseServiceUri.Sso);
|
||||
SpEntityIdStatic = SsoConfigurationData.BuildSaml2ModulePath(globalSettings.BaseServiceUri.Sso);
|
||||
SpEntityId = SsoConfigurationData.BuildSaml2ModulePath(globalSettings.BaseServiceUri.Sso, organizationId);
|
||||
SpMetadataUrl = SsoConfigurationData.BuildSaml2MetadataUrl(globalSettings.BaseServiceUri.Sso, organizationId);
|
||||
SpAcsUrl = SsoConfigurationData.BuildSaml2AcsUrl(globalSettings.BaseServiceUri.Sso, organizationId);
|
||||
}
|
||||
@ -41,6 +42,7 @@ public class SsoUrls
|
||||
public string CallbackPath { get; set; }
|
||||
public string SignedOutCallbackPath { get; set; }
|
||||
public string SpEntityId { get; set; }
|
||||
public string SpEntityIdStatic { get; set; }
|
||||
public string SpMetadataUrl { get; set; }
|
||||
public string SpAcsUrl { get; set; }
|
||||
}
|
||||
|
@ -28,8 +28,8 @@ public class CollectionsController : Controller
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IBulkAddCollectionAccessCommand _bulkAddCollectionAccessCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
|
||||
public CollectionsController(
|
||||
ICollectionRepository collectionRepository,
|
||||
@ -39,8 +39,8 @@ public class CollectionsController : Controller
|
||||
IAuthorizationService authorizationService,
|
||||
ICurrentContext currentContext,
|
||||
IBulkAddCollectionAccessCommand bulkAddCollectionAccessCommand,
|
||||
IFeatureService featureService,
|
||||
IOrganizationUserRepository organizationUserRepository)
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IApplicationCacheService applicationCacheService)
|
||||
{
|
||||
_collectionRepository = collectionRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -50,16 +50,14 @@ public class CollectionsController : Controller
|
||||
_authorizationService = authorizationService;
|
||||
_currentContext = currentContext;
|
||||
_bulkAddCollectionAccessCommand = bulkAddCollectionAccessCommand;
|
||||
_featureService = featureService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
}
|
||||
|
||||
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<CollectionResponseModel> Get(Guid orgId, Guid id)
|
||||
{
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
if (await FlexibleCollectionsIsEnabledAsync(orgId))
|
||||
{
|
||||
// New flexible collections logic
|
||||
return await Get_vNext(id);
|
||||
@ -79,7 +77,7 @@ public class CollectionsController : Controller
|
||||
[HttpGet("{id}/details")]
|
||||
public async Task<CollectionAccessDetailsResponseModel> GetDetails(Guid orgId, Guid id)
|
||||
{
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
if (await FlexibleCollectionsIsEnabledAsync(orgId))
|
||||
{
|
||||
// New flexible collections logic
|
||||
return await GetDetails_vNext(id);
|
||||
@ -104,7 +102,7 @@ public class CollectionsController : Controller
|
||||
else
|
||||
{
|
||||
(var collection, var access) = await _collectionRepository.GetByIdWithAccessAsync(id,
|
||||
_currentContext.UserId.Value, FlexibleCollectionsIsEnabled);
|
||||
_currentContext.UserId.Value, false);
|
||||
if (collection == null || collection.OrganizationId != orgId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
@ -117,7 +115,7 @@ public class CollectionsController : Controller
|
||||
[HttpGet("details")]
|
||||
public async Task<ListResponseModel<CollectionAccessDetailsResponseModel>> GetManyWithDetails(Guid orgId)
|
||||
{
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
if (await FlexibleCollectionsIsEnabledAsync(orgId))
|
||||
{
|
||||
// New flexible collections logic
|
||||
return await GetManyWithDetails_vNext(orgId);
|
||||
@ -132,7 +130,7 @@ public class CollectionsController : Controller
|
||||
// We always need to know which collections the current user is assigned to
|
||||
var assignedOrgCollections =
|
||||
await _collectionRepository.GetManyByUserIdWithAccessAsync(_currentContext.UserId.Value, orgId,
|
||||
FlexibleCollectionsIsEnabled);
|
||||
false);
|
||||
|
||||
if (await _currentContext.ViewAllCollections(orgId) || await _currentContext.ManageUsers(orgId))
|
||||
{
|
||||
@ -159,7 +157,7 @@ public class CollectionsController : Controller
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<CollectionResponseModel>> Get(Guid orgId)
|
||||
{
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
if (await FlexibleCollectionsIsEnabledAsync(orgId))
|
||||
{
|
||||
// New flexible collections logic
|
||||
return await GetByOrgId_vNext(orgId);
|
||||
@ -191,7 +189,7 @@ public class CollectionsController : Controller
|
||||
public async Task<ListResponseModel<CollectionDetailsResponseModel>> GetUser()
|
||||
{
|
||||
var collections = await _collectionRepository.GetManyByUserIdAsync(
|
||||
_userService.GetProperUserId(User).Value, FlexibleCollectionsIsEnabled);
|
||||
_userService.GetProperUserId(User).Value, false);
|
||||
var responses = collections.Select(c => new CollectionDetailsResponseModel(c));
|
||||
return new ListResponseModel<CollectionDetailsResponseModel>(responses);
|
||||
}
|
||||
@ -199,7 +197,7 @@ public class CollectionsController : Controller
|
||||
[HttpGet("{id}/users")]
|
||||
public async Task<IEnumerable<SelectionReadOnlyResponseModel>> GetUsers(Guid orgId, Guid id)
|
||||
{
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
if (await FlexibleCollectionsIsEnabledAsync(orgId))
|
||||
{
|
||||
// New flexible collections logic
|
||||
return await GetUsers_vNext(id);
|
||||
@ -215,11 +213,15 @@ public class CollectionsController : Controller
|
||||
[HttpPost("")]
|
||||
public async Task<CollectionResponseModel> Post(Guid orgId, [FromBody] CollectionRequestModel model)
|
||||
{
|
||||
if (await FlexibleCollectionsIsEnabledAsync(orgId))
|
||||
{
|
||||
// New flexible collections logic
|
||||
return await Post_vNext(orgId, model);
|
||||
}
|
||||
|
||||
var collection = model.ToCollection(orgId);
|
||||
|
||||
var authorized = FlexibleCollectionsIsEnabled
|
||||
? (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Create)).Succeeded
|
||||
: await CanCreateCollection(orgId, collection.Id) || await CanEditCollectionAsync(orgId, collection.Id);
|
||||
var authorized = await CanCreateCollection(orgId, collection.Id) || await CanEditCollectionAsync(orgId, collection.Id);
|
||||
if (!authorized)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
@ -230,7 +232,6 @@ public class CollectionsController : Controller
|
||||
|
||||
// Pre-flexible collections logic assigned Managers to collections they create
|
||||
var assignUserToCollection =
|
||||
!FlexibleCollectionsIsEnabled &&
|
||||
!await _currentContext.EditAnyCollection(orgId) &&
|
||||
await _currentContext.EditAssignedCollections(orgId);
|
||||
var isNewCollection = collection.Id == default;
|
||||
@ -251,14 +252,26 @@ public class CollectionsController : Controller
|
||||
}
|
||||
|
||||
await _collectionService.SaveAsync(collection, groups, users);
|
||||
|
||||
if (!_currentContext.UserId.HasValue || await _currentContext.ProviderUserForOrgAsync(orgId))
|
||||
{
|
||||
return new CollectionResponseModel(collection);
|
||||
}
|
||||
|
||||
// If we have a user, fetch the collection to get the latest permission details
|
||||
var userCollectionDetails = await _collectionRepository.GetByIdAsync(collection.Id,
|
||||
_currentContext.UserId.Value, await FlexibleCollectionsIsEnabledAsync(collection.OrganizationId));
|
||||
|
||||
return userCollectionDetails == null
|
||||
? new CollectionResponseModel(collection)
|
||||
: new CollectionDetailsResponseModel(userCollectionDetails);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPost("{id}")]
|
||||
public async Task<CollectionResponseModel> Put(Guid orgId, Guid id, [FromBody] CollectionRequestModel model)
|
||||
{
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
if (await FlexibleCollectionsIsEnabledAsync(orgId))
|
||||
{
|
||||
// New flexible collections logic
|
||||
return await Put_vNext(id, model);
|
||||
@ -274,13 +287,24 @@ public class CollectionsController : Controller
|
||||
var groups = model.Groups?.Select(g => g.ToSelectionReadOnly());
|
||||
var users = model.Users?.Select(g => g.ToSelectionReadOnly());
|
||||
await _collectionService.SaveAsync(model.ToCollection(collection), groups, users);
|
||||
|
||||
if (!_currentContext.UserId.HasValue || await _currentContext.ProviderUserForOrgAsync(collection.OrganizationId))
|
||||
{
|
||||
return new CollectionResponseModel(collection);
|
||||
}
|
||||
|
||||
// If we have a user, fetch the collection details to get the latest permission details for the user
|
||||
var updatedCollectionDetails = await _collectionRepository.GetByIdAsync(id, _currentContext.UserId.Value, await FlexibleCollectionsIsEnabledAsync(collection.OrganizationId));
|
||||
|
||||
return updatedCollectionDetails == null
|
||||
? new CollectionResponseModel(collection)
|
||||
: new CollectionDetailsResponseModel(updatedCollectionDetails);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/users")]
|
||||
public async Task PutUsers(Guid orgId, Guid id, [FromBody] IEnumerable<SelectionReadOnlyRequestModel> model)
|
||||
{
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
if (await FlexibleCollectionsIsEnabledAsync(orgId))
|
||||
{
|
||||
// New flexible collections logic
|
||||
await PutUsers_vNext(id, model);
|
||||
@ -299,14 +323,17 @@ public class CollectionsController : Controller
|
||||
|
||||
[HttpPost("bulk-access")]
|
||||
[RequireFeature(FeatureFlagKeys.BulkCollectionAccess)]
|
||||
// Also gated behind Flexible Collections flag because it only has new authorization logic.
|
||||
// Could be removed if legacy authorization logic were implemented for many collections.
|
||||
[RequireFeature(FeatureFlagKeys.FlexibleCollections)]
|
||||
public async Task PostBulkCollectionAccess([FromBody] BulkCollectionAccessRequestModel model)
|
||||
public async Task PostBulkCollectionAccess(Guid orgId, [FromBody] BulkCollectionAccessRequestModel model)
|
||||
{
|
||||
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.CollectionIds);
|
||||
// Authorization logic assumes flexible collections is enabled
|
||||
// Remove after all organizations have been migrated
|
||||
if (!await FlexibleCollectionsIsEnabledAsync(orgId))
|
||||
{
|
||||
throw new NotFoundException("Feature disabled.");
|
||||
}
|
||||
|
||||
if (collections.Count != model.CollectionIds.Count())
|
||||
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.CollectionIds);
|
||||
if (collections.Count(c => c.OrganizationId == orgId) != model.CollectionIds.Count())
|
||||
{
|
||||
throw new NotFoundException("One or more collections not found.");
|
||||
}
|
||||
@ -328,7 +355,7 @@ public class CollectionsController : Controller
|
||||
[HttpPost("{id}/delete")]
|
||||
public async Task Delete(Guid orgId, Guid id)
|
||||
{
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
if (await FlexibleCollectionsIsEnabledAsync(orgId))
|
||||
{
|
||||
// New flexible collections logic
|
||||
await Delete_vNext(id);
|
||||
@ -349,7 +376,7 @@ public class CollectionsController : Controller
|
||||
[HttpPost("delete")]
|
||||
public async Task DeleteMany(Guid orgId, [FromBody] CollectionBulkDeleteRequestModel model)
|
||||
{
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
if (await FlexibleCollectionsIsEnabledAsync(orgId))
|
||||
{
|
||||
// New flexible collections logic
|
||||
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Ids);
|
||||
@ -385,7 +412,7 @@ public class CollectionsController : Controller
|
||||
[HttpPost("{id}/delete-user/{orgUserId}")]
|
||||
public async Task DeleteUser(Guid orgId, Guid id, Guid orgUserId)
|
||||
{
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
if (await FlexibleCollectionsIsEnabledAsync(orgId))
|
||||
{
|
||||
// New flexible collections logic
|
||||
await DeleteUser_vNext(id, orgUserId);
|
||||
@ -397,19 +424,9 @@ public class CollectionsController : Controller
|
||||
await _collectionService.DeleteUserAsync(collection, orgUserId);
|
||||
}
|
||||
|
||||
private void DeprecatedPermissionsGuard()
|
||||
{
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
{
|
||||
throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF.");
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
|
||||
private async Task<Collection> GetCollectionAsync(Guid id, Guid orgId)
|
||||
{
|
||||
DeprecatedPermissionsGuard();
|
||||
|
||||
Collection collection = default;
|
||||
if (await _currentContext.ViewAllCollections(orgId))
|
||||
{
|
||||
@ -417,7 +434,7 @@ public class CollectionsController : Controller
|
||||
}
|
||||
else if (await _currentContext.ViewAssignedCollections(orgId))
|
||||
{
|
||||
collection = await _collectionRepository.GetByIdAsync(id, _currentContext.UserId.Value, FlexibleCollectionsIsEnabled);
|
||||
collection = await _collectionRepository.GetByIdAsync(id, _currentContext.UserId.Value, false);
|
||||
}
|
||||
|
||||
if (collection == null || collection.OrganizationId != orgId)
|
||||
@ -431,8 +448,6 @@ public class CollectionsController : Controller
|
||||
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
|
||||
private async Task<bool> CanCreateCollection(Guid orgId, Guid collectionId)
|
||||
{
|
||||
DeprecatedPermissionsGuard();
|
||||
|
||||
if (collectionId != default)
|
||||
{
|
||||
return false;
|
||||
@ -445,8 +460,6 @@ public class CollectionsController : Controller
|
||||
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
|
||||
private async Task<bool> CanEditCollectionAsync(Guid orgId, Guid collectionId)
|
||||
{
|
||||
DeprecatedPermissionsGuard();
|
||||
|
||||
if (collectionId == default)
|
||||
{
|
||||
return false;
|
||||
@ -460,7 +473,7 @@ public class CollectionsController : Controller
|
||||
if (await _currentContext.EditAssignedCollections(orgId))
|
||||
{
|
||||
var collectionDetails =
|
||||
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value, FlexibleCollectionsIsEnabled);
|
||||
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value, false);
|
||||
return collectionDetails != null;
|
||||
}
|
||||
|
||||
@ -470,8 +483,6 @@ public class CollectionsController : Controller
|
||||
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
|
||||
private async Task<bool> CanDeleteCollectionAsync(Guid orgId, Guid collectionId)
|
||||
{
|
||||
DeprecatedPermissionsGuard();
|
||||
|
||||
if (collectionId == default)
|
||||
{
|
||||
return false;
|
||||
@ -485,7 +496,7 @@ public class CollectionsController : Controller
|
||||
if (await _currentContext.DeleteAssignedCollections(orgId))
|
||||
{
|
||||
var collectionDetails =
|
||||
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value, FlexibleCollectionsIsEnabled);
|
||||
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value, false);
|
||||
return collectionDetails != null;
|
||||
}
|
||||
|
||||
@ -495,8 +506,6 @@ public class CollectionsController : Controller
|
||||
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
|
||||
private async Task<bool> DeleteAnyCollection(Guid orgId)
|
||||
{
|
||||
DeprecatedPermissionsGuard();
|
||||
|
||||
return await _currentContext.OrganizationAdmin(orgId) ||
|
||||
(_currentContext.Organizations?.Any(o => o.Id == orgId
|
||||
&& (o.Permissions?.DeleteAnyCollection ?? false)) ?? false);
|
||||
@ -505,8 +514,6 @@ public class CollectionsController : Controller
|
||||
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
|
||||
private async Task<bool> CanViewCollectionAsync(Guid orgId, Guid collectionId)
|
||||
{
|
||||
DeprecatedPermissionsGuard();
|
||||
|
||||
if (collectionId == default)
|
||||
{
|
||||
return false;
|
||||
@ -520,7 +527,7 @@ public class CollectionsController : Controller
|
||||
if (await _currentContext.ViewAssignedCollections(orgId))
|
||||
{
|
||||
var collectionDetails =
|
||||
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value, FlexibleCollectionsIsEnabled);
|
||||
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value, false);
|
||||
return collectionDetails != null;
|
||||
}
|
||||
|
||||
@ -530,8 +537,6 @@ public class CollectionsController : Controller
|
||||
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
|
||||
private async Task<bool> ViewAtLeastOneCollectionAsync(Guid orgId)
|
||||
{
|
||||
DeprecatedPermissionsGuard();
|
||||
|
||||
return await _currentContext.ViewAllCollections(orgId) || await _currentContext.ViewAssignedCollections(orgId);
|
||||
}
|
||||
|
||||
@ -564,7 +569,7 @@ public class CollectionsController : Controller
|
||||
{
|
||||
// We always need to know which collections the current user is assigned to
|
||||
var assignedOrgCollections = await _collectionRepository
|
||||
.GetManyByUserIdWithAccessAsync(_currentContext.UserId.Value, orgId, FlexibleCollectionsIsEnabled);
|
||||
.GetManyByUserIdWithAccessAsync(_currentContext.UserId.Value, orgId, true);
|
||||
|
||||
var readAllAuthorized =
|
||||
(await _authorizationService.AuthorizeAsync(User, CollectionOperations.ReadAllWithAccess(orgId))).Succeeded;
|
||||
@ -604,7 +609,7 @@ public class CollectionsController : Controller
|
||||
}
|
||||
else
|
||||
{
|
||||
var assignedCollections = await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId.Value, FlexibleCollectionsIsEnabled);
|
||||
var assignedCollections = await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId.Value, false);
|
||||
orgCollections = assignedCollections.Where(c => c.OrganizationId == orgId && c.Manage).ToList();
|
||||
}
|
||||
|
||||
@ -626,6 +631,35 @@ public class CollectionsController : Controller
|
||||
return responses;
|
||||
}
|
||||
|
||||
private async Task<CollectionResponseModel> Post_vNext(Guid orgId, [FromBody] CollectionRequestModel model)
|
||||
{
|
||||
var collection = model.ToCollection(orgId);
|
||||
|
||||
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Create)).Succeeded;
|
||||
if (!authorized)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var groups = model.Groups?.Select(g => g.ToSelectionReadOnly());
|
||||
var users = model.Users?.Select(g => g.ToSelectionReadOnly()).ToList() ?? new List<CollectionAccessSelection>();
|
||||
|
||||
await _collectionService.SaveAsync(collection, groups, users);
|
||||
|
||||
if (!_currentContext.UserId.HasValue || await _currentContext.ProviderUserForOrgAsync(orgId))
|
||||
{
|
||||
return new CollectionResponseModel(collection);
|
||||
}
|
||||
|
||||
// If we have a user, fetch the collection to get the latest permission details
|
||||
var userCollectionDetails = await _collectionRepository.GetByIdAsync(collection.Id,
|
||||
_currentContext.UserId.Value, await FlexibleCollectionsIsEnabledAsync(collection.OrganizationId));
|
||||
|
||||
return userCollectionDetails == null
|
||||
? new CollectionResponseModel(collection)
|
||||
: new CollectionDetailsResponseModel(userCollectionDetails);
|
||||
}
|
||||
|
||||
private async Task<CollectionResponseModel> Put_vNext(Guid id, CollectionRequestModel model)
|
||||
{
|
||||
var collection = await _collectionRepository.GetByIdAsync(id);
|
||||
@ -638,9 +672,20 @@ public class CollectionsController : Controller
|
||||
var groups = model.Groups?.Select(g => g.ToSelectionReadOnly());
|
||||
var users = model.Users?.Select(g => g.ToSelectionReadOnly());
|
||||
await _collectionService.SaveAsync(model.ToCollection(collection), groups, users);
|
||||
|
||||
if (!_currentContext.UserId.HasValue || await _currentContext.ProviderUserForOrgAsync(collection.OrganizationId))
|
||||
{
|
||||
return new CollectionResponseModel(collection);
|
||||
}
|
||||
|
||||
// If we have a user, fetch the collection details to get the latest permission details for the user
|
||||
var updatedCollectionDetails = await _collectionRepository.GetByIdAsync(id, _currentContext.UserId.Value, await FlexibleCollectionsIsEnabledAsync(collection.OrganizationId));
|
||||
|
||||
return updatedCollectionDetails == null
|
||||
? new CollectionResponseModel(collection)
|
||||
: new CollectionDetailsResponseModel(updatedCollectionDetails);
|
||||
}
|
||||
|
||||
private async Task PutUsers_vNext(Guid id, IEnumerable<SelectionReadOnlyRequestModel> model)
|
||||
{
|
||||
var collection = await _collectionRepository.GetByIdAsync(id);
|
||||
@ -676,4 +721,10 @@ public class CollectionsController : Controller
|
||||
|
||||
await _collectionService.DeleteUserAsync(collection, orgUserId);
|
||||
}
|
||||
|
||||
private async Task<bool> FlexibleCollectionsIsEnabledAsync(Guid organizationId)
|
||||
{
|
||||
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
|
||||
return organizationAbility?.FlexibleCollections ?? false;
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user