mirror of
https://github.com/bitwarden/server.git
synced 2025-06-13 14:30:50 -05:00
Merge remote-tracking branch 'origin/main' into user-service-test-tweaks
This commit is contained in:
commit
137b29f4c0
7
.github/renovate.json5
vendored
7
.github/renovate.json5
vendored
@ -15,8 +15,7 @@
|
|||||||
matchManagers: ["github-actions"],
|
matchManagers: ["github-actions"],
|
||||||
matchFileNames: [
|
matchFileNames: [
|
||||||
".github/workflows/publish.yml",
|
".github/workflows/publish.yml",
|
||||||
".github/workflows/release.yml",
|
".github/workflows/release.yml"
|
||||||
".github/workflows/repository-management.yml"
|
|
||||||
],
|
],
|
||||||
commitMessagePrefix: "[deps] BRE:",
|
commitMessagePrefix: "[deps] BRE:",
|
||||||
reviewers: ["team:dept-bre"],
|
reviewers: ["team:dept-bre"],
|
||||||
@ -134,8 +133,8 @@
|
|||||||
reviewers: ["team:dept-dbops"],
|
reviewers: ["team:dept-dbops"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
matchPackageNames: ["CommandDotNet", "YamlDotNet"],
|
matchPackageNames: ["YamlDotNet"],
|
||||||
description: "DevOps owned dependencies",
|
description: "BRE owned dependencies",
|
||||||
commitMessagePrefix: "[deps] BRE:",
|
commitMessagePrefix: "[deps] BRE:",
|
||||||
reviewers: ["team:dept-bre"],
|
reviewers: ["team:dept-bre"],
|
||||||
},
|
},
|
||||||
|
222
.github/workflows/build.yml
vendored
222
.github/workflows/build.yml
vendored
@ -19,7 +19,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: Lint
|
name: Lint
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
@ -33,115 +33,15 @@ jobs:
|
|||||||
run: dotnet format --verify-no-changes
|
run: dotnet format --verify-no-changes
|
||||||
|
|
||||||
build-artifacts:
|
build-artifacts:
|
||||||
name: Build artifacts
|
name: Build Docker images
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- lint
|
- lint
|
||||||
outputs:
|
outputs:
|
||||||
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
|
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- project_name: Admin
|
|
||||||
base_path: ./src
|
|
||||||
node: true
|
|
||||||
- project_name: Api
|
|
||||||
base_path: ./src
|
|
||||||
- project_name: Billing
|
|
||||||
base_path: ./src
|
|
||||||
- project_name: Events
|
|
||||||
base_path: ./src
|
|
||||||
- project_name: EventsProcessor
|
|
||||||
base_path: ./src
|
|
||||||
- project_name: Icons
|
|
||||||
base_path: ./src
|
|
||||||
- project_name: Identity
|
|
||||||
base_path: ./src
|
|
||||||
- project_name: MsSqlMigratorUtility
|
|
||||||
base_path: ./util
|
|
||||||
dotnet: true
|
|
||||||
- project_name: Notifications
|
|
||||||
base_path: ./src
|
|
||||||
- project_name: Scim
|
|
||||||
base_path: ./bitwarden_license/src
|
|
||||||
dotnet: true
|
|
||||||
- project_name: Server
|
|
||||||
base_path: ./util
|
|
||||||
- project_name: Setup
|
|
||||||
base_path: ./util
|
|
||||||
- project_name: Sso
|
|
||||||
base_path: ./bitwarden_license/src
|
|
||||||
node: true
|
|
||||||
steps:
|
|
||||||
- name: Check secrets
|
|
||||||
id: check-secrets
|
|
||||||
env:
|
|
||||||
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
|
||||||
run: |
|
|
||||||
has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }}
|
|
||||||
echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Check out repo
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
|
|
||||||
- name: Set up .NET
|
|
||||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
|
||||||
|
|
||||||
- name: Set up Node
|
|
||||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
|
||||||
with:
|
|
||||||
cache: "npm"
|
|
||||||
cache-dependency-path: "**/package-lock.json"
|
|
||||||
node-version: "16"
|
|
||||||
|
|
||||||
- name: Print environment
|
|
||||||
run: |
|
|
||||||
whoami
|
|
||||||
dotnet --info
|
|
||||||
node --version
|
|
||||||
npm --version
|
|
||||||
echo "GitHub ref: $GITHUB_REF"
|
|
||||||
echo "GitHub event: $GITHUB_EVENT"
|
|
||||||
|
|
||||||
- name: Build node
|
|
||||||
if: ${{ matrix.node }}
|
|
||||||
working-directory: ${{ matrix.base_path }}/${{ matrix.project_name }}
|
|
||||||
run: |
|
|
||||||
npm ci
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
- name: Publish project
|
|
||||||
working-directory: ${{ matrix.base_path }}/${{ matrix.project_name }}
|
|
||||||
run: |
|
|
||||||
echo "Publish"
|
|
||||||
dotnet publish -c "Release" -o obj/build-output/publish
|
|
||||||
|
|
||||||
cd obj/build-output/publish
|
|
||||||
zip -r ${{ matrix.project_name }}.zip .
|
|
||||||
mv ${{ matrix.project_name }}.zip ../../../
|
|
||||||
|
|
||||||
pwd
|
|
||||||
ls -atlh ../../../
|
|
||||||
|
|
||||||
- name: Upload project artifact
|
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.project_name }}.zip
|
|
||||||
path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
build-docker:
|
|
||||||
name: Build Docker images
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
permissions:
|
permissions:
|
||||||
security-events: write
|
security-events: write
|
||||||
id-token: write
|
id-token: write
|
||||||
needs:
|
|
||||||
- build-artifacts
|
|
||||||
if: ${{ needs.build-artifacts.outputs.has_secrets == 'true' }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@ -149,6 +49,7 @@ jobs:
|
|||||||
- project_name: Admin
|
- project_name: Admin
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
dotnet: true
|
dotnet: true
|
||||||
|
node: true
|
||||||
- project_name: Api
|
- project_name: Api
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
dotnet: true
|
dotnet: true
|
||||||
@ -182,9 +83,6 @@ jobs:
|
|||||||
- project_name: Scim
|
- project_name: Scim
|
||||||
base_path: ./bitwarden_license/src
|
base_path: ./bitwarden_license/src
|
||||||
dotnet: true
|
dotnet: true
|
||||||
- project_name: Server
|
|
||||||
base_path: ./util
|
|
||||||
dotnet: true
|
|
||||||
- project_name: Setup
|
- project_name: Setup
|
||||||
base_path: ./util
|
base_path: ./util
|
||||||
dotnet: true
|
dotnet: true
|
||||||
@ -192,6 +90,14 @@ jobs:
|
|||||||
base_path: ./bitwarden_license/src
|
base_path: ./bitwarden_license/src
|
||||||
dotnet: true
|
dotnet: true
|
||||||
steps:
|
steps:
|
||||||
|
- name: Check secrets
|
||||||
|
id: check-secrets
|
||||||
|
env:
|
||||||
|
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
|
run: |
|
||||||
|
has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }}
|
||||||
|
echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
@ -203,13 +109,67 @@ jobs:
|
|||||||
id: publish-branch-check
|
id: publish-branch-check
|
||||||
run: |
|
run: |
|
||||||
IFS="," read -a publish_branches <<< $PUBLISH_BRANCHES
|
IFS="," read -a publish_branches <<< $PUBLISH_BRANCHES
|
||||||
|
|
||||||
if [[ " ${publish_branches[*]} " =~ " ${GITHUB_REF:11} " ]]; then
|
if [[ " ${publish_branches[*]} " =~ " ${GITHUB_REF:11} " ]]; then
|
||||||
echo "is_publish_branch=true" >> $GITHUB_ENV
|
echo "is_publish_branch=true" >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo "is_publish_branch=false" >> $GITHUB_ENV
|
echo "is_publish_branch=false" >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Set up .NET
|
||||||
|
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||||
|
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||||
|
with:
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: "**/package-lock.json"
|
||||||
|
node-version: "16"
|
||||||
|
|
||||||
|
- name: Print environment
|
||||||
|
run: |
|
||||||
|
whoami
|
||||||
|
dotnet --info
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
|
- name: Build node
|
||||||
|
if: ${{ matrix.node }}
|
||||||
|
working-directory: ${{ matrix.base_path }}/${{ matrix.project_name }}
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Publish project
|
||||||
|
working-directory: ${{ matrix.base_path }}/${{ matrix.project_name }}
|
||||||
|
if: ${{ matrix.dotnet }}
|
||||||
|
run: |
|
||||||
|
echo "Publish"
|
||||||
|
dotnet publish -c "Release" -o obj/build-output/publish
|
||||||
|
|
||||||
|
cd obj/build-output/publish
|
||||||
|
zip -r ${{ matrix.project_name }}.zip .
|
||||||
|
mv ${{ matrix.project_name }}.zip ../../../
|
||||||
|
|
||||||
|
pwd
|
||||||
|
ls -atlh ../../../
|
||||||
|
|
||||||
|
- name: Upload project artifact
|
||||||
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
|
if: ${{ matrix.dotnet }}
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.project_name }}.zip
|
||||||
|
path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
########## Set up Docker ##########
|
||||||
|
- name: Set up QEMU emulators
|
||||||
|
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
|
||||||
|
|
||||||
########## ACRs ##########
|
########## ACRs ##########
|
||||||
- name: Log in to Azure - production subscription
|
- name: Log in to Azure - production subscription
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||||
@ -277,26 +237,24 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Get build artifact
|
- name: Generate image full name
|
||||||
if: ${{ matrix.dotnet }}
|
id: cache-name
|
||||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
env:
|
||||||
with:
|
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||||
name: ${{ matrix.project_name }}.zip
|
run: echo "name=${_AZ_REGISTRY}/${PROJECT_NAME}:buildcache" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Set up build artifact
|
|
||||||
if: ${{ matrix.dotnet }}
|
|
||||||
run: |
|
|
||||||
mkdir -p ${{ matrix.base_path}}/${{ matrix.project_name }}/obj/build-output/publish
|
|
||||||
unzip ${{ matrix.project_name }}.zip \
|
|
||||||
-d ${{ matrix.base_path }}/${{ matrix.project_name }}/obj/build-output/publish
|
|
||||||
|
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
id: build-docker
|
id: build-artifacts
|
||||||
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0
|
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0
|
||||||
with:
|
with:
|
||||||
context: ${{ matrix.base_path }}/${{ matrix.project_name }}
|
cache-from: type=registry,ref=${{ steps.cache-name.outputs.name }}
|
||||||
|
cache-to: type=registry,ref=${{ steps.cache-name.outputs.name}},mode=max
|
||||||
|
context: .
|
||||||
file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile
|
file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile
|
||||||
platforms: linux/amd64
|
platforms: |
|
||||||
|
linux/amd64,
|
||||||
|
linux/arm/v7,
|
||||||
|
linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.image-tags.outputs.tags }}
|
tags: ${{ steps.image-tags.outputs.tags }}
|
||||||
secrets: |
|
secrets: |
|
||||||
@ -309,7 +267,7 @@ jobs:
|
|||||||
- name: Sign image with Cosign
|
- name: Sign image with Cosign
|
||||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
env:
|
env:
|
||||||
DIGEST: ${{ steps.build-docker.outputs.digest }}
|
DIGEST: ${{ steps.build-artifacts.outputs.digest }}
|
||||||
TAGS: ${{ steps.image-tags.outputs.tags }}
|
TAGS: ${{ steps.image-tags.outputs.tags }}
|
||||||
run: |
|
run: |
|
||||||
IFS="," read -a tags <<< "${TAGS}"
|
IFS="," read -a tags <<< "${TAGS}"
|
||||||
@ -336,8 +294,8 @@ jobs:
|
|||||||
|
|
||||||
upload:
|
upload:
|
||||||
name: Upload
|
name: Upload
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
needs: build-docker
|
needs: build-artifacts
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
@ -377,9 +335,9 @@ jobs:
|
|||||||
|
|
||||||
# Run setup
|
# Run setup
|
||||||
docker run -i --rm --name setup -v $STUB_OUTPUT/US:/bitwarden $SETUP_IMAGE \
|
docker run -i --rm --name setup -v $STUB_OUTPUT/US:/bitwarden $SETUP_IMAGE \
|
||||||
dotnet Setup.dll -stub 1 -install 1 -domain bitwarden.example.com -os lin -cloud-region US
|
/app/Setup -stub 1 -install 1 -domain bitwarden.example.com -os lin -cloud-region US
|
||||||
docker run -i --rm --name setup -v $STUB_OUTPUT/EU:/bitwarden $SETUP_IMAGE \
|
docker run -i --rm --name setup -v $STUB_OUTPUT/EU:/bitwarden $SETUP_IMAGE \
|
||||||
dotnet Setup.dll -stub 1 -install 1 -domain bitwarden.example.com -os lin -cloud-region EU
|
/app/Setup -stub 1 -install 1 -domain bitwarden.example.com -os lin -cloud-region EU
|
||||||
|
|
||||||
sudo chown -R $(whoami):$(whoami) $STUB_OUTPUT
|
sudo chown -R $(whoami):$(whoami) $STUB_OUTPUT
|
||||||
|
|
||||||
@ -512,7 +470,7 @@ jobs:
|
|||||||
|
|
||||||
build-mssqlmigratorutility:
|
build-mssqlmigratorutility:
|
||||||
name: Build MSSQL migrator utility
|
name: Build MSSQL migrator utility
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- lint
|
- lint
|
||||||
defaults:
|
defaults:
|
||||||
@ -568,9 +526,9 @@ jobs:
|
|||||||
if: |
|
if: |
|
||||||
github.event_name != 'pull_request'
|
github.event_name != 'pull_request'
|
||||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- build-docker
|
- build-artifacts
|
||||||
steps:
|
steps:
|
||||||
- name: Log in to Azure - CI subscription
|
- name: Log in to Azure - CI subscription
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||||
@ -604,7 +562,7 @@ jobs:
|
|||||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- build-docker
|
- build-artifacts
|
||||||
steps:
|
steps:
|
||||||
- name: Log in to Azure - CI subscription
|
- name: Log in to Azure - CI subscription
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||||
@ -638,7 +596,6 @@ jobs:
|
|||||||
name: Setup Ephemeral Environment
|
name: Setup Ephemeral Environment
|
||||||
needs:
|
needs:
|
||||||
- build-artifacts
|
- build-artifacts
|
||||||
- build-docker
|
|
||||||
if: |
|
if: |
|
||||||
needs.build-artifacts.outputs.has_secrets == 'true'
|
needs.build-artifacts.outputs.has_secrets == 'true'
|
||||||
&& github.event_name == 'pull_request'
|
&& github.event_name == 'pull_request'
|
||||||
@ -656,7 +613,6 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- lint
|
- lint
|
||||||
- build-artifacts
|
- build-artifacts
|
||||||
- build-docker
|
|
||||||
- upload
|
- upload
|
||||||
- build-mssqlmigratorutility
|
- build-mssqlmigratorutility
|
||||||
- self-host-build
|
- self-host-build
|
||||||
|
1
.github/workflows/code-references.yml
vendored
1
.github/workflows/code-references.yml
vendored
@ -44,6 +44,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
accessToken: ${{ secrets.LD_ACCESS_TOKEN }}
|
accessToken: ${{ secrets.LD_ACCESS_TOKEN }}
|
||||||
projKey: default
|
projKey: default
|
||||||
|
allowTags: true
|
||||||
|
|
||||||
- name: Add label
|
- name: Add label
|
||||||
if: steps.collect.outputs.any-changed == 'true'
|
if: steps.collect.outputs.any-changed == 'true'
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
|
||||||
<Version>2025.5.1</Version>
|
<Version>2025.6.0</Version>
|
||||||
|
|
||||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
@ -69,5 +69,4 @@
|
|||||||
</AssemblyAttribute>
|
</AssemblyAttribute>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
@ -5,9 +5,6 @@
|
|||||||
<a href="https://github.com/bitwarden/server/actions/workflows/build.yml?query=branch:main" target="_blank">
|
<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" />
|
<img src="https://github.com/bitwarden/server/actions/workflows/build.yml/badge.svg?branch=main" alt="Github Workflow build on main" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://hub.docker.com/u/bitwarden/" target="_blank">
|
|
||||||
<img src="https://img.shields.io/docker/pulls/bitwarden/api.svg" alt="DockerHub" />
|
|
||||||
</a>
|
|
||||||
<a href="https://gitter.im/bitwarden/Lobby" target="_blank">
|
<a href="https://gitter.im/bitwarden/Lobby" target="_blank">
|
||||||
<img src="https://badges.gitter.im/bitwarden/Lobby.svg" alt="gitter chat" />
|
<img src="https://badges.gitter.im/bitwarden/Lobby.svg" alt="gitter chat" />
|
||||||
</a>
|
</a>
|
||||||
@ -26,12 +23,12 @@ Please refer to the [Server Setup Guide](https://contributing.bitwarden.com/gett
|
|||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://hub.docker.com/u/bitwarden/" target="_blank">
|
<a href="https://github.com/orgs/bitwarden/packages" target="_blank">
|
||||||
<img src="https://i.imgur.com/SZc8JnH.png" alt="docker" />
|
<img src="https://i.imgur.com/SZc8JnH.png" alt="docker" />
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
You can deploy Bitwarden using Docker containers on Windows, macOS, and Linux distributions. Use the provided PowerShell and Bash scripts to get started quickly. Find all of the Bitwarden images on [Docker Hub](https://hub.docker.com/u/bitwarden/).
|
You can deploy Bitwarden using Docker containers on Windows, macOS, and Linux distributions. Use the provided PowerShell and Bash scripts to get started quickly. Find all of the Bitwarden images on [GitHub Container Registry](https://github.com/orgs/bitwarden/packages).
|
||||||
|
|
||||||
Full documentation for deploying Bitwarden with Docker can be found in our help center at: https://help.bitwarden.com/article/install-on-premise/
|
Full documentation for deploying Bitwarden with Docker can be found in our help center at: https://help.bitwarden.com/article/install-on-premise/
|
||||||
|
|
||||||
|
@ -287,11 +287,10 @@ public class ProviderService : IProviderService
|
|||||||
|
|
||||||
foreach (var user in users)
|
foreach (var user in users)
|
||||||
{
|
{
|
||||||
if (!keyedFilteredUsers.ContainsKey(user.Id))
|
if (!keyedFilteredUsers.TryGetValue(user.Id, out var providerUser))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var providerUser = keyedFilteredUsers[user.Id];
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (providerUser.Status != ProviderUserStatusType.Accepted || providerUser.ProviderId != providerId)
|
if (providerUser.Status != ProviderUserStatusType.Accepted || providerUser.ProviderId != providerId)
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
@ -27,7 +26,6 @@ using Stripe;
|
|||||||
|
|
||||||
namespace Bit.Commercial.Core.Billing.Providers.Services;
|
namespace Bit.Commercial.Core.Billing.Providers.Services;
|
||||||
|
|
||||||
[RequireFeature(FeatureFlagKeys.PM18770_EnableOrganizationBusinessUnitConversion)]
|
|
||||||
public class BusinessUnitConverter(
|
public class BusinessUnitConverter(
|
||||||
IDataProtectionProvider dataProtectionProvider,
|
IDataProtectionProvider dataProtectionProvider,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
*
|
|
||||||
!obj/build-output/publish/*
|
|
||||||
!obj/Docker/empty/
|
|
||||||
!entrypoint.sh
|
|
@ -1,6 +1,50 @@
|
|||||||
|
###############################################
|
||||||
|
# Build stage #
|
||||||
|
###############################################
|
||||||
|
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||||
|
|
||||||
|
# Docker buildx supplies the value for this arg
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
|
# Determine proper runtime value for .NET
|
||||||
|
# We put the value in a file to be read by later layers.
|
||||||
|
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||||
|
RID=linux-x64 ; \
|
||||||
|
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||||
|
RID=linux-arm64 ; \
|
||||||
|
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
|
||||||
|
RID=linux-arm ; \
|
||||||
|
fi \
|
||||||
|
&& echo "RID=$RID" > /tmp/rid.txt
|
||||||
|
|
||||||
|
# Copy required project files
|
||||||
|
WORKDIR /source
|
||||||
|
COPY . ./
|
||||||
|
|
||||||
|
# Restore project dependencies and tools
|
||||||
|
WORKDIR /source/bitwarden_license/src/Scim
|
||||||
|
RUN . /tmp/rid.txt && dotnet restore -r $RID
|
||||||
|
|
||||||
|
# Build project
|
||||||
|
RUN . /tmp/rid.txt && dotnet publish \
|
||||||
|
-c release \
|
||||||
|
--no-restore \
|
||||||
|
--self-contained \
|
||||||
|
/p:PublishSingleFile=true \
|
||||||
|
-r $RID \
|
||||||
|
-o out
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# App stage #
|
||||||
|
###############################################
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||||
|
|
||||||
|
ARG TARGETPLATFORM
|
||||||
LABEL com.bitwarden.product="bitwarden"
|
LABEL com.bitwarden.product="bitwarden"
|
||||||
|
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
ENV ASPNETCORE_URLS=http://+:5000
|
||||||
|
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
@ -9,11 +53,10 @@ RUN apt-get update \
|
|||||||
krb5-user \
|
krb5-user \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
ENV ASPNETCORE_URLS http://+:5000
|
# Copy app from the build stage
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 5000
|
COPY --from=build /source/bitwarden_license/src/Scim/out /app
|
||||||
COPY obj/build-output/publish .
|
COPY ./bitwarden_license/src/Scim/entrypoint.sh /entrypoint.sh
|
||||||
COPY entrypoint.sh /
|
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1
|
HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1
|
||||||
|
@ -16,8 +16,8 @@ public class Program
|
|||||||
{
|
{
|
||||||
var context = e.Properties["SourceContext"].ToString();
|
var context = e.Properties["SourceContext"].ToString();
|
||||||
|
|
||||||
if (e.Properties.ContainsKey("RequestPath") &&
|
if (e.Properties.TryGetValue("RequestPath", out var requestPath) &&
|
||||||
!string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) &&
|
!string.IsNullOrWhiteSpace(requestPath?.ToString()) &&
|
||||||
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
|
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
|
|
||||||
@ -19,31 +19,42 @@ then
|
|||||||
LGID=65534
|
LGID=65534
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create user and group
|
if [ "$(id -u)" = "0" ]
|
||||||
|
then
|
||||||
|
# Create user and group
|
||||||
|
|
||||||
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
|
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
|
||||||
groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1
|
groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1
|
||||||
useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||
|
useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||
|
||||||
usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1
|
usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1
|
||||||
mkhomedir_helper $USERNAME
|
mkhomedir_helper $USERNAME
|
||||||
|
|
||||||
# The rest...
|
# The rest...
|
||||||
|
|
||||||
chown -R $USERNAME:$GROUPNAME /app
|
chown -R $USERNAME:$GROUPNAME /app
|
||||||
mkdir -p /etc/bitwarden/core
|
mkdir -p /etc/bitwarden/core
|
||||||
mkdir -p /etc/bitwarden/logs
|
mkdir -p /etc/bitwarden/logs
|
||||||
mkdir -p /etc/bitwarden/ca-certificates
|
mkdir -p /etc/bitwarden/ca-certificates
|
||||||
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
|
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
|
||||||
|
|
||||||
if [[ $globalSettings__selfHosted == "true" ]]; then
|
if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then
|
||||||
cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \
|
chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos
|
||||||
&& update-ca-certificates
|
fi
|
||||||
|
|
||||||
|
gosu_cmd="gosu $USERNAME:$GROUPNAME"
|
||||||
|
else
|
||||||
|
gosu_cmd=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then
|
if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then
|
||||||
chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos
|
cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf
|
||||||
cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf
|
$gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab
|
||||||
gosu $USERNAME:$GROUPNAME kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec gosu $USERNAME:$GROUPNAME dotnet /app/Scim.dll
|
if [[ $globalSettings__selfHosted == "true" ]]; then
|
||||||
|
if [[ -z $globalSettings__identityServer__certificateLocation ]]; then
|
||||||
|
export globalSettings__identityServer__certificateLocation=/etc/bitwarden/identity/identity.pfx
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec $gosu_cmd /app/Scim
|
||||||
|
@ -370,8 +370,8 @@ public class AccountController : Controller
|
|||||||
// for the user identifier.
|
// for the user identifier.
|
||||||
static bool nameIdIsNotTransient(Claim c) => c.Type == ClaimTypes.NameIdentifier
|
static bool nameIdIsNotTransient(Claim c) => c.Type == ClaimTypes.NameIdentifier
|
||||||
&& (c.Properties == null
|
&& (c.Properties == null
|
||||||
|| !c.Properties.ContainsKey(SamlPropertyKeys.ClaimFormat)
|
|| !c.Properties.TryGetValue(SamlPropertyKeys.ClaimFormat, out var claimFormat)
|
||||||
|| c.Properties[SamlPropertyKeys.ClaimFormat] != SamlNameIdFormats.Transient);
|
|| claimFormat != SamlNameIdFormats.Transient);
|
||||||
|
|
||||||
// Try to determine the unique id of the external user (issued by the provider)
|
// Try to determine the unique id of the external user (issued by the provider)
|
||||||
// the most common claim type for that are the sub claim and the NameIdentifier
|
// the most common claim type for that are the sub claim and the NameIdentifier
|
||||||
|
@ -1,6 +1,50 @@
|
|||||||
|
###############################################
|
||||||
|
# Build stage #
|
||||||
|
###############################################
|
||||||
|
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||||
|
|
||||||
|
# Docker buildx supplies the value for this arg
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
|
# Determine proper runtime value for .NET
|
||||||
|
# We put the value in a file to be read by later layers.
|
||||||
|
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||||
|
RID=linux-x64 ; \
|
||||||
|
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||||
|
RID=linux-arm64 ; \
|
||||||
|
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
|
||||||
|
RID=linux-arm ; \
|
||||||
|
fi \
|
||||||
|
&& echo "RID=$RID" > /tmp/rid.txt
|
||||||
|
|
||||||
|
# Copy required project files
|
||||||
|
WORKDIR /source
|
||||||
|
COPY . ./
|
||||||
|
|
||||||
|
# Restore project dependencies and tools
|
||||||
|
WORKDIR /source/bitwarden_license/src/Sso
|
||||||
|
RUN . /tmp/rid.txt && dotnet restore -r $RID
|
||||||
|
|
||||||
|
# Build project
|
||||||
|
RUN . /tmp/rid.txt && dotnet publish \
|
||||||
|
-c release \
|
||||||
|
--no-restore \
|
||||||
|
--self-contained \
|
||||||
|
/p:PublishSingleFile=true \
|
||||||
|
-r $RID \
|
||||||
|
-o out
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# App stage #
|
||||||
|
###############################################
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||||
|
|
||||||
|
ARG TARGETPLATFORM
|
||||||
LABEL com.bitwarden.product="bitwarden"
|
LABEL com.bitwarden.product="bitwarden"
|
||||||
|
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
ENV ASPNETCORE_URLS=http://+:5000
|
||||||
|
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
@ -9,11 +53,10 @@ RUN apt-get update \
|
|||||||
krb5-user \
|
krb5-user \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
ENV ASPNETCORE_URLS http://+:5000
|
# Copy app from the build stage
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 5000
|
COPY --from=build /source/bitwarden_license/src/Sso/out /app
|
||||||
COPY obj/build-output/publish .
|
COPY ./bitwarden_license/src/Sso/entrypoint.sh /entrypoint.sh
|
||||||
COPY entrypoint.sh /
|
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1
|
HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1
|
||||||
|
@ -17,8 +17,8 @@ public class Program
|
|||||||
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
||||||
{
|
{
|
||||||
var context = e.Properties["SourceContext"].ToString();
|
var context = e.Properties["SourceContext"].ToString();
|
||||||
if (e.Properties.ContainsKey("RequestPath") &&
|
if (e.Properties.TryGetValue("RequestPath", out var requestPath) &&
|
||||||
!string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) &&
|
!string.IsNullOrWhiteSpace(requestPath?.ToString()) &&
|
||||||
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
|
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
@ -46,9 +46,9 @@ public static class OpenIdConnectOptionsExtensions
|
|||||||
|
|
||||||
// Handle State if we've gotten that back
|
// Handle State if we've gotten that back
|
||||||
var decodedState = options.StateDataFormat.Unprotect(state);
|
var decodedState = options.StateDataFormat.Unprotect(state);
|
||||||
if (decodedState != null && decodedState.Items.ContainsKey("scheme"))
|
if (decodedState != null && decodedState.Items.TryGetValue("scheme", out var stateScheme))
|
||||||
{
|
{
|
||||||
return decodedState.Items["scheme"] == scheme;
|
return stateScheme == scheme;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
|
|
||||||
@ -19,37 +19,42 @@ then
|
|||||||
LGID=65534
|
LGID=65534
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create user and group
|
if [ "$(id -u)" = "0" ]
|
||||||
|
then
|
||||||
|
# Create user and group
|
||||||
|
|
||||||
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
|
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
|
||||||
groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1
|
groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1
|
||||||
useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||
|
useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||
|
||||||
usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1
|
usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1
|
||||||
mkhomedir_helper $USERNAME
|
mkhomedir_helper $USERNAME
|
||||||
|
|
||||||
# The rest...
|
# The rest...
|
||||||
|
|
||||||
mkdir -p /etc/bitwarden/identity
|
chown -R $USERNAME:$GROUPNAME /app
|
||||||
mkdir -p /etc/bitwarden/core
|
mkdir -p /etc/bitwarden/core
|
||||||
mkdir -p /etc/bitwarden/logs
|
mkdir -p /etc/bitwarden/logs
|
||||||
mkdir -p /etc/bitwarden/ca-certificates
|
mkdir -p /etc/bitwarden/ca-certificates
|
||||||
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
|
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
|
||||||
|
|
||||||
if [[ $globalSettings__selfHosted == "true" ]]; then
|
if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then
|
||||||
cp /etc/bitwarden/identity/identity.pfx /app/identity.pfx
|
chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos
|
||||||
fi
|
fi
|
||||||
|
|
||||||
chown -R $USERNAME:$GROUPNAME /app
|
gosu_cmd="gosu $USERNAME:$GROUPNAME"
|
||||||
|
else
|
||||||
if [[ $globalSettings__selfHosted == "true" ]]; then
|
gosu_cmd=""
|
||||||
cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \
|
|
||||||
&& update-ca-certificates
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then
|
if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then
|
||||||
chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos
|
cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf
|
||||||
cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf
|
$gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab
|
||||||
gosu $USERNAME:$GROUPNAME kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec gosu $USERNAME:$GROUPNAME dotnet /app/Sso.dll
|
if [[ $globalSettings__selfHosted == "true" ]]; then
|
||||||
|
if [[ -z $globalSettings__identityServer__certificateLocation ]]; then
|
||||||
|
export globalSettings__identityServer__certificateLocation=/etc/bitwarden/identity/identity.pfx
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec $gosu_cmd /app/Sso
|
||||||
|
@ -33,6 +33,39 @@
|
|||||||
"Name": "events-webhook-subscription"
|
"Name": "events-webhook-subscription"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "event-integrations",
|
||||||
|
"Subscriptions": [
|
||||||
|
{
|
||||||
|
"Name": "integration-slack-subscription",
|
||||||
|
"Rules": [
|
||||||
|
{
|
||||||
|
"Name": "slack-integration-filter",
|
||||||
|
"Properties": {
|
||||||
|
"FilterType": "Correlation",
|
||||||
|
"CorrelationFilter": {
|
||||||
|
"Label": "slack"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "integration-webhook-subscription",
|
||||||
|
"Rules": [
|
||||||
|
{
|
||||||
|
"Name": "webhook-integration-filter",
|
||||||
|
"Properties": {
|
||||||
|
"FilterType": "Correlation",
|
||||||
|
"CorrelationFilter": {
|
||||||
|
"Label": "webhook"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
*
|
|
||||||
!obj/build-output/publish/*
|
|
||||||
!obj/Docker/empty/
|
|
||||||
!entrypoint.sh
|
|
@ -12,7 +12,6 @@ using Bit.Core.Billing.Enums;
|
|||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Providers.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Context;
|
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.OrganizationConnectionConfigs;
|
using Bit.Core.Models.OrganizationConnectionConfigs;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||||
@ -20,9 +19,6 @@ using Bit.Core.Repositories;
|
|||||||
using Bit.Core.SecretsManager.Repositories;
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
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 Bit.Core.Utilities;
|
||||||
using Bit.Core.Vault.Repositories;
|
using Bit.Core.Vault.Repositories;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@ -45,12 +41,9 @@ public class OrganizationsController : Controller
|
|||||||
private readonly IPaymentService _paymentService;
|
private readonly IPaymentService _paymentService;
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
|
||||||
private readonly IUserService _userService;
|
|
||||||
private readonly IProviderRepository _providerRepository;
|
private readonly IProviderRepository _providerRepository;
|
||||||
private readonly ILogger<OrganizationsController> _logger;
|
private readonly ILogger<OrganizationsController> _logger;
|
||||||
private readonly IAccessControlService _accessControlService;
|
private readonly IAccessControlService _accessControlService;
|
||||||
private readonly ICurrentContext _currentContext;
|
|
||||||
private readonly ISecretRepository _secretRepository;
|
private readonly ISecretRepository _secretRepository;
|
||||||
private readonly IProjectRepository _projectRepository;
|
private readonly IProjectRepository _projectRepository;
|
||||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||||
@ -73,12 +66,9 @@ public class OrganizationsController : Controller
|
|||||||
IPaymentService paymentService,
|
IPaymentService paymentService,
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IReferenceEventService referenceEventService,
|
|
||||||
IUserService userService,
|
|
||||||
IProviderRepository providerRepository,
|
IProviderRepository providerRepository,
|
||||||
ILogger<OrganizationsController> logger,
|
ILogger<OrganizationsController> logger,
|
||||||
IAccessControlService accessControlService,
|
IAccessControlService accessControlService,
|
||||||
ICurrentContext currentContext,
|
|
||||||
ISecretRepository secretRepository,
|
ISecretRepository secretRepository,
|
||||||
IProjectRepository projectRepository,
|
IProjectRepository projectRepository,
|
||||||
IServiceAccountRepository serviceAccountRepository,
|
IServiceAccountRepository serviceAccountRepository,
|
||||||
@ -100,12 +90,9 @@ public class OrganizationsController : Controller
|
|||||||
_paymentService = paymentService;
|
_paymentService = paymentService;
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_referenceEventService = referenceEventService;
|
|
||||||
_userService = userService;
|
|
||||||
_providerRepository = providerRepository;
|
_providerRepository = providerRepository;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_accessControlService = accessControlService;
|
_accessControlService = accessControlService;
|
||||||
_currentContext = currentContext;
|
|
||||||
_secretRepository = secretRepository;
|
_secretRepository = secretRepository;
|
||||||
_projectRepository = projectRepository;
|
_projectRepository = projectRepository;
|
||||||
_serviceAccountRepository = serviceAccountRepository;
|
_serviceAccountRepository = serviceAccountRepository;
|
||||||
@ -272,11 +259,6 @@ public class OrganizationsController : Controller
|
|||||||
await _organizationRepository.ReplaceAsync(organization);
|
await _organizationRepository.ReplaceAsync(organization);
|
||||||
|
|
||||||
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
|
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
|
||||||
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.OrganizationEditedByAdmin, organization, _currentContext)
|
|
||||||
{
|
|
||||||
EventRaisedByUser = _userService.GetUserName(User),
|
|
||||||
SalesAssistedTrialStarted = model.SalesAssistedTrialStarted,
|
|
||||||
});
|
|
||||||
|
|
||||||
return RedirectToAction("Edit", new { id });
|
return RedirectToAction("Edit", new { id });
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,8 @@ public class OrganizationViewModel
|
|||||||
orgUsers
|
orgUsers
|
||||||
.Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus)
|
.Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus)
|
||||||
.Select(u => u.Email));
|
.Select(u => u.Email));
|
||||||
|
OwnersDetails = orgUsers.Where(u => u.Type == OrganizationUserType.Owner && u.Status == organizationUserStatus);
|
||||||
|
AdminsDetails = orgUsers.Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus);
|
||||||
SecretsCount = secretsCount;
|
SecretsCount = secretsCount;
|
||||||
ProjectsCount = projectCount;
|
ProjectsCount = projectCount;
|
||||||
ServiceAccountsCount = serviceAccountsCount;
|
ServiceAccountsCount = serviceAccountsCount;
|
||||||
@ -70,4 +72,6 @@ public class OrganizationViewModel
|
|||||||
public int OccupiedSmSeatsCount { get; set; }
|
public int OccupiedSmSeatsCount { get; set; }
|
||||||
public bool UseSecretsManager => Organization.UseSecretsManager;
|
public bool UseSecretsManager => Organization.UseSecretsManager;
|
||||||
public bool UseRiskInsights => Organization.UseRiskInsights;
|
public bool UseRiskInsights => Organization.UseRiskInsights;
|
||||||
|
public IEnumerable<OrganizationUserUserDetails> OwnersDetails { get; set; }
|
||||||
|
public IEnumerable<OrganizationUserUserDetails> AdminsDetails { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ public class ProviderViewModel
|
|||||||
{
|
{
|
||||||
Provider = provider;
|
Provider = provider;
|
||||||
UserCount = providerUsers.Count();
|
UserCount = providerUsers.Count();
|
||||||
ProviderAdmins = providerUsers.Where(u => u.Type == ProviderUserType.ProviderAdmin);
|
ProviderUsers = providerUsers;
|
||||||
ProviderOrganizations = organizations.Where(o => o.ProviderId == provider.Id);
|
ProviderOrganizations = organizations.Where(o => o.ProviderId == provider.Id);
|
||||||
|
|
||||||
if (Provider.Type == ProviderType.Msp)
|
if (Provider.Type == ProviderType.Msp)
|
||||||
@ -61,7 +61,7 @@ public class ProviderViewModel
|
|||||||
|
|
||||||
public int UserCount { get; set; }
|
public int UserCount { get; set; }
|
||||||
public Provider Provider { get; set; }
|
public Provider Provider { get; set; }
|
||||||
public IEnumerable<ProviderUserUserDetails> ProviderAdmins { get; set; }
|
public IEnumerable<ProviderUserUserDetails> ProviderUsers { get; set; }
|
||||||
public IEnumerable<ProviderOrganizationOrganizationDetails> ProviderOrganizations { get; set; }
|
public IEnumerable<ProviderOrganizationOrganizationDetails> ProviderOrganizations { get; set; }
|
||||||
public List<ProviderPlanViewModel> ProviderPlanViewModels { get; set; } = [];
|
public List<ProviderPlanViewModel> ProviderPlanViewModels { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
@using Bit.Admin.Enums;
|
@using Bit.Admin.Enums;
|
||||||
@using Bit.Admin.Models
|
@using Bit.Admin.Models
|
||||||
@using Bit.Core
|
|
||||||
@using Bit.Core.AdminConsole.Enums.Provider
|
@using Bit.Core.AdminConsole.Enums.Provider
|
||||||
@using Bit.Core.Billing.Enums
|
@using Bit.Core.Billing.Enums
|
||||||
@using Bit.Core.Billing.Extensions
|
@using Bit.Core.Billing.Extensions
|
||||||
@using Bit.Core.Services
|
|
||||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
|
||||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||||
@inject IFeatureService FeatureService
|
|
||||||
@model OrganizationEditModel
|
@model OrganizationEditModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = (Model.Provider != null ? "Client " : string.Empty) + "Organization: " + Model.Name;
|
ViewData["Title"] = (Model.Provider != null ? "Client " : string.Empty) + "Organization: " + Model.Name;
|
||||||
@ -19,12 +15,10 @@
|
|||||||
var canDelete = AccessControlService.UserHasPermission(Permission.Org_Delete);
|
var canDelete = AccessControlService.UserHasPermission(Permission.Org_Delete);
|
||||||
var canUnlinkFromProvider = AccessControlService.UserHasPermission(Permission.Provider_Edit);
|
var canUnlinkFromProvider = AccessControlService.UserHasPermission(Permission.Provider_Edit);
|
||||||
|
|
||||||
var canConvertToBusinessUnit =
|
var canConvertToBusinessUnit = AccessControlService.UserHasPermission(Permission.Org_Billing_ConvertToBusinessUnit) &&
|
||||||
FeatureService.IsEnabled(FeatureFlagKeys.PM18770_EnableOrganizationBusinessUnitConversion) &&
|
Model.Organization.PlanType.GetProductTier() == ProductTierType.Enterprise &&
|
||||||
AccessControlService.UserHasPermission(Permission.Org_Billing_ConvertToBusinessUnit) &&
|
!string.IsNullOrEmpty(Model.Organization.GatewaySubscriptionId) &&
|
||||||
Model.Organization.PlanType.GetProductTier() == ProductTierType.Enterprise &&
|
Model.Provider is null or { Type: ProviderType.BusinessUnit, Status: ProviderStatusType.Pending };
|
||||||
!string.IsNullOrEmpty(Model.Organization.GatewaySubscriptionId) &&
|
|
||||||
Model.Provider is null or { Type: ProviderType.BusinessUnit, Status: ProviderStatusType.Pending };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
|
@ -19,12 +19,6 @@
|
|||||||
<span id="org-confirmed-users" title="Confirmed">@Model.UserConfirmedCount</span>)
|
<span id="org-confirmed-users" title="Confirmed">@Model.UserConfirmedCount</span>)
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt class="col-sm-4 col-lg-3">Owners</dt>
|
|
||||||
<dd id="org-owner" class="col-sm-8 col-lg-9">@(string.IsNullOrWhiteSpace(Model.Owners) ? "None" : Model.Owners)</dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-4 col-lg-3">Admins</dt>
|
|
||||||
<dd id="org-admins" class="col-sm-8 col-lg-9">@(string.IsNullOrWhiteSpace(Model.Admins) ? "None" : Model.Admins)</dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-4 col-lg-3">Using 2FA</dt>
|
<dt class="col-sm-4 col-lg-3">Using 2FA</dt>
|
||||||
<dd id="org-2fa" class="col-sm-8 col-lg-9">@(Model.Organization.TwoFactorIsEnabled() ? "Yes" : "No")</dd>
|
<dd id="org-2fa" class="col-sm-8 col-lg-9">@(Model.Organization.TwoFactorIsEnabled() ? "Yes" : "No")</dd>
|
||||||
|
|
||||||
@ -76,3 +70,49 @@
|
|||||||
<dt class="col-sm-4 col-lg-3">Secrets Manager Seats</dt>
|
<dt class="col-sm-4 col-lg-3">Secrets Manager Seats</dt>
|
||||||
<dd id="sm-seat-count" class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.OccupiedSmSeatsCount: "N/A" )</dd>
|
<dd id="sm-seat-count" class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.OccupiedSmSeatsCount: "N/A" )</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
|
<h2>Administrators</h2>
|
||||||
|
<dl class="row">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<div class="col-8">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 190px;">Email</th>
|
||||||
|
<th style="width: 60px;">Role</th>
|
||||||
|
<th style="width: 40px;">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@if(!Model.Admins.Any() && !Model.Owners.Any())
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td colspan="6">No results to list.</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@foreach(var owner in Model.OwnersDetails)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td class="align-middle">@owner.Email</td>
|
||||||
|
<td class="align-middle">Owner</td>
|
||||||
|
<td class="align-middle">@owner.Status</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
|
||||||
|
@foreach(var admin in Model.AdminsDetails)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td class="align-middle">@admin.Email</td>
|
||||||
|
<td class="align-middle">Admin</td>
|
||||||
|
<td class="align-middle">@admin.Status</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
var canResendEmailInvite = AccessControlService.UserHasPermission(Permission.Provider_ResendEmailInvite);
|
var canResendEmailInvite = AccessControlService.UserHasPermission(Permission.Provider_ResendEmailInvite);
|
||||||
}
|
}
|
||||||
|
|
||||||
<h2>Provider Admins</h2>
|
<h2>Administrators</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
@ -15,12 +15,13 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 190px;">Email</th>
|
<th style="width: 190px;">Email</th>
|
||||||
|
<th style="width: 160px;">Role</th>
|
||||||
<th style="width: 40px;">Status</th>
|
<th style="width: 40px;">Status</th>
|
||||||
<th style="width: 30px;"></th>
|
<th style="width: 30px;"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@if(!Model.ProviderAdmins.Any())
|
@if(!Model.ProviderUsers.Any())
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6">No results to list.</td>
|
<td colspan="6">No results to list.</td>
|
||||||
@ -28,29 +29,39 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@foreach(var admin in Model.ProviderAdmins)
|
@foreach(var user in Model.ProviderUsers)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
@admin.Email
|
@user.Email
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
@admin.Status
|
@if(@user.Type == 0)
|
||||||
|
{
|
||||||
|
<span>Provider Admin</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>Service User</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
@user.Status
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@if(admin.Status.Equals(ProviderUserStatusType.Confirmed)
|
@if(user.Status.Equals(ProviderUserStatusType.Confirmed)
|
||||||
&& @Model.Provider.Status.Equals(ProviderStatusType.Pending)
|
&& @Model.Provider.Status.Equals(ProviderStatusType.Pending)
|
||||||
&& canResendEmailInvite)
|
&& canResendEmailInvite)
|
||||||
{
|
{
|
||||||
@if(@TempData["InviteResentTo"] != null && @TempData["InviteResentTo"].ToString() == @admin.UserId.Value.ToString())
|
@if(@TempData["InviteResentTo"] != null && @TempData["InviteResentTo"].ToString() == @user.UserId.Value.ToString())
|
||||||
{
|
{
|
||||||
<button class="btn btn-outline-success btn-sm disabled" disabled>Invite Resent!</button>
|
<button class="btn btn-outline-success btn-sm disabled" disabled>Invite Resent!</button>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<a class="btn btn-outline-secondary btn-sm"
|
<a class="btn btn-outline-secondary btn-sm"
|
||||||
data-id="@admin.Id" asp-controller="Providers"
|
data-id="@user.Id" asp-controller="Providers"
|
||||||
asp-action="ResendInvite" asp-route-ownerId="@admin.UserId"
|
asp-action="ResendInvite" asp-route-ownerId="@user.UserId"
|
||||||
asp-route-providerId="@Model.Provider.Id">
|
asp-route-providerId="@Model.Provider.Id">
|
||||||
Resend Setup Invite
|
Resend Setup Invite
|
||||||
</a>
|
</a>
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
using Bit.Admin.Billing.Models;
|
using Bit.Admin.Billing.Models;
|
||||||
using Bit.Admin.Enums;
|
using Bit.Admin.Enums;
|
||||||
using Bit.Admin.Utilities;
|
using Bit.Admin.Utilities;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
@ -18,7 +17,6 @@ namespace Bit.Admin.Billing.Controllers;
|
|||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[Route("organizations/billing/{organizationId:guid}/business-unit")]
|
[Route("organizations/billing/{organizationId:guid}/business-unit")]
|
||||||
[RequireFeature(FeatureFlagKeys.PM18770_EnableOrganizationBusinessUnitConversion)]
|
|
||||||
public class BusinessUnitConversionController(
|
public class BusinessUnitConversionController(
|
||||||
IBusinessUnitConverter businessUnitConverter,
|
IBusinessUnitConverter businessUnitConverter,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
|
@ -1,21 +1,71 @@
|
|||||||
|
###############################################
|
||||||
|
# Build stage #
|
||||||
|
###############################################
|
||||||
|
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||||
|
|
||||||
|
# Docker buildx supplies the value for this arg
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
|
# Determine proper runtime value for .NET
|
||||||
|
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||||
|
RID=linux-x64 ; \
|
||||||
|
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||||
|
RID=linux-arm64 ; \
|
||||||
|
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
|
||||||
|
RID=linux-arm ; \
|
||||||
|
fi \
|
||||||
|
&& echo "RID=$RID" > /tmp/rid.txt
|
||||||
|
|
||||||
|
# Set up Node
|
||||||
|
ARG NODE_VERSION=20
|
||||||
|
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y nodejs \
|
||||||
|
&& npm install -g npm@latest && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy required project files
|
||||||
|
WORKDIR /source
|
||||||
|
COPY . ./
|
||||||
|
|
||||||
|
# Restore project dependencies and tools
|
||||||
|
WORKDIR /source/src/Admin
|
||||||
|
RUN npm ci
|
||||||
|
RUN . /tmp/rid.txt && dotnet restore -r $RID
|
||||||
|
|
||||||
|
# Build project
|
||||||
|
RUN npm run build
|
||||||
|
RUN . /tmp/rid.txt && dotnet publish \
|
||||||
|
-c release \
|
||||||
|
--no-restore \
|
||||||
|
--self-contained \
|
||||||
|
/p:PublishSingleFile=true \
|
||||||
|
-r $RID \
|
||||||
|
-o out
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# App stage #
|
||||||
|
###############################################
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||||
|
|
||||||
|
ARG TARGETPLATFORM
|
||||||
LABEL com.bitwarden.product="bitwarden"
|
LABEL com.bitwarden.product="bitwarden"
|
||||||
|
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
ENV ASPNETCORE_URLS=http://+:5000
|
||||||
|
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
gosu \
|
gosu \
|
||||||
curl \
|
curl \
|
||||||
krb5-user \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
ENV ASPNETCORE_URLS http://+:5000
|
# Copy app from the build stage
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 5000
|
COPY --from=build /source/src/Admin/out /app
|
||||||
COPY obj/build-output/publish .
|
COPY ./src/Admin/entrypoint.sh /entrypoint.sh
|
||||||
COPY entrypoint.sh /
|
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1
|
||||||
HEALTHCHECK CMD curl -f http://localhost:5000 || exit 1
|
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
@ -39,7 +39,7 @@ public class ReadOnlyEnvIdentityUserStore : ReadOnlyIdentityUserStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var userStamp = usersDict.ContainsKey(normalizedEmail) ? usersDict[normalizedEmail] : null;
|
var userStamp = usersDict.GetValueOrDefault(normalizedEmail);
|
||||||
if (userStamp == null)
|
if (userStamp == null)
|
||||||
{
|
{
|
||||||
return Task.FromResult<IdentityUser>(null);
|
return Task.FromResult<IdentityUser>(null);
|
||||||
|
@ -20,8 +20,8 @@ public class Program
|
|||||||
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
||||||
{
|
{
|
||||||
var context = e.Properties["SourceContext"].ToString();
|
var context = e.Properties["SourceContext"].ToString();
|
||||||
if (e.Properties.ContainsKey("RequestPath") &&
|
if (e.Properties.TryGetValue("RequestPath", out var requestPath) &&
|
||||||
!string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) &&
|
!string.IsNullOrWhiteSpace(requestPath?.ToString()) &&
|
||||||
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
|
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
@ -29,12 +29,12 @@ public class AccessControlService : IAccessControlService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var userRole = GetUserRoleFromClaim();
|
var userRole = GetUserRoleFromClaim();
|
||||||
if (string.IsNullOrEmpty(userRole) || !RolePermissionMapping.RolePermissions.ContainsKey(userRole))
|
if (string.IsNullOrEmpty(userRole) || !RolePermissionMapping.RolePermissions.TryGetValue(userRole, out var rolePermissions))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return RolePermissionMapping.RolePermissions[userRole].Contains(permission);
|
return rolePermissions.Contains(permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetUserRole(string userEmail)
|
public string GetUserRole(string userEmail)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
|
|
||||||
@ -19,31 +19,36 @@ then
|
|||||||
LGID=65534
|
LGID=65534
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create user and group
|
if [ "$(id -u)" = "0" ]
|
||||||
|
then
|
||||||
|
# Create user and group
|
||||||
|
|
||||||
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
|
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
|
||||||
groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1
|
groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1
|
||||||
useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||
|
useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||
|
||||||
usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1
|
usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1
|
||||||
mkhomedir_helper $USERNAME
|
mkhomedir_helper $USERNAME
|
||||||
|
|
||||||
# The rest...
|
# The rest...
|
||||||
|
|
||||||
chown -R $USERNAME:$GROUPNAME /app
|
chown -R $USERNAME:$GROUPNAME /app
|
||||||
mkdir -p /etc/bitwarden/core
|
mkdir -p /etc/bitwarden/core
|
||||||
mkdir -p /etc/bitwarden/logs
|
mkdir -p /etc/bitwarden/logs
|
||||||
mkdir -p /etc/bitwarden/ca-certificates
|
mkdir -p /etc/bitwarden/ca-certificates
|
||||||
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
|
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
|
||||||
|
|
||||||
if [[ $globalSettings__selfHosted == "true" ]]; then
|
if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then
|
||||||
cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \
|
chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos
|
||||||
&& update-ca-certificates
|
fi
|
||||||
|
|
||||||
|
gosu_cmd="gosu $USERNAME:$GROUPNAME"
|
||||||
|
else
|
||||||
|
gosu_cmd=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then
|
if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then
|
||||||
chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos
|
cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf
|
||||||
cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf
|
$gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab
|
||||||
gosu $USERNAME:$GROUPNAME kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec gosu $USERNAME:$GROUPNAME dotnet /app/Admin.dll
|
exec $gosu_cmd /app/Admin
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
*
|
|
||||||
!obj/build-output/publish/*
|
|
||||||
!obj/Docker/empty/
|
|
||||||
!entrypoint.sh
|
|
@ -2,10 +2,10 @@
|
|||||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Data.Integrations;
|
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data.Integrations;
|
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ public class UpdateTwoFactorAuthenticatorRequestModel : SecretVerificationReques
|
|||||||
{
|
{
|
||||||
providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
|
providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
|
||||||
}
|
}
|
||||||
else if (providers.ContainsKey(TwoFactorProviderType.Authenticator))
|
else
|
||||||
{
|
{
|
||||||
providers.Remove(TwoFactorProviderType.Authenticator);
|
providers.Remove(TwoFactorProviderType.Authenticator);
|
||||||
}
|
}
|
||||||
@ -62,7 +62,7 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV
|
|||||||
{
|
{
|
||||||
providers = [];
|
providers = [];
|
||||||
}
|
}
|
||||||
else if (providers.ContainsKey(TwoFactorProviderType.Duo))
|
else
|
||||||
{
|
{
|
||||||
providers.Remove(TwoFactorProviderType.Duo);
|
providers.Remove(TwoFactorProviderType.Duo);
|
||||||
}
|
}
|
||||||
@ -88,7 +88,7 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV
|
|||||||
{
|
{
|
||||||
providers = [];
|
providers = [];
|
||||||
}
|
}
|
||||||
else if (providers.ContainsKey(TwoFactorProviderType.OrganizationDuo))
|
else
|
||||||
{
|
{
|
||||||
providers.Remove(TwoFactorProviderType.OrganizationDuo);
|
providers.Remove(TwoFactorProviderType.OrganizationDuo);
|
||||||
}
|
}
|
||||||
@ -145,7 +145,7 @@ public class UpdateTwoFactorYubicoOtpRequestModel : SecretVerificationRequestMod
|
|||||||
{
|
{
|
||||||
providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
|
providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
|
||||||
}
|
}
|
||||||
else if (providers.ContainsKey(TwoFactorProviderType.YubiKey))
|
else
|
||||||
{
|
{
|
||||||
providers.Remove(TwoFactorProviderType.YubiKey);
|
providers.Remove(TwoFactorProviderType.YubiKey);
|
||||||
}
|
}
|
||||||
@ -228,7 +228,7 @@ public class TwoFactorEmailRequestModel : SecretVerificationRequestModel
|
|||||||
{
|
{
|
||||||
providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
|
providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
|
||||||
}
|
}
|
||||||
else if (providers.ContainsKey(TwoFactorProviderType.Email))
|
else
|
||||||
{
|
{
|
||||||
providers.Remove(TwoFactorProviderType.Email);
|
providers.Remove(TwoFactorProviderType.Email);
|
||||||
}
|
}
|
||||||
|
@ -90,6 +90,13 @@ public class EmergencyAccessGrantorDetailsResponseModel : EmergencyAccessRespons
|
|||||||
|
|
||||||
public class EmergencyAccessTakeoverResponseModel : ResponseModel
|
public class EmergencyAccessTakeoverResponseModel : ResponseModel
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the <see cref="EmergencyAccessTakeoverResponseModel"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="emergencyAccess">Consumed for the Encrypted Key value</param>
|
||||||
|
/// <param name="grantor">consumed for the KDF configuration</param>
|
||||||
|
/// <param name="obj">name of the object</param>
|
||||||
|
/// <exception cref="ArgumentNullException">emergencyAccess cannot be null</exception>
|
||||||
public EmergencyAccessTakeoverResponseModel(EmergencyAccess emergencyAccess, User grantor, string obj = "emergencyAccessTakeover") : base(obj)
|
public EmergencyAccessTakeoverResponseModel(EmergencyAccess emergencyAccess, User grantor, string obj = "emergencyAccessTakeover") : base(obj)
|
||||||
{
|
{
|
||||||
if (emergencyAccess == null)
|
if (emergencyAccess == null)
|
||||||
|
@ -13,9 +13,9 @@ public class TwoFactorAuthenticatorResponseModel : ResponseModel
|
|||||||
ArgumentNullException.ThrowIfNull(user);
|
ArgumentNullException.ThrowIfNull(user);
|
||||||
|
|
||||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator);
|
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator);
|
||||||
if (provider?.MetaData?.ContainsKey("Key") ?? false)
|
if (provider?.MetaData?.TryGetValue("Key", out var keyValue) ?? false)
|
||||||
{
|
{
|
||||||
Key = (string)provider.MetaData["Key"];
|
Key = (string)keyValue;
|
||||||
Enabled = provider.Enabled;
|
Enabled = provider.Enabled;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -15,9 +15,9 @@ public class TwoFactorEmailResponseModel : ResponseModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email);
|
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email);
|
||||||
if (provider?.MetaData?.ContainsKey("Email") ?? false)
|
if (provider?.MetaData?.TryGetValue("Email", out var email) ?? false)
|
||||||
{
|
{
|
||||||
Email = (string)provider.MetaData["Email"];
|
Email = (string)email;
|
||||||
Enabled = provider.Enabled;
|
Enabled = provider.Enabled;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -19,29 +19,29 @@ public class TwoFactorYubiKeyResponseModel : ResponseModel
|
|||||||
{
|
{
|
||||||
Enabled = provider.Enabled;
|
Enabled = provider.Enabled;
|
||||||
|
|
||||||
if (provider.MetaData.ContainsKey("Key1"))
|
if (provider.MetaData.TryGetValue("Key1", out var key1))
|
||||||
{
|
{
|
||||||
Key1 = (string)provider.MetaData["Key1"];
|
Key1 = (string)key1;
|
||||||
}
|
}
|
||||||
if (provider.MetaData.ContainsKey("Key2"))
|
if (provider.MetaData.TryGetValue("Key2", out var key2))
|
||||||
{
|
{
|
||||||
Key2 = (string)provider.MetaData["Key2"];
|
Key2 = (string)key2;
|
||||||
}
|
}
|
||||||
if (provider.MetaData.ContainsKey("Key3"))
|
if (provider.MetaData.TryGetValue("Key3", out var key3))
|
||||||
{
|
{
|
||||||
Key3 = (string)provider.MetaData["Key3"];
|
Key3 = (string)key3;
|
||||||
}
|
}
|
||||||
if (provider.MetaData.ContainsKey("Key4"))
|
if (provider.MetaData.TryGetValue("Key4", out var key4))
|
||||||
{
|
{
|
||||||
Key4 = (string)provider.MetaData["Key4"];
|
Key4 = (string)key4;
|
||||||
}
|
}
|
||||||
if (provider.MetaData.ContainsKey("Key5"))
|
if (provider.MetaData.TryGetValue("Key5", out var key5))
|
||||||
{
|
{
|
||||||
Key5 = (string)provider.MetaData["Key5"];
|
Key5 = (string)key5;
|
||||||
}
|
}
|
||||||
if (provider.MetaData.ContainsKey("Nfc"))
|
if (provider.MetaData.TryGetValue("Nfc", out var nfc))
|
||||||
{
|
{
|
||||||
Nfc = (bool)provider.MetaData["Nfc"];
|
Nfc = (bool)nfc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -6,14 +6,10 @@ using Bit.Api.Utilities;
|
|||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Context;
|
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
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 Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -161,8 +157,6 @@ public class AccountsController(
|
|||||||
[HttpPost("cancel")]
|
[HttpPost("cancel")]
|
||||||
public async Task PostCancelAsync(
|
public async Task PostCancelAsync(
|
||||||
[FromBody] SubscriptionCancellationRequestModel request,
|
[FromBody] SubscriptionCancellationRequestModel request,
|
||||||
[FromServices] ICurrentContext currentContext,
|
|
||||||
[FromServices] IReferenceEventService referenceEventService,
|
|
||||||
[FromServices] ISubscriberService subscriberService)
|
[FromServices] ISubscriberService subscriberService)
|
||||||
{
|
{
|
||||||
var user = await userService.GetUserByPrincipalAsync(User);
|
var user = await userService.GetUserByPrincipalAsync(User);
|
||||||
@ -175,12 +169,6 @@ public class AccountsController(
|
|||||||
await subscriberService.CancelSubscription(user,
|
await subscriberService.CancelSubscription(user,
|
||||||
new OffboardingSurveyResponse { UserId = user.Id, Reason = request.Reason, Feedback = request.Feedback },
|
new OffboardingSurveyResponse { UserId = user.Id, Reason = request.Reason, Feedback = request.Feedback },
|
||||||
user.IsExpired());
|
user.IsExpired());
|
||||||
|
|
||||||
await referenceEventService.RaiseEventAsync(new ReferenceEvent(
|
|
||||||
ReferenceEventType.CancelSubscription,
|
|
||||||
user,
|
|
||||||
currentContext)
|
|
||||||
{ EndOfPeriod = user.IsExpired() });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("reinstate-premium")]
|
[HttpPost("reinstate-premium")]
|
||||||
|
@ -4,7 +4,6 @@ using Bit.Api.AdminConsole.Models.Request.Organizations;
|
|||||||
using Bit.Api.Billing.Models.Requests;
|
using Bit.Api.Billing.Models.Requests;
|
||||||
using Bit.Api.Billing.Models.Responses;
|
using Bit.Api.Billing.Models.Responses;
|
||||||
using Bit.Api.Billing.Queries.Organizations;
|
using Bit.Api.Billing.Queries.Organizations;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Models.Sales;
|
using Bit.Core.Billing.Models.Sales;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
@ -25,7 +24,6 @@ namespace Bit.Api.Billing.Controllers;
|
|||||||
public class OrganizationBillingController(
|
public class OrganizationBillingController(
|
||||||
IBusinessUnitConverter businessUnitConverter,
|
IBusinessUnitConverter businessUnitConverter,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IFeatureService featureService,
|
|
||||||
IOrganizationBillingService organizationBillingService,
|
IOrganizationBillingService organizationBillingService,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationWarningsQuery organizationWarningsQuery,
|
IOrganizationWarningsQuery organizationWarningsQuery,
|
||||||
@ -318,14 +316,6 @@ public class OrganizationBillingController(
|
|||||||
[FromRoute] Guid organizationId,
|
[FromRoute] Guid organizationId,
|
||||||
[FromBody] SetupBusinessUnitRequestBody requestBody)
|
[FromBody] SetupBusinessUnitRequestBody requestBody)
|
||||||
{
|
{
|
||||||
var enableOrganizationBusinessUnitConversion =
|
|
||||||
featureService.IsEnabled(FeatureFlagKeys.PM18770_EnableOrganizationBusinessUnitConversion);
|
|
||||||
|
|
||||||
if (!enableOrganizationBusinessUnitConversion)
|
|
||||||
{
|
|
||||||
return Error.NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
|
||||||
if (organization == null)
|
if (organization == null)
|
||||||
|
@ -20,9 +20,6 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
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 Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -44,7 +41,6 @@ public class OrganizationsController(
|
|||||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
||||||
IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand,
|
IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand,
|
||||||
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand,
|
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand,
|
||||||
IReferenceEventService referenceEventService,
|
|
||||||
ISubscriberService subscriberService,
|
ISubscriberService subscriberService,
|
||||||
IOrganizationInstallationRepository organizationInstallationRepository,
|
IOrganizationInstallationRepository organizationInstallationRepository,
|
||||||
IPricingClient pricingClient)
|
IPricingClient pricingClient)
|
||||||
@ -246,14 +242,6 @@ public class OrganizationsController(
|
|||||||
Feedback = request.Feedback
|
Feedback = request.Feedback
|
||||||
},
|
},
|
||||||
organization.IsExpired());
|
organization.IsExpired());
|
||||||
|
|
||||||
await referenceEventService.RaiseEventAsync(new ReferenceEvent(
|
|
||||||
ReferenceEventType.CancelSubscription,
|
|
||||||
organization,
|
|
||||||
currentContext)
|
|
||||||
{
|
|
||||||
EndOfPeriod = organization.IsExpired()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id:guid}/reinstate")]
|
[HttpPost("{id:guid}/reinstate")]
|
||||||
|
@ -12,7 +12,8 @@ public record OrganizationMetadataResponse(
|
|||||||
bool IsSubscriptionCanceled,
|
bool IsSubscriptionCanceled,
|
||||||
DateTime? InvoiceDueDate,
|
DateTime? InvoiceDueDate,
|
||||||
DateTime? InvoiceCreatedDate,
|
DateTime? InvoiceCreatedDate,
|
||||||
DateTime? SubPeriodEndDate)
|
DateTime? SubPeriodEndDate,
|
||||||
|
int OrganizationOccupiedSeats)
|
||||||
{
|
{
|
||||||
public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
|
public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
|
||||||
=> new(
|
=> new(
|
||||||
@ -25,5 +26,6 @@ public record OrganizationMetadataResponse(
|
|||||||
metadata.IsSubscriptionCanceled,
|
metadata.IsSubscriptionCanceled,
|
||||||
metadata.InvoiceDueDate,
|
metadata.InvoiceDueDate,
|
||||||
metadata.InvoiceCreatedDate,
|
metadata.InvoiceCreatedDate,
|
||||||
metadata.SubPeriodEndDate);
|
metadata.SubPeriodEndDate,
|
||||||
|
metadata.OrganizationOccupiedSeats);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,50 @@
|
|||||||
|
###############################################
|
||||||
|
# Build stage #
|
||||||
|
###############################################
|
||||||
|
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||||
|
|
||||||
|
# Docker buildx supplies the value for this arg
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
|
# Determine proper runtime value for .NET
|
||||||
|
# We put the value in a file to be read by later layers.
|
||||||
|
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||||
|
RID=linux-x64 ; \
|
||||||
|
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||||
|
RID=linux-arm64 ; \
|
||||||
|
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
|
||||||
|
RID=linux-arm ; \
|
||||||
|
fi \
|
||||||
|
&& echo "RID=$RID" > /tmp/rid.txt
|
||||||
|
|
||||||
|
# Copy required project files
|
||||||
|
WORKDIR /source
|
||||||
|
COPY . ./
|
||||||
|
|
||||||
|
# Restore project dependencies and tools
|
||||||
|
WORKDIR /source/src/Api
|
||||||
|
RUN . /tmp/rid.txt && dotnet restore -r $RID
|
||||||
|
|
||||||
|
# Build project
|
||||||
|
RUN . /tmp/rid.txt && dotnet publish \
|
||||||
|
-c release \
|
||||||
|
--no-restore \
|
||||||
|
--self-contained \
|
||||||
|
/p:PublishSingleFile=true \
|
||||||
|
-r $RID \
|
||||||
|
-o out
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# App stage #
|
||||||
|
###############################################
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||||
|
|
||||||
|
ARG TARGETPLATFORM
|
||||||
LABEL com.bitwarden.product="bitwarden"
|
LABEL com.bitwarden.product="bitwarden"
|
||||||
|
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
ENV ASPNETCORE_URLS=http://+:5000
|
||||||
|
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
@ -9,13 +53,11 @@ RUN apt-get update \
|
|||||||
krb5-user \
|
krb5-user \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
ENV ASPNETCORE_URLS http://+:5000
|
# Copy app from the build stage
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 5000
|
COPY --from=build /source/src/Api/out /app
|
||||||
COPY obj/build-output/publish .
|
COPY ./src/Api/entrypoint.sh /entrypoint.sh
|
||||||
COPY entrypoint.sh /
|
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1
|
HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
@ -5,7 +5,6 @@ using Bit.Core.Context;
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Identity;
|
using Bit.Core.Identity;
|
||||||
using Bit.Core.Repositories;
|
|
||||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||||
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
|
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Entities;
|
using Bit.Core.SecretsManager.Entities;
|
||||||
@ -16,9 +15,6 @@ using Bit.Core.SecretsManager.Queries.Interfaces;
|
|||||||
using Bit.Core.SecretsManager.Queries.Secrets.Interfaces;
|
using Bit.Core.SecretsManager.Queries.Secrets.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Enums;
|
|
||||||
using Bit.Core.Tools.Models.Business;
|
|
||||||
using Bit.Core.Tools.Services;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -30,7 +26,6 @@ public class SecretsController : Controller
|
|||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly IProjectRepository _projectRepository;
|
private readonly IProjectRepository _projectRepository;
|
||||||
private readonly ISecretRepository _secretRepository;
|
private readonly ISecretRepository _secretRepository;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
|
||||||
private readonly ICreateSecretCommand _createSecretCommand;
|
private readonly ICreateSecretCommand _createSecretCommand;
|
||||||
private readonly IUpdateSecretCommand _updateSecretCommand;
|
private readonly IUpdateSecretCommand _updateSecretCommand;
|
||||||
private readonly IDeleteSecretCommand _deleteSecretCommand;
|
private readonly IDeleteSecretCommand _deleteSecretCommand;
|
||||||
@ -39,14 +34,12 @@ public class SecretsController : Controller
|
|||||||
private readonly ISecretAccessPoliciesUpdatesQuery _secretAccessPoliciesUpdatesQuery;
|
private readonly ISecretAccessPoliciesUpdatesQuery _secretAccessPoliciesUpdatesQuery;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
|
||||||
private readonly IAuthorizationService _authorizationService;
|
private readonly IAuthorizationService _authorizationService;
|
||||||
|
|
||||||
public SecretsController(
|
public SecretsController(
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IProjectRepository projectRepository,
|
IProjectRepository projectRepository,
|
||||||
ISecretRepository secretRepository,
|
ISecretRepository secretRepository,
|
||||||
IOrganizationRepository organizationRepository,
|
|
||||||
ICreateSecretCommand createSecretCommand,
|
ICreateSecretCommand createSecretCommand,
|
||||||
IUpdateSecretCommand updateSecretCommand,
|
IUpdateSecretCommand updateSecretCommand,
|
||||||
IDeleteSecretCommand deleteSecretCommand,
|
IDeleteSecretCommand deleteSecretCommand,
|
||||||
@ -55,13 +48,11 @@ public class SecretsController : Controller
|
|||||||
ISecretAccessPoliciesUpdatesQuery secretAccessPoliciesUpdatesQuery,
|
ISecretAccessPoliciesUpdatesQuery secretAccessPoliciesUpdatesQuery,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IReferenceEventService referenceEventService,
|
|
||||||
IAuthorizationService authorizationService)
|
IAuthorizationService authorizationService)
|
||||||
{
|
{
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_projectRepository = projectRepository;
|
_projectRepository = projectRepository;
|
||||||
_secretRepository = secretRepository;
|
_secretRepository = secretRepository;
|
||||||
_organizationRepository = organizationRepository;
|
|
||||||
_createSecretCommand = createSecretCommand;
|
_createSecretCommand = createSecretCommand;
|
||||||
_updateSecretCommand = updateSecretCommand;
|
_updateSecretCommand = updateSecretCommand;
|
||||||
_deleteSecretCommand = deleteSecretCommand;
|
_deleteSecretCommand = deleteSecretCommand;
|
||||||
@ -70,7 +61,6 @@ public class SecretsController : Controller
|
|||||||
_secretAccessPoliciesUpdatesQuery = secretAccessPoliciesUpdatesQuery;
|
_secretAccessPoliciesUpdatesQuery = secretAccessPoliciesUpdatesQuery;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_referenceEventService = referenceEventService;
|
|
||||||
_authorizationService = authorizationService;
|
_authorizationService = authorizationService;
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -148,9 +138,6 @@ public class SecretsController : Controller
|
|||||||
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount)
|
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount)
|
||||||
{
|
{
|
||||||
await _eventService.LogServiceAccountSecretEventAsync(userId, secret, EventType.Secret_Retrieved);
|
await _eventService.LogServiceAccountSecretEventAsync(userId, secret, EventType.Secret_Retrieved);
|
||||||
|
|
||||||
var org = await _organizationRepository.GetByIdAsync(secret.OrganizationId);
|
|
||||||
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.SmServiceAccountAccessedSecret, org, _currentContext));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SecretResponseModel(secret, access.Read, access.Write);
|
return new SecretResponseModel(secret, access.Read, access.Write);
|
||||||
@ -266,7 +253,7 @@ public class SecretsController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await LogSecretsRetrievalAsync(secrets.First().OrganizationId, secrets);
|
await LogSecretsRetrievalAsync(secrets);
|
||||||
|
|
||||||
var responses = secrets.Select(s => new BaseSecretResponseModel(s));
|
var responses = secrets.Select(s => new BaseSecretResponseModel(s));
|
||||||
return new ListResponseModel<BaseSecretResponseModel>(responses);
|
return new ListResponseModel<BaseSecretResponseModel>(responses);
|
||||||
@ -303,21 +290,18 @@ public class SecretsController : Controller
|
|||||||
|
|
||||||
if (syncResult.HasChanges)
|
if (syncResult.HasChanges)
|
||||||
{
|
{
|
||||||
await LogSecretsRetrievalAsync(organizationId, syncResult.Secrets);
|
await LogSecretsRetrievalAsync(syncResult.Secrets);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SecretsSyncResponseModel(syncResult.HasChanges, syncResult.Secrets);
|
return new SecretsSyncResponseModel(syncResult.HasChanges, syncResult.Secrets);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LogSecretsRetrievalAsync(Guid organizationId, IEnumerable<Secret> secrets)
|
private async Task LogSecretsRetrievalAsync(IEnumerable<Secret> secrets)
|
||||||
{
|
{
|
||||||
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount)
|
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount)
|
||||||
{
|
{
|
||||||
var userId = _userService.GetProperUserId(User)!.Value;
|
var userId = _userService.GetProperUserId(User)!.Value;
|
||||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
|
||||||
await _eventService.LogServiceAccountSecretsEventAsync(userId, secrets, EventType.Secret_Retrieved);
|
await _eventService.LogServiceAccountSecretsEventAsync(userId, secrets, EventType.Secret_Retrieved);
|
||||||
await _referenceEventService.RaiseEventAsync(
|
|
||||||
new ReferenceEvent(ReferenceEventType.SmServiceAccountAccessedSecret, org, _currentContext));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,10 +28,8 @@ using Bit.Core.Tools.Entities;
|
|||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||||
using Bit.Api.Billing;
|
using Bit.Api.Billing;
|
||||||
using Bit.Core.AdminConsole.Services.NoopImplementations;
|
|
||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
using Bit.Core.Auth.Identity.TokenProviders;
|
using Bit.Core.Auth.Identity.TokenProviders;
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Core.Tools.ImportFeatures;
|
using Bit.Core.Tools.ImportFeatures;
|
||||||
using Bit.Core.Tools.ReportFeatures;
|
using Bit.Core.Tools.ReportFeatures;
|
||||||
using Bit.Core.Auth.Models.Api.Request;
|
using Bit.Core.Auth.Models.Api.Request;
|
||||||
@ -224,18 +222,8 @@ public class Startup
|
|||||||
services.AddHostedService<Core.HostedServices.ApplicationCacheHostedService>();
|
services.AddHostedService<Core.HostedServices.ApplicationCacheHostedService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slack
|
// Add SlackService for OAuth API requests - if configured
|
||||||
if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) &&
|
services.AddSlackService(globalSettings);
|
||||||
CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) &&
|
|
||||||
CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes))
|
|
||||||
{
|
|
||||||
services.AddHttpClient(SlackService.HttpClientName);
|
|
||||||
services.AddSingleton<ISlackService, SlackService>();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
services.AddSingleton<ISlackService, NoopSlackService>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Configure(
|
public void Configure(
|
||||||
|
@ -5,7 +5,6 @@ using Bit.Api.Tools.Models.Request;
|
|||||||
using Bit.Api.Tools.Models.Response;
|
using Bit.Api.Tools.Models.Response;
|
||||||
using Bit.Api.Utilities;
|
using Bit.Api.Utilities;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Context;
|
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
@ -33,7 +32,6 @@ public class SendsController : Controller
|
|||||||
private readonly INonAnonymousSendCommand _nonAnonymousSendCommand;
|
private readonly INonAnonymousSendCommand _nonAnonymousSendCommand;
|
||||||
private readonly ILogger<SendsController> _logger;
|
private readonly ILogger<SendsController> _logger;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly ICurrentContext _currentContext;
|
|
||||||
|
|
||||||
public SendsController(
|
public SendsController(
|
||||||
ISendRepository sendRepository,
|
ISendRepository sendRepository,
|
||||||
@ -43,8 +41,7 @@ public class SendsController : Controller
|
|||||||
INonAnonymousSendCommand nonAnonymousSendCommand,
|
INonAnonymousSendCommand nonAnonymousSendCommand,
|
||||||
ISendFileStorageService sendFileStorageService,
|
ISendFileStorageService sendFileStorageService,
|
||||||
ILogger<SendsController> logger,
|
ILogger<SendsController> logger,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings)
|
||||||
ICurrentContext currentContext)
|
|
||||||
{
|
{
|
||||||
_sendRepository = sendRepository;
|
_sendRepository = sendRepository;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
@ -54,7 +51,6 @@ public class SendsController : Controller
|
|||||||
_sendFileStorageService = sendFileStorageService;
|
_sendFileStorageService = sendFileStorageService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_currentContext = currentContext;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Anonymous endpoints
|
#region Anonymous endpoints
|
||||||
|
@ -62,9 +62,9 @@ public static class ApiHelpers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventTypeHandlers.ContainsKey(eventGridEvent.EventType))
|
if (eventTypeHandlers.TryGetValue(eventGridEvent.EventType, out var eventTypeHandler))
|
||||||
{
|
{
|
||||||
await eventTypeHandlers[eventGridEvent.EventType](eventGridEvent);
|
await eventTypeHandler(eventGridEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1064,7 +1064,7 @@ public class CiphersController : Controller
|
|||||||
|
|
||||||
[HttpPut("share")]
|
[HttpPut("share")]
|
||||||
[HttpPost("share")]
|
[HttpPost("share")]
|
||||||
public async Task PutShareMany([FromBody] CipherBulkShareRequestModel model)
|
public async Task<CipherMiniResponseModel[]> PutShareMany([FromBody] CipherBulkShareRequestModel model)
|
||||||
{
|
{
|
||||||
var organizationId = new Guid(model.Ciphers.First().OrganizationId);
|
var organizationId = new Guid(model.Ciphers.First().OrganizationId);
|
||||||
if (!await _currentContext.OrganizationUser(organizationId))
|
if (!await _currentContext.OrganizationUser(organizationId))
|
||||||
@ -1073,38 +1073,40 @@ public class CiphersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
|
|
||||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: false);
|
var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: false);
|
||||||
var ciphersDict = ciphers.ToDictionary(c => c.Id);
|
var ciphersDict = ciphers.ToDictionary(c => c.Id);
|
||||||
|
|
||||||
// Validate the model was encrypted for the posting user
|
// Validate the model was encrypted for the posting user
|
||||||
foreach (var cipher in model.Ciphers)
|
foreach (var cipher in model.Ciphers)
|
||||||
{
|
{
|
||||||
if (cipher.EncryptedFor != null)
|
if (cipher.EncryptedFor.HasValue && cipher.EncryptedFor.Value != userId)
|
||||||
{
|
{
|
||||||
if (cipher.EncryptedFor != userId)
|
throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
|
||||||
{
|
|
||||||
throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var shareCiphers = new List<(Cipher, DateTime?)>();
|
var shareCiphers = new List<(Cipher, DateTime?)>();
|
||||||
foreach (var cipher in model.Ciphers)
|
foreach (var cipher in model.Ciphers)
|
||||||
{
|
{
|
||||||
if (!ciphersDict.ContainsKey(cipher.Id.Value))
|
if (!ciphersDict.TryGetValue(cipher.Id.Value, out var existingCipher))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Trying to move ciphers that you do not own.");
|
throw new BadRequestException("Trying to share ciphers that you do not own.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var existingCipher = ciphersDict[cipher.Id.Value];
|
|
||||||
|
|
||||||
ValidateClientVersionForFido2CredentialSupport(existingCipher);
|
ValidateClientVersionForFido2CredentialSupport(existingCipher);
|
||||||
|
|
||||||
shareCiphers.Add((cipher.ToCipher(existingCipher), cipher.LastKnownRevisionDate));
|
shareCiphers.Add(((Cipher)existingCipher, cipher.LastKnownRevisionDate));
|
||||||
}
|
}
|
||||||
|
|
||||||
await _cipherService.ShareManyAsync(shareCiphers, organizationId,
|
var updated = await _cipherService.ShareManyAsync(
|
||||||
model.CollectionIds.Select(c => new Guid(c)), userId);
|
shareCiphers,
|
||||||
|
organizationId,
|
||||||
|
model.CollectionIds.Select(Guid.Parse),
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
|
return updated.Select(c => new CipherMiniResponseModel(c, _globalSettings, false)).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("purge")]
|
[HttpPost("purge")]
|
||||||
@ -1186,14 +1188,14 @@ public class CiphersController : Controller
|
|||||||
var cipher = await GetByIdAsync(id, userId);
|
var cipher = await GetByIdAsync(id, userId);
|
||||||
var attachments = cipher?.GetAttachments();
|
var attachments = cipher?.GetAttachments();
|
||||||
|
|
||||||
if (attachments == null || !attachments.ContainsKey(attachmentId) || attachments[attachmentId].Validated)
|
if (attachments == null || !attachments.TryGetValue(attachmentId, out var attachment) || attachment.Validated)
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AttachmentUploadDataResponseModel
|
return new AttachmentUploadDataResponseModel
|
||||||
{
|
{
|
||||||
Url = await _attachmentStorageService.GetAttachmentUploadUrlAsync(cipher, attachments[attachmentId]),
|
Url = await _attachmentStorageService.GetAttachmentUploadUrlAsync(cipher, attachment),
|
||||||
FileUploadType = _attachmentStorageService.FileUploadType,
|
FileUploadType = _attachmentStorageService.FileUploadType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1212,11 +1214,10 @@ public class CiphersController : Controller
|
|||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
var cipher = await GetByIdAsync(id, userId);
|
var cipher = await GetByIdAsync(id, userId);
|
||||||
var attachments = cipher?.GetAttachments();
|
var attachments = cipher?.GetAttachments();
|
||||||
if (attachments == null || !attachments.ContainsKey(attachmentId))
|
if (attachments == null || !attachments.TryGetValue(attachmentId, out var attachmentData))
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
var attachmentData = attachments[attachmentId];
|
|
||||||
|
|
||||||
await Request.GetFileAsync(async (stream) =>
|
await Request.GetFileAsync(async (stream) =>
|
||||||
{
|
{
|
||||||
@ -1366,7 +1367,7 @@ public class CiphersController : Controller
|
|||||||
var cipher = await _cipherRepository.GetByIdAsync(new Guid(cipherId));
|
var cipher = await _cipherRepository.GetByIdAsync(new Guid(cipherId));
|
||||||
var attachments = cipher?.GetAttachments() ?? new Dictionary<string, CipherAttachment.MetaData>();
|
var attachments = cipher?.GetAttachments() ?? new Dictionary<string, CipherAttachment.MetaData>();
|
||||||
|
|
||||||
if (cipher == null || !attachments.ContainsKey(attachmentId) || attachments[attachmentId].Validated)
|
if (cipher == null || !attachments.TryGetValue(attachmentId, out var attachment) || attachment.Validated)
|
||||||
{
|
{
|
||||||
if (_attachmentStorageService is AzureSendFileStorageService azureFileStorageService)
|
if (_attachmentStorageService is AzureSendFileStorageService azureFileStorageService)
|
||||||
{
|
{
|
||||||
@ -1376,7 +1377,7 @@ public class CiphersController : Controller
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _cipherService.ValidateCipherAttachmentFile(cipher, attachments[attachmentId]);
|
await _cipherService.ValidateCipherAttachmentFile(cipher, attachment);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
@ -113,18 +113,25 @@ public class CipherRequestModel
|
|||||||
|
|
||||||
if (hasAttachments2)
|
if (hasAttachments2)
|
||||||
{
|
{
|
||||||
foreach (var attachment in attachments.Where(a => Attachments2.ContainsKey(a.Key)))
|
foreach (var attachment in attachments)
|
||||||
{
|
{
|
||||||
var attachment2 = Attachments2[attachment.Key];
|
if (!Attachments2.TryGetValue(attachment.Key, out var attachment2))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
attachment.Value.FileName = attachment2.FileName;
|
attachment.Value.FileName = attachment2.FileName;
|
||||||
attachment.Value.Key = attachment2.Key;
|
attachment.Value.Key = attachment2.Key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (hasAttachments)
|
else if (hasAttachments)
|
||||||
{
|
{
|
||||||
foreach (var attachment in attachments.Where(a => Attachments.ContainsKey(a.Key)))
|
foreach (var attachment in attachments)
|
||||||
{
|
{
|
||||||
attachment.Value.FileName = Attachments[attachment.Key];
|
if (!Attachments.TryGetValue(attachment.Key, out var attachmentForKey))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
attachment.Value.FileName = attachmentForKey;
|
||||||
attachment.Value.Key = null;
|
attachment.Value.Key = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,13 +129,13 @@ public class CipherDetailsResponseModel : CipherResponseModel
|
|||||||
IDictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphers, string obj = "cipherDetails")
|
IDictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphers, string obj = "cipherDetails")
|
||||||
: base(cipher, user, organizationAbilities, globalSettings, obj)
|
: base(cipher, user, organizationAbilities, globalSettings, obj)
|
||||||
{
|
{
|
||||||
if (collectionCiphers?.ContainsKey(cipher.Id) ?? false)
|
if (collectionCiphers?.TryGetValue(cipher.Id, out var collectionCipher) ?? false)
|
||||||
{
|
{
|
||||||
CollectionIds = collectionCiphers[cipher.Id].Select(c => c.CollectionId);
|
CollectionIds = collectionCipher.Select(c => c.CollectionId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
CollectionIds = new Guid[] { };
|
CollectionIds = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ public class CipherDetailsResponseModel : CipherResponseModel
|
|||||||
IEnumerable<CollectionCipher> collectionCiphers, string obj = "cipherDetails")
|
IEnumerable<CollectionCipher> collectionCiphers, string obj = "cipherDetails")
|
||||||
: base(cipher, user, organizationAbilities, globalSettings, obj)
|
: base(cipher, user, organizationAbilities, globalSettings, obj)
|
||||||
{
|
{
|
||||||
CollectionIds = collectionCiphers?.Select(c => c.CollectionId) ?? new List<Guid>();
|
CollectionIds = collectionCiphers?.Select(c => c.CollectionId) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public CipherDetailsResponseModel(
|
public CipherDetailsResponseModel(
|
||||||
@ -158,7 +158,7 @@ public class CipherDetailsResponseModel : CipherResponseModel
|
|||||||
string obj = "cipherDetails")
|
string obj = "cipherDetails")
|
||||||
: base(cipher, user, organizationAbilities, globalSettings, obj)
|
: base(cipher, user, organizationAbilities, globalSettings, obj)
|
||||||
{
|
{
|
||||||
CollectionIds = cipher.CollectionIds ?? new List<Guid>();
|
CollectionIds = cipher.CollectionIds ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<Guid> CollectionIds { get; set; }
|
public IEnumerable<Guid> CollectionIds { get; set; }
|
||||||
@ -170,13 +170,13 @@ public class CipherMiniDetailsResponseModel : CipherMiniResponseModel
|
|||||||
IDictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphers, bool orgUseTotp, string obj = "cipherMiniDetails")
|
IDictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphers, bool orgUseTotp, string obj = "cipherMiniDetails")
|
||||||
: base(cipher, globalSettings, orgUseTotp, obj)
|
: base(cipher, globalSettings, orgUseTotp, obj)
|
||||||
{
|
{
|
||||||
if (collectionCiphers?.ContainsKey(cipher.Id) ?? false)
|
if (collectionCiphers?.TryGetValue(cipher.Id, out var collectionCipher) ?? false)
|
||||||
{
|
{
|
||||||
CollectionIds = collectionCiphers[cipher.Id].Select(c => c.CollectionId);
|
CollectionIds = collectionCipher.Select(c => c.CollectionId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
CollectionIds = new Guid[] { };
|
CollectionIds = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,7 +184,7 @@ public class CipherMiniDetailsResponseModel : CipherMiniResponseModel
|
|||||||
GlobalSettings globalSettings, bool orgUseTotp, string obj = "cipherMiniDetails")
|
GlobalSettings globalSettings, bool orgUseTotp, string obj = "cipherMiniDetails")
|
||||||
: base(cipher, globalSettings, orgUseTotp, obj)
|
: base(cipher, globalSettings, orgUseTotp, obj)
|
||||||
{
|
{
|
||||||
CollectionIds = cipher.CollectionIds ?? new List<Guid>();
|
CollectionIds = cipher.CollectionIds ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public CipherMiniDetailsResponseModel(CipherOrganizationDetailsWithCollections cipher,
|
public CipherMiniDetailsResponseModel(CipherOrganizationDetailsWithCollections cipher,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
|
|
||||||
@ -19,31 +19,36 @@ then
|
|||||||
LGID=65534
|
LGID=65534
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create user and group
|
if [ "$(id -u)" = "0" ]
|
||||||
|
then
|
||||||
|
# Create user and group
|
||||||
|
|
||||||
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
|
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
|
||||||
groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1
|
groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1
|
||||||
useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||
|
useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||
|
||||||
usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1
|
usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1
|
||||||
mkhomedir_helper $USERNAME
|
mkhomedir_helper $USERNAME
|
||||||
|
|
||||||
# The rest...
|
# The rest...
|
||||||
|
|
||||||
chown -R $USERNAME:$GROUPNAME /app
|
chown -R $USERNAME:$GROUPNAME /app
|
||||||
mkdir -p /etc/bitwarden/core
|
mkdir -p /etc/bitwarden/core
|
||||||
mkdir -p /etc/bitwarden/logs
|
mkdir -p /etc/bitwarden/logs
|
||||||
mkdir -p /etc/bitwarden/ca-certificates
|
mkdir -p /etc/bitwarden/ca-certificates
|
||||||
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
|
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
|
||||||
|
|
||||||
if [[ $globalSettings__selfHosted == "true" ]]; then
|
if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then
|
||||||
cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \
|
chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos
|
||||||
&& update-ca-certificates
|
fi
|
||||||
|
|
||||||
|
gosu_cmd="gosu $USERNAME:$GROUPNAME"
|
||||||
|
else
|
||||||
|
gosu_cmd=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then
|
if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then
|
||||||
chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos
|
cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf
|
||||||
cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf
|
$gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab
|
||||||
gosu $USERNAME:$GROUPNAME kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec gosu $USERNAME:$GROUPNAME dotnet /app/Api.dll
|
exec $gosu_cmd /app/Api
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
*
|
|
||||||
!obj/build-output/publish/*
|
|
||||||
!obj/Docker/empty/
|
|
||||||
!entrypoint.sh
|
|
@ -28,8 +28,8 @@ public class AppleController : Controller
|
|||||||
return new BadRequestResult();
|
return new BadRequestResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
var key = HttpContext.Request.Query.ContainsKey("key") ?
|
var key = HttpContext.Request.Query.TryGetValue("key", out var keyValue) ?
|
||||||
HttpContext.Request.Query["key"].ToString() : null;
|
keyValue.ToString() : null;
|
||||||
if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.AppleWebhookKey))
|
if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.AppleWebhookKey))
|
||||||
{
|
{
|
||||||
return new BadRequestResult();
|
return new BadRequestResult();
|
||||||
|
@ -51,8 +51,8 @@ public class PayPalController : Controller
|
|||||||
[HttpPost("ipn")]
|
[HttpPost("ipn")]
|
||||||
public async Task<IActionResult> PostIpn()
|
public async Task<IActionResult> PostIpn()
|
||||||
{
|
{
|
||||||
var key = HttpContext.Request.Query.ContainsKey("key")
|
var key = HttpContext.Request.Query.TryGetValue("key", out var keyValue)
|
||||||
? HttpContext.Request.Query["key"].ToString()
|
? keyValue.ToString()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(key))
|
if (string.IsNullOrEmpty(key))
|
||||||
|
@ -1,6 +1,50 @@
|
|||||||
|
###############################################
|
||||||
|
# Build stage #
|
||||||
|
###############################################
|
||||||
|
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||||
|
|
||||||
|
# Docker buildx supplies the value for this arg
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
|
# Determine proper runtime value for .NET
|
||||||
|
# We put the value in a file to be read by later layers.
|
||||||
|
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||||
|
RID=linux-x64 ; \
|
||||||
|
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||||
|
RID=linux-arm64 ; \
|
||||||
|
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
|
||||||
|
RID=linux-arm ; \
|
||||||
|
fi \
|
||||||
|
&& echo "RID=$RID" > /tmp/rid.txt
|
||||||
|
|
||||||
|
# Copy required project files
|
||||||
|
WORKDIR /source
|
||||||
|
COPY . ./
|
||||||
|
|
||||||
|
# Restore project dependencies and tools
|
||||||
|
WORKDIR /source/src/Billing
|
||||||
|
RUN . /tmp/rid.txt && dotnet restore -r $RID
|
||||||
|
|
||||||
|
# Build project
|
||||||
|
RUN . /tmp/rid.txt && dotnet publish \
|
||||||
|
-c release \
|
||||||
|
--no-restore \
|
||||||
|
--self-contained \
|
||||||
|
/p:PublishSingleFile=true \
|
||||||
|
-r $RID \
|
||||||
|
-o out
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# App stage #
|
||||||
|
###############################################
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||||
|
|
||||||
|
ARG TARGETPLATFORM
|
||||||
LABEL com.bitwarden.product="bitwarden"
|
LABEL com.bitwarden.product="bitwarden"
|
||||||
|
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
ENV ASPNETCORE_URLS=http://+:5000
|
||||||
|
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
@ -8,14 +52,11 @@ RUN apt-get update \
|
|||||||
curl \
|
curl \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
ENV ASPNETCORE_URLS http://+:5000
|
# Copy app from the build stage
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 5000
|
COPY --from=build /source/src/Billing/out /app
|
||||||
COPY entrypoint.sh /
|
COPY ./src/Billing/entrypoint.sh /entrypoint.sh
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
COPY obj/build-output/publish .
|
|
||||||
|
|
||||||
HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1
|
HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
@ -20,8 +20,8 @@ public class Program
|
|||||||
return e.Level >= globalSettings.MinLogLevel.BillingSettings.Jobs;
|
return e.Level >= globalSettings.MinLogLevel.BillingSettings.Jobs;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.Properties.ContainsKey("RequestPath") &&
|
if (e.Properties.TryGetValue("RequestPath", out var requestPath) &&
|
||||||
!string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) &&
|
!string.IsNullOrWhiteSpace(requestPath?.ToString()) &&
|
||||||
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
|
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
using Bit.Core.Context;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Repositories;
|
|
||||||
using Bit.Core.Tools.Enums;
|
|
||||||
using Bit.Core.Tools.Models.Business;
|
|
||||||
using Bit.Core.Tools.Services;
|
|
||||||
using Event = Stripe.Event;
|
using Event = Stripe.Event;
|
||||||
|
|
||||||
namespace Bit.Billing.Services.Implementations;
|
namespace Bit.Billing.Services.Implementations;
|
||||||
@ -10,23 +6,17 @@ namespace Bit.Billing.Services.Implementations;
|
|||||||
public class CustomerUpdatedHandler : ICustomerUpdatedHandler
|
public class CustomerUpdatedHandler : ICustomerUpdatedHandler
|
||||||
{
|
{
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
|
||||||
private readonly ICurrentContext _currentContext;
|
|
||||||
private readonly IStripeEventService _stripeEventService;
|
private readonly IStripeEventService _stripeEventService;
|
||||||
private readonly IStripeEventUtilityService _stripeEventUtilityService;
|
private readonly IStripeEventUtilityService _stripeEventUtilityService;
|
||||||
private readonly ILogger<CustomerUpdatedHandler> _logger;
|
private readonly ILogger<CustomerUpdatedHandler> _logger;
|
||||||
|
|
||||||
public CustomerUpdatedHandler(
|
public CustomerUpdatedHandler(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IReferenceEventService referenceEventService,
|
|
||||||
ICurrentContext currentContext,
|
|
||||||
IStripeEventService stripeEventService,
|
IStripeEventService stripeEventService,
|
||||||
IStripeEventUtilityService stripeEventUtilityService,
|
IStripeEventUtilityService stripeEventUtilityService,
|
||||||
ILogger<CustomerUpdatedHandler> logger)
|
ILogger<CustomerUpdatedHandler> logger)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository ?? throw new ArgumentNullException(nameof(organizationRepository));
|
_organizationRepository = organizationRepository ?? throw new ArgumentNullException(nameof(organizationRepository));
|
||||||
_referenceEventService = referenceEventService;
|
|
||||||
_currentContext = currentContext;
|
|
||||||
_stripeEventService = stripeEventService;
|
_stripeEventService = stripeEventService;
|
||||||
_stripeEventUtilityService = stripeEventUtilityService;
|
_stripeEventUtilityService = stripeEventUtilityService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@ -95,20 +85,5 @@ public class CustomerUpdatedHandler : ICustomerUpdatedHandler
|
|||||||
|
|
||||||
organization.BillingEmail = customer.Email;
|
organization.BillingEmail = customer.Email;
|
||||||
await _organizationRepository.ReplaceAsync(organization);
|
await _organizationRepository.ReplaceAsync(organization);
|
||||||
|
|
||||||
if (_referenceEventService == null)
|
|
||||||
{
|
|
||||||
_logger.LogError("ReferenceEventService was not initialized in CustomerUpdatedHandler");
|
|
||||||
throw new InvalidOperationException($"{nameof(_referenceEventService)} is not initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_currentContext == null)
|
|
||||||
{
|
|
||||||
_logger.LogError("CurrentContext was not initialized in CustomerUpdatedHandler");
|
|
||||||
throw new InvalidOperationException($"{nameof(_currentContext)} is not initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
await _referenceEventService.RaiseEventAsync(
|
|
||||||
new ReferenceEvent(ReferenceEventType.OrganizationEditedInStripe, organization, _currentContext));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,9 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Context;
|
|
||||||
using Bit.Core.Platform.Push;
|
using Bit.Core.Platform.Push;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Enums;
|
|
||||||
using Bit.Core.Tools.Models.Business;
|
|
||||||
using Bit.Core.Tools.Services;
|
|
||||||
using Event = Stripe.Event;
|
using Event = Stripe.Event;
|
||||||
|
|
||||||
namespace Bit.Billing.Services.Implementations;
|
namespace Bit.Billing.Services.Implementations;
|
||||||
@ -22,9 +18,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
|
|||||||
private readonly IStripeFacade _stripeFacade;
|
private readonly IStripeFacade _stripeFacade;
|
||||||
private readonly IProviderRepository _providerRepository;
|
private readonly IProviderRepository _providerRepository;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
|
||||||
private readonly ICurrentContext _currentContext;
|
|
||||||
private readonly IUserRepository _userRepository;
|
|
||||||
private readonly IStripeEventUtilityService _stripeEventUtilityService;
|
private readonly IStripeEventUtilityService _stripeEventUtilityService;
|
||||||
private readonly IPushNotificationService _pushNotificationService;
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
private readonly IOrganizationEnableCommand _organizationEnableCommand;
|
private readonly IOrganizationEnableCommand _organizationEnableCommand;
|
||||||
@ -36,9 +29,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
|
|||||||
IStripeFacade stripeFacade,
|
IStripeFacade stripeFacade,
|
||||||
IProviderRepository providerRepository,
|
IProviderRepository providerRepository,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IReferenceEventService referenceEventService,
|
|
||||||
ICurrentContext currentContext,
|
|
||||||
IUserRepository userRepository,
|
|
||||||
IStripeEventUtilityService stripeEventUtilityService,
|
IStripeEventUtilityService stripeEventUtilityService,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IPushNotificationService pushNotificationService,
|
IPushNotificationService pushNotificationService,
|
||||||
@ -50,9 +40,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
|
|||||||
_stripeFacade = stripeFacade;
|
_stripeFacade = stripeFacade;
|
||||||
_providerRepository = providerRepository;
|
_providerRepository = providerRepository;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_referenceEventService = referenceEventService;
|
|
||||||
_currentContext = currentContext;
|
|
||||||
_userRepository = userRepository;
|
|
||||||
_stripeEventUtilityService = stripeEventUtilityService;
|
_stripeEventUtilityService = stripeEventUtilityService;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_pushNotificationService = pushNotificationService;
|
_pushNotificationService = pushNotificationService;
|
||||||
@ -116,27 +103,7 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
|
|||||||
_logger.LogError("invoice.payment_succeeded webhook ({EventID}) for Provider ({ProviderID}) indicates missing subscription line items",
|
_logger.LogError("invoice.payment_succeeded webhook ({EventID}) for Provider ({ProviderID}) indicates missing subscription line items",
|
||||||
parsedEvent.Id,
|
parsedEvent.Id,
|
||||||
provider.Id);
|
provider.Id);
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await _referenceEventService.RaiseEventAsync(new ReferenceEvent
|
|
||||||
{
|
|
||||||
Type = ReferenceEventType.Rebilled,
|
|
||||||
Source = ReferenceEventSource.Provider,
|
|
||||||
Id = provider.Id,
|
|
||||||
PlanType = PlanType.TeamsMonthly,
|
|
||||||
Seats = (int)teamsMonthlyLineItem.Quantity
|
|
||||||
});
|
|
||||||
|
|
||||||
await _referenceEventService.RaiseEventAsync(new ReferenceEvent
|
|
||||||
{
|
|
||||||
Type = ReferenceEventType.Rebilled,
|
|
||||||
Source = ReferenceEventSource.Provider,
|
|
||||||
Id = provider.Id,
|
|
||||||
PlanType = PlanType.EnterpriseMonthly,
|
|
||||||
Seats = (int)enterpriseMonthlyLineItem.Quantity
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else if (organizationId.HasValue)
|
else if (organizationId.HasValue)
|
||||||
{
|
{
|
||||||
@ -156,15 +123,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
|
|||||||
|
|
||||||
await _organizationEnableCommand.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
|
await _organizationEnableCommand.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
|
||||||
await _pushNotificationService.PushSyncOrganizationStatusAsync(organization);
|
await _pushNotificationService.PushSyncOrganizationStatusAsync(organization);
|
||||||
|
|
||||||
await _referenceEventService.RaiseEventAsync(
|
|
||||||
new ReferenceEvent(ReferenceEventType.Rebilled, organization, _currentContext)
|
|
||||||
{
|
|
||||||
PlanName = organization?.Plan,
|
|
||||||
PlanType = organization?.PlanType,
|
|
||||||
Seats = organization?.Seats,
|
|
||||||
Storage = organization?.MaxStorageGb,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else if (userId.HasValue)
|
else if (userId.HasValue)
|
||||||
{
|
{
|
||||||
@ -174,14 +132,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _userService.EnablePremiumAsync(userId.Value, subscription.CurrentPeriodEnd);
|
await _userService.EnablePremiumAsync(userId.Value, subscription.CurrentPeriodEnd);
|
||||||
|
|
||||||
var user = await _userRepository.GetByIdAsync(userId.Value);
|
|
||||||
await _referenceEventService.RaiseEventAsync(
|
|
||||||
new ReferenceEvent(ReferenceEventType.Rebilled, user, _currentContext)
|
|
||||||
{
|
|
||||||
PlanName = IStripeEventUtilityService.PremiumPlanId,
|
|
||||||
Storage = user?.MaxStorageGb,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
|
|
||||||
@ -19,25 +19,27 @@ then
|
|||||||
LGID=65534
|
LGID=65534
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create user and group
|
if [ "$(id -u)" = "0" ]
|
||||||
|
then
|
||||||
|
# Create user and group
|
||||||
|
|
||||||
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
|
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
|
||||||
groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1
|
groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1
|
||||||
useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||
|
useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||
|
||||||
usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1
|
usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1
|
||||||
mkhomedir_helper $USERNAME
|
mkhomedir_helper $USERNAME
|
||||||
|
|
||||||
# The rest...
|
# The rest...
|
||||||
|
|
||||||
chown -R $USERNAME:$GROUPNAME /app
|
chown -R $USERNAME:$GROUPNAME /app
|
||||||
mkdir -p /etc/bitwarden/core
|
mkdir -p /etc/bitwarden/core
|
||||||
mkdir -p /etc/bitwarden/logs
|
mkdir -p /etc/bitwarden/logs
|
||||||
mkdir -p /etc/bitwarden/ca-certificates
|
mkdir -p /etc/bitwarden/ca-certificates
|
||||||
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
|
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
|
||||||
|
|
||||||
if [[ $globalSettings__selfHosted == "true" ]]; then
|
gosu_cmd="gosu $USERNAME:$GROUPNAME"
|
||||||
cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \
|
else
|
||||||
&& update-ca-certificates
|
gosu_cmd=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec gosu $USERNAME:$GROUPNAME dotnet /app/Billing.dll
|
exec $gosu_cmd /app/Billing
|
||||||
|
@ -8,14 +8,13 @@ using Bit.Core.Entities;
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Entities;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Entities;
|
namespace Bit.Core.AdminConsole.Entities;
|
||||||
|
|
||||||
public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable, IReferenceable
|
public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable
|
||||||
{
|
{
|
||||||
private Dictionary<TwoFactorProviderType, TwoFactorProvider>? _twoFactorProviders;
|
private Dictionary<TwoFactorProviderType, TwoFactorProvider>? _twoFactorProviders;
|
||||||
|
|
||||||
@ -258,12 +257,12 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
|||||||
public bool TwoFactorProviderIsEnabled(TwoFactorProviderType provider)
|
public bool TwoFactorProviderIsEnabled(TwoFactorProviderType provider)
|
||||||
{
|
{
|
||||||
var providers = GetTwoFactorProviders();
|
var providers = GetTwoFactorProviders();
|
||||||
if (providers == null || !providers.ContainsKey(provider))
|
if (providers == null || !providers.TryGetValue(provider, out var twoFactorProvider))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return providers[provider].Enabled && Use2fa;
|
return twoFactorProvider.Enabled && Use2fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TwoFactorIsEnabled()
|
public bool TwoFactorIsEnabled()
|
||||||
@ -280,12 +279,7 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
|||||||
public TwoFactorProvider? GetTwoFactorProvider(TwoFactorProviderType provider)
|
public TwoFactorProvider? GetTwoFactorProvider(TwoFactorProviderType provider)
|
||||||
{
|
{
|
||||||
var providers = GetTwoFactorProviders();
|
var providers = GetTwoFactorProviders();
|
||||||
if (providers == null || !providers.ContainsKey(provider))
|
return providers?.GetValueOrDefault(provider);
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return providers[provider];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateFromLicense(OrganizationLicense license, IFeatureService featureService)
|
public void UpdateFromLicense(OrganizationLicense license, IFeatureService featureService)
|
||||||
|
@ -7,3 +7,19 @@ public enum IntegrationType : int
|
|||||||
Slack = 3,
|
Slack = 3,
|
||||||
Webhook = 4,
|
Webhook = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class IntegrationTypeExtensions
|
||||||
|
{
|
||||||
|
public static string ToRoutingKey(this IntegrationType type)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case IntegrationType.Slack:
|
||||||
|
return "slack";
|
||||||
|
case IntegrationType.Webhook:
|
||||||
|
return "webhook";
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(type), $"Unsupported integration type: {type}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||||
|
|
||||||
|
public interface IIntegrationMessage
|
||||||
|
{
|
||||||
|
IntegrationType IntegrationType { get; }
|
||||||
|
int RetryCount { get; set; }
|
||||||
|
DateTime? DelayUntilDate { get; set; }
|
||||||
|
void ApplyRetry(DateTime? handlerDelayUntilDate);
|
||||||
|
string ToJson();
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||||
|
|
||||||
|
public class IntegrationHandlerResult
|
||||||
|
{
|
||||||
|
public IntegrationHandlerResult(bool success, IIntegrationMessage message)
|
||||||
|
{
|
||||||
|
Success = success;
|
||||||
|
Message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Success { get; set; } = false;
|
||||||
|
public bool Retryable { get; set; } = false;
|
||||||
|
public IIntegrationMessage Message { get; set; }
|
||||||
|
public DateTime? DelayUntilDate { get; set; }
|
||||||
|
public string FailureReason { get; set; } = string.Empty;
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||||
|
|
||||||
|
public class IntegrationMessage<T> : IIntegrationMessage
|
||||||
|
{
|
||||||
|
public IntegrationType IntegrationType { get; set; }
|
||||||
|
public T Configuration { get; set; }
|
||||||
|
public string RenderedTemplate { get; set; }
|
||||||
|
public int RetryCount { get; set; } = 0;
|
||||||
|
public DateTime? DelayUntilDate { get; set; }
|
||||||
|
|
||||||
|
public void ApplyRetry(DateTime? handlerDelayUntilDate)
|
||||||
|
{
|
||||||
|
RetryCount++;
|
||||||
|
|
||||||
|
var baseTime = handlerDelayUntilDate ?? DateTime.UtcNow;
|
||||||
|
var backoffSeconds = Math.Pow(2, RetryCount);
|
||||||
|
var jitterSeconds = Random.Shared.Next(0, 3);
|
||||||
|
|
||||||
|
DelayUntilDate = baseTime.AddSeconds(backoffSeconds + jitterSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToJson()
|
||||||
|
{
|
||||||
|
return JsonSerializer.Serialize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IntegrationMessage<T> FromJson(string json)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<IntegrationMessage<T>>(json);
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,11 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
#nullable enable
|
||||||
|
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
|
||||||
#nullable enable
|
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Data.Integrations;
|
|
||||||
|
|
||||||
public class IntegrationTemplateContext(EventMessage eventMessage)
|
public class IntegrationTemplateContext(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
namespace Bit.Core.Models.Data.Integrations;
|
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||||
|
|
||||||
public record SlackIntegration(string token);
|
public record SlackIntegration(string token);
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
namespace Bit.Core.Models.Data.Integrations;
|
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||||
|
|
||||||
public record SlackIntegrationConfiguration(string channelId);
|
public record SlackIntegrationConfiguration(string channelId);
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
namespace Bit.Core.Models.Data.Integrations;
|
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||||
|
|
||||||
public record SlackIntegrationConfigurationDetails(string channelId, string token);
|
public record SlackIntegrationConfigurationDetails(string channelId, string token);
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
namespace Bit.Core.Models.Data.Integrations;
|
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||||
|
|
||||||
public record WebhookIntegrationConfiguration(string url);
|
public record WebhookIntegrationConfiguration(string url);
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||||
|
|
||||||
|
public record WebhookIntegrationConfigurationDetails(string url);
|
@ -1,3 +0,0 @@
|
|||||||
namespace Bit.Core.Models.Data.Integrations;
|
|
||||||
|
|
||||||
public record WebhookIntegrationConfigurationDetils(string url);
|
|
@ -1,15 +1,11 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Context;
|
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Enums;
|
|
||||||
using Bit.Core.Tools.Models.Business;
|
|
||||||
using Bit.Core.Tools.Services;
|
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Groups;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Groups;
|
||||||
|
|
||||||
@ -18,21 +14,16 @@ public class CreateGroupCommand : ICreateGroupCommand
|
|||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
private readonly IGroupRepository _groupRepository;
|
private readonly IGroupRepository _groupRepository;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
|
||||||
private readonly ICurrentContext _currentContext;
|
|
||||||
|
|
||||||
public CreateGroupCommand(
|
public CreateGroupCommand(
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IGroupRepository groupRepository,
|
IGroupRepository groupRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository
|
||||||
IReferenceEventService referenceEventService,
|
)
|
||||||
ICurrentContext currentContext)
|
|
||||||
{
|
{
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_groupRepository = groupRepository;
|
_groupRepository = groupRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_referenceEventService = referenceEventService;
|
|
||||||
_currentContext = currentContext;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateGroupAsync(Group group, Organization organization,
|
public async Task CreateGroupAsync(Group group, Organization organization,
|
||||||
@ -77,8 +68,6 @@ public class CreateGroupCommand : ICreateGroupCommand
|
|||||||
{
|
{
|
||||||
await _groupRepository.CreateAsync(group, collections);
|
await _groupRepository.CreateAsync(group, collections);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.GroupCreated, organization, _currentContext));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task GroupRepositoryUpdateUsersAsync(Group group, IEnumerable<Guid> userIds,
|
private async Task GroupRepositoryUpdateUsersAsync(Group group, IEnumerable<Guid> userIds,
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
@ -27,6 +29,8 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
|||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||||
|
|
||||||
public AcceptOrgUserCommand(
|
public AcceptOrgUserCommand(
|
||||||
IDataProtectionProvider dataProtectionProvider,
|
IDataProtectionProvider dataProtectionProvider,
|
||||||
@ -37,9 +41,10 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
|||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory)
|
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
|
||||||
|
IFeatureService featureService,
|
||||||
|
IPolicyRequirementQuery policyRequirementQuery)
|
||||||
{
|
{
|
||||||
|
|
||||||
// TODO: remove data protector when old token validation removed
|
// TODO: remove data protector when old token validation removed
|
||||||
_dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose);
|
_dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose);
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
@ -50,6 +55,8 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
|||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||||
|
_featureService = featureService;
|
||||||
|
_policyRequirementQuery = policyRequirementQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<OrganizationUser> AcceptOrgUserByEmailTokenAsync(Guid organizationUserId, User user, string emailToken,
|
public async Task<OrganizationUser> AcceptOrgUserByEmailTokenAsync(Guid organizationUserId, User user, string emailToken,
|
||||||
@ -196,15 +203,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enforce Two Factor Authentication Policy of organization user is trying to join
|
// Enforce Two Factor Authentication Policy of organization user is trying to join
|
||||||
if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
|
await ValidateTwoFactorAuthenticationPolicyAsync(user, orgUser.OrganizationId);
|
||||||
{
|
|
||||||
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
|
|
||||||
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
|
|
||||||
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
|
|
||||||
{
|
|
||||||
throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
orgUser.Status = OrganizationUserStatusType.Accepted;
|
orgUser.Status = OrganizationUserStatusType.Accepted;
|
||||||
orgUser.UserId = user.Id;
|
orgUser.UserId = user.Id;
|
||||||
@ -224,4 +223,33 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
|||||||
return orgUser;
|
return orgUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ValidateTwoFactorAuthenticationPolicyAsync(User user, Guid organizationId)
|
||||||
|
{
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
|
||||||
|
{
|
||||||
|
if (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
|
||||||
|
{
|
||||||
|
// If the user has two-step login enabled, we skip checking the 2FA policy
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var twoFactorPolicyRequirement = await _policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id);
|
||||||
|
if (twoFactorPolicyRequirement.IsTwoFactorRequiredForOrganization(organizationId))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
|
||||||
|
{
|
||||||
|
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
|
||||||
|
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
|
||||||
|
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == organizationId))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
@ -24,6 +26,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
|||||||
private readonly IPushRegistrationService _pushRegistrationService;
|
private readonly IPushRegistrationService _pushRegistrationService;
|
||||||
private readonly IPolicyService _policyService;
|
private readonly IPolicyService _policyService;
|
||||||
private readonly IDeviceRepository _deviceRepository;
|
private readonly IDeviceRepository _deviceRepository;
|
||||||
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
|
||||||
public ConfirmOrganizationUserCommand(
|
public ConfirmOrganizationUserCommand(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@ -35,7 +39,9 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
|||||||
IPushNotificationService pushNotificationService,
|
IPushNotificationService pushNotificationService,
|
||||||
IPushRegistrationService pushRegistrationService,
|
IPushRegistrationService pushRegistrationService,
|
||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
IDeviceRepository deviceRepository)
|
IDeviceRepository deviceRepository,
|
||||||
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
|
IFeatureService featureService)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
@ -47,6 +53,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
|||||||
_pushRegistrationService = pushRegistrationService;
|
_pushRegistrationService = pushRegistrationService;
|
||||||
_policyService = policyService;
|
_policyService = policyService;
|
||||||
_deviceRepository = deviceRepository;
|
_deviceRepository = deviceRepository;
|
||||||
|
_policyRequirementQuery = policyRequirementQuery;
|
||||||
|
_featureService = featureService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
||||||
@ -118,8 +126,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var twoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled;
|
var userTwoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled;
|
||||||
await CheckPoliciesAsync(organizationId, user, orgUsers, twoFactorEnabled);
|
await CheckPoliciesAsync(organizationId, user, orgUsers, userTwoFactorEnabled);
|
||||||
orgUser.Status = OrganizationUserStatusType.Confirmed;
|
orgUser.Status = OrganizationUserStatusType.Confirmed;
|
||||||
orgUser.Key = keys[orgUser.Id];
|
orgUser.Key = keys[orgUser.Id];
|
||||||
orgUser.Email = null;
|
orgUser.Email = null;
|
||||||
@ -142,15 +150,10 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task CheckPoliciesAsync(Guid organizationId, User user,
|
private async Task CheckPoliciesAsync(Guid organizationId, User user,
|
||||||
ICollection<OrganizationUser> userOrgs, bool twoFactorEnabled)
|
ICollection<OrganizationUser> userOrgs, bool userTwoFactorEnabled)
|
||||||
{
|
{
|
||||||
// Enforce Two Factor Authentication Policy for this organization
|
// Enforce Two Factor Authentication Policy for this organization
|
||||||
var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication))
|
await ValidateTwoFactorAuthenticationPolicyAsync(user, organizationId, userTwoFactorEnabled);
|
||||||
.Any(p => p.OrganizationId == organizationId);
|
|
||||||
if (orgRequiresTwoFactor && !twoFactorEnabled)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("User does not have two-step login enabled.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId);
|
var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId);
|
||||||
var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg);
|
var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg);
|
||||||
@ -168,6 +171,33 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ValidateTwoFactorAuthenticationPolicyAsync(User user, Guid organizationId, bool userTwoFactorEnabled)
|
||||||
|
{
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
|
||||||
|
{
|
||||||
|
if (userTwoFactorEnabled)
|
||||||
|
{
|
||||||
|
// If the user has two-step login enabled, we skip checking the 2FA policy
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var twoFactorPolicyRequirement = await _policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id);
|
||||||
|
if (twoFactorPolicyRequirement.IsTwoFactorRequiredForOrganization(organizationId))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("User does not have two-step login enabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication))
|
||||||
|
.Any(p => p.OrganizationId == organizationId);
|
||||||
|
if (orgRequiresTwoFactor && !userTwoFactorEnabled)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("User does not have two-step login enabled.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
|
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
|
||||||
{
|
{
|
||||||
var devices = await GetUserDeviceIdsAsync(userId);
|
var devices = await GetUserDeviceIdsAsync(userId);
|
||||||
|
@ -7,9 +7,6 @@ using Bit.Core.Exceptions;
|
|||||||
using Bit.Core.Platform.Push;
|
using Bit.Core.Platform.Push;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Enums;
|
|
||||||
using Bit.Core.Tools.Models.Business;
|
|
||||||
using Bit.Core.Tools.Services;
|
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
@ -24,7 +21,6 @@ public class DeleteClaimedOrganizationUserAccountCommand : IDeleteClaimedOrganiz
|
|||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
|
||||||
private readonly IPushNotificationService _pushService;
|
private readonly IPushNotificationService _pushService;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IProviderUserRepository _providerUserRepository;
|
private readonly IProviderUserRepository _providerUserRepository;
|
||||||
@ -36,7 +32,6 @@ public class DeleteClaimedOrganizationUserAccountCommand : IDeleteClaimedOrganiz
|
|||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
||||||
IReferenceEventService referenceEventService,
|
|
||||||
IPushNotificationService pushService,
|
IPushNotificationService pushService,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IProviderUserRepository providerUserRepository)
|
IProviderUserRepository providerUserRepository)
|
||||||
@ -48,7 +43,6 @@ public class DeleteClaimedOrganizationUserAccountCommand : IDeleteClaimedOrganiz
|
|||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
||||||
_referenceEventService = referenceEventService;
|
|
||||||
_pushService = pushService;
|
_pushService = pushService;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_providerUserRepository = providerUserRepository;
|
_providerUserRepository = providerUserRepository;
|
||||||
@ -195,8 +189,6 @@ public class DeleteClaimedOrganizationUserAccountCommand : IDeleteClaimedOrganiz
|
|||||||
await _userRepository.DeleteManyAsync(users);
|
await _userRepository.DeleteManyAsync(users);
|
||||||
foreach (var user in users)
|
foreach (var user in users)
|
||||||
{
|
{
|
||||||
await _referenceEventService.RaiseEventAsync(
|
|
||||||
new ReferenceEvent(ReferenceEventType.DeleteAccount, user, _currentContext));
|
|
||||||
await _pushService.PushLogOutAsync(user.Id);
|
await _pushService.PushLogOutAsync(user.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,15 +9,11 @@ using Bit.Core.AdminConsole.Repositories;
|
|||||||
using Bit.Core.AdminConsole.Utilities.Commands;
|
using Bit.Core.AdminConsole.Utilities.Commands;
|
||||||
using Bit.Core.AdminConsole.Utilities.Errors;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
using Bit.Core.AdminConsole.Utilities.Validation;
|
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||||
using Bit.Core.Context;
|
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Enums;
|
|
||||||
using Bit.Core.Tools.Models.Business;
|
|
||||||
using Bit.Core.Tools.Services;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using OrganizationUserInvite = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite;
|
using OrganizationUserInvite = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite;
|
||||||
|
|
||||||
@ -28,8 +24,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
|||||||
IInviteUsersValidator inviteUsersValidator,
|
IInviteUsersValidator inviteUsersValidator,
|
||||||
IPaymentService paymentService,
|
IPaymentService paymentService,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IReferenceEventService referenceEventService,
|
|
||||||
ICurrentContext currentContext,
|
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
ILogger<InviteOrganizationUsersCommand> logger,
|
ILogger<InviteOrganizationUsersCommand> logger,
|
||||||
@ -121,8 +115,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
|||||||
await SendAdditionalEmailsAsync(validatedRequest, organization);
|
await SendAdditionalEmailsAsync(validatedRequest, organization);
|
||||||
|
|
||||||
await SendInvitesAsync(organizationUserToInviteEntities, organization);
|
await SendInvitesAsync(organizationUserToInviteEntities, organization);
|
||||||
|
|
||||||
await PublishReferenceEventAsync(validatedRequest, organization);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -190,14 +182,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PublishReferenceEventAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult,
|
|
||||||
Organization organization) =>
|
|
||||||
await referenceEventService.RaiseEventAsync(
|
|
||||||
new ReferenceEvent(ReferenceEventType.InvitedUsers, organization, currentContext)
|
|
||||||
{
|
|
||||||
Users = validatedResult.Value.Invites.Length
|
|
||||||
});
|
|
||||||
|
|
||||||
private async Task SendInvitesAsync(IEnumerable<CreateOrganizationUser> users, Organization organization) =>
|
private async Task SendInvitesAsync(IEnumerable<CreateOrganizationUser> users, Organization organization) =>
|
||||||
await sendOrganizationInvitesCommand.SendInvitesAsync(
|
await sendOrganizationInvitesCommand.SendInvitesAsync(
|
||||||
new SendInvitesRequest(
|
new SendInvitesRequest(
|
||||||
@ -284,15 +268,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
|||||||
|
|
||||||
await organizationRepository.ReplaceAsync(organization); // could optimize this with only a property update
|
await organizationRepository.ReplaceAsync(organization); // could optimize this with only a property update
|
||||||
await applicationCacheService.UpsertOrganizationAbilityAsync(organization);
|
await applicationCacheService.UpsertOrganizationAbilityAsync(organization);
|
||||||
|
|
||||||
await referenceEventService.RaiseEventAsync(
|
|
||||||
new ReferenceEvent(ReferenceEventType.AdjustSeats, organization, currentContext)
|
|
||||||
{
|
|
||||||
PlanName = validatedResult.Value.InviteOrganization.Plan.Name,
|
|
||||||
PlanType = validatedResult.Value.InviteOrganization.Plan.Type,
|
|
||||||
Seats = validatedResult.Value.PasswordManagerSubscriptionUpdate.UpdatedSeatTotal,
|
|
||||||
PreviousSeats = validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
@ -22,7 +24,9 @@ public class RestoreOrganizationUserCommand(
|
|||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IOrganizationService organizationService) : IRestoreOrganizationUserCommand
|
IOrganizationService organizationService,
|
||||||
|
IFeatureService featureService,
|
||||||
|
IPolicyRequirementQuery policyRequirementQuery) : IRestoreOrganizationUserCommand
|
||||||
{
|
{
|
||||||
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId)
|
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId)
|
||||||
{
|
{
|
||||||
@ -270,12 +274,7 @@ public class RestoreOrganizationUserCommand(
|
|||||||
// Enforce 2FA Policy of organization user is trying to join
|
// Enforce 2FA Policy of organization user is trying to join
|
||||||
if (!userHasTwoFactorEnabled)
|
if (!userHasTwoFactorEnabled)
|
||||||
{
|
{
|
||||||
var invitedTwoFactorPolicies = await policyService.GetPoliciesApplicableToUserAsync(userId,
|
twoFactorCompliant = !await IsTwoFactorRequiredForOrganizationAsync(userId, orgUser.OrganizationId);
|
||||||
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked);
|
|
||||||
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
|
|
||||||
{
|
|
||||||
twoFactorCompliant = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = await userRepository.GetByIdAsync(userId);
|
var user = await userRepository.GetByIdAsync(userId);
|
||||||
@ -299,4 +298,17 @@ public class RestoreOrganizationUserCommand(
|
|||||||
throw new BadRequestException(user.Email + " is not compliant with the two-step login policy");
|
throw new BadRequestException(user.Email + " is not compliant with the two-step login policy");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> IsTwoFactorRequiredForOrganizationAsync(Guid userId, Guid organizationId)
|
||||||
|
{
|
||||||
|
if (featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
|
||||||
|
{
|
||||||
|
var requirement = await policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(userId);
|
||||||
|
return requirement.IsTwoFactorRequiredForOrganization(organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
var invitedTwoFactorPolicies = await policyService.GetPoliciesApplicableToUserAsync(userId,
|
||||||
|
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked);
|
||||||
|
return invitedTwoFactorPolicies.Any(p => p.OrganizationId == organizationId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ using Bit.Core.Billing.Enums;
|
|||||||
using Bit.Core.Billing.Models.Sales;
|
using Bit.Core.Billing.Models.Sales;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Context;
|
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -15,9 +14,6 @@ using Bit.Core.Models.StaticStore;
|
|||||||
using Bit.Core.Platform.Push;
|
using Bit.Core.Platform.Push;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Enums;
|
|
||||||
using Bit.Core.Tools.Models.Business;
|
|
||||||
using Bit.Core.Tools.Services;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||||
@ -36,8 +32,6 @@ public class CloudOrganizationSignUpCommand(
|
|||||||
IOrganizationBillingService organizationBillingService,
|
IOrganizationBillingService organizationBillingService,
|
||||||
IPaymentService paymentService,
|
IPaymentService paymentService,
|
||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
IReferenceEventService referenceEventService,
|
|
||||||
ICurrentContext currentContext,
|
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
@ -132,17 +126,6 @@ public class CloudOrganizationSignUpCommand(
|
|||||||
|
|
||||||
var ownerId = signup.IsFromProvider ? default : signup.Owner.Id;
|
var ownerId = signup.IsFromProvider ? default : signup.Owner.Id;
|
||||||
var returnValue = await SignUpAsync(organization, ownerId, signup.OwnerKey, signup.CollectionName, true);
|
var returnValue = await SignUpAsync(organization, ownerId, signup.OwnerKey, signup.CollectionName, true);
|
||||||
await referenceEventService.RaiseEventAsync(
|
|
||||||
new ReferenceEvent(ReferenceEventType.Signup, organization, currentContext)
|
|
||||||
{
|
|
||||||
PlanName = plan.Name,
|
|
||||||
PlanType = plan.Type,
|
|
||||||
Seats = returnValue.Item1.Seats,
|
|
||||||
SignupInitiationPath = signup.InitiationPath,
|
|
||||||
Storage = returnValue.Item1.MaxStorageGb,
|
|
||||||
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
|
|
||||||
});
|
|
||||||
|
|
||||||
return new SignUpOrganizationResponse(returnValue.organization, returnValue.organizationUser);
|
return new SignUpOrganizationResponse(returnValue.organization, returnValue.organizationUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,38 +2,28 @@
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Context;
|
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Enums;
|
|
||||||
using Bit.Core.Tools.Models.Business;
|
|
||||||
using Bit.Core.Tools.Services;
|
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||||
|
|
||||||
public class OrganizationDeleteCommand : IOrganizationDeleteCommand
|
public class OrganizationDeleteCommand : IOrganizationDeleteCommand
|
||||||
{
|
{
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
private readonly ICurrentContext _currentContext;
|
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IPaymentService _paymentService;
|
private readonly IPaymentService _paymentService;
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
|
||||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||||
|
|
||||||
public OrganizationDeleteCommand(
|
public OrganizationDeleteCommand(
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
ICurrentContext currentContext,
|
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IPaymentService paymentService,
|
IPaymentService paymentService,
|
||||||
IReferenceEventService referenceEventService,
|
|
||||||
ISsoConfigRepository ssoConfigRepository)
|
ISsoConfigRepository ssoConfigRepository)
|
||||||
{
|
{
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
_currentContext = currentContext;
|
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_paymentService = paymentService;
|
_paymentService = paymentService;
|
||||||
_referenceEventService = referenceEventService;
|
|
||||||
_ssoConfigRepository = ssoConfigRepository;
|
_ssoConfigRepository = ssoConfigRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,8 +38,6 @@ public class OrganizationDeleteCommand : IOrganizationDeleteCommand
|
|||||||
var eop = !organization.ExpirationDate.HasValue ||
|
var eop = !organization.ExpirationDate.HasValue ||
|
||||||
organization.ExpirationDate.Value >= DateTime.UtcNow;
|
organization.ExpirationDate.Value >= DateTime.UtcNow;
|
||||||
await _paymentService.CancelSubscriptionAsync(organization, eop);
|
await _paymentService.CancelSubscriptionAsync(organization, eop);
|
||||||
await _referenceEventService.RaiseEventAsync(
|
|
||||||
new ReferenceEvent(ReferenceEventType.DeleteAccount, organization, _currentContext));
|
|
||||||
}
|
}
|
||||||
catch (GatewayException) { }
|
catch (GatewayException) { }
|
||||||
}
|
}
|
||||||
|
@ -8,9 +8,6 @@ using Bit.Core.Models.Business;
|
|||||||
using Bit.Core.Models.StaticStore;
|
using Bit.Core.Models.StaticStore;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Enums;
|
|
||||||
using Bit.Core.Tools.Models.Business;
|
|
||||||
using Bit.Core.Tools.Services;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||||
@ -37,7 +34,6 @@ public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizati
|
|||||||
|
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
@ -46,7 +42,6 @@ public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizati
|
|||||||
public ProviderClientOrganizationSignUpCommand(
|
public ProviderClientOrganizationSignUpCommand(
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IPricingClient pricingClient,
|
IPricingClient pricingClient,
|
||||||
IReferenceEventService referenceEventService,
|
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
@ -54,7 +49,6 @@ public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizati
|
|||||||
{
|
{
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
_referenceEventService = referenceEventService;
|
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationApiKeyRepository = organizationApiKeyRepository;
|
_organizationApiKeyRepository = organizationApiKeyRepository;
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
@ -108,16 +102,6 @@ public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizati
|
|||||||
|
|
||||||
var returnValue = await SignUpAsync(organization, signup.CollectionName);
|
var returnValue = await SignUpAsync(organization, signup.CollectionName);
|
||||||
|
|
||||||
await _referenceEventService.RaiseEventAsync(
|
|
||||||
new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext)
|
|
||||||
{
|
|
||||||
PlanName = plan.Name,
|
|
||||||
PlanType = plan.Type,
|
|
||||||
Seats = returnValue.Organization.Seats,
|
|
||||||
SignupInitiationPath = signup.InitiationPath,
|
|
||||||
Storage = returnValue.Organization.MaxStorageGb,
|
|
||||||
});
|
|
||||||
|
|
||||||
return returnValue;
|
return returnValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,8 +104,8 @@ public class SavePolicyCommand : ISavePolicyCommand
|
|||||||
var dependentPolicyTypes = _policyValidators.Values
|
var dependentPolicyTypes = _policyValidators.Values
|
||||||
.Where(otherValidator => otherValidator.RequiredPolicies.Contains(policyUpdate.Type))
|
.Where(otherValidator => otherValidator.RequiredPolicies.Contains(policyUpdate.Type))
|
||||||
.Select(otherValidator => otherValidator.Type)
|
.Select(otherValidator => otherValidator.Type)
|
||||||
.Where(otherPolicyType => savedPoliciesDict.ContainsKey(otherPolicyType) &&
|
.Where(otherPolicyType => savedPoliciesDict.TryGetValue(otherPolicyType, out var savedPolicy) &&
|
||||||
savedPoliciesDict[otherPolicyType].Enabled)
|
savedPolicy.Enabled)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
switch (dependentPolicyTypes)
|
switch (dependentPolicyTypes)
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Policy requirements for the Require Two-Factor Authentication policy.
|
||||||
|
/// </summary>
|
||||||
|
public class RequireTwoFactorPolicyRequirement : IPolicyRequirement
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<PolicyDetails> _policyDetails;
|
||||||
|
|
||||||
|
public RequireTwoFactorPolicyRequirement(IEnumerable<PolicyDetails> policyDetails)
|
||||||
|
{
|
||||||
|
_policyDetails = policyDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if two-factor authentication is required for the organization due to an active policy.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationId">The ID of the organization to check.</param>
|
||||||
|
/// <returns>True if two-factor authentication is required for the organization, false otherwise.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// This should be used to check whether the member needs to have 2FA enabled before being
|
||||||
|
/// accepted, confirmed, or restored to the organization.
|
||||||
|
/// </remarks>
|
||||||
|
public bool IsTwoFactorRequiredForOrganization(Guid organizationId) =>
|
||||||
|
_policyDetails.Any(p => p.OrganizationId == organizationId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns tuples of (OrganizationId, OrganizationUserId) for active memberships where two-factor authentication is required.
|
||||||
|
/// Users should be revoked from these organizations if they disable all 2FA methods.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<(Guid OrganizationId, Guid OrganizationUserId)> OrganizationsRequiringTwoFactor =>
|
||||||
|
_policyDetails
|
||||||
|
.Where(p => p.OrganizationUserStatus is
|
||||||
|
OrganizationUserStatusType.Accepted or
|
||||||
|
OrganizationUserStatusType.Confirmed)
|
||||||
|
.Select(p => (p.OrganizationId, p.OrganizationUserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RequireTwoFactorPolicyRequirementFactory : BasePolicyRequirementFactory<RequireTwoFactorPolicyRequirement>
|
||||||
|
{
|
||||||
|
public override PolicyType PolicyType => PolicyType.TwoFactorAuthentication;
|
||||||
|
protected override IEnumerable<OrganizationUserStatusType> ExemptStatuses => [];
|
||||||
|
|
||||||
|
public override RequireTwoFactorPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
|
||||||
|
{
|
||||||
|
return new RequireTwoFactorPolicyRequirement(policyDetails);
|
||||||
|
}
|
||||||
|
}
|
@ -36,5 +36,6 @@ public static class PolicyServiceCollectionExtensions
|
|||||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, ResetPasswordPolicyRequirementFactory>();
|
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, ResetPasswordPolicyRequirementFactory>();
|
||||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, PersonalOwnershipPolicyRequirementFactory>();
|
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, PersonalOwnershipPolicyRequirementFactory>();
|
||||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireSsoPolicyRequirementFactory>();
|
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireSsoPolicyRequirementFactory>();
|
||||||
|
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireTwoFactorPolicyRequirementFactory>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
24
src/Core/AdminConsole/Services/IIntegrationHandler.cs
Normal file
24
src/Core/AdminConsole/Services/IIntegrationHandler.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public interface IIntegrationHandler
|
||||||
|
{
|
||||||
|
Task<IntegrationHandlerResult> HandleAsync(string json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IIntegrationHandler<T> : IIntegrationHandler
|
||||||
|
{
|
||||||
|
Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<T> message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class IntegrationHandlerBase<T> : IIntegrationHandler<T>
|
||||||
|
{
|
||||||
|
public async Task<IntegrationHandlerResult> HandleAsync(string json)
|
||||||
|
{
|
||||||
|
var message = IntegrationMessage<T>.FromJson(json);
|
||||||
|
return await HandleAsync(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<T> message);
|
||||||
|
}
|
8
src/Core/AdminConsole/Services/IIntegrationPublisher.cs
Normal file
8
src/Core/AdminConsole/Services/IIntegrationPublisher.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public interface IIntegrationPublisher
|
||||||
|
{
|
||||||
|
Task PublishAsync(IIntegrationMessage message);
|
||||||
|
}
|
@ -20,7 +20,7 @@ public class AzureServiceBusEventListenerService : EventLoggingListenerService
|
|||||||
string subscriptionName) : base(handler)
|
string subscriptionName) : base(handler)
|
||||||
{
|
{
|
||||||
_client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString);
|
_client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString);
|
||||||
_processor = _client.CreateProcessor(globalSettings.EventLogging.AzureServiceBus.TopicName, subscriptionName, new ServiceBusProcessorOptions());
|
_processor = _client.CreateProcessor(globalSettings.EventLogging.AzureServiceBus.EventTopicName, subscriptionName, new ServiceBusProcessorOptions());
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ public class AzureServiceBusEventWriteService : IEventWriteService, IAsyncDispos
|
|||||||
public AzureServiceBusEventWriteService(GlobalSettings globalSettings)
|
public AzureServiceBusEventWriteService(GlobalSettings globalSettings)
|
||||||
{
|
{
|
||||||
_client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString);
|
_client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString);
|
||||||
_sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.TopicName);
|
_sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.EventTopicName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateAsync(IEvent e)
|
public async Task CreateAsync(IEvent e)
|
||||||
|
@ -0,0 +1,101 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using Azure.Messaging.ServiceBus;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public class AzureServiceBusIntegrationListenerService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly int _maxRetries;
|
||||||
|
private readonly string _subscriptionName;
|
||||||
|
private readonly string _topicName;
|
||||||
|
private readonly IIntegrationHandler _handler;
|
||||||
|
private readonly ServiceBusClient _client;
|
||||||
|
private readonly ServiceBusProcessor _processor;
|
||||||
|
private readonly ServiceBusSender _sender;
|
||||||
|
private readonly ILogger<AzureServiceBusIntegrationListenerService> _logger;
|
||||||
|
|
||||||
|
public AzureServiceBusIntegrationListenerService(
|
||||||
|
IIntegrationHandler handler,
|
||||||
|
string subscriptionName,
|
||||||
|
GlobalSettings globalSettings,
|
||||||
|
ILogger<AzureServiceBusIntegrationListenerService> logger)
|
||||||
|
{
|
||||||
|
_handler = handler;
|
||||||
|
_logger = logger;
|
||||||
|
_maxRetries = globalSettings.EventLogging.AzureServiceBus.MaxRetries;
|
||||||
|
_topicName = globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName;
|
||||||
|
_subscriptionName = subscriptionName;
|
||||||
|
|
||||||
|
_client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString);
|
||||||
|
_processor = _client.CreateProcessor(_topicName, _subscriptionName, new ServiceBusProcessorOptions());
|
||||||
|
_sender = _client.CreateSender(_topicName);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_processor.ProcessMessageAsync += HandleMessageAsync;
|
||||||
|
_processor.ProcessErrorAsync += args =>
|
||||||
|
{
|
||||||
|
_logger.LogError(args.Exception, "Azure Service Bus error");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
await _processor.StartProcessingAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _processor.StopProcessingAsync(cancellationToken);
|
||||||
|
await _processor.DisposeAsync();
|
||||||
|
await _sender.DisposeAsync();
|
||||||
|
await _client.DisposeAsync();
|
||||||
|
await base.StopAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleMessageAsync(ProcessMessageEventArgs args)
|
||||||
|
{
|
||||||
|
var json = args.Message.Body.ToString();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _handler.HandleAsync(json);
|
||||||
|
var message = result.Message;
|
||||||
|
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
await args.CompleteMessageAsync(args.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.ApplyRetry(result.DelayUntilDate);
|
||||||
|
|
||||||
|
if (result.Retryable && message.RetryCount < _maxRetries)
|
||||||
|
{
|
||||||
|
var scheduledTime = (DateTime)message.DelayUntilDate!;
|
||||||
|
var retryMsg = new ServiceBusMessage(message.ToJson())
|
||||||
|
{
|
||||||
|
Subject = args.Message.Subject,
|
||||||
|
ScheduledEnqueueTime = scheduledTime
|
||||||
|
};
|
||||||
|
|
||||||
|
await _sender.SendMessageAsync(retryMsg);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await args.DeadLetterMessageAsync(args.Message, "Retry limit exceeded or non-retryable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await args.CompleteMessageAsync(args.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Unhandled error processing ASB message");
|
||||||
|
await args.CompleteMessageAsync(args.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
using Azure.Messaging.ServiceBus;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public class AzureServiceBusIntegrationPublisher : IIntegrationPublisher, IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly ServiceBusClient _client;
|
||||||
|
private readonly ServiceBusSender _sender;
|
||||||
|
|
||||||
|
public AzureServiceBusIntegrationPublisher(GlobalSettings globalSettings)
|
||||||
|
{
|
||||||
|
_client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString);
|
||||||
|
_sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PublishAsync(IIntegrationMessage message)
|
||||||
|
{
|
||||||
|
var json = message.ToJson();
|
||||||
|
|
||||||
|
var serviceBusMessage = new ServiceBusMessage(json)
|
||||||
|
{
|
||||||
|
Subject = message.IntegrationType.ToRoutingKey(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await _sender.SendMessageAsync(serviceBusMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await _sender.DisposeAsync();
|
||||||
|
await _client.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
@ -1,32 +1,53 @@
|
|||||||
using System.Text.Json.Nodes;
|
using System.Text.Json;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||||
using Bit.Core.AdminConsole.Utilities;
|
using Bit.Core.AdminConsole.Utilities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Data.Integrations;
|
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
public abstract class IntegrationEventHandlerBase(
|
#nullable enable
|
||||||
|
|
||||||
|
public class EventIntegrationHandler<T>(
|
||||||
|
IntegrationType integrationType,
|
||||||
|
IIntegrationPublisher integrationPublisher,
|
||||||
|
IOrganizationIntegrationConfigurationRepository configurationRepository,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository)
|
||||||
IOrganizationIntegrationConfigurationRepository configurationRepository)
|
|
||||||
: IEventMessageHandler
|
: IEventMessageHandler
|
||||||
{
|
{
|
||||||
public async Task HandleEventAsync(EventMessage eventMessage)
|
public async Task HandleEventAsync(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
var organizationId = eventMessage.OrganizationId ?? Guid.Empty;
|
if (eventMessage.OrganizationId is not Guid organizationId)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var configurations = await configurationRepository.GetConfigurationDetailsAsync(
|
var configurations = await configurationRepository.GetConfigurationDetailsAsync(
|
||||||
organizationId,
|
organizationId,
|
||||||
GetIntegrationType(),
|
integrationType,
|
||||||
eventMessage.Type);
|
eventMessage.Type);
|
||||||
|
|
||||||
foreach (var configuration in configurations)
|
foreach (var configuration in configurations)
|
||||||
{
|
{
|
||||||
var context = await BuildContextAsync(eventMessage, configuration.Template);
|
var template = configuration.Template ?? string.Empty;
|
||||||
var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(configuration.Template, context);
|
var context = await BuildContextAsync(eventMessage, template);
|
||||||
|
var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(template, context);
|
||||||
|
|
||||||
await ProcessEventIntegrationAsync(configuration.MergedConfiguration, renderedTemplate);
|
var config = configuration.MergedConfiguration.Deserialize<T>()
|
||||||
|
?? throw new InvalidOperationException($"Failed to deserialize to {typeof(T).Name}");
|
||||||
|
|
||||||
|
var message = new IntegrationMessage<T>
|
||||||
|
{
|
||||||
|
IntegrationType = integrationType,
|
||||||
|
Configuration = config,
|
||||||
|
RenderedTemplate = renderedTemplate,
|
||||||
|
RetryCount = 0,
|
||||||
|
DelayUntilDate = null
|
||||||
|
};
|
||||||
|
|
||||||
|
await integrationPublisher.PublishAsync(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,8 +80,4 @@ public abstract class IntegrationEventHandlerBase(
|
|||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract IntegrationType GetIntegrationType();
|
|
||||||
|
|
||||||
protected abstract Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration, string renderedTemplate);
|
|
||||||
}
|
}
|
@ -462,13 +462,13 @@ public class EventService : IEventService
|
|||||||
|
|
||||||
private bool CanUseEvents(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)
|
private bool CanUseEvents(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)
|
||||||
{
|
{
|
||||||
return orgAbilities != null && orgAbilities.ContainsKey(orgId) &&
|
return orgAbilities != null && orgAbilities.TryGetValue(orgId, out var orgAbility) &&
|
||||||
orgAbilities[orgId].Enabled && orgAbilities[orgId].UseEvents;
|
orgAbility.Enabled && orgAbility.UseEvents;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanUseProviderEvents(IDictionary<Guid, ProviderAbility> providerAbilities, Guid providerId)
|
private bool CanUseProviderEvents(IDictionary<Guid, ProviderAbility> providerAbilities, Guid providerId)
|
||||||
{
|
{
|
||||||
return providerAbilities != null && providerAbilities.ContainsKey(providerId) &&
|
return providerAbilities != null && providerAbilities.TryGetValue(providerId, out var providerAbility) &&
|
||||||
providerAbilities[providerId].Enabled && providerAbilities[providerId].UseEvents;
|
providerAbility.Enabled && providerAbility.UseEvents;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,6 @@ using Bit.Core.AdminConsole.Repositories;
|
|||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
@ -30,9 +29,6 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
|||||||
using Bit.Core.Platform.Push;
|
using Bit.Core.Platform.Push;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Settings;
|
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 Bit.Core.Utilities;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
@ -45,7 +41,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly ICollectionRepository _collectionRepository;
|
private readonly ICollectionRepository _collectionRepository;
|
||||||
private readonly IUserRepository _userRepository;
|
|
||||||
private readonly IGroupRepository _groupRepository;
|
private readonly IGroupRepository _groupRepository;
|
||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
private readonly IPushNotificationService _pushNotificationService;
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
@ -58,7 +53,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyRepository _policyRepository;
|
||||||
private readonly IPolicyService _policyService;
|
private readonly IPolicyService _policyService;
|
||||||
private readonly ISsoUserRepository _ssoUserRepository;
|
private readonly ISsoUserRepository _ssoUserRepository;
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
|
||||||
private readonly IGlobalSettings _globalSettings;
|
private readonly IGlobalSettings _globalSettings;
|
||||||
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
@ -69,7 +63,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||||
private readonly IProviderRepository _providerRepository;
|
private readonly IProviderRepository _providerRepository;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
|
||||||
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||||
@ -79,7 +72,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
ICollectionRepository collectionRepository,
|
ICollectionRepository collectionRepository,
|
||||||
IUserRepository userRepository,
|
|
||||||
IGroupRepository groupRepository,
|
IGroupRepository groupRepository,
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
IPushNotificationService pushNotificationService,
|
IPushNotificationService pushNotificationService,
|
||||||
@ -92,7 +84,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
IPolicyRepository policyRepository,
|
IPolicyRepository policyRepository,
|
||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
ISsoUserRepository ssoUserRepository,
|
ISsoUserRepository ssoUserRepository,
|
||||||
IReferenceEventService referenceEventService,
|
|
||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
@ -103,7 +94,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
||||||
IProviderRepository providerRepository,
|
IProviderRepository providerRepository,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
|
||||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
||||||
IPricingClient pricingClient,
|
IPricingClient pricingClient,
|
||||||
IPolicyRequirementQuery policyRequirementQuery,
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
@ -113,7 +103,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_collectionRepository = collectionRepository;
|
_collectionRepository = collectionRepository;
|
||||||
_userRepository = userRepository;
|
|
||||||
_groupRepository = groupRepository;
|
_groupRepository = groupRepository;
|
||||||
_mailService = mailService;
|
_mailService = mailService;
|
||||||
_pushNotificationService = pushNotificationService;
|
_pushNotificationService = pushNotificationService;
|
||||||
@ -126,7 +115,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
_policyRepository = policyRepository;
|
_policyRepository = policyRepository;
|
||||||
_policyService = policyService;
|
_policyService = policyService;
|
||||||
_ssoUserRepository = ssoUserRepository;
|
_ssoUserRepository = ssoUserRepository;
|
||||||
_referenceEventService = referenceEventService;
|
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_organizationApiKeyRepository = organizationApiKeyRepository;
|
_organizationApiKeyRepository = organizationApiKeyRepository;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
@ -137,7 +125,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
||||||
_providerRepository = providerRepository;
|
_providerRepository = providerRepository;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
|
||||||
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
_policyRequirementQuery = policyRequirementQuery;
|
_policyRequirementQuery = policyRequirementQuery;
|
||||||
@ -160,11 +147,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _paymentService.CancelSubscriptionAsync(organization, eop);
|
await _paymentService.CancelSubscriptionAsync(organization, eop);
|
||||||
await _referenceEventService.RaiseEventAsync(
|
|
||||||
new ReferenceEvent(ReferenceEventType.CancelSubscription, organization, _currentContext)
|
|
||||||
{
|
|
||||||
EndOfPeriod = endOfPeriod,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ReinstateSubscriptionAsync(Guid organizationId)
|
public async Task ReinstateSubscriptionAsync(Guid organizationId)
|
||||||
@ -176,8 +158,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _paymentService.ReinstateSubscriptionAsync(organization);
|
await _paymentService.ReinstateSubscriptionAsync(organization);
|
||||||
await _referenceEventService.RaiseEventAsync(
|
|
||||||
new ReferenceEvent(ReferenceEventType.ReinstateSubscription, organization, _currentContext));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb)
|
public async Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb)
|
||||||
@ -197,13 +177,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, organization, storageAdjustmentGb,
|
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, organization, storageAdjustmentGb,
|
||||||
plan.PasswordManager.StripeStoragePlanId);
|
plan.PasswordManager.StripeStoragePlanId);
|
||||||
await _referenceEventService.RaiseEventAsync(
|
|
||||||
new ReferenceEvent(ReferenceEventType.AdjustStorage, organization, _currentContext)
|
|
||||||
{
|
|
||||||
PlanName = plan.Name,
|
|
||||||
PlanType = plan.Type,
|
|
||||||
Storage = storageAdjustmentGb,
|
|
||||||
});
|
|
||||||
await ReplaceAndUpdateCacheAsync(organization);
|
await ReplaceAndUpdateCacheAsync(organization);
|
||||||
return secret;
|
return secret;
|
||||||
}
|
}
|
||||||
@ -335,14 +308,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var paymentIntentClientSecret = await _paymentService.AdjustSeatsAsync(organization, plan, additionalSeats);
|
var paymentIntentClientSecret = await _paymentService.AdjustSeatsAsync(organization, plan, additionalSeats);
|
||||||
await _referenceEventService.RaiseEventAsync(
|
|
||||||
new ReferenceEvent(ReferenceEventType.AdjustSeats, organization, _currentContext)
|
|
||||||
{
|
|
||||||
PlanName = plan.Name,
|
|
||||||
PlanType = plan.Type,
|
|
||||||
Seats = newSeatTotal,
|
|
||||||
PreviousSeats = organization.Seats
|
|
||||||
});
|
|
||||||
organization.Seats = (short?)newSeatTotal;
|
organization.Seats = (short?)newSeatTotal;
|
||||||
await ReplaceAndUpdateCacheAsync(organization);
|
await ReplaceAndUpdateCacheAsync(organization);
|
||||||
|
|
||||||
@ -647,12 +612,12 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var providers = organization.GetTwoFactorProviders();
|
var providers = organization.GetTwoFactorProviders();
|
||||||
if (!providers?.ContainsKey(type) ?? true)
|
if (providers is null || !providers.TryGetValue(type, out var provider))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
providers[type].Enabled = true;
|
provider.Enabled = true;
|
||||||
organization.SetTwoFactorProviders(providers);
|
organization.SetTwoFactorProviders(providers);
|
||||||
await UpdateAsync(organization);
|
await UpdateAsync(organization);
|
||||||
}
|
}
|
||||||
@ -893,12 +858,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
await SendInvitesAsync(allOrgUsers, organization);
|
await SendInvitesAsync(allOrgUsers, organization);
|
||||||
|
|
||||||
await _referenceEventService.RaiseEventAsync(
|
|
||||||
new ReferenceEvent(ReferenceEventType.InvitedUsers, organization, _currentContext)
|
|
||||||
{
|
|
||||||
Users = orgUserInvitedCount
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -1156,7 +1115,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
var existingUsersDict = existingExternalUsers.ToDictionary(u => u.ExternalId);
|
var existingUsersDict = existingExternalUsers.ToDictionary(u => u.ExternalId);
|
||||||
var removeUsersSet = new HashSet<string>(removeUserExternalIds)
|
var removeUsersSet = new HashSet<string>(removeUserExternalIds)
|
||||||
.Except(newUsersSet)
|
.Except(newUsersSet)
|
||||||
.Where(u => existingUsersDict.ContainsKey(u) && existingUsersDict[u].Type != OrganizationUserType.Owner)
|
.Where(u => existingUsersDict.TryGetValue(u, out var existingUser) && existingUser.Type != OrganizationUserType.Owner)
|
||||||
.Select(u => existingUsersDict[u]);
|
.Select(u => existingUsersDict[u]);
|
||||||
|
|
||||||
await _organizationUserRepository.DeleteManyAsync(removeUsersSet.Select(u => u.Id));
|
await _organizationUserRepository.DeleteManyAsync(removeUsersSet.Select(u => u.Id));
|
||||||
@ -1324,8 +1283,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _eventService.LogOrganizationUserEventsAsync(events.Select(e => (e.ou, e.e, eventSystemUser, e.d)));
|
await _eventService.LogOrganizationUserEventsAsync(events.Select(e => (e.ou, e.e, eventSystemUser, e.d)));
|
||||||
await _referenceEventService.RaiseEventAsync(
|
|
||||||
new ReferenceEvent(ReferenceEventType.DirectorySynced, organization, _currentContext));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteSsoUserAsync(Guid userId, Guid? organizationId)
|
public async Task DeleteSsoUserAsync(Guid userId, Guid? organizationId)
|
||||||
@ -1722,72 +1679,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, bool userHasTwoFactorEnabled)
|
|
||||||
{
|
|
||||||
// An invited OrganizationUser isn't linked with a user account yet, so these checks are irrelevant
|
|
||||||
// The user will be subject to the same checks when they try to accept the invite
|
|
||||||
if (GetPriorActiveOrganizationUserStatusType(orgUser) == OrganizationUserStatusType.Invited)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var userId = orgUser.UserId.Value;
|
|
||||||
|
|
||||||
// Enforce Single Organization Policy of organization user is being restored to
|
|
||||||
var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(userId);
|
|
||||||
var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId);
|
|
||||||
var singleOrgPoliciesApplyingToRevokedUsers = await _policyService.GetPoliciesApplicableToUserAsync(userId,
|
|
||||||
PolicyType.SingleOrg, OrganizationUserStatusType.Revoked);
|
|
||||||
var singleOrgPolicyApplies = singleOrgPoliciesApplyingToRevokedUsers.Any(p => p.OrganizationId == orgUser.OrganizationId);
|
|
||||||
|
|
||||||
var singleOrgCompliant = true;
|
|
||||||
var belongsToOtherOrgCompliant = true;
|
|
||||||
var twoFactorCompliant = true;
|
|
||||||
|
|
||||||
if (hasOtherOrgs && singleOrgPolicyApplies)
|
|
||||||
{
|
|
||||||
singleOrgCompliant = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce Single Organization Policy of other organizations user is a member of
|
|
||||||
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId,
|
|
||||||
PolicyType.SingleOrg);
|
|
||||||
if (anySingleOrgPolicies)
|
|
||||||
{
|
|
||||||
belongsToOtherOrgCompliant = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce Two Factor Authentication Policy of organization user is trying to join
|
|
||||||
if (!userHasTwoFactorEnabled)
|
|
||||||
{
|
|
||||||
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId,
|
|
||||||
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked);
|
|
||||||
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
|
|
||||||
{
|
|
||||||
twoFactorCompliant = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = await _userRepository.GetByIdAsync(userId);
|
|
||||||
|
|
||||||
if (!singleOrgCompliant && !twoFactorCompliant)
|
|
||||||
{
|
|
||||||
throw new BadRequestException(user.Email + " is not compliant with the single organization and two-step login polciy");
|
|
||||||
}
|
|
||||||
else if (!singleOrgCompliant)
|
|
||||||
{
|
|
||||||
throw new BadRequestException(user.Email + " is not compliant with the single organization policy");
|
|
||||||
}
|
|
||||||
else if (!belongsToOtherOrgCompliant)
|
|
||||||
{
|
|
||||||
throw new BadRequestException(user.Email + " belongs to an organization that doesn't allow them to join multiple organizations");
|
|
||||||
}
|
|
||||||
else if (!twoFactorCompliant)
|
|
||||||
{
|
|
||||||
throw new BadRequestException(user.Email + " is not compliant with the two-step login policy");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser)
|
public static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser)
|
||||||
{
|
{
|
||||||
// Determine status to revert back to
|
// Determine status to revert back to
|
||||||
@ -1827,11 +1718,5 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
await SendInviteAsync(ownerOrganizationUser, organization, true);
|
await SendInviteAsync(ownerOrganizationUser, organization, true);
|
||||||
await _eventService.LogOrganizationUserEventAsync(ownerOrganizationUser, EventType.OrganizationUser_Invited);
|
await _eventService.LogOrganizationUserEventAsync(ownerOrganizationUser, EventType.OrganizationUser_Invited);
|
||||||
|
|
||||||
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.OrganizationCreatedByAdmin, organization, _currentContext)
|
|
||||||
{
|
|
||||||
EventRaisedByUser = userService.GetUserName(user),
|
|
||||||
SalesAssistedTrialStarted = salesAssistedTrialStarted,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@ public class PolicyService : IPolicyService
|
|||||||
var excludedUserTypes = GetUserTypesExcludedFromPolicy(policyType);
|
var excludedUserTypes = GetUserTypesExcludedFromPolicy(policyType);
|
||||||
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||||
return organizationUserPolicyDetails.Where(o =>
|
return organizationUserPolicyDetails.Where(o =>
|
||||||
(!orgAbilities.ContainsKey(o.OrganizationId) || orgAbilities[o.OrganizationId].UsePolicies) &&
|
(!orgAbilities.TryGetValue(o.OrganizationId, out var orgAbility) || orgAbility.UsePolicies) &&
|
||||||
o.PolicyEnabled &&
|
o.PolicyEnabled &&
|
||||||
!excludedUserTypes.Contains(o.OrganizationUserType) &&
|
!excludedUserTypes.Contains(o.OrganizationUserType) &&
|
||||||
o.OrganizationUserStatus >= minStatus &&
|
o.OrganizationUserStatus >= minStatus &&
|
||||||
|
@ -29,7 +29,7 @@ public class RabbitMqEventListenerService : EventLoggingListenerService
|
|||||||
UserName = globalSettings.EventLogging.RabbitMq.Username,
|
UserName = globalSettings.EventLogging.RabbitMq.Username,
|
||||||
Password = globalSettings.EventLogging.RabbitMq.Password
|
Password = globalSettings.EventLogging.RabbitMq.Password
|
||||||
};
|
};
|
||||||
_exchangeName = globalSettings.EventLogging.RabbitMq.ExchangeName;
|
_exchangeName = globalSettings.EventLogging.RabbitMq.EventExchangeName;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_queueName = queueName;
|
_queueName = queueName;
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ public class RabbitMqEventWriteService : IEventWriteService, IAsyncDisposable
|
|||||||
UserName = globalSettings.EventLogging.RabbitMq.Username,
|
UserName = globalSettings.EventLogging.RabbitMq.Username,
|
||||||
Password = globalSettings.EventLogging.RabbitMq.Password
|
Password = globalSettings.EventLogging.RabbitMq.Password
|
||||||
};
|
};
|
||||||
_exchangeName = globalSettings.EventLogging.RabbitMq.ExchangeName;
|
_exchangeName = globalSettings.EventLogging.RabbitMq.EventExchangeName;
|
||||||
|
|
||||||
_lazyConnection = new Lazy<Task<IConnection>>(CreateConnectionAsync);
|
_lazyConnection = new Lazy<Task<IConnection>>(CreateConnectionAsync);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,191 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using RabbitMQ.Client;
|
||||||
|
using RabbitMQ.Client.Events;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public class RabbitMqIntegrationListenerService : BackgroundService
|
||||||
|
{
|
||||||
|
private const string _deadLetterRoutingKey = "dead-letter";
|
||||||
|
private IChannel _channel;
|
||||||
|
private IConnection _connection;
|
||||||
|
private readonly string _exchangeName;
|
||||||
|
private readonly string _queueName;
|
||||||
|
private readonly string _retryQueueName;
|
||||||
|
private readonly string _deadLetterQueueName;
|
||||||
|
private readonly string _routingKey;
|
||||||
|
private readonly string _retryRoutingKey;
|
||||||
|
private readonly int _maxRetries;
|
||||||
|
private readonly IIntegrationHandler _handler;
|
||||||
|
private readonly ConnectionFactory _factory;
|
||||||
|
private readonly ILogger<RabbitMqIntegrationListenerService> _logger;
|
||||||
|
private readonly int _retryTiming;
|
||||||
|
|
||||||
|
public RabbitMqIntegrationListenerService(IIntegrationHandler handler,
|
||||||
|
string routingKey,
|
||||||
|
string queueName,
|
||||||
|
string retryQueueName,
|
||||||
|
string deadLetterQueueName,
|
||||||
|
GlobalSettings globalSettings,
|
||||||
|
ILogger<RabbitMqIntegrationListenerService> logger)
|
||||||
|
{
|
||||||
|
_handler = handler;
|
||||||
|
_routingKey = routingKey;
|
||||||
|
_retryRoutingKey = $"{_routingKey}-retry";
|
||||||
|
_queueName = queueName;
|
||||||
|
_retryQueueName = retryQueueName;
|
||||||
|
_deadLetterQueueName = deadLetterQueueName;
|
||||||
|
_logger = logger;
|
||||||
|
_exchangeName = globalSettings.EventLogging.RabbitMq.IntegrationExchangeName;
|
||||||
|
_maxRetries = globalSettings.EventLogging.RabbitMq.MaxRetries;
|
||||||
|
_retryTiming = globalSettings.EventLogging.RabbitMq.RetryTiming;
|
||||||
|
|
||||||
|
_factory = new ConnectionFactory
|
||||||
|
{
|
||||||
|
HostName = globalSettings.EventLogging.RabbitMq.HostName,
|
||||||
|
UserName = globalSettings.EventLogging.RabbitMq.Username,
|
||||||
|
Password = globalSettings.EventLogging.RabbitMq.Password
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_connection = await _factory.CreateConnectionAsync(cancellationToken);
|
||||||
|
_channel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
await _channel.ExchangeDeclareAsync(exchange: _exchangeName,
|
||||||
|
type: ExchangeType.Direct,
|
||||||
|
durable: true,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
// Declare main queue
|
||||||
|
await _channel.QueueDeclareAsync(queue: _queueName,
|
||||||
|
durable: true,
|
||||||
|
exclusive: false,
|
||||||
|
autoDelete: false,
|
||||||
|
arguments: null,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
await _channel.QueueBindAsync(queue: _queueName,
|
||||||
|
exchange: _exchangeName,
|
||||||
|
routingKey: _routingKey,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
// Declare retry queue (Configurable TTL, dead-letters back to main queue)
|
||||||
|
await _channel.QueueDeclareAsync(queue: _retryQueueName,
|
||||||
|
durable: true,
|
||||||
|
exclusive: false,
|
||||||
|
autoDelete: false,
|
||||||
|
arguments: new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "x-dead-letter-exchange", _exchangeName },
|
||||||
|
{ "x-dead-letter-routing-key", _routingKey },
|
||||||
|
{ "x-message-ttl", _retryTiming }
|
||||||
|
},
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
await _channel.QueueBindAsync(queue: _retryQueueName,
|
||||||
|
exchange: _exchangeName,
|
||||||
|
routingKey: _retryRoutingKey,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
// Declare dead letter queue
|
||||||
|
await _channel.QueueDeclareAsync(queue: _deadLetterQueueName,
|
||||||
|
durable: true,
|
||||||
|
exclusive: false,
|
||||||
|
autoDelete: false,
|
||||||
|
arguments: null,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
await _channel.QueueBindAsync(queue: _deadLetterQueueName,
|
||||||
|
exchange: _exchangeName,
|
||||||
|
routingKey: _deadLetterRoutingKey,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
await base.StartAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var consumer = new AsyncEventingBasicConsumer(_channel);
|
||||||
|
consumer.ReceivedAsync += async (_, ea) =>
|
||||||
|
{
|
||||||
|
var json = Encoding.UTF8.GetString(ea.Body.Span);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _handler.HandleAsync(json);
|
||||||
|
var message = result.Message;
|
||||||
|
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
// Successful integration send. Acknowledge message delivery and return
|
||||||
|
await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Retryable)
|
||||||
|
{
|
||||||
|
// Integration failed, but is retryable - apply delay and check max retries
|
||||||
|
message.ApplyRetry(result.DelayUntilDate);
|
||||||
|
|
||||||
|
if (message.RetryCount < _maxRetries)
|
||||||
|
{
|
||||||
|
// Publish message to the retry queue. It will be re-published for retry after a delay
|
||||||
|
await _channel.BasicPublishAsync(
|
||||||
|
exchange: _exchangeName,
|
||||||
|
routingKey: _retryRoutingKey,
|
||||||
|
body: Encoding.UTF8.GetBytes(message.ToJson()),
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Exceeded the max number of retries; fail and send to dead letter queue
|
||||||
|
await PublishToDeadLetterAsync(message.ToJson());
|
||||||
|
_logger.LogWarning("Max retry attempts reached. Sent to DLQ.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fatal error (i.e. not retryable) occurred. Send message to dead letter queue without any retries
|
||||||
|
await PublishToDeadLetterAsync(message.ToJson());
|
||||||
|
_logger.LogWarning("Non-retryable failure. Sent to DLQ.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message has been sent to retry or dead letter queues.
|
||||||
|
// Acknowledge receipt so Rabbit knows it's been processed
|
||||||
|
await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Unknown error occurred. Acknowledge so Rabbit doesn't keep attempting. Log the error
|
||||||
|
_logger.LogError(ex, "Unhandled error processing integration message.");
|
||||||
|
await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await _channel.BasicConsumeAsync(queue: _queueName, autoAck: false, consumer: consumer, cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PublishToDeadLetterAsync(string json)
|
||||||
|
{
|
||||||
|
await _channel.BasicPublishAsync(
|
||||||
|
exchange: _exchangeName,
|
||||||
|
routingKey: _deadLetterRoutingKey,
|
||||||
|
body: Encoding.UTF8.GetBytes(json));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _channel.CloseAsync(cancellationToken);
|
||||||
|
await _connection.CloseAsync(cancellationToken);
|
||||||
|
await base.StopAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
_channel.Dispose();
|
||||||
|
_connection.Dispose();
|
||||||
|
base.Dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using RabbitMQ.Client;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public class RabbitMqIntegrationPublisher : IIntegrationPublisher, IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly ConnectionFactory _factory;
|
||||||
|
private readonly Lazy<Task<IConnection>> _lazyConnection;
|
||||||
|
private readonly string _exchangeName;
|
||||||
|
|
||||||
|
public RabbitMqIntegrationPublisher(GlobalSettings globalSettings)
|
||||||
|
{
|
||||||
|
_factory = new ConnectionFactory
|
||||||
|
{
|
||||||
|
HostName = globalSettings.EventLogging.RabbitMq.HostName,
|
||||||
|
UserName = globalSettings.EventLogging.RabbitMq.Username,
|
||||||
|
Password = globalSettings.EventLogging.RabbitMq.Password
|
||||||
|
};
|
||||||
|
_exchangeName = globalSettings.EventLogging.RabbitMq.IntegrationExchangeName;
|
||||||
|
|
||||||
|
_lazyConnection = new Lazy<Task<IConnection>>(CreateConnectionAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PublishAsync(IIntegrationMessage message)
|
||||||
|
{
|
||||||
|
var routingKey = message.IntegrationType.ToRoutingKey();
|
||||||
|
var connection = await _lazyConnection.Value;
|
||||||
|
await using var channel = await connection.CreateChannelAsync();
|
||||||
|
|
||||||
|
await channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Direct, durable: true);
|
||||||
|
|
||||||
|
var body = Encoding.UTF8.GetBytes(message.ToJson());
|
||||||
|
|
||||||
|
await channel.BasicPublishAsync(exchange: _exchangeName, routingKey: routingKey, body: body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (_lazyConnection.IsValueCreated)
|
||||||
|
{
|
||||||
|
var connection = await _lazyConnection.Value;
|
||||||
|
await connection.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IConnection> CreateConnectionAsync()
|
||||||
|
{
|
||||||
|
return await _factory.CreateConnectionAsync();
|
||||||
|
}
|
||||||
|
}
|
@ -1,35 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Nodes;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.Data.Integrations;
|
|
||||||
using Bit.Core.Repositories;
|
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
|
||||||
|
|
||||||
public class SlackEventHandler(
|
|
||||||
IUserRepository userRepository,
|
|
||||||
IOrganizationRepository organizationRepository,
|
|
||||||
IOrganizationIntegrationConfigurationRepository configurationRepository,
|
|
||||||
ISlackService slackService)
|
|
||||||
: IntegrationEventHandlerBase(userRepository, organizationRepository, configurationRepository)
|
|
||||||
{
|
|
||||||
protected override IntegrationType GetIntegrationType() => IntegrationType.Slack;
|
|
||||||
|
|
||||||
protected override async Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration,
|
|
||||||
string renderedTemplate)
|
|
||||||
{
|
|
||||||
var config = mergedConfiguration.Deserialize<SlackIntegrationConfigurationDetails>();
|
|
||||||
if (config is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await slackService.SendSlackMessageByChannelIdAsync(
|
|
||||||
config.token,
|
|
||||||
renderedTemplate,
|
|
||||||
config.channelId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,19 @@
|
|||||||
|
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public class SlackIntegrationHandler(
|
||||||
|
ISlackService slackService)
|
||||||
|
: IntegrationHandlerBase<SlackIntegrationConfigurationDetails>
|
||||||
|
{
|
||||||
|
public override async Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<SlackIntegrationConfigurationDetails> message)
|
||||||
|
{
|
||||||
|
await slackService.SendSlackMessageByChannelIdAsync(
|
||||||
|
message.Configuration.token,
|
||||||
|
message.RenderedTemplate,
|
||||||
|
message.Configuration.channelId
|
||||||
|
);
|
||||||
|
|
||||||
|
return new IntegrationHandlerResult(success: true, message: message);
|
||||||
|
}
|
||||||
|
}
|
@ -1,38 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Nodes;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.Data.Integrations;
|
|
||||||
using Bit.Core.Repositories;
|
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
|
||||||
|
|
||||||
public class WebhookEventHandler(
|
|
||||||
IHttpClientFactory httpClientFactory,
|
|
||||||
IUserRepository userRepository,
|
|
||||||
IOrganizationRepository organizationRepository,
|
|
||||||
IOrganizationIntegrationConfigurationRepository configurationRepository)
|
|
||||||
: IntegrationEventHandlerBase(userRepository, organizationRepository, configurationRepository)
|
|
||||||
{
|
|
||||||
private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName);
|
|
||||||
|
|
||||||
public const string HttpClientName = "WebhookEventHandlerHttpClient";
|
|
||||||
|
|
||||||
protected override IntegrationType GetIntegrationType() => IntegrationType.Webhook;
|
|
||||||
|
|
||||||
protected override async Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration,
|
|
||||||
string renderedTemplate)
|
|
||||||
{
|
|
||||||
var config = mergedConfiguration.Deserialize<WebhookIntegrationConfigurationDetils>();
|
|
||||||
if (config is null || string.IsNullOrEmpty(config.url))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var content = new StringContent(renderedTemplate, Encoding.UTF8, "application/json");
|
|
||||||
var response = await _httpClient.PostAsync(config.url, content);
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user