diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 19eea71b6a..c6b0ecd9d0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,6 +11,9 @@ on: types: [opened, synchronize] workflow_call: inputs: {} + +permissions: + contents: read env: _AZ_REGISTRY: "bitwardenprod.azurecr.io" @@ -19,7 +22,7 @@ env: jobs: lint: name: Lint - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -33,115 +36,15 @@ jobs: run: dotnet format --verify-no-changes build-artifacts: - name: Build artifacts - runs-on: ubuntu-22.04 + name: Build Docker images + runs-on: ubuntu-24.04 needs: - lint outputs: 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: security-events: write id-token: write - needs: - - build-artifacts - if: ${{ needs.build-artifacts.outputs.has_secrets == 'true' }} strategy: fail-fast: false matrix: @@ -149,6 +52,7 @@ jobs: - project_name: Admin base_path: ./src dotnet: true + node: true - project_name: Api base_path: ./src dotnet: true @@ -182,9 +86,6 @@ jobs: - project_name: Scim base_path: ./bitwarden_license/src dotnet: true - - project_name: Server - base_path: ./util - dotnet: true - project_name: Setup base_path: ./util dotnet: true @@ -192,6 +93,14 @@ jobs: base_path: ./bitwarden_license/src dotnet: 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: @@ -203,13 +112,67 @@ jobs: id: publish-branch-check run: | IFS="," read -a publish_branches <<< $PUBLISH_BRANCHES - if [[ " ${publish_branches[*]} " =~ " ${GITHUB_REF:11} " ]]; then echo "is_publish_branch=true" >> $GITHUB_ENV else echo "is_publish_branch=false" >> $GITHUB_ENV 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 ########## - name: Log in to Azure - production subscription uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -277,26 +240,16 @@ jobs: fi echo "tags=$TAGS" >> $GITHUB_OUTPUT - - name: Get build artifact - if: ${{ matrix.dotnet }} - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: ${{ matrix.project_name }}.zip - - - 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 - id: build-docker + id: build-artifacts uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0 with: - context: ${{ matrix.base_path }}/${{ matrix.project_name }} + context: . file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile - platforms: linux/amd64 + platforms: | + linux/amd64, + linux/arm/v7, + linux/arm64 push: true tags: ${{ steps.image-tags.outputs.tags }} secrets: | @@ -309,7 +262,7 @@ jobs: - name: Sign image with Cosign if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' env: - DIGEST: ${{ steps.build-docker.outputs.digest }} + DIGEST: ${{ steps.build-artifacts.outputs.digest }} TAGS: ${{ steps.image-tags.outputs.tags }} run: | IFS="," read -a tags <<< "${TAGS}" @@ -336,8 +289,8 @@ jobs: upload: name: Upload - runs-on: ubuntu-22.04 - needs: build-docker + runs-on: ubuntu-24.04 + needs: build-artifacts steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -377,9 +330,9 @@ jobs: # Run setup 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 \ - 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 @@ -512,7 +465,7 @@ jobs: build-mssqlmigratorutility: name: Build MSSQL migrator utility - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 needs: - lint defaults: @@ -568,9 +521,9 @@ jobs: if: | github.event_name != 'pull_request' && (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: - - build-docker + - build-artifacts steps: - name: Log in to Azure - CI subscription uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -604,7 +557,7 @@ jobs: if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' runs-on: ubuntu-22.04 needs: - - build-docker + - build-artifacts steps: - name: Log in to Azure - CI subscription uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -638,7 +591,6 @@ jobs: name: Setup Ephemeral Environment needs: - build-artifacts - - build-docker if: | needs.build-artifacts.outputs.has_secrets == 'true' && github.event_name == 'pull_request' @@ -648,6 +600,7 @@ jobs: project: server pull_request_number: ${{ github.event.number }} secrets: inherit + permissions: read-all check-failures: name: Check for failures @@ -656,7 +609,6 @@ jobs: needs: - lint - build-artifacts - - build-docker - upload - build-mssqlmigratorutility - self-host-build diff --git a/Directory.Build.props b/Directory.Build.props index f403c0f692..b369d6574d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.5.2 + 2025.6.0 Bit.$(MSBuildProjectName) enable @@ -69,5 +69,4 @@ - \ No newline at end of file diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs index ad2d2d2aa1..3c75be756a 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs @@ -287,11 +287,10 @@ public class ProviderService : IProviderService foreach (var user in users) { - if (!keyedFilteredUsers.ContainsKey(user.Id)) + if (!keyedFilteredUsers.TryGetValue(user.Id, out var providerUser)) { continue; } - var providerUser = keyedFilteredUsers[user.Id]; try { if (providerUser.Status != ProviderUserStatusType.Accepted || providerUser.ProviderId != providerId) diff --git a/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/BusinessUnitConverter.cs b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/BusinessUnitConverter.cs index 8f6eb07fe1..8e8a89ae58 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/BusinessUnitConverter.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/BusinessUnitConverter.cs @@ -1,6 +1,5 @@ #nullable enable using System.Diagnostics.CodeAnalysis; -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; @@ -27,7 +26,6 @@ using Stripe; namespace Bit.Commercial.Core.Billing.Providers.Services; -[RequireFeature(FeatureFlagKeys.PM18770_EnableOrganizationBusinessUnitConversion)] public class BusinessUnitConverter( IDataProtectionProvider dataProtectionProvider, GlobalSettings globalSettings, diff --git a/bitwarden_license/src/Scim/.dockerignore b/bitwarden_license/src/Scim/.dockerignore deleted file mode 100644 index fc12f25146..0000000000 --- a/bitwarden_license/src/Scim/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!obj/build-output/publish/* -!obj/Docker/empty/ -!entrypoint.sh diff --git a/bitwarden_license/src/Scim/Dockerfile b/bitwarden_license/src/Scim/Dockerfile index 6970dfa7bb..a0c5c88e49 100644 --- a/bitwarden_license/src/Scim/Dockerfile +++ b/bitwarden_license/src/Scim/Dockerfile @@ -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 +ARG TARGETPLATFORM 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 \ && apt-get install -y --no-install-recommends \ @@ -9,11 +53,10 @@ RUN apt-get update \ krb5-user \ && rm -rf /var/lib/apt/lists/* -ENV ASPNETCORE_URLS http://+:5000 +# Copy app from the build stage WORKDIR /app -EXPOSE 5000 -COPY obj/build-output/publish . -COPY entrypoint.sh / +COPY --from=build /source/bitwarden_license/src/Scim/out /app +COPY ./bitwarden_license/src/Scim/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1 diff --git a/bitwarden_license/src/Scim/Program.cs b/bitwarden_license/src/Scim/Program.cs index 5d7d505aac..92f12f59dd 100644 --- a/bitwarden_license/src/Scim/Program.cs +++ b/bitwarden_license/src/Scim/Program.cs @@ -16,8 +16,8 @@ public class Program { var context = e.Properties["SourceContext"].ToString(); - if (e.Properties.ContainsKey("RequestPath") && - !string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) && + if (e.Properties.TryGetValue("RequestPath", out var requestPath) && + !string.IsNullOrWhiteSpace(requestPath?.ToString()) && (context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer"))) { return false; diff --git a/bitwarden_license/src/Scim/entrypoint.sh b/bitwarden_license/src/Scim/entrypoint.sh index edc3bbe14a..41930504d3 100644 --- a/bitwarden_license/src/Scim/entrypoint.sh +++ b/bitwarden_license/src/Scim/entrypoint.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Setup @@ -19,31 +19,42 @@ then LGID=65534 fi -# Create user and group +if [ "$(id -u)" = "0" ] +then + # Create user and group -groupadd -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 || -usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 -mkhomedir_helper $USERNAME + groupadd -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 || + usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 + mkhomedir_helper $USERNAME -# The rest... + # The rest... -chown -R $USERNAME:$GROUPNAME /app -mkdir -p /etc/bitwarden/core -mkdir -p /etc/bitwarden/logs -mkdir -p /etc/bitwarden/ca-certificates -chown -R $USERNAME:$GROUPNAME /etc/bitwarden + chown -R $USERNAME:$GROUPNAME /app + mkdir -p /etc/bitwarden/core + mkdir -p /etc/bitwarden/logs + mkdir -p /etc/bitwarden/ca-certificates + chown -R $USERNAME:$GROUPNAME /etc/bitwarden -if [[ $globalSettings__selfHosted == "true" ]]; then - cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ - && update-ca-certificates + if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then + chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos + fi + + gosu_cmd="gosu $USERNAME:$GROUPNAME" +else + gosu_cmd="" fi 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 - gosu $USERNAME:$GROUPNAME kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab + cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf + $gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab 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 diff --git a/bitwarden_license/src/Sso/Controllers/AccountController.cs b/bitwarden_license/src/Sso/Controllers/AccountController.cs index f41d2d3c65..5c03ba0017 100644 --- a/bitwarden_license/src/Sso/Controllers/AccountController.cs +++ b/bitwarden_license/src/Sso/Controllers/AccountController.cs @@ -370,8 +370,8 @@ public class AccountController : Controller // for the user identifier. static bool nameIdIsNotTransient(Claim c) => c.Type == ClaimTypes.NameIdentifier && (c.Properties == null - || !c.Properties.ContainsKey(SamlPropertyKeys.ClaimFormat) - || c.Properties[SamlPropertyKeys.ClaimFormat] != SamlNameIdFormats.Transient); + || !c.Properties.TryGetValue(SamlPropertyKeys.ClaimFormat, out var claimFormat) + || claimFormat != SamlNameIdFormats.Transient); // 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 diff --git a/bitwarden_license/src/Sso/Dockerfile b/bitwarden_license/src/Sso/Dockerfile index 6970dfa7bb..d5d012b416 100644 --- a/bitwarden_license/src/Sso/Dockerfile +++ b/bitwarden_license/src/Sso/Dockerfile @@ -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 +ARG TARGETPLATFORM 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 \ && apt-get install -y --no-install-recommends \ @@ -9,11 +53,10 @@ RUN apt-get update \ krb5-user \ && rm -rf /var/lib/apt/lists/* -ENV ASPNETCORE_URLS http://+:5000 +# Copy app from the build stage WORKDIR /app -EXPOSE 5000 -COPY obj/build-output/publish . -COPY entrypoint.sh / +COPY --from=build /source/bitwarden_license/src/Sso/out /app +COPY ./bitwarden_license/src/Sso/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1 diff --git a/bitwarden_license/src/Sso/Program.cs b/bitwarden_license/src/Sso/Program.cs index 051caca9c2..1a8ce6eb88 100644 --- a/bitwarden_license/src/Sso/Program.cs +++ b/bitwarden_license/src/Sso/Program.cs @@ -17,8 +17,8 @@ public class Program logging.AddSerilog(hostingContext, (e, globalSettings) => { var context = e.Properties["SourceContext"].ToString(); - if (e.Properties.ContainsKey("RequestPath") && - !string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) && + if (e.Properties.TryGetValue("RequestPath", out var requestPath) && + !string.IsNullOrWhiteSpace(requestPath?.ToString()) && (context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer"))) { return false; diff --git a/bitwarden_license/src/Sso/Utilities/OpenIdConnectOptionsExtensions.cs b/bitwarden_license/src/Sso/Utilities/OpenIdConnectOptionsExtensions.cs index 9221877a04..825ed74dc8 100644 --- a/bitwarden_license/src/Sso/Utilities/OpenIdConnectOptionsExtensions.cs +++ b/bitwarden_license/src/Sso/Utilities/OpenIdConnectOptionsExtensions.cs @@ -46,9 +46,9 @@ public static class OpenIdConnectOptionsExtensions // Handle State if we've gotten that back 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 diff --git a/bitwarden_license/src/Sso/entrypoint.sh b/bitwarden_license/src/Sso/entrypoint.sh index 2c7bd18b84..c762659fb3 100644 --- a/bitwarden_license/src/Sso/entrypoint.sh +++ b/bitwarden_license/src/Sso/entrypoint.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Setup @@ -19,37 +19,42 @@ then LGID=65534 fi -# Create user and group +if [ "$(id -u)" = "0" ] +then + # Create user and group -groupadd -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 || -usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 -mkhomedir_helper $USERNAME + groupadd -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 || + usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 + mkhomedir_helper $USERNAME -# The rest... + # The rest... -mkdir -p /etc/bitwarden/identity -mkdir -p /etc/bitwarden/core -mkdir -p /etc/bitwarden/logs -mkdir -p /etc/bitwarden/ca-certificates -chown -R $USERNAME:$GROUPNAME /etc/bitwarden + chown -R $USERNAME:$GROUPNAME /app + mkdir -p /etc/bitwarden/core + mkdir -p /etc/bitwarden/logs + mkdir -p /etc/bitwarden/ca-certificates + chown -R $USERNAME:$GROUPNAME /etc/bitwarden -if [[ $globalSettings__selfHosted == "true" ]]; then - cp /etc/bitwarden/identity/identity.pfx /app/identity.pfx -fi + if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then + chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos + fi -chown -R $USERNAME:$GROUPNAME /app - -if [[ $globalSettings__selfHosted == "true" ]]; then - cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ - && update-ca-certificates + gosu_cmd="gosu $USERNAME:$GROUPNAME" +else + gosu_cmd="" fi 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 - gosu $USERNAME:$GROUPNAME kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab + cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf + $gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab 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 diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 601989a473..0ee4aa53a9 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -99,7 +99,7 @@ services: - idp rabbitmq: - image: rabbitmq:management + image: rabbitmq:4.1.0-management container_name: rabbitmq ports: - "5672:5672" @@ -108,7 +108,7 @@ services: RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER} RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS} volumes: - - rabbitmq_data:/var/lib/rabbitmq_data + - rabbitmq_data:/var/lib/rabbitmq profiles: - rabbitmq diff --git a/global.json b/global.json index d04c13bbb5..d25197db39 100644 --- a/global.json +++ b/global.json @@ -5,6 +5,6 @@ }, "msbuild-sdks": { "Microsoft.Build.Traversal": "4.1.0", - "Microsoft.Build.Sql": "0.1.9-preview" + "Microsoft.Build.Sql": "1.0.0" } } diff --git a/src/Admin/.dockerignore b/src/Admin/.dockerignore deleted file mode 100644 index fc12f25146..0000000000 --- a/src/Admin/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!obj/build-output/publish/* -!obj/Docker/empty/ -!entrypoint.sh diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index 8cd2222dbf..6d38a77d8b 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -12,7 +12,6 @@ using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Providers.Services; -using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models.OrganizationConnectionConfigs; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; @@ -20,9 +19,6 @@ using Bit.Core.Repositories; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Bit.Core.Vault.Repositories; using Microsoft.AspNetCore.Authorization; @@ -45,12 +41,9 @@ public class OrganizationsController : Controller private readonly IPaymentService _paymentService; private readonly IApplicationCacheService _applicationCacheService; private readonly GlobalSettings _globalSettings; - private readonly IReferenceEventService _referenceEventService; - private readonly IUserService _userService; private readonly IProviderRepository _providerRepository; private readonly ILogger _logger; private readonly IAccessControlService _accessControlService; - private readonly ICurrentContext _currentContext; private readonly ISecretRepository _secretRepository; private readonly IProjectRepository _projectRepository; private readonly IServiceAccountRepository _serviceAccountRepository; @@ -73,12 +66,9 @@ public class OrganizationsController : Controller IPaymentService paymentService, IApplicationCacheService applicationCacheService, GlobalSettings globalSettings, - IReferenceEventService referenceEventService, - IUserService userService, IProviderRepository providerRepository, ILogger logger, IAccessControlService accessControlService, - ICurrentContext currentContext, ISecretRepository secretRepository, IProjectRepository projectRepository, IServiceAccountRepository serviceAccountRepository, @@ -100,12 +90,9 @@ public class OrganizationsController : Controller _paymentService = paymentService; _applicationCacheService = applicationCacheService; _globalSettings = globalSettings; - _referenceEventService = referenceEventService; - _userService = userService; _providerRepository = providerRepository; _logger = logger; _accessControlService = accessControlService; - _currentContext = currentContext; _secretRepository = secretRepository; _projectRepository = projectRepository; _serviceAccountRepository = serviceAccountRepository; @@ -272,11 +259,6 @@ public class OrganizationsController : Controller await _organizationRepository.ReplaceAsync(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 }); } diff --git a/src/Admin/AdminConsole/Views/Organizations/Edit.cshtml b/src/Admin/AdminConsole/Views/Organizations/Edit.cshtml index f240cb192f..690ee3d778 100644 --- a/src/Admin/AdminConsole/Views/Organizations/Edit.cshtml +++ b/src/Admin/AdminConsole/Views/Organizations/Edit.cshtml @@ -1,13 +1,9 @@ @using Bit.Admin.Enums; @using Bit.Admin.Models -@using Bit.Core @using Bit.Core.AdminConsole.Enums.Provider @using Bit.Core.Billing.Enums @using Bit.Core.Billing.Extensions -@using Bit.Core.Services -@using Microsoft.AspNetCore.Mvc.TagHelpers @inject Bit.Admin.Services.IAccessControlService AccessControlService -@inject IFeatureService FeatureService @model OrganizationEditModel @{ ViewData["Title"] = (Model.Provider != null ? "Client " : string.Empty) + "Organization: " + Model.Name; @@ -19,12 +15,10 @@ var canDelete = AccessControlService.UserHasPermission(Permission.Org_Delete); var canUnlinkFromProvider = AccessControlService.UserHasPermission(Permission.Provider_Edit); - var canConvertToBusinessUnit = - FeatureService.IsEnabled(FeatureFlagKeys.PM18770_EnableOrganizationBusinessUnitConversion) && - AccessControlService.UserHasPermission(Permission.Org_Billing_ConvertToBusinessUnit) && - Model.Organization.PlanType.GetProductTier() == ProductTierType.Enterprise && - !string.IsNullOrEmpty(Model.Organization.GatewaySubscriptionId) && - Model.Provider is null or { Type: ProviderType.BusinessUnit, Status: ProviderStatusType.Pending }; + var canConvertToBusinessUnit = AccessControlService.UserHasPermission(Permission.Org_Billing_ConvertToBusinessUnit) && + Model.Organization.PlanType.GetProductTier() == ProductTierType.Enterprise && + !string.IsNullOrEmpty(Model.Organization.GatewaySubscriptionId) && + Model.Provider is null or { Type: ProviderType.BusinessUnit, Status: ProviderStatusType.Pending }; } @section Scripts { diff --git a/src/Admin/Billing/Controllers/BusinessUnitConversionController.cs b/src/Admin/Billing/Controllers/BusinessUnitConversionController.cs index be3a94949f..9275f41932 100644 --- a/src/Admin/Billing/Controllers/BusinessUnitConversionController.cs +++ b/src/Admin/Billing/Controllers/BusinessUnitConversionController.cs @@ -2,7 +2,6 @@ using Bit.Admin.Billing.Models; using Bit.Admin.Enums; using Bit.Admin.Utilities; -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; @@ -18,7 +17,6 @@ namespace Bit.Admin.Billing.Controllers; [Authorize] [Route("organizations/billing/{organizationId:guid}/business-unit")] -[RequireFeature(FeatureFlagKeys.PM18770_EnableOrganizationBusinessUnitConversion)] public class BusinessUnitConversionController( IBusinessUnitConverter businessUnitConverter, IOrganizationRepository organizationRepository, diff --git a/src/Admin/Dockerfile b/src/Admin/Dockerfile index 79d117681c..d6b42eadfb 100644 --- a/src/Admin/Dockerfile +++ b/src/Admin/Dockerfile @@ -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 +ARG TARGETPLATFORM 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 \ && apt-get install -y --no-install-recommends \ gosu \ curl \ - krb5-user \ && rm -rf /var/lib/apt/lists/* -ENV ASPNETCORE_URLS http://+:5000 +# Copy app from the build stage WORKDIR /app -EXPOSE 5000 -COPY obj/build-output/publish . -COPY entrypoint.sh / +COPY --from=build /source/src/Admin/out /app +COPY ./src/Admin/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh - -HEALTHCHECK CMD curl -f http://localhost:5000 || exit 1 +HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1 ENTRYPOINT ["/entrypoint.sh"] diff --git a/src/Admin/IdentityServer/ReadOnlyEnvIdentityUserStore.cs b/src/Admin/IdentityServer/ReadOnlyEnvIdentityUserStore.cs index 15b8d894b7..89f04230b3 100644 --- a/src/Admin/IdentityServer/ReadOnlyEnvIdentityUserStore.cs +++ b/src/Admin/IdentityServer/ReadOnlyEnvIdentityUserStore.cs @@ -39,7 +39,7 @@ public class ReadOnlyEnvIdentityUserStore : ReadOnlyIdentityUserStore } } - var userStamp = usersDict.ContainsKey(normalizedEmail) ? usersDict[normalizedEmail] : null; + var userStamp = usersDict.GetValueOrDefault(normalizedEmail); if (userStamp == null) { return Task.FromResult(null); diff --git a/src/Admin/Program.cs b/src/Admin/Program.cs index fb5dc7e08b..05bf35d41d 100644 --- a/src/Admin/Program.cs +++ b/src/Admin/Program.cs @@ -20,8 +20,8 @@ public class Program logging.AddSerilog(hostingContext, (e, globalSettings) => { var context = e.Properties["SourceContext"].ToString(); - if (e.Properties.ContainsKey("RequestPath") && - !string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) && + if (e.Properties.TryGetValue("RequestPath", out var requestPath) && + !string.IsNullOrWhiteSpace(requestPath?.ToString()) && (context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer"))) { return false; diff --git a/src/Admin/Services/AccessControlService.cs b/src/Admin/Services/AccessControlService.cs index f45f30e216..a2ba9fa6ff 100644 --- a/src/Admin/Services/AccessControlService.cs +++ b/src/Admin/Services/AccessControlService.cs @@ -29,12 +29,12 @@ public class AccessControlService : IAccessControlService } 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 RolePermissionMapping.RolePermissions[userRole].Contains(permission); + return rolePermissions.Contains(permission); } public string GetUserRole(string userEmail) diff --git a/src/Admin/entrypoint.sh b/src/Admin/entrypoint.sh index 2c564b1ce6..4d7d238d25 100644 --- a/src/Admin/entrypoint.sh +++ b/src/Admin/entrypoint.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Setup @@ -19,31 +19,36 @@ then LGID=65534 fi -# Create user and group +if [ "$(id -u)" = "0" ] +then + # Create user and group -groupadd -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 || -usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 -mkhomedir_helper $USERNAME + groupadd -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 || + usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 + mkhomedir_helper $USERNAME -# The rest... + # The rest... -chown -R $USERNAME:$GROUPNAME /app -mkdir -p /etc/bitwarden/core -mkdir -p /etc/bitwarden/logs -mkdir -p /etc/bitwarden/ca-certificates -chown -R $USERNAME:$GROUPNAME /etc/bitwarden + chown -R $USERNAME:$GROUPNAME /app + mkdir -p /etc/bitwarden/core + mkdir -p /etc/bitwarden/logs + mkdir -p /etc/bitwarden/ca-certificates + chown -R $USERNAME:$GROUPNAME /etc/bitwarden -if [[ $globalSettings__selfHosted == "true" ]]; then - cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ - && update-ca-certificates + if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then + chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos + fi + + gosu_cmd="gosu $USERNAME:$GROUPNAME" +else + gosu_cmd="" fi 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 - gosu $USERNAME:$GROUPNAME kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab + cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf + $gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab fi -exec gosu $USERNAME:$GROUPNAME dotnet /app/Admin.dll +exec $gosu_cmd /app/Admin diff --git a/src/Api/.dockerignore b/src/Api/.dockerignore deleted file mode 100644 index fc12f25146..0000000000 --- a/src/Api/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!obj/build-output/publish/* -!obj/Docker/empty/ -!entrypoint.sh diff --git a/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs b/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs index 357db5ad1e..8d7df4160d 100644 --- a/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs +++ b/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs @@ -25,7 +25,7 @@ public class UpdateTwoFactorAuthenticatorRequestModel : SecretVerificationReques { providers = new Dictionary(); } - else if (providers.ContainsKey(TwoFactorProviderType.Authenticator)) + else { providers.Remove(TwoFactorProviderType.Authenticator); } @@ -62,7 +62,7 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV { providers = []; } - else if (providers.ContainsKey(TwoFactorProviderType.Duo)) + else { providers.Remove(TwoFactorProviderType.Duo); } @@ -88,7 +88,7 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV { providers = []; } - else if (providers.ContainsKey(TwoFactorProviderType.OrganizationDuo)) + else { providers.Remove(TwoFactorProviderType.OrganizationDuo); } @@ -145,7 +145,7 @@ public class UpdateTwoFactorYubicoOtpRequestModel : SecretVerificationRequestMod { providers = new Dictionary(); } - else if (providers.ContainsKey(TwoFactorProviderType.YubiKey)) + else { providers.Remove(TwoFactorProviderType.YubiKey); } @@ -228,7 +228,7 @@ public class TwoFactorEmailRequestModel : SecretVerificationRequestModel { providers = new Dictionary(); } - else if (providers.ContainsKey(TwoFactorProviderType.Email)) + else { providers.Remove(TwoFactorProviderType.Email); } diff --git a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorAuthenticatorResponseModel.cs b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorAuthenticatorResponseModel.cs index f791c6fb1e..71569174a7 100644 --- a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorAuthenticatorResponseModel.cs +++ b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorAuthenticatorResponseModel.cs @@ -13,9 +13,9 @@ public class TwoFactorAuthenticatorResponseModel : ResponseModel ArgumentNullException.ThrowIfNull(user); 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; } else diff --git a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorEmailResponseModel.cs b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorEmailResponseModel.cs index ee1797f83e..d1d87d85b5 100644 --- a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorEmailResponseModel.cs +++ b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorEmailResponseModel.cs @@ -15,9 +15,9 @@ public class TwoFactorEmailResponseModel : ResponseModel } 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; } else diff --git a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorYubiKeyResponseModel.cs b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorYubiKeyResponseModel.cs index 014863497d..0a97367017 100644 --- a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorYubiKeyResponseModel.cs +++ b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorYubiKeyResponseModel.cs @@ -19,29 +19,29 @@ public class TwoFactorYubiKeyResponseModel : ResponseModel { 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 diff --git a/src/Api/Billing/Controllers/AccountsController.cs b/src/Api/Billing/Controllers/AccountsController.cs index 49ff679bb8..10d386641d 100644 --- a/src/Api/Billing/Controllers/AccountsController.cs +++ b/src/Api/Billing/Controllers/AccountsController.cs @@ -6,14 +6,10 @@ using Bit.Api.Utilities; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Models; using Bit.Core.Billing.Services; -using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -161,8 +157,6 @@ public class AccountsController( [HttpPost("cancel")] public async Task PostCancelAsync( [FromBody] SubscriptionCancellationRequestModel request, - [FromServices] ICurrentContext currentContext, - [FromServices] IReferenceEventService referenceEventService, [FromServices] ISubscriberService subscriberService) { var user = await userService.GetUserByPrincipalAsync(User); @@ -175,12 +169,6 @@ public class AccountsController( await subscriberService.CancelSubscription(user, new OffboardingSurveyResponse { UserId = user.Id, Reason = request.Reason, Feedback = request.Feedback }, user.IsExpired()); - - await referenceEventService.RaiseEventAsync(new ReferenceEvent( - ReferenceEventType.CancelSubscription, - user, - currentContext) - { EndOfPeriod = user.IsExpired() }); } [HttpPost("reinstate-premium")] diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 094ca0a435..071aae5060 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -4,7 +4,6 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; using Bit.Api.Billing.Queries.Organizations; -using Bit.Core; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Pricing; @@ -25,7 +24,6 @@ namespace Bit.Api.Billing.Controllers; public class OrganizationBillingController( IBusinessUnitConverter businessUnitConverter, ICurrentContext currentContext, - IFeatureService featureService, IOrganizationBillingService organizationBillingService, IOrganizationRepository organizationRepository, IOrganizationWarningsQuery organizationWarningsQuery, @@ -318,14 +316,6 @@ public class OrganizationBillingController( [FromRoute] Guid organizationId, [FromBody] SetupBusinessUnitRequestBody requestBody) { - var enableOrganizationBusinessUnitConversion = - featureService.IsEnabled(FeatureFlagKeys.PM18770_EnableOrganizationBusinessUnitConversion); - - if (!enableOrganizationBusinessUnitConversion) - { - return Error.NotFound(); - } - var organization = await organizationRepository.GetByIdAsync(organizationId); if (organization == null) diff --git a/src/Api/Billing/Controllers/OrganizationsController.cs b/src/Api/Billing/Controllers/OrganizationsController.cs index bd5ab8cef4..c8a3c20c91 100644 --- a/src/Api/Billing/Controllers/OrganizationsController.cs +++ b/src/Api/Billing/Controllers/OrganizationsController.cs @@ -20,9 +20,6 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -44,7 +41,6 @@ public class OrganizationsController( IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand, IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand, - IReferenceEventService referenceEventService, ISubscriberService subscriberService, IOrganizationInstallationRepository organizationInstallationRepository, IPricingClient pricingClient) @@ -246,14 +242,6 @@ public class OrganizationsController( Feedback = request.Feedback }, organization.IsExpired()); - - await referenceEventService.RaiseEventAsync(new ReferenceEvent( - ReferenceEventType.CancelSubscription, - organization, - currentContext) - { - EndOfPeriod = organization.IsExpired() - }); } [HttpPost("{id:guid}/reinstate")] diff --git a/src/Api/Billing/Controllers/ProviderBillingController.cs b/src/Api/Billing/Controllers/ProviderBillingController.cs index 37130d54ce..1309b2df6d 100644 --- a/src/Api/Billing/Controllers/ProviderBillingController.cs +++ b/src/Api/Billing/Controllers/ProviderBillingController.cs @@ -81,13 +81,6 @@ public class ProviderBillingController( [FromRoute] Guid providerId, [FromBody] UpdatePaymentMethodRequestBody requestBody) { - var allowProviderPaymentMethod = featureService.IsEnabled(FeatureFlagKeys.PM18794_ProviderPaymentMethod); - - if (!allowProviderPaymentMethod) - { - return TypedResults.NotFound(); - } - var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId); if (provider == null) @@ -111,13 +104,6 @@ public class ProviderBillingController( [FromRoute] Guid providerId, [FromBody] VerifyBankAccountRequestBody requestBody) { - var allowProviderPaymentMethod = featureService.IsEnabled(FeatureFlagKeys.PM18794_ProviderPaymentMethod); - - if (!allowProviderPaymentMethod) - { - return TypedResults.NotFound(); - } - var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId); if (provider == null) diff --git a/src/Api/Dockerfile b/src/Api/Dockerfile index 6970dfa7bb..29adde878c 100644 --- a/src/Api/Dockerfile +++ b/src/Api/Dockerfile @@ -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 +ARG TARGETPLATFORM 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 \ && apt-get install -y --no-install-recommends \ @@ -9,13 +53,11 @@ RUN apt-get update \ krb5-user \ && rm -rf /var/lib/apt/lists/* -ENV ASPNETCORE_URLS http://+:5000 +# Copy app from the build stage WORKDIR /app -EXPOSE 5000 -COPY obj/build-output/publish . -COPY entrypoint.sh / +COPY --from=build /source/src/Api/out /app +COPY ./src/Api/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh - HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1 ENTRYPOINT ["/entrypoint.sh"] diff --git a/src/Api/SecretsManager/Controllers/SecretsController.cs b/src/Api/SecretsManager/Controllers/SecretsController.cs index 9997e7502c..dd653bb873 100644 --- a/src/Api/SecretsManager/Controllers/SecretsController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsController.cs @@ -5,7 +5,6 @@ using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Identity; -using Bit.Core.Repositories; using Bit.Core.SecretsManager.AuthorizationRequirements; using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; 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.Repositories; 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.Mvc; @@ -30,7 +26,6 @@ public class SecretsController : Controller private readonly ICurrentContext _currentContext; private readonly IProjectRepository _projectRepository; private readonly ISecretRepository _secretRepository; - private readonly IOrganizationRepository _organizationRepository; private readonly ICreateSecretCommand _createSecretCommand; private readonly IUpdateSecretCommand _updateSecretCommand; private readonly IDeleteSecretCommand _deleteSecretCommand; @@ -39,14 +34,12 @@ public class SecretsController : Controller private readonly ISecretAccessPoliciesUpdatesQuery _secretAccessPoliciesUpdatesQuery; private readonly IUserService _userService; private readonly IEventService _eventService; - private readonly IReferenceEventService _referenceEventService; private readonly IAuthorizationService _authorizationService; public SecretsController( ICurrentContext currentContext, IProjectRepository projectRepository, ISecretRepository secretRepository, - IOrganizationRepository organizationRepository, ICreateSecretCommand createSecretCommand, IUpdateSecretCommand updateSecretCommand, IDeleteSecretCommand deleteSecretCommand, @@ -55,13 +48,11 @@ public class SecretsController : Controller ISecretAccessPoliciesUpdatesQuery secretAccessPoliciesUpdatesQuery, IUserService userService, IEventService eventService, - IReferenceEventService referenceEventService, IAuthorizationService authorizationService) { _currentContext = currentContext; _projectRepository = projectRepository; _secretRepository = secretRepository; - _organizationRepository = organizationRepository; _createSecretCommand = createSecretCommand; _updateSecretCommand = updateSecretCommand; _deleteSecretCommand = deleteSecretCommand; @@ -70,7 +61,6 @@ public class SecretsController : Controller _secretAccessPoliciesUpdatesQuery = secretAccessPoliciesUpdatesQuery; _userService = userService; _eventService = eventService; - _referenceEventService = referenceEventService; _authorizationService = authorizationService; } @@ -148,9 +138,6 @@ public class SecretsController : Controller if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount) { 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); @@ -266,7 +253,7 @@ public class SecretsController : Controller throw new NotFoundException(); } - await LogSecretsRetrievalAsync(secrets.First().OrganizationId, secrets); + await LogSecretsRetrievalAsync(secrets); var responses = secrets.Select(s => new BaseSecretResponseModel(s)); return new ListResponseModel(responses); @@ -303,21 +290,18 @@ public class SecretsController : Controller if (syncResult.HasChanges) { - await LogSecretsRetrievalAsync(organizationId, syncResult.Secrets); + await LogSecretsRetrievalAsync(syncResult.Secrets); } return new SecretsSyncResponseModel(syncResult.HasChanges, syncResult.Secrets); } - private async Task LogSecretsRetrievalAsync(Guid organizationId, IEnumerable secrets) + private async Task LogSecretsRetrievalAsync(IEnumerable secrets) { if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount) { var userId = _userService.GetProperUserId(User)!.Value; - var org = await _organizationRepository.GetByIdAsync(organizationId); await _eventService.LogServiceAccountSecretsEventAsync(userId, secrets, EventType.Secret_Retrieved); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.SmServiceAccountAccessedSecret, org, _currentContext)); } } } diff --git a/src/Api/Tools/Controllers/SendsController.cs b/src/Api/Tools/Controllers/SendsController.cs index b18e603c0f..a51ec942cf 100644 --- a/src/Api/Tools/Controllers/SendsController.cs +++ b/src/Api/Tools/Controllers/SendsController.cs @@ -5,7 +5,6 @@ using Bit.Api.Tools.Models.Request; using Bit.Api.Tools.Models.Response; using Bit.Api.Utilities; using Bit.Core; -using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Core.Settings; @@ -33,7 +32,6 @@ public class SendsController : Controller private readonly INonAnonymousSendCommand _nonAnonymousSendCommand; private readonly ILogger _logger; private readonly GlobalSettings _globalSettings; - private readonly ICurrentContext _currentContext; public SendsController( ISendRepository sendRepository, @@ -43,8 +41,7 @@ public class SendsController : Controller INonAnonymousSendCommand nonAnonymousSendCommand, ISendFileStorageService sendFileStorageService, ILogger logger, - GlobalSettings globalSettings, - ICurrentContext currentContext) + GlobalSettings globalSettings) { _sendRepository = sendRepository; _userService = userService; @@ -54,7 +51,6 @@ public class SendsController : Controller _sendFileStorageService = sendFileStorageService; _logger = logger; _globalSettings = globalSettings; - _currentContext = currentContext; } #region Anonymous endpoints diff --git a/src/Api/Utilities/ApiHelpers.cs b/src/Api/Utilities/ApiHelpers.cs index f4f1830e16..2c6dc8b73b 100644 --- a/src/Api/Utilities/ApiHelpers.cs +++ b/src/Api/Utilities/ApiHelpers.cs @@ -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); } } diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 251362589e..7b302f3724 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -1064,7 +1064,7 @@ public class CiphersController : Controller [HttpPut("share")] [HttpPost("share")] - public async Task PutShareMany([FromBody] CipherBulkShareRequestModel model) + public async Task> PutShareMany([FromBody] CipherBulkShareRequestModel model) { var organizationId = new Guid(model.Ciphers.First().OrganizationId); if (!await _currentContext.OrganizationUser(organizationId)) @@ -1073,38 +1073,41 @@ public class CiphersController : Controller } var userId = _userService.GetProperUserId(User).Value; + var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: false); var ciphersDict = ciphers.ToDictionary(c => c.Id); // Validate the model was encrypted for the posting user 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<(CipherDetails, DateTime?)>(); 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); - shareCiphers.Add((cipher.ToCipher(existingCipher), cipher.LastKnownRevisionDate)); + shareCiphers.Add((cipher.ToCipherDetails(existingCipher), cipher.LastKnownRevisionDate)); } - await _cipherService.ShareManyAsync(shareCiphers, organizationId, - model.CollectionIds.Select(c => new Guid(c)), userId); + var updated = await _cipherService.ShareManyAsync( + shareCiphers, + organizationId, + model.CollectionIds.Select(Guid.Parse), + userId + ); + + var response = updated.Select(c => new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp)); + return new ListResponseModel(response); } [HttpPost("purge")] @@ -1186,14 +1189,14 @@ public class CiphersController : Controller var cipher = await GetByIdAsync(id, userId); 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(); } return new AttachmentUploadDataResponseModel { - Url = await _attachmentStorageService.GetAttachmentUploadUrlAsync(cipher, attachments[attachmentId]), + Url = await _attachmentStorageService.GetAttachmentUploadUrlAsync(cipher, attachment), FileUploadType = _attachmentStorageService.FileUploadType, }; } @@ -1212,11 +1215,10 @@ public class CiphersController : Controller var userId = _userService.GetProperUserId(User).Value; var cipher = await GetByIdAsync(id, userId); var attachments = cipher?.GetAttachments(); - if (attachments == null || !attachments.ContainsKey(attachmentId)) + if (attachments == null || !attachments.TryGetValue(attachmentId, out var attachmentData)) { throw new NotFoundException(); } - var attachmentData = attachments[attachmentId]; await Request.GetFileAsync(async (stream) => { @@ -1366,7 +1368,7 @@ public class CiphersController : Controller var cipher = await _cipherRepository.GetByIdAsync(new Guid(cipherId)); var attachments = cipher?.GetAttachments() ?? new Dictionary(); - 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) { @@ -1376,7 +1378,7 @@ public class CiphersController : Controller return; } - await _cipherService.ValidateCipherAttachmentFile(cipher, attachments[attachmentId]); + await _cipherService.ValidateCipherAttachmentFile(cipher, attachment); } catch (Exception e) { diff --git a/src/Api/Vault/Models/Request/CipherRequestModel.cs b/src/Api/Vault/Models/Request/CipherRequestModel.cs index 5c288ab66d..229d27e484 100644 --- a/src/Api/Vault/Models/Request/CipherRequestModel.cs +++ b/src/Api/Vault/Models/Request/CipherRequestModel.cs @@ -113,18 +113,25 @@ public class CipherRequestModel 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.Key = attachment2.Key; } } 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; } } diff --git a/src/Api/Vault/Models/Response/CipherResponseModel.cs b/src/Api/Vault/Models/Response/CipherResponseModel.cs index 358da3e62a..240783837e 100644 --- a/src/Api/Vault/Models/Response/CipherResponseModel.cs +++ b/src/Api/Vault/Models/Response/CipherResponseModel.cs @@ -129,13 +129,13 @@ public class CipherDetailsResponseModel : CipherResponseModel IDictionary> collectionCiphers, string obj = "cipherDetails") : 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 { - CollectionIds = new Guid[] { }; + CollectionIds = []; } } @@ -147,7 +147,7 @@ public class CipherDetailsResponseModel : CipherResponseModel IEnumerable collectionCiphers, string obj = "cipherDetails") : base(cipher, user, organizationAbilities, globalSettings, obj) { - CollectionIds = collectionCiphers?.Select(c => c.CollectionId) ?? new List(); + CollectionIds = collectionCiphers?.Select(c => c.CollectionId) ?? []; } public CipherDetailsResponseModel( @@ -158,7 +158,7 @@ public class CipherDetailsResponseModel : CipherResponseModel string obj = "cipherDetails") : base(cipher, user, organizationAbilities, globalSettings, obj) { - CollectionIds = cipher.CollectionIds ?? new List(); + CollectionIds = cipher.CollectionIds ?? []; } public IEnumerable CollectionIds { get; set; } @@ -170,13 +170,13 @@ public class CipherMiniDetailsResponseModel : CipherMiniResponseModel IDictionary> collectionCiphers, bool orgUseTotp, string obj = "cipherMiniDetails") : 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 { - CollectionIds = new Guid[] { }; + CollectionIds = []; } } @@ -184,7 +184,7 @@ public class CipherMiniDetailsResponseModel : CipherMiniResponseModel GlobalSettings globalSettings, bool orgUseTotp, string obj = "cipherMiniDetails") : base(cipher, globalSettings, orgUseTotp, obj) { - CollectionIds = cipher.CollectionIds ?? new List(); + CollectionIds = cipher.CollectionIds ?? []; } public CipherMiniDetailsResponseModel(CipherOrganizationDetailsWithCollections cipher, diff --git a/src/Api/entrypoint.sh b/src/Api/entrypoint.sh index 37d117215c..d89a4648ec 100644 --- a/src/Api/entrypoint.sh +++ b/src/Api/entrypoint.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Setup @@ -19,31 +19,36 @@ then LGID=65534 fi -# Create user and group +if [ "$(id -u)" = "0" ] +then + # Create user and group -groupadd -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 || -usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 -mkhomedir_helper $USERNAME + groupadd -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 || + usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 + mkhomedir_helper $USERNAME -# The rest... + # The rest... -chown -R $USERNAME:$GROUPNAME /app -mkdir -p /etc/bitwarden/core -mkdir -p /etc/bitwarden/logs -mkdir -p /etc/bitwarden/ca-certificates -chown -R $USERNAME:$GROUPNAME /etc/bitwarden + chown -R $USERNAME:$GROUPNAME /app + mkdir -p /etc/bitwarden/core + mkdir -p /etc/bitwarden/logs + mkdir -p /etc/bitwarden/ca-certificates + chown -R $USERNAME:$GROUPNAME /etc/bitwarden -if [[ $globalSettings__selfHosted == "true" ]]; then - cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ - && update-ca-certificates + if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then + chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos + fi + + gosu_cmd="gosu $USERNAME:$GROUPNAME" +else + gosu_cmd="" fi 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 - gosu $USERNAME:$GROUPNAME kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab + cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf + $gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab fi -exec gosu $USERNAME:$GROUPNAME dotnet /app/Api.dll +exec $gosu_cmd /app/Api diff --git a/src/Billing/.dockerignore b/src/Billing/.dockerignore deleted file mode 100644 index fc12f25146..0000000000 --- a/src/Billing/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!obj/build-output/publish/* -!obj/Docker/empty/ -!entrypoint.sh diff --git a/src/Billing/Controllers/AppleController.cs b/src/Billing/Controllers/AppleController.cs index 1bcbbf2ad6..5c231de8ed 100644 --- a/src/Billing/Controllers/AppleController.cs +++ b/src/Billing/Controllers/AppleController.cs @@ -28,8 +28,8 @@ public class AppleController : Controller return new BadRequestResult(); } - var key = HttpContext.Request.Query.ContainsKey("key") ? - HttpContext.Request.Query["key"].ToString() : null; + var key = HttpContext.Request.Query.TryGetValue("key", out var keyValue) ? + keyValue.ToString() : null; if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.AppleWebhookKey)) { return new BadRequestResult(); diff --git a/src/Billing/Controllers/PayPalController.cs b/src/Billing/Controllers/PayPalController.cs index 2afde80601..36987c6e44 100644 --- a/src/Billing/Controllers/PayPalController.cs +++ b/src/Billing/Controllers/PayPalController.cs @@ -51,8 +51,8 @@ public class PayPalController : Controller [HttpPost("ipn")] public async Task PostIpn() { - var key = HttpContext.Request.Query.ContainsKey("key") - ? HttpContext.Request.Query["key"].ToString() + var key = HttpContext.Request.Query.TryGetValue("key", out var keyValue) + ? keyValue.ToString() : null; if (string.IsNullOrEmpty(key)) diff --git a/src/Billing/Dockerfile b/src/Billing/Dockerfile index 9abbe16477..5eb4e9c0e0 100644 --- a/src/Billing/Dockerfile +++ b/src/Billing/Dockerfile @@ -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 +ARG TARGETPLATFORM 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 \ && apt-get install -y --no-install-recommends \ @@ -8,14 +52,11 @@ RUN apt-get update \ curl \ && rm -rf /var/lib/apt/lists/* -ENV ASPNETCORE_URLS http://+:5000 +# Copy app from the build stage WORKDIR /app -EXPOSE 5000 -COPY entrypoint.sh / +COPY --from=build /source/src/Billing/out /app +COPY ./src/Billing/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh - -COPY obj/build-output/publish . - HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1 ENTRYPOINT ["/entrypoint.sh"] diff --git a/src/Billing/Program.cs b/src/Billing/Program.cs index 33e2665427..3e005ce7fd 100644 --- a/src/Billing/Program.cs +++ b/src/Billing/Program.cs @@ -20,8 +20,8 @@ public class Program return e.Level >= globalSettings.MinLogLevel.BillingSettings.Jobs; } - if (e.Properties.ContainsKey("RequestPath") && - !string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) && + if (e.Properties.TryGetValue("RequestPath", out var requestPath) && + !string.IsNullOrWhiteSpace(requestPath?.ToString()) && (context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer"))) { return false; diff --git a/src/Billing/Services/Implementations/CustomerUpdatedHandler.cs b/src/Billing/Services/Implementations/CustomerUpdatedHandler.cs index 6deb0bc330..fe7745f760 100644 --- a/src/Billing/Services/Implementations/CustomerUpdatedHandler.cs +++ b/src/Billing/Services/Implementations/CustomerUpdatedHandler.cs @@ -1,8 +1,4 @@ -using Bit.Core.Context; -using Bit.Core.Repositories; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; +using Bit.Core.Repositories; using Event = Stripe.Event; namespace Bit.Billing.Services.Implementations; @@ -10,23 +6,17 @@ namespace Bit.Billing.Services.Implementations; public class CustomerUpdatedHandler : ICustomerUpdatedHandler { private readonly IOrganizationRepository _organizationRepository; - private readonly IReferenceEventService _referenceEventService; - private readonly ICurrentContext _currentContext; private readonly IStripeEventService _stripeEventService; private readonly IStripeEventUtilityService _stripeEventUtilityService; private readonly ILogger _logger; public CustomerUpdatedHandler( IOrganizationRepository organizationRepository, - IReferenceEventService referenceEventService, - ICurrentContext currentContext, IStripeEventService stripeEventService, IStripeEventUtilityService stripeEventUtilityService, ILogger logger) { _organizationRepository = organizationRepository ?? throw new ArgumentNullException(nameof(organizationRepository)); - _referenceEventService = referenceEventService; - _currentContext = currentContext; _stripeEventService = stripeEventService; _stripeEventUtilityService = stripeEventUtilityService; _logger = logger; @@ -95,20 +85,5 @@ public class CustomerUpdatedHandler : ICustomerUpdatedHandler organization.BillingEmail = customer.Email; 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)); } } diff --git a/src/Billing/Services/Implementations/PaymentSucceededHandler.cs b/src/Billing/Services/Implementations/PaymentSucceededHandler.cs index 40d8c8349d..4c256e3d85 100644 --- a/src/Billing/Services/Implementations/PaymentSucceededHandler.cs +++ b/src/Billing/Services/Implementations/PaymentSucceededHandler.cs @@ -3,13 +3,9 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; -using Bit.Core.Context; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Event = Stripe.Event; namespace Bit.Billing.Services.Implementations; @@ -22,9 +18,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler private readonly IStripeFacade _stripeFacade; private readonly IProviderRepository _providerRepository; private readonly IOrganizationRepository _organizationRepository; - private readonly IReferenceEventService _referenceEventService; - private readonly ICurrentContext _currentContext; - private readonly IUserRepository _userRepository; private readonly IStripeEventUtilityService _stripeEventUtilityService; private readonly IPushNotificationService _pushNotificationService; private readonly IOrganizationEnableCommand _organizationEnableCommand; @@ -36,9 +29,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler IStripeFacade stripeFacade, IProviderRepository providerRepository, IOrganizationRepository organizationRepository, - IReferenceEventService referenceEventService, - ICurrentContext currentContext, - IUserRepository userRepository, IStripeEventUtilityService stripeEventUtilityService, IUserService userService, IPushNotificationService pushNotificationService, @@ -50,9 +40,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler _stripeFacade = stripeFacade; _providerRepository = providerRepository; _organizationRepository = organizationRepository; - _referenceEventService = referenceEventService; - _currentContext = currentContext; - _userRepository = userRepository; _stripeEventUtilityService = stripeEventUtilityService; _userService = userService; _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", parsedEvent.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) { @@ -156,15 +123,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler await _organizationEnableCommand.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd); 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) { @@ -174,14 +132,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler } 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, - }); } } } diff --git a/src/Billing/entrypoint.sh b/src/Billing/entrypoint.sh index 6d98cfa6f6..66540416f5 100644 --- a/src/Billing/entrypoint.sh +++ b/src/Billing/entrypoint.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Setup @@ -19,25 +19,27 @@ then LGID=65534 fi -# Create user and group +if [ "$(id -u)" = "0" ] +then + # Create user and group -groupadd -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 || -usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 -mkhomedir_helper $USERNAME + groupadd -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 || + usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 + mkhomedir_helper $USERNAME -# The rest... + # The rest... -chown -R $USERNAME:$GROUPNAME /app -mkdir -p /etc/bitwarden/core -mkdir -p /etc/bitwarden/logs -mkdir -p /etc/bitwarden/ca-certificates -chown -R $USERNAME:$GROUPNAME /etc/bitwarden + chown -R $USERNAME:$GROUPNAME /app + mkdir -p /etc/bitwarden/core + mkdir -p /etc/bitwarden/logs + mkdir -p /etc/bitwarden/ca-certificates + chown -R $USERNAME:$GROUPNAME /etc/bitwarden -if [[ $globalSettings__selfHosted == "true" ]]; then - cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ - && update-ca-certificates + gosu_cmd="gosu $USERNAME:$GROUPNAME" +else + gosu_cmd="" fi -exec gosu $USERNAME:$GROUPNAME dotnet /app/Billing.dll +exec $gosu_cmd /app/Billing diff --git a/src/Core/AdminConsole/Entities/Organization.cs b/src/Core/AdminConsole/Entities/Organization.cs index e649406bb0..274c7f8ddb 100644 --- a/src/Core/AdminConsole/Entities/Organization.cs +++ b/src/Core/AdminConsole/Entities/Organization.cs @@ -8,14 +8,13 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.Services; -using Bit.Core.Tools.Entities; using Bit.Core.Utilities; #nullable enable namespace Bit.Core.AdminConsole.Entities; -public class Organization : ITableObject, IStorableSubscriber, IRevisable, IReferenceable +public class Organization : ITableObject, IStorableSubscriber, IRevisable { private Dictionary? _twoFactorProviders; @@ -258,12 +257,12 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable, public bool TwoFactorProviderIsEnabled(TwoFactorProviderType provider) { var providers = GetTwoFactorProviders(); - if (providers == null || !providers.ContainsKey(provider)) + if (providers == null || !providers.TryGetValue(provider, out var twoFactorProvider)) { return false; } - return providers[provider].Enabled && Use2fa; + return twoFactorProvider.Enabled && Use2fa; } public bool TwoFactorIsEnabled() @@ -280,12 +279,7 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable, public TwoFactorProvider? GetTwoFactorProvider(TwoFactorProviderType provider) { var providers = GetTwoFactorProviders(); - if (providers == null || !providers.ContainsKey(provider)) - { - return null; - } - - return providers[provider]; + return providers?.GetValueOrDefault(provider); } public void UpdateFromLicense(OrganizationLicense license, IFeatureService featureService) diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs b/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs index bd1f280cad..c94794765b 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs @@ -1,12 +1,15 @@ -using Bit.Core.Enums; +#nullable enable + +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; } + string MessageId { get; set; } + int RetryCount { get; } + DateTime? DelayUntilDate { get; } void ApplyRetry(DateTime? handlerDelayUntilDate); string ToJson(); } diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs index d2f0bde693..ecf5d25c51 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs @@ -1,4 +1,6 @@ -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public class IntegrationHandlerResult { diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs index 1f288914d0..018d453cb9 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs @@ -1,13 +1,15 @@ -using System.Text.Json; +#nullable enable + +using System.Text.Json; using Bit.Core.Enums; namespace Bit.Core.AdminConsole.Models.Data.Integrations; -public class IntegrationMessage : IIntegrationMessage +public class IntegrationMessage : IIntegrationMessage { public IntegrationType IntegrationType { get; set; } - public T Configuration { get; set; } - public string RenderedTemplate { get; set; } + public required string MessageId { get; set; } + public required string RenderedTemplate { get; set; } public int RetryCount { get; set; } = 0; public DateTime? DelayUntilDate { get; set; } @@ -22,12 +24,22 @@ public class IntegrationMessage : IIntegrationMessage DelayUntilDate = baseTime.AddSeconds(backoffSeconds + jitterSeconds); } - public string ToJson() + public virtual string ToJson() + { + return JsonSerializer.Serialize(this); + } +} + +public class IntegrationMessage : IntegrationMessage +{ + public required T Configuration { get; set; } + + public override string ToJson() { return JsonSerializer.Serialize(this); } - public static IntegrationMessage FromJson(string json) + public static IntegrationMessage? FromJson(string json) { return JsonSerializer.Deserialize>(json); } diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs index 4fcce542ce..4f2c434ff6 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs @@ -1,3 +1,5 @@ -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record SlackIntegration(string token); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs index 2930004cbf..18b13248ec 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs @@ -1,3 +1,5 @@ -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record SlackIntegrationConfiguration(string channelId); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs index b81e50d403..a9b4150419 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs @@ -1,3 +1,5 @@ -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record SlackIntegrationConfigurationDetails(string channelId, string token); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs index e8217d3ad3..47e014ee2a 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs @@ -1,3 +1,5 @@ -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record WebhookIntegrationConfiguration(string url); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs index e3e92c900f..c4c41db24f 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs @@ -1,3 +1,5 @@ -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record WebhookIntegrationConfigurationDetails(string url); diff --git a/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs b/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs index 59debed746..ede2123f7e 100644 --- a/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs +++ b/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs @@ -1,4 +1,5 @@ - +#nullable enable + using System.Text.Json.Serialization; namespace Bit.Core.Models.Slack; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Groups/CreateGroupCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Groups/CreateGroupCommand.cs index 11bf6d7f66..f514beed38 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Groups/CreateGroupCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Groups/CreateGroupCommand.cs @@ -1,15 +1,11 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Repositories; 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; @@ -18,21 +14,16 @@ public class CreateGroupCommand : ICreateGroupCommand private readonly IEventService _eventService; private readonly IGroupRepository _groupRepository; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IReferenceEventService _referenceEventService; - private readonly ICurrentContext _currentContext; public CreateGroupCommand( IEventService eventService, IGroupRepository groupRepository, - IOrganizationUserRepository organizationUserRepository, - IReferenceEventService referenceEventService, - ICurrentContext currentContext) + IOrganizationUserRepository organizationUserRepository + ) { _eventService = eventService; _groupRepository = groupRepository; _organizationUserRepository = organizationUserRepository; - _referenceEventService = referenceEventService; - _currentContext = currentContext; } public async Task CreateGroupAsync(Group group, Organization organization, @@ -77,8 +68,6 @@ public class CreateGroupCommand : ICreateGroupCommand { await _groupRepository.CreateAsync(group, collections); } - - await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.GroupCreated, organization, _currentContext)); } private async Task GroupRepositoryUpdateUsersAsync(Group group, IEnumerable userIds, diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedOrganizationUserAccountCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedOrganizationUserAccountCommand.cs index 49ddf0a548..60a1c8bfbf 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedOrganizationUserAccountCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedOrganizationUserAccountCommand.cs @@ -7,9 +7,6 @@ using Bit.Core.Exceptions; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; #nullable enable @@ -24,7 +21,6 @@ public class DeleteClaimedOrganizationUserAccountCommand : IDeleteClaimedOrganiz private readonly IUserRepository _userRepository; private readonly ICurrentContext _currentContext; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; - private readonly IReferenceEventService _referenceEventService; private readonly IPushNotificationService _pushService; private readonly IOrganizationRepository _organizationRepository; private readonly IProviderUserRepository _providerUserRepository; @@ -36,7 +32,6 @@ public class DeleteClaimedOrganizationUserAccountCommand : IDeleteClaimedOrganiz IUserRepository userRepository, ICurrentContext currentContext, IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, - IReferenceEventService referenceEventService, IPushNotificationService pushService, IOrganizationRepository organizationRepository, IProviderUserRepository providerUserRepository) @@ -48,7 +43,6 @@ public class DeleteClaimedOrganizationUserAccountCommand : IDeleteClaimedOrganiz _userRepository = userRepository; _currentContext = currentContext; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; - _referenceEventService = referenceEventService; _pushService = pushService; _organizationRepository = organizationRepository; _providerUserRepository = providerUserRepository; @@ -195,8 +189,6 @@ public class DeleteClaimedOrganizationUserAccountCommand : IDeleteClaimedOrganiz await _userRepository.DeleteManyAsync(users); foreach (var user in users) { - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.DeleteAccount, user, _currentContext)); await _pushService.PushLogOutAsync(user.Id); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs index 072bc5fc05..db5d011e1d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs @@ -9,15 +9,11 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Utilities.Commands; using Bit.Core.AdminConsole.Utilities.Errors; using Bit.Core.AdminConsole.Utilities.Validation; -using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; 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 OrganizationUserInvite = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite; @@ -28,8 +24,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService, IInviteUsersValidator inviteUsersValidator, IPaymentService paymentService, IOrganizationRepository organizationRepository, - IReferenceEventService referenceEventService, - ICurrentContext currentContext, IApplicationCacheService applicationCacheService, IMailService mailService, ILogger logger, @@ -121,8 +115,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService, await SendAdditionalEmailsAsync(validatedRequest, organization); await SendInvitesAsync(organizationUserToInviteEntities, organization); - - await PublishReferenceEventAsync(validatedRequest, organization); } catch (Exception ex) { @@ -190,14 +182,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService, } } - private async Task PublishReferenceEventAsync(Valid validatedResult, - Organization organization) => - await referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.InvitedUsers, organization, currentContext) - { - Users = validatedResult.Value.Invites.Length - }); - private async Task SendInvitesAsync(IEnumerable users, Organization organization) => await sendOrganizationInvitesCommand.SendInvitesAsync( new SendInvitesRequest( @@ -284,15 +268,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService, await organizationRepository.ReplaceAsync(organization); // could optimize this with only a property update 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 - }); } } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs index 7449628ed0..f26061cbd2 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs @@ -5,7 +5,6 @@ using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; -using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -15,9 +14,6 @@ using Bit.Core.Models.StaticStore; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; @@ -36,8 +32,6 @@ public class CloudOrganizationSignUpCommand( IOrganizationBillingService organizationBillingService, IPaymentService paymentService, IPolicyService policyService, - IReferenceEventService referenceEventService, - ICurrentContext currentContext, IOrganizationRepository organizationRepository, IOrganizationApiKeyRepository organizationApiKeyRepository, IApplicationCacheService applicationCacheService, @@ -132,17 +126,6 @@ public class CloudOrganizationSignUpCommand( var ownerId = signup.IsFromProvider ? default : signup.Owner.Id; 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); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDeleteCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDeleteCommand.cs index 185d5c5ac0..6a81130402 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDeleteCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDeleteCommand.cs @@ -2,38 +2,28 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; -using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Repositories; 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; public class OrganizationDeleteCommand : IOrganizationDeleteCommand { private readonly IApplicationCacheService _applicationCacheService; - private readonly ICurrentContext _currentContext; private readonly IOrganizationRepository _organizationRepository; private readonly IPaymentService _paymentService; - private readonly IReferenceEventService _referenceEventService; private readonly ISsoConfigRepository _ssoConfigRepository; public OrganizationDeleteCommand( IApplicationCacheService applicationCacheService, - ICurrentContext currentContext, IOrganizationRepository organizationRepository, IPaymentService paymentService, - IReferenceEventService referenceEventService, ISsoConfigRepository ssoConfigRepository) { _applicationCacheService = applicationCacheService; - _currentContext = currentContext; _organizationRepository = organizationRepository; _paymentService = paymentService; - _referenceEventService = referenceEventService; _ssoConfigRepository = ssoConfigRepository; } @@ -48,8 +38,6 @@ public class OrganizationDeleteCommand : IOrganizationDeleteCommand var eop = !organization.ExpirationDate.HasValue || organization.ExpirationDate.Value >= DateTime.UtcNow; await _paymentService.CancelSubscriptionAsync(organization, eop); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.DeleteAccount, organization, _currentContext)); } catch (GatewayException) { } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs index b8802ffd0c..c3e945b65f 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs @@ -8,9 +8,6 @@ using Bit.Core.Models.Business; using Bit.Core.Models.StaticStore; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; @@ -37,7 +34,6 @@ public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizati private readonly ICurrentContext _currentContext; private readonly IPricingClient _pricingClient; - private readonly IReferenceEventService _referenceEventService; private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; private readonly IApplicationCacheService _applicationCacheService; @@ -46,7 +42,6 @@ public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizati public ProviderClientOrganizationSignUpCommand( ICurrentContext currentContext, IPricingClient pricingClient, - IReferenceEventService referenceEventService, IOrganizationRepository organizationRepository, IOrganizationApiKeyRepository organizationApiKeyRepository, IApplicationCacheService applicationCacheService, @@ -54,7 +49,6 @@ public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizati { _currentContext = currentContext; _pricingClient = pricingClient; - _referenceEventService = referenceEventService; _organizationRepository = organizationRepository; _organizationApiKeyRepository = organizationApiKeyRepository; _applicationCacheService = applicationCacheService; @@ -108,16 +102,6 @@ public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizati 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; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs index cf332e689a..71212aaf4c 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs @@ -104,8 +104,8 @@ public class SavePolicyCommand : ISavePolicyCommand var dependentPolicyTypes = _policyValidators.Values .Where(otherValidator => otherValidator.RequiredPolicies.Contains(policyUpdate.Type)) .Select(otherValidator => otherValidator.Type) - .Where(otherPolicyType => savedPoliciesDict.ContainsKey(otherPolicyType) && - savedPoliciesDict[otherPolicyType].Enabled) + .Where(otherPolicyType => savedPoliciesDict.TryGetValue(otherPolicyType, out var savedPolicy) && + savedPolicy.Enabled) .ToList(); switch (dependentPolicyTypes) diff --git a/src/Core/AdminConsole/Services/EventLoggingListenerService.cs b/src/Core/AdminConsole/Services/EventLoggingListenerService.cs index 60b8789a6b..ec2db121db 100644 --- a/src/Core/AdminConsole/Services/EventLoggingListenerService.cs +++ b/src/Core/AdminConsole/Services/EventLoggingListenerService.cs @@ -1,13 +1,87 @@ -using Microsoft.Extensions.Hosting; +#nullable enable + +using System.Text.Json; +using Bit.Core.Models.Data; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace Bit.Core.Services; public abstract class EventLoggingListenerService : BackgroundService { protected readonly IEventMessageHandler _handler; + protected ILogger _logger; - protected EventLoggingListenerService(IEventMessageHandler handler) + protected EventLoggingListenerService(IEventMessageHandler handler, ILogger logger) { - _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + _handler = handler; + _logger = logger; + } + + internal async Task ProcessReceivedMessageAsync(string body, string? messageId) + { + try + { + using var jsonDocument = JsonDocument.Parse(body); + var root = jsonDocument.RootElement; + + if (root.ValueKind == JsonValueKind.Array) + { + var eventMessages = root.Deserialize>(); + await _handler.HandleManyEventsAsync(eventMessages); + } + else if (root.ValueKind == JsonValueKind.Object) + { + var eventMessage = root.Deserialize(); + await _handler.HandleEventAsync(eventMessage); + } + else + { + if (!string.IsNullOrEmpty(messageId)) + { + _logger.LogError("An error occurred while processing message: {MessageId} - Invalid JSON", messageId); + } + else + { + _logger.LogError("An Invalid JSON error occurred while processing a message with an empty message id"); + } + } + } + catch (JsonException exception) + { + if (!string.IsNullOrEmpty(messageId)) + { + _logger.LogError( + exception, + "An error occurred while processing message: {MessageId} - Invalid JSON", + messageId + ); + } + else + { + _logger.LogError( + exception, + "An Invalid JSON error occurred while processing a message with an empty message id" + ); + } + } + catch (Exception exception) + { + if (!string.IsNullOrEmpty(messageId)) + { + _logger.LogError( + exception, + "An error occurred while processing message: {MessageId}", + messageId + ); + } + else + { + _logger.LogError( + exception, + "An error occurred while processing a message with an empty message id" + ); + } + } } } diff --git a/src/Core/AdminConsole/Services/IAzureServiceBusService.cs b/src/Core/AdminConsole/Services/IAzureServiceBusService.cs new file mode 100644 index 0000000000..d254e763d5 --- /dev/null +++ b/src/Core/AdminConsole/Services/IAzureServiceBusService.cs @@ -0,0 +1,10 @@ +using Azure.Messaging.ServiceBus; +using Bit.Core.AdminConsole.Models.Data.Integrations; + +namespace Bit.Core.Services; + +public interface IAzureServiceBusService : IEventIntegrationPublisher, IAsyncDisposable +{ + ServiceBusProcessor CreateProcessor(string topicName, string subscriptionName, ServiceBusProcessorOptions options); + Task PublishToRetryAsync(IIntegrationMessage message); +} diff --git a/src/Core/AdminConsole/Services/IIntegrationPublisher.cs b/src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs similarity index 58% rename from src/Core/AdminConsole/Services/IIntegrationPublisher.cs rename to src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs index 986ea776e1..560da576b7 100644 --- a/src/Core/AdminConsole/Services/IIntegrationPublisher.cs +++ b/src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs @@ -2,7 +2,8 @@ namespace Bit.Core.Services; -public interface IIntegrationPublisher +public interface IEventIntegrationPublisher : IAsyncDisposable { Task PublishAsync(IIntegrationMessage message); + Task PublishEventAsync(string body); } diff --git a/src/Core/AdminConsole/Services/IRabbitMqService.cs b/src/Core/AdminConsole/Services/IRabbitMqService.cs new file mode 100644 index 0000000000..b0b9a72eac --- /dev/null +++ b/src/Core/AdminConsole/Services/IRabbitMqService.cs @@ -0,0 +1,19 @@ +using Bit.Core.AdminConsole.Models.Data.Integrations; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace Bit.Core.Services; + +public interface IRabbitMqService : IEventIntegrationPublisher +{ + Task CreateChannelAsync(CancellationToken cancellationToken = default); + Task CreateEventQueueAsync(string queueName, CancellationToken cancellationToken = default); + Task CreateIntegrationQueuesAsync( + string queueName, + string retryQueueName, + string routingKey, + CancellationToken cancellationToken = default); + Task PublishToRetryAsync(IChannel channel, IIntegrationMessage message, CancellationToken cancellationToken); + Task PublishToDeadLetterAsync(IChannel channel, IIntegrationMessage message, CancellationToken cancellationToken); + Task RepublishToRetryQueueAsync(IChannel channel, BasicDeliverEventArgs eventArgs); +} diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs index 2ab10418a3..8b00204775 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs @@ -1,7 +1,7 @@ -using System.Text; -using System.Text.Json; +#nullable enable + +using System.Text; using Azure.Messaging.ServiceBus; -using Bit.Core.Models.Data; using Bit.Core.Settings; using Microsoft.Extensions.Logging; @@ -9,67 +9,47 @@ namespace Bit.Core.Services; public class AzureServiceBusEventListenerService : EventLoggingListenerService { - private readonly ILogger _logger; - private readonly ServiceBusClient _client; private readonly ServiceBusProcessor _processor; public AzureServiceBusEventListenerService( IEventMessageHandler handler, - ILogger logger, + IAzureServiceBusService serviceBusService, + string subscriptionName, GlobalSettings globalSettings, - string subscriptionName) : base(handler) + ILogger logger) : base(handler, logger) { - _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); - _processor = _client.CreateProcessor(globalSettings.EventLogging.AzureServiceBus.EventTopicName, subscriptionName, new ServiceBusProcessorOptions()); + _processor = serviceBusService.CreateProcessor( + globalSettings.EventLogging.AzureServiceBus.EventTopicName, + subscriptionName, + new ServiceBusProcessorOptions()); _logger = logger; } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { - _processor.ProcessMessageAsync += async args => - { - try - { - using var jsonDocument = JsonDocument.Parse(Encoding.UTF8.GetString(args.Message.Body)); - var root = jsonDocument.RootElement; - - if (root.ValueKind == JsonValueKind.Array) - { - var eventMessages = root.Deserialize>(); - await _handler.HandleManyEventsAsync(eventMessages); - } - else if (root.ValueKind == JsonValueKind.Object) - { - var eventMessage = root.Deserialize(); - await _handler.HandleEventAsync(eventMessage); - - } - await args.CompleteMessageAsync(args.Message); - } - catch (Exception exception) - { - _logger.LogError( - exception, - "An error occured while processing message: {MessageId}", - args.Message.MessageId - ); - } - }; - - _processor.ProcessErrorAsync += args => - { - _logger.LogError( - args.Exception, - "An error occurred. Entity Path: {EntityPath}, Error Source: {ErrorSource}", - args.EntityPath, - args.ErrorSource - ); - return Task.CompletedTask; - }; + _processor.ProcessMessageAsync += ProcessReceivedMessageAsync; + _processor.ProcessErrorAsync += ProcessErrorAsync; await _processor.StartProcessingAsync(cancellationToken); } + internal Task ProcessErrorAsync(ProcessErrorEventArgs args) + { + _logger.LogError( + args.Exception, + "An error occurred. Entity Path: {EntityPath}, Error Source: {ErrorSource}", + args.EntityPath, + args.ErrorSource + ); + return Task.CompletedTask; + } + + private async Task ProcessReceivedMessageAsync(ProcessMessageEventArgs args) + { + await ProcessReceivedMessageAsync(Encoding.UTF8.GetString(args.Message.Body), args.Message.MessageId); + await args.CompleteMessageAsync(args.Message); + } + public override async Task StopAsync(CancellationToken cancellationToken) { await _processor.StopProcessingAsync(cancellationToken); @@ -79,7 +59,6 @@ public class AzureServiceBusEventListenerService : EventLoggingListenerService public override void Dispose() { _processor.DisposeAsync().GetAwaiter().GetResult(); - _client.DisposeAsync().GetAwaiter().GetResult(); base.Dispose(); } } diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs deleted file mode 100644 index 224f86a802..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Text.Json; -using Azure.Messaging.ServiceBus; -using Bit.Core.Models.Data; -using Bit.Core.Services; -using Bit.Core.Settings; - -namespace Bit.Core.AdminConsole.Services.Implementations; - -public class AzureServiceBusEventWriteService : IEventWriteService, IAsyncDisposable -{ - private readonly ServiceBusClient _client; - private readonly ServiceBusSender _sender; - - public AzureServiceBusEventWriteService(GlobalSettings globalSettings) - { - _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); - _sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.EventTopicName); - } - - public async Task CreateAsync(IEvent e) - { - var message = new ServiceBusMessage(JsonSerializer.SerializeToUtf8Bytes(e)) - { - ContentType = "application/json" - }; - - await _sender.SendMessageAsync(message); - } - - public async Task CreateManyAsync(IEnumerable events) - { - var message = new ServiceBusMessage(JsonSerializer.SerializeToUtf8Bytes(events)) - { - ContentType = "application/json" - }; - - await _sender.SendMessageAsync(message); - } - - public async ValueTask DisposeAsync() - { - await _sender.DisposeAsync(); - await _client.DisposeAsync(); - } -} diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs index 8244f39c09..55a39ec774 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs @@ -1,7 +1,6 @@ #nullable enable using Azure.Messaging.ServiceBus; -using Bit.Core.Settings; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -10,39 +9,30 @@ namespace Bit.Core.Services; public class AzureServiceBusIntegrationListenerService : BackgroundService { private readonly int _maxRetries; - private readonly string _subscriptionName; - private readonly string _topicName; + private readonly IAzureServiceBusService _serviceBusService; private readonly IIntegrationHandler _handler; - private readonly ServiceBusClient _client; private readonly ServiceBusProcessor _processor; - private readonly ServiceBusSender _sender; private readonly ILogger _logger; - public AzureServiceBusIntegrationListenerService( - IIntegrationHandler handler, + public AzureServiceBusIntegrationListenerService(IIntegrationHandler handler, + string topicName, string subscriptionName, - GlobalSettings globalSettings, + int maxRetries, + IAzureServiceBusService serviceBusService, ILogger logger) { _handler = handler; _logger = logger; - _maxRetries = globalSettings.EventLogging.AzureServiceBus.MaxRetries; - _topicName = globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName; - _subscriptionName = subscriptionName; + _maxRetries = maxRetries; + _serviceBusService = serviceBusService; - _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); - _processor = _client.CreateProcessor(_topicName, _subscriptionName, new ServiceBusProcessorOptions()); - _sender = _client.CreateSender(_topicName); + _processor = _serviceBusService.CreateProcessor(topicName, subscriptionName, new ServiceBusProcessorOptions()); } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { _processor.ProcessMessageAsync += HandleMessageAsync; - _processor.ProcessErrorAsync += args => - { - _logger.LogError(args.Exception, "Azure Service Bus error"); - return Task.CompletedTask; - }; + _processor.ProcessErrorAsync += ProcessErrorAsync; await _processor.StartProcessingAsync(cancellationToken); } @@ -51,51 +41,67 @@ public class AzureServiceBusIntegrationListenerService : BackgroundService { await _processor.StopProcessingAsync(cancellationToken); await _processor.DisposeAsync(); - await _sender.DisposeAsync(); - await _client.DisposeAsync(); await base.StopAsync(cancellationToken); } - private async Task HandleMessageAsync(ProcessMessageEventArgs args) + internal Task ProcessErrorAsync(ProcessErrorEventArgs args) { - var json = args.Message.Body.ToString(); + _logger.LogError( + args.Exception, + "An error occurred. Entity Path: {EntityPath}, Error Source: {ErrorSource}", + args.EntityPath, + args.ErrorSource + ); + return Task.CompletedTask; + } + internal async Task HandleMessageAsync(string body) + { try { - var result = await _handler.HandleAsync(json); + var result = await _handler.HandleAsync(body); var message = result.Message; if (result.Success) { - await args.CompleteMessageAsync(args.Message); - return; + // Successful integration. Return true to indicate the message has been handled + return true; } 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); + // Publish message to the retry queue. It will be re-published for retry after a delay + // Return true to indicate the message has been handled + await _serviceBusService.PublishToRetryAsync(message); + return true; } else { - await args.DeadLetterMessageAsync(args.Message, "Retry limit exceeded or non-retryable"); - return; + // Non-recoverable failure or exceeded the max number of retries + // Return false to indicate this message should be dead-lettered + return false; } - - await args.CompleteMessageAsync(args.Message); } catch (Exception ex) { + // Unknown exception - log error, return true so the message will be acknowledged and not resent _logger.LogError(ex, "Unhandled error processing ASB message"); + return true; + } + } + + private async Task HandleMessageAsync(ProcessMessageEventArgs args) + { + var json = args.Message.Body.ToString(); + if (await HandleMessageAsync(json)) + { await args.CompleteMessageAsync(args.Message); } + else + { + await args.DeadLetterMessageAsync(args.Message, "Retry limit exceeded or non-retryable"); + } } } diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationPublisher.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationPublisher.cs deleted file mode 100644 index 4a906e719f..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationPublisher.cs +++ /dev/null @@ -1,36 +0,0 @@ -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(); - } -} diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusService.cs new file mode 100644 index 0000000000..7d24095819 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusService.cs @@ -0,0 +1,70 @@ +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 AzureServiceBusService : IAzureServiceBusService +{ + private readonly ServiceBusClient _client; + private readonly ServiceBusSender _eventSender; + private readonly ServiceBusSender _integrationSender; + + public AzureServiceBusService(GlobalSettings globalSettings) + { + _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); + _eventSender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.EventTopicName); + _integrationSender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName); + } + + public ServiceBusProcessor CreateProcessor(string topicName, string subscriptionName, ServiceBusProcessorOptions options) + { + return _client.CreateProcessor(topicName, subscriptionName, options); + } + + public async Task PublishAsync(IIntegrationMessage message) + { + var json = message.ToJson(); + + var serviceBusMessage = new ServiceBusMessage(json) + { + Subject = message.IntegrationType.ToRoutingKey(), + MessageId = message.MessageId + }; + + await _integrationSender.SendMessageAsync(serviceBusMessage); + } + + public async Task PublishToRetryAsync(IIntegrationMessage message) + { + var json = message.ToJson(); + + var serviceBusMessage = new ServiceBusMessage(json) + { + Subject = message.IntegrationType.ToRoutingKey(), + ScheduledEnqueueTime = message.DelayUntilDate ?? DateTime.UtcNow, + MessageId = message.MessageId + }; + + await _integrationSender.SendMessageAsync(serviceBusMessage); + } + + public async Task PublishEventAsync(string body) + { + var message = new ServiceBusMessage(body) + { + ContentType = "application/json", + MessageId = Guid.NewGuid().ToString() + }; + + await _eventSender.SendMessageAsync(message); + } + + public async ValueTask DisposeAsync() + { + await _eventSender.DisposeAsync(); + await _integrationSender.DisposeAsync(); + await _client.DisposeAsync(); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs index aa545913b1..578dde9485 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs @@ -1,4 +1,6 @@ -using Bit.Core.Models.Data; +#nullable enable + +using Bit.Core.Models.Data; using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrationEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrationEventWriteService.cs new file mode 100644 index 0000000000..519f8aeb32 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrationEventWriteService.cs @@ -0,0 +1,32 @@ +#nullable enable + +using System.Text.Json; +using Bit.Core.Models.Data; + +namespace Bit.Core.Services; +public class EventIntegrationEventWriteService : IEventWriteService, IAsyncDisposable +{ + private readonly IEventIntegrationPublisher _eventIntegrationPublisher; + + public EventIntegrationEventWriteService(IEventIntegrationPublisher eventIntegrationPublisher) + { + _eventIntegrationPublisher = eventIntegrationPublisher; + } + + public async Task CreateAsync(IEvent e) + { + var body = JsonSerializer.Serialize(e); + await _eventIntegrationPublisher.PublishEventAsync(body: body); + } + + public async Task CreateManyAsync(IEnumerable events) + { + var body = JsonSerializer.Serialize(events); + await _eventIntegrationPublisher.PublishEventAsync(body: body); + } + + public async ValueTask DisposeAsync() + { + await _eventIntegrationPublisher.DisposeAsync(); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs index 9a80ed67b2..aa76fdf8bc 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs @@ -1,4 +1,6 @@ -using System.Text.Json; +#nullable enable + +using System.Text.Json; using Bit.Core.AdminConsole.Models.Data.Integrations; using Bit.Core.AdminConsole.Utilities; using Bit.Core.Enums; @@ -7,11 +9,9 @@ using Bit.Core.Repositories; namespace Bit.Core.Services; -#nullable enable - public class EventIntegrationHandler( IntegrationType integrationType, - IIntegrationPublisher integrationPublisher, + IEventIntegrationPublisher eventIntegrationPublisher, IOrganizationIntegrationConfigurationRepository configurationRepository, IUserRepository userRepository, IOrganizationRepository organizationRepository) @@ -34,6 +34,7 @@ public class EventIntegrationHandler( var template = configuration.Template ?? string.Empty; var context = await BuildContextAsync(eventMessage, template); var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(template, context); + var messageId = eventMessage.IdempotencyId ?? Guid.NewGuid(); var config = configuration.MergedConfiguration.Deserialize() ?? throw new InvalidOperationException($"Failed to deserialize to {typeof(T).Name}"); @@ -41,13 +42,14 @@ public class EventIntegrationHandler( var message = new IntegrationMessage { IntegrationType = integrationType, + MessageId = messageId.ToString(), Configuration = config, RenderedTemplate = renderedTemplate, RetryCount = 0, DelayUntilDate = null }; - await integrationPublisher.PublishAsync(message); + await eventIntegrationPublisher.PublishAsync(message); } } diff --git a/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs index ee3a2d5db2..0fab787589 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs @@ -1,4 +1,6 @@ -using Bit.Core.Models.Data; +#nullable enable + +using Bit.Core.Models.Data; using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/Implementations/EventRouteService.cs b/src/Core/AdminConsole/Services/Implementations/EventRouteService.cs index a542e75a7b..df0819b409 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventRouteService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventRouteService.cs @@ -1,4 +1,6 @@ -using Bit.Core.Models.Data; +#nullable enable + +using Bit.Core.Models.Data; using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/Implementations/EventService.cs b/src/Core/AdminConsole/Services/Implementations/EventService.cs index 0cecda61a7..88d9595b4a 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventService.cs @@ -462,13 +462,13 @@ public class EventService : IEventService private bool CanUseEvents(IDictionary orgAbilities, Guid orgId) { - return orgAbilities != null && orgAbilities.ContainsKey(orgId) && - orgAbilities[orgId].Enabled && orgAbilities[orgId].UseEvents; + return orgAbilities != null && orgAbilities.TryGetValue(orgId, out var orgAbility) && + orgAbility.Enabled && orgAbility.UseEvents; } private bool CanUseProviderEvents(IDictionary providerAbilities, Guid providerId) { - return providerAbilities != null && providerAbilities.ContainsKey(providerId) && - providerAbilities[providerId].Enabled && providerAbilities[providerId].UseEvents; + return providerAbilities != null && providerAbilities.TryGetValue(providerId, out var providerAbility) && + providerAbility.Enabled && providerAbility.UseEvents; } } diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 7640a82fcb..16e58d27ad 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -29,9 +29,6 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Settings; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Microsoft.Extensions.Logging; using Stripe; @@ -56,7 +53,6 @@ public class OrganizationService : IOrganizationService private readonly IPolicyRepository _policyRepository; private readonly IPolicyService _policyService; private readonly ISsoUserRepository _ssoUserRepository; - private readonly IReferenceEventService _referenceEventService; private readonly IGlobalSettings _globalSettings; private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; private readonly ICurrentContext _currentContext; @@ -88,7 +84,6 @@ public class OrganizationService : IOrganizationService IPolicyRepository policyRepository, IPolicyService policyService, ISsoUserRepository ssoUserRepository, - IReferenceEventService referenceEventService, IGlobalSettings globalSettings, IOrganizationApiKeyRepository organizationApiKeyRepository, ICurrentContext currentContext, @@ -120,7 +115,6 @@ public class OrganizationService : IOrganizationService _policyRepository = policyRepository; _policyService = policyService; _ssoUserRepository = ssoUserRepository; - _referenceEventService = referenceEventService; _globalSettings = globalSettings; _organizationApiKeyRepository = organizationApiKeyRepository; _currentContext = currentContext; @@ -153,11 +147,6 @@ public class OrganizationService : IOrganizationService } await _paymentService.CancelSubscriptionAsync(organization, eop); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.CancelSubscription, organization, _currentContext) - { - EndOfPeriod = endOfPeriod, - }); } public async Task ReinstateSubscriptionAsync(Guid organizationId) @@ -169,8 +158,6 @@ public class OrganizationService : IOrganizationService } await _paymentService.ReinstateSubscriptionAsync(organization); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.ReinstateSubscription, organization, _currentContext)); } public async Task AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb) @@ -190,13 +177,6 @@ public class OrganizationService : IOrganizationService var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, organization, storageAdjustmentGb, plan.PasswordManager.StripeStoragePlanId); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.AdjustStorage, organization, _currentContext) - { - PlanName = plan.Name, - PlanType = plan.Type, - Storage = storageAdjustmentGb, - }); await ReplaceAndUpdateCacheAsync(organization); return secret; } @@ -328,14 +308,6 @@ public class OrganizationService : IOrganizationService } 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; await ReplaceAndUpdateCacheAsync(organization); @@ -640,12 +612,12 @@ public class OrganizationService : IOrganizationService } var providers = organization.GetTwoFactorProviders(); - if (!providers?.ContainsKey(type) ?? true) + if (providers is null || !providers.TryGetValue(type, out var provider)) { return; } - providers[type].Enabled = true; + provider.Enabled = true; organization.SetTwoFactorProviders(providers); await UpdateAsync(organization); } @@ -886,12 +858,6 @@ public class OrganizationService : IOrganizationService } await SendInvitesAsync(allOrgUsers, organization); - - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.InvitedUsers, organization, _currentContext) - { - Users = orgUserInvitedCount - }); } catch (Exception e) { @@ -1149,7 +1115,7 @@ public class OrganizationService : IOrganizationService var existingUsersDict = existingExternalUsers.ToDictionary(u => u.ExternalId); var removeUsersSet = new HashSet(removeUserExternalIds) .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]); await _organizationUserRepository.DeleteManyAsync(removeUsersSet.Select(u => u.Id)); @@ -1317,8 +1283,6 @@ public class OrganizationService : IOrganizationService } 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) @@ -1754,11 +1718,5 @@ public class OrganizationService : IOrganizationService await SendInviteAsync(ownerOrganizationUser, organization, true); await _eventService.LogOrganizationUserEventAsync(ownerOrganizationUser, EventType.OrganizationUser_Invited); - - await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.OrganizationCreatedByAdmin, organization, _currentContext) - { - EventRaisedByUser = userService.GetUserName(user), - SalesAssistedTrialStarted = salesAssistedTrialStarted, - }); } } diff --git a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs index c3eb2272d0..d424bd8fff 100644 --- a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs +++ b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs @@ -68,7 +68,7 @@ public class PolicyService : IPolicyService var excludedUserTypes = GetUserTypesExcludedFromPolicy(policyType); var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); return organizationUserPolicyDetails.Where(o => - (!orgAbilities.ContainsKey(o.OrganizationId) || orgAbilities[o.OrganizationId].UsePolicies) && + (!orgAbilities.TryGetValue(o.OrganizationId, out var orgAbility) || orgAbility.UsePolicies) && o.PolicyEnabled && !excludedUserTypes.Contains(o.OrganizationUserType) && o.OrganizationUserStatus >= minStatus && diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs index 74833f38a0..bc2329930d 100644 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs @@ -1,7 +1,6 @@ -using System.Text; -using System.Text.Json; -using Bit.Core.Models.Data; -using Bit.Core.Settings; +#nullable enable + +using System.Text; using Microsoft.Extensions.Logging; using RabbitMQ.Client; using RabbitMQ.Client.Events; @@ -10,94 +9,60 @@ namespace Bit.Core.Services; public class RabbitMqEventListenerService : EventLoggingListenerService { - private IChannel _channel; - private IConnection _connection; - private readonly string _exchangeName; - private readonly ConnectionFactory _factory; - private readonly ILogger _logger; + private readonly Lazy> _lazyChannel; private readonly string _queueName; + private readonly IRabbitMqService _rabbitMqService; public RabbitMqEventListenerService( IEventMessageHandler handler, - ILogger logger, - GlobalSettings globalSettings, - string queueName) : base(handler) + string queueName, + IRabbitMqService rabbitMqService, + ILogger logger) : base(handler, logger) { - _factory = new ConnectionFactory - { - HostName = globalSettings.EventLogging.RabbitMq.HostName, - UserName = globalSettings.EventLogging.RabbitMq.Username, - Password = globalSettings.EventLogging.RabbitMq.Password - }; - _exchangeName = globalSettings.EventLogging.RabbitMq.EventExchangeName; _logger = logger; _queueName = queueName; + _rabbitMqService = rabbitMqService; + _lazyChannel = new Lazy>(() => _rabbitMqService.CreateChannelAsync()); } 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.Fanout, - durable: true, - cancellationToken: cancellationToken); - await _channel.QueueDeclareAsync(queue: _queueName, - durable: true, - exclusive: false, - autoDelete: false, - arguments: null, - cancellationToken: cancellationToken); - await _channel.QueueBindAsync(queue: _queueName, - exchange: _exchangeName, - routingKey: string.Empty, - cancellationToken: cancellationToken); + await _rabbitMqService.CreateEventQueueAsync(_queueName, cancellationToken); await base.StartAsync(cancellationToken); } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { - var consumer = new AsyncEventingBasicConsumer(_channel); - consumer.ReceivedAsync += async (_, eventArgs) => - { - try - { - using var jsonDocument = JsonDocument.Parse(Encoding.UTF8.GetString(eventArgs.Body.Span)); - var root = jsonDocument.RootElement; + var channel = await _lazyChannel.Value; + var consumer = new AsyncEventingBasicConsumer(channel); + consumer.ReceivedAsync += async (_, eventArgs) => { await ProcessReceivedMessageAsync(eventArgs); }; - if (root.ValueKind == JsonValueKind.Array) - { - var eventMessages = root.Deserialize>(); - await _handler.HandleManyEventsAsync(eventMessages); - } - else if (root.ValueKind == JsonValueKind.Object) - { - var eventMessage = root.Deserialize(); - await _handler.HandleEventAsync(eventMessage); + await channel.BasicConsumeAsync(_queueName, autoAck: true, consumer: consumer, cancellationToken: cancellationToken); + } - } - } - catch (Exception ex) - { - _logger.LogError(ex, "An error occurred while processing the message"); - } - }; - - await _channel.BasicConsumeAsync(_queueName, autoAck: true, consumer: consumer, cancellationToken: cancellationToken); + internal async Task ProcessReceivedMessageAsync(BasicDeliverEventArgs eventArgs) + { + await ProcessReceivedMessageAsync( + Encoding.UTF8.GetString(eventArgs.Body.Span), + eventArgs.BasicProperties.MessageId); } public override async Task StopAsync(CancellationToken cancellationToken) { - await _channel.CloseAsync(cancellationToken); - await _connection.CloseAsync(cancellationToken); + if (_lazyChannel.IsValueCreated) + { + var channel = await _lazyChannel.Value; + await channel.CloseAsync(cancellationToken); + } await base.StopAsync(cancellationToken); } public override void Dispose() { - _channel.Dispose(); - _connection.Dispose(); + if (_lazyChannel.IsValueCreated && _lazyChannel.Value.IsCompletedSuccessfully) + { + _lazyChannel.Value.Result.Dispose(); + } base.Dispose(); } } diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs deleted file mode 100644 index 05fcf71a92..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Text.Json; -using Bit.Core.Models.Data; -using Bit.Core.Settings; -using RabbitMQ.Client; - -namespace Bit.Core.Services; -public class RabbitMqEventWriteService : IEventWriteService, IAsyncDisposable -{ - private readonly ConnectionFactory _factory; - private readonly Lazy> _lazyConnection; - private readonly string _exchangeName; - - public RabbitMqEventWriteService(GlobalSettings globalSettings) - { - _factory = new ConnectionFactory - { - HostName = globalSettings.EventLogging.RabbitMq.HostName, - UserName = globalSettings.EventLogging.RabbitMq.Username, - Password = globalSettings.EventLogging.RabbitMq.Password - }; - _exchangeName = globalSettings.EventLogging.RabbitMq.EventExchangeName; - - _lazyConnection = new Lazy>(CreateConnectionAsync); - } - - public async Task CreateAsync(IEvent e) - { - var connection = await _lazyConnection.Value; - using var channel = await connection.CreateChannelAsync(); - - await channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true); - - var body = JsonSerializer.SerializeToUtf8Bytes(e); - - await channel.BasicPublishAsync(exchange: _exchangeName, routingKey: string.Empty, body: body); - } - - public async Task CreateManyAsync(IEnumerable events) - { - var connection = await _lazyConnection.Value; - using var channel = await connection.CreateChannelAsync(); - await channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true); - - var body = JsonSerializer.SerializeToUtf8Bytes(events); - - await channel.BasicPublishAsync(exchange: _exchangeName, routingKey: string.Empty, body: body); - } - - public async ValueTask DisposeAsync() - { - if (_lazyConnection.IsValueCreated) - { - var connection = await _lazyConnection.Value; - await connection.DisposeAsync(); - } - } - - private async Task CreateConnectionAsync() - { - return await _factory.CreateConnectionAsync(); - } -} diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs index 1d6910db95..5b18d8817c 100644 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs @@ -1,5 +1,8 @@ -using System.Text; -using Bit.Core.Settings; +#nullable enable + +using System.Text; +using System.Text.Json; +using Bit.Core.AdminConsole.Models.Data.Integrations; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using RabbitMQ.Client; @@ -9,183 +12,137 @@ 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 string _queueName; + private readonly string _routingKey; + private readonly string _retryQueueName; private readonly IIntegrationHandler _handler; - private readonly ConnectionFactory _factory; + private readonly Lazy> _lazyChannel; + private readonly IRabbitMqService _rabbitMqService; private readonly ILogger _logger; - private readonly int _retryTiming; public RabbitMqIntegrationListenerService(IIntegrationHandler handler, string routingKey, string queueName, string retryQueueName, - string deadLetterQueueName, - GlobalSettings globalSettings, + int maxRetries, + IRabbitMqService rabbitMqService, ILogger logger) { _handler = handler; _routingKey = routingKey; - _retryRoutingKey = $"{_routingKey}-retry"; - _queueName = queueName; _retryQueueName = retryQueueName; - _deadLetterQueueName = deadLetterQueueName; + _queueName = queueName; + _rabbitMqService = rabbitMqService; _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 - }; + _maxRetries = maxRetries; + _lazyChannel = new Lazy>(() => _rabbitMqService.CreateChannelAsync()); } 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 - { - { "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 _rabbitMqService.CreateIntegrationQueuesAsync( + _queueName, + _retryQueueName, + _routingKey, + cancellationToken: cancellationToken); await base.StartAsync(cancellationToken); } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { - var consumer = new AsyncEventingBasicConsumer(_channel); + var channel = await _lazyChannel.Value; + var consumer = new AsyncEventingBasicConsumer(channel); consumer.ReceivedAsync += async (_, ea) => + { + await ProcessReceivedMessageAsync(ea, cancellationToken); + }; + + await channel.BasicConsumeAsync(queue: _queueName, autoAck: false, consumer: consumer, cancellationToken: cancellationToken); + } + + internal async Task ProcessReceivedMessageAsync(BasicDeliverEventArgs ea, CancellationToken cancellationToken) + { + var channel = await _lazyChannel.Value; + try { var json = Encoding.UTF8.GetString(ea.Body.Span); - try + // Determine if the message came off of the retry queue too soon + // If so, place it back on the retry queue + var integrationMessage = JsonSerializer.Deserialize(json); + if (integrationMessage is not null && + integrationMessage.DelayUntilDate.HasValue && + integrationMessage.DelayUntilDate.Value > DateTime.UtcNow) { - var result = await _handler.HandleAsync(json); - var message = result.Message; + await _rabbitMqService.RepublishToRetryQueueAsync(channel, ea); + await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); + return; + } - if (result.Success) + 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) { - // 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."); - } + // Publish message to the retry queue. It will be re-published for retry after a delay + await _rabbitMqService.PublishToRetryAsync(channel, message, cancellationToken); } 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."); + // Exceeded the max number of retries; fail and send to dead letter queue + await _rabbitMqService.PublishToDeadLetterAsync(channel, message, cancellationToken); + _logger.LogWarning("Max retry attempts reached. 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) + else { - // 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); + // Fatal error (i.e. not retryable) occurred. Send message to dead letter queue without any retries + await _rabbitMqService.PublishToDeadLetterAsync(channel, message, cancellationToken); + _logger.LogWarning("Non-retryable failure. Sent to DLQ."); } - }; - 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)); + // 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); + } } public override async Task StopAsync(CancellationToken cancellationToken) { - await _channel.CloseAsync(cancellationToken); - await _connection.CloseAsync(cancellationToken); + if (_lazyChannel.IsValueCreated) + { + var channel = await _lazyChannel.Value; + await channel.CloseAsync(cancellationToken); + } await base.StopAsync(cancellationToken); } public override void Dispose() { - _channel.Dispose(); - _connection.Dispose(); + if (_lazyChannel.IsValueCreated && _lazyChannel.Value.IsCompletedSuccessfully) + { + _lazyChannel.Value.Result.Dispose(); + } base.Dispose(); } } diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationPublisher.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationPublisher.cs deleted file mode 100644 index 12801e3216..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationPublisher.cs +++ /dev/null @@ -1,54 +0,0 @@ -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> _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>(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 CreateConnectionAsync() - { - return await _factory.CreateConnectionAsync(); - } -} diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqService.cs new file mode 100644 index 0000000000..617d1b41fb --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqService.cs @@ -0,0 +1,244 @@ +#nullable enable + +using System.Text; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Enums; +using Bit.Core.Settings; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace Bit.Core.Services; + +public class RabbitMqService : IRabbitMqService +{ + private const string _deadLetterRoutingKey = "dead-letter"; + + private readonly ConnectionFactory _factory; + private readonly Lazy> _lazyConnection; + private readonly string _deadLetterQueueName; + private readonly string _eventExchangeName; + private readonly string _integrationExchangeName; + private readonly int _retryTiming; + private readonly bool _useDelayPlugin; + + public RabbitMqService(GlobalSettings globalSettings) + { + _factory = new ConnectionFactory + { + HostName = globalSettings.EventLogging.RabbitMq.HostName, + UserName = globalSettings.EventLogging.RabbitMq.Username, + Password = globalSettings.EventLogging.RabbitMq.Password + }; + _deadLetterQueueName = globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName; + _eventExchangeName = globalSettings.EventLogging.RabbitMq.EventExchangeName; + _integrationExchangeName = globalSettings.EventLogging.RabbitMq.IntegrationExchangeName; + _retryTiming = globalSettings.EventLogging.RabbitMq.RetryTiming; + _useDelayPlugin = globalSettings.EventLogging.RabbitMq.UseDelayPlugin; + + _lazyConnection = new Lazy>(CreateConnectionAsync); + } + + public async Task CreateChannelAsync(CancellationToken cancellationToken = default) + { + var connection = await _lazyConnection.Value; + return await connection.CreateChannelAsync(cancellationToken: cancellationToken); + } + + public async Task CreateEventQueueAsync(string queueName, CancellationToken cancellationToken = default) + { + using var channel = await CreateChannelAsync(cancellationToken); + await channel.QueueDeclareAsync(queue: queueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null, + cancellationToken: cancellationToken); + await channel.QueueBindAsync(queue: queueName, + exchange: _eventExchangeName, + routingKey: string.Empty, + cancellationToken: cancellationToken); + } + + public async Task CreateIntegrationQueuesAsync( + string queueName, + string retryQueueName, + string routingKey, + CancellationToken cancellationToken = default) + { + using var channel = await CreateChannelAsync(cancellationToken); + var retryRoutingKey = $"{routingKey}-retry"; + + // Declare main integration queue + await channel.QueueDeclareAsync( + queue: queueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null, + cancellationToken: cancellationToken); + await channel.QueueBindAsync( + queue: queueName, + exchange: _integrationExchangeName, + routingKey: routingKey, + cancellationToken: cancellationToken); + + if (!_useDelayPlugin) + { + // Declare retry queue (Configurable TTL, dead-letters back to main queue) + // Only needed if NOT using delay plugin + await channel.QueueDeclareAsync(queue: retryQueueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: new Dictionary + { + { "x-dead-letter-exchange", _integrationExchangeName }, + { "x-dead-letter-routing-key", routingKey }, + { "x-message-ttl", _retryTiming } + }, + cancellationToken: cancellationToken); + await channel.QueueBindAsync(queue: retryQueueName, + exchange: _integrationExchangeName, + routingKey: retryRoutingKey, + cancellationToken: cancellationToken); + } + } + + public async Task PublishAsync(IIntegrationMessage message) + { + var routingKey = message.IntegrationType.ToRoutingKey(); + await using var channel = await CreateChannelAsync(); + + var body = Encoding.UTF8.GetBytes(message.ToJson()); + var properties = new BasicProperties + { + MessageId = message.MessageId, + Persistent = true + }; + + await channel.BasicPublishAsync( + exchange: _integrationExchangeName, + mandatory: true, + basicProperties: properties, + routingKey: routingKey, + body: body); + } + + public async Task PublishEventAsync(string body) + { + await using var channel = await CreateChannelAsync(); + var properties = new BasicProperties + { + MessageId = Guid.NewGuid().ToString(), + Persistent = true + }; + + await channel.BasicPublishAsync( + exchange: _eventExchangeName, + mandatory: true, + basicProperties: properties, + routingKey: string.Empty, + body: Encoding.UTF8.GetBytes(body)); + } + + public async Task PublishToRetryAsync(IChannel channel, IIntegrationMessage message, CancellationToken cancellationToken) + { + var routingKey = message.IntegrationType.ToRoutingKey(); + var retryRoutingKey = $"{routingKey}-retry"; + var properties = new BasicProperties + { + Persistent = true, + MessageId = message.MessageId, + Headers = _useDelayPlugin && message.DelayUntilDate.HasValue ? + new Dictionary + { + ["x-delay"] = Math.Max((int)(message.DelayUntilDate.Value - DateTime.UtcNow).TotalMilliseconds, 0) + } : + null + }; + + await channel.BasicPublishAsync( + exchange: _integrationExchangeName, + routingKey: _useDelayPlugin ? routingKey : retryRoutingKey, + mandatory: true, + basicProperties: properties, + body: Encoding.UTF8.GetBytes(message.ToJson()), + cancellationToken: cancellationToken); + } + + public async Task PublishToDeadLetterAsync( + IChannel channel, + IIntegrationMessage message, + CancellationToken cancellationToken) + { + var properties = new BasicProperties + { + MessageId = message.MessageId, + Persistent = true + }; + + await channel.BasicPublishAsync( + exchange: _integrationExchangeName, + mandatory: true, + basicProperties: properties, + routingKey: _deadLetterRoutingKey, + body: Encoding.UTF8.GetBytes(message.ToJson()), + cancellationToken: cancellationToken); + } + + public async Task RepublishToRetryQueueAsync(IChannel channel, BasicDeliverEventArgs eventArgs) + { + await channel.BasicPublishAsync( + exchange: _integrationExchangeName, + routingKey: eventArgs.RoutingKey, + mandatory: true, + basicProperties: new BasicProperties(eventArgs.BasicProperties), + body: eventArgs.Body); + } + + public async ValueTask DisposeAsync() + { + if (_lazyConnection.IsValueCreated) + { + var connection = await _lazyConnection.Value; + await connection.DisposeAsync(); + } + } + + private async Task CreateConnectionAsync() + { + var connection = await _factory.CreateConnectionAsync(); + using var channel = await connection.CreateChannelAsync(); + + // Declare Exchanges + await channel.ExchangeDeclareAsync(exchange: _eventExchangeName, type: ExchangeType.Fanout, durable: true); + if (_useDelayPlugin) + { + await channel.ExchangeDeclareAsync( + exchange: _integrationExchangeName, + type: "x-delayed-message", + durable: true, + arguments: new Dictionary + { + { "x-delayed-type", "direct" } + } + ); + } + else + { + await channel.ExchangeDeclareAsync(exchange: _integrationExchangeName, type: ExchangeType.Direct, durable: true); + } + + // Declare dead letter queue for Integration exchange + await channel.QueueDeclareAsync(queue: _deadLetterQueueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null); + await channel.QueueBindAsync(queue: _deadLetterQueueName, + exchange: _integrationExchangeName, + routingKey: _deadLetterRoutingKey); + + return connection; + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs index 134e93e838..fe0f6eabe1 100644 --- a/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs @@ -1,4 +1,6 @@ -using Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +using Bit.Core.AdminConsole.Models.Data.Integrations; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/Implementations/SlackService.cs b/src/Core/AdminConsole/Services/Implementations/SlackService.cs index effcfdf1ce..3f82217830 100644 --- a/src/Core/AdminConsole/Services/Implementations/SlackService.cs +++ b/src/Core/AdminConsole/Services/Implementations/SlackService.cs @@ -1,4 +1,6 @@ -using System.Net.Http.Headers; +#nullable enable + +using System.Net.Http.Headers; using System.Net.Http.Json; using System.Web; using Bit.Core.Models.Slack; @@ -22,7 +24,7 @@ public class SlackService( public async Task GetChannelIdAsync(string token, string channelName) { - return (await GetChannelIdsAsync(token, [channelName])).FirstOrDefault(); + return (await GetChannelIdsAsync(token, [channelName])).FirstOrDefault() ?? string.Empty; } public async Task> GetChannelIdsAsync(string token, List channelNames) @@ -58,7 +60,7 @@ public class SlackService( } else { - logger.LogError("Error getting Channel Ids: {Error}", result.Error); + logger.LogError("Error getting Channel Ids: {Error}", result?.Error ?? "Unknown Error"); nextCursor = string.Empty; } @@ -89,7 +91,7 @@ public class SlackService( new KeyValuePair("redirect_uri", redirectUrl) })); - SlackOAuthResponse result; + SlackOAuthResponse? result; try { result = await tokenResponse.Content.ReadFromJsonAsync(); @@ -99,7 +101,7 @@ public class SlackService( result = null; } - if (result == null) + if (result is null) { logger.LogError("Error obtaining token via OAuth: Unknown error"); return string.Empty; @@ -130,6 +132,11 @@ public class SlackService( var response = await _httpClient.SendAsync(request); var result = await response.Content.ReadFromJsonAsync(); + if (result is null) + { + logger.LogError("Error retrieving Slack user ID: Unknown error"); + return string.Empty; + } if (!result.Ok) { logger.LogError("Error retrieving Slack user ID: {Error}", result.Error); @@ -151,6 +158,11 @@ public class SlackService( var response = await _httpClient.SendAsync(request); var result = await response.Content.ReadFromJsonAsync(); + if (result is null) + { + logger.LogError("Error opening DM channel: Unknown error"); + return string.Empty; + } if (!result.Ok) { logger.LogError("Error opening DM channel: {Error}", result.Error); diff --git a/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs index 5f9898afe8..df364b2a48 100644 --- a/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs @@ -1,4 +1,6 @@ -using System.Globalization; +#nullable enable + +using System.Globalization; using System.Net; using System.Text; using Bit.Core.AdminConsole.Models.Data.Integrations; @@ -29,7 +31,7 @@ public class WebhookIntegrationHandler(IHttpClientFactory httpClientFactory) case HttpStatusCode.ServiceUnavailable: case HttpStatusCode.GatewayTimeout: result.Retryable = true; - result.FailureReason = response.ReasonPhrase; + result.FailureReason = response.ReasonPhrase ?? $"Failure with status code: {(int)response.StatusCode}"; if (response.Headers.TryGetValues("Retry-After", out var values)) { @@ -52,7 +54,7 @@ public class WebhookIntegrationHandler(IHttpClientFactory httpClientFactory) break; default: result.Retryable = false; - result.FailureReason = response.ReasonPhrase; + result.FailureReason = response.ReasonPhrase ?? $"Failure with status code {(int)response.StatusCode}"; break; } diff --git a/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs index 718e44ae5f..2f8481cea2 100644 --- a/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs @@ -43,7 +43,7 @@ public class EmailTwoFactorTokenProvider : EmailTokenProvider private static bool HasProperMetaData(TwoFactorProvider provider) { - return provider?.MetaData != null && provider.MetaData.ContainsKey("Email") && - !string.IsNullOrWhiteSpace((string)provider.MetaData["Email"]); + return provider?.MetaData != null && provider.MetaData.TryGetValue("Email", out var emailValue) && + !string.IsNullOrWhiteSpace((string)emailValue); } } diff --git a/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs index 0bf75d0fc3..3b4b0fa520 100644 --- a/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs @@ -80,7 +80,7 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); var keys = LoadKeys(provider); - if (!provider.MetaData.TryGetValue("login", out var value)) + if (!provider.MetaData.TryGetValue("login", out var login)) { return false; } @@ -88,7 +88,7 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider var clientResponse = JsonSerializer.Deserialize(token, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - var jsonOptions = value.ToString(); + var jsonOptions = login.ToString(); var options = AssertionOptions.FromJson(jsonOptions); var webAuthCred = keys.Find(k => k.Item2.Descriptor.Id.SequenceEqual(clientResponse.Id)); @@ -148,9 +148,9 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider for (var i = 1; i <= 5; i++) { var keyName = $"Key{i}"; - if (provider.MetaData.ContainsKey(keyName)) + if (provider.MetaData.TryGetValue(keyName, out var value)) { - var key = new TwoFactorProvider.WebAuthnData((dynamic)provider.MetaData[keyName]); + var key = new TwoFactorProvider.WebAuthnData((dynamic)value); keys.Add(new Tuple(keyName, key)); } diff --git a/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs b/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs index e721649dc9..289bbff7f8 100644 --- a/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs +++ b/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs @@ -3,7 +3,6 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; @@ -11,9 +10,6 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Tokens; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Identity; @@ -26,15 +22,12 @@ public class RegisterUserCommand : IRegisterUserCommand private readonly IGlobalSettings _globalSettings; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IPolicyRepository _policyRepository; - private readonly IReferenceEventService _referenceEventService; private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; private readonly IDataProtectorTokenFactory _registrationEmailVerificationTokenDataFactory; private readonly IDataProtector _organizationServiceDataProtector; private readonly IDataProtector _providerServiceDataProtector; - private readonly ICurrentContext _currentContext; - private readonly IUserService _userService; private readonly IMailService _mailService; @@ -48,11 +41,9 @@ public class RegisterUserCommand : IRegisterUserCommand IGlobalSettings globalSettings, IOrganizationUserRepository organizationUserRepository, IPolicyRepository policyRepository, - IReferenceEventService referenceEventService, IDataProtectionProvider dataProtectionProvider, IDataProtectorTokenFactory orgUserInviteTokenDataFactory, IDataProtectorTokenFactory registrationEmailVerificationTokenDataFactory, - ICurrentContext currentContext, IUserService userService, IMailService mailService, IValidateRedemptionTokenCommand validateRedemptionTokenCommand, @@ -62,14 +53,12 @@ public class RegisterUserCommand : IRegisterUserCommand _globalSettings = globalSettings; _organizationUserRepository = organizationUserRepository; _policyRepository = policyRepository; - _referenceEventService = referenceEventService; _organizationServiceDataProtector = dataProtectionProvider.CreateProtector( "OrganizationServiceDataProtector"); _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; _registrationEmailVerificationTokenDataFactory = registrationEmailVerificationTokenDataFactory; - _currentContext = currentContext; _userService = userService; _mailService = mailService; @@ -86,7 +75,6 @@ public class RegisterUserCommand : IRegisterUserCommand if (result == IdentityResult.Success) { await _mailService.SendWelcomeEmailAsync(user); - await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext)); } return result; @@ -119,12 +107,6 @@ public class RegisterUserCommand : IRegisterUserCommand sentWelcomeEmail = true; if (!string.IsNullOrEmpty(initiationPath)) { - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext) - { - SignupInitiationPath = initiationPath - }); - return result; } } @@ -134,8 +116,6 @@ public class RegisterUserCommand : IRegisterUserCommand { await _mailService.SendWelcomeEmailAsync(user); } - - await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext)); } return result; @@ -263,10 +243,6 @@ public class RegisterUserCommand : IRegisterUserCommand if (result == IdentityResult.Success) { await _mailService.SendWelcomeEmailAsync(user); - await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext) - { - ReceiveMarketingEmails = tokenable.ReceiveMarketingEmails - }); } return result; @@ -285,7 +261,6 @@ public class RegisterUserCommand : IRegisterUserCommand if (result == IdentityResult.Success) { await _mailService.SendWelcomeEmailAsync(user); - await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext)); } return result; @@ -306,7 +281,6 @@ public class RegisterUserCommand : IRegisterUserCommand if (result == IdentityResult.Success) { await _mailService.SendWelcomeEmailAsync(user); - await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext)); } return result; @@ -325,7 +299,6 @@ public class RegisterUserCommand : IRegisterUserCommand if (result == IdentityResult.Success) { await _mailService.SendWelcomeEmailAsync(user); - await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext)); } return result; diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index c647e825b6..1a89d2a7e2 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -420,7 +420,7 @@ public class OrganizationBillingService( var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); - if (setNonUSBusinessUseToReverseCharge) + if (setNonUSBusinessUseToReverseCharge && customer.HasBillingLocation()) { subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 78a8364695..e6a822452a 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -23,6 +23,7 @@ public static class Constants public const string Fido2KeyCipherMinimumVersion = "2023.10.0"; public const string SSHKeyCipherMinimumVersion = "2024.12.0"; + public const string DenyLegacyUserMinimumVersion = "2025.6.0"; /// /// Used by IdentityServer to identify our own provider. @@ -144,9 +145,7 @@ public static class FeatureFlagKeys public const string PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships"; public const string UsePricingService = "use-pricing-service"; public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features"; - public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method"; public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates"; - public const string PM18770_EnableOrganizationBusinessUnitConversion = "pm-18770-enable-organization-business-unit-conversion"; public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically"; public const string PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup"; public const string UseOrganizationWarningsService = "use-organization-warnings-service"; diff --git a/src/Core/Context/CurrentContext.cs b/src/Core/Context/CurrentContext.cs index cbd90055b0..68d4606907 100644 --- a/src/Core/Context/CurrentContext.cs +++ b/src/Core/Context/CurrentContext.cs @@ -64,39 +64,39 @@ public class CurrentContext : ICurrentContext HttpContext = httpContext; await BuildAsync(httpContext.User, globalSettings); - if (DeviceIdentifier == null && httpContext.Request.Headers.ContainsKey("Device-Identifier")) + if (DeviceIdentifier == null && httpContext.Request.Headers.TryGetValue("Device-Identifier", out var deviceIdentifier)) { - DeviceIdentifier = httpContext.Request.Headers["Device-Identifier"]; + DeviceIdentifier = deviceIdentifier; } - if (httpContext.Request.Headers.ContainsKey("Device-Type") && - Enum.TryParse(httpContext.Request.Headers["Device-Type"].ToString(), out DeviceType dType)) + if (httpContext.Request.Headers.TryGetValue("Device-Type", out var deviceType) && + Enum.TryParse(deviceType.ToString(), out DeviceType dType)) { DeviceType = dType; } - if (!BotScore.HasValue && httpContext.Request.Headers.ContainsKey("X-Cf-Bot-Score") && - int.TryParse(httpContext.Request.Headers["X-Cf-Bot-Score"], out var parsedBotScore)) + if (!BotScore.HasValue && httpContext.Request.Headers.TryGetValue("X-Cf-Bot-Score", out var cfBotScore) && + int.TryParse(cfBotScore, out var parsedBotScore)) { BotScore = parsedBotScore; } - if (httpContext.Request.Headers.ContainsKey("X-Cf-Worked-Proxied")) + if (httpContext.Request.Headers.TryGetValue("X-Cf-Worked-Proxied", out var cfWorkedProxied)) { - CloudflareWorkerProxied = httpContext.Request.Headers["X-Cf-Worked-Proxied"] == "1"; + CloudflareWorkerProxied = cfWorkedProxied == "1"; } - if (httpContext.Request.Headers.ContainsKey("X-Cf-Is-Bot")) + if (httpContext.Request.Headers.TryGetValue("X-Cf-Is-Bot", out var cfIsBot)) { - IsBot = httpContext.Request.Headers["X-Cf-Is-Bot"] == "1"; + IsBot = cfIsBot == "1"; } - if (httpContext.Request.Headers.ContainsKey("X-Cf-Maybe-Bot")) + if (httpContext.Request.Headers.TryGetValue("X-Cf-Maybe-Bot", out var cfMaybeBot)) { - MaybeBot = httpContext.Request.Headers["X-Cf-Maybe-Bot"] == "1"; + MaybeBot = cfMaybeBot == "1"; } - if (httpContext.Request.Headers.ContainsKey("Bitwarden-Client-Version") && Version.TryParse(httpContext.Request.Headers["Bitwarden-Client-Version"], out var cVersion)) + if (httpContext.Request.Headers.TryGetValue("Bitwarden-Client-Version", out var bitWardenClientVersion) && Version.TryParse(bitWardenClientVersion, out var cVersion)) { ClientVersion = cVersion; } @@ -190,14 +190,14 @@ public class CurrentContext : ICurrentContext private List GetOrganizations(Dictionary> claimsDict, bool orgApi) { - var accessSecretsManager = claimsDict.ContainsKey(Claims.SecretsManagerAccess) - ? claimsDict[Claims.SecretsManagerAccess].ToDictionary(s => s.Value, _ => true) + var accessSecretsManager = claimsDict.TryGetValue(Claims.SecretsManagerAccess, out var secretsManagerAccessClaim) + ? secretsManagerAccessClaim.ToDictionary(s => s.Value, _ => true) : new Dictionary(); var organizations = new List(); - if (claimsDict.ContainsKey(Claims.OrganizationOwner)) + if (claimsDict.TryGetValue(Claims.OrganizationOwner, out var organizationOwnerClaim)) { - organizations.AddRange(claimsDict[Claims.OrganizationOwner].Select(c => + organizations.AddRange(organizationOwnerClaim.Select(c => new CurrentContextOrganization { Id = new Guid(c.Value), @@ -214,9 +214,9 @@ public class CurrentContext : ICurrentContext }); } - if (claimsDict.ContainsKey(Claims.OrganizationAdmin)) + if (claimsDict.TryGetValue(Claims.OrganizationAdmin, out var organizationAdminClaim)) { - organizations.AddRange(claimsDict[Claims.OrganizationAdmin].Select(c => + organizations.AddRange(organizationAdminClaim.Select(c => new CurrentContextOrganization { Id = new Guid(c.Value), @@ -225,9 +225,9 @@ public class CurrentContext : ICurrentContext })); } - if (claimsDict.ContainsKey(Claims.OrganizationUser)) + if (claimsDict.TryGetValue(Claims.OrganizationUser, out var organizationUserClaim)) { - organizations.AddRange(claimsDict[Claims.OrganizationUser].Select(c => + organizations.AddRange(organizationUserClaim.Select(c => new CurrentContextOrganization { Id = new Guid(c.Value), @@ -236,9 +236,9 @@ public class CurrentContext : ICurrentContext })); } - if (claimsDict.ContainsKey(Claims.OrganizationCustom)) + if (claimsDict.TryGetValue(Claims.OrganizationCustom, out var organizationCustomClaim)) { - organizations.AddRange(claimsDict[Claims.OrganizationCustom].Select(c => + organizations.AddRange(organizationCustomClaim.Select(c => new CurrentContextOrganization { Id = new Guid(c.Value), @@ -254,9 +254,9 @@ public class CurrentContext : ICurrentContext private List GetProviders(Dictionary> claimsDict) { var providers = new List(); - if (claimsDict.ContainsKey(Claims.ProviderAdmin)) + if (claimsDict.TryGetValue(Claims.ProviderAdmin, out var providerAdminClaim)) { - providers.AddRange(claimsDict[Claims.ProviderAdmin].Select(c => + providers.AddRange(providerAdminClaim.Select(c => new CurrentContextProvider { Id = new Guid(c.Value), @@ -264,9 +264,9 @@ public class CurrentContext : ICurrentContext })); } - if (claimsDict.ContainsKey(Claims.ProviderServiceUser)) + if (claimsDict.TryGetValue(Claims.ProviderServiceUser, out var providerServiceUserClaim)) { - providers.AddRange(claimsDict[Claims.ProviderServiceUser].Select(c => + providers.AddRange(providerServiceUserClaim.Select(c => new CurrentContextProvider { Id = new Guid(c.Value), @@ -504,20 +504,20 @@ public class CurrentContext : ICurrentContext private string GetClaimValue(Dictionary> claims, string type) { - if (!claims.ContainsKey(type)) + if (!claims.TryGetValue(type, out var claim)) { return null; } - return claims[type].FirstOrDefault()?.Value; + return claim.FirstOrDefault()?.Value; } private Permissions SetOrganizationPermissionsFromClaims(string organizationId, Dictionary> claimsDict) { bool hasClaim(string claimKey) { - return claimsDict.ContainsKey(claimKey) ? - claimsDict[claimKey].Any(x => x.Value == organizationId) : false; + return claimsDict.TryGetValue(claimKey, out var claim) ? + claim.Any(x => x.Value == organizationId) : false; } return new Permissions diff --git a/src/Core/Entities/User.cs b/src/Core/Entities/User.cs index 08981ca2d3..b92d22b0e3 100644 --- a/src/Core/Entities/User.cs +++ b/src/Core/Entities/User.cs @@ -3,7 +3,6 @@ using System.Text.Json; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Enums; -using Bit.Core.Tools.Entities; using Bit.Core.Utilities; using Microsoft.AspNetCore.Identity; @@ -11,7 +10,7 @@ using Microsoft.AspNetCore.Identity; namespace Bit.Core.Entities; -public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFactorProvidersUser, IReferenceable +public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFactorProvidersUser { private Dictionary? _twoFactorProviders; @@ -196,12 +195,7 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac public TwoFactorProvider? GetTwoFactorProvider(TwoFactorProviderType provider) { var providers = GetTwoFactorProviders(); - if (providers == null || !providers.TryGetValue(provider, out var value)) - { - return null; - } - - return value; + return providers?.GetValueOrDefault(provider); } public long StorageBytesRemaining() diff --git a/src/Core/IdentityServer/DistributedCacheCookieManager.cs b/src/Core/IdentityServer/DistributedCacheCookieManager.cs index 9771b40662..5d6717ac41 100644 --- a/src/Core/IdentityServer/DistributedCacheCookieManager.cs +++ b/src/Core/IdentityServer/DistributedCacheCookieManager.cs @@ -63,6 +63,6 @@ public class DistributedCacheCookieManager : ICookieManager private string GetKey(string key, string id) => $"{CacheKeyPrefix}-{key}-{id}"; private string GetId(HttpContext context, string key) => - context.Request.Cookies.ContainsKey(key) ? - context.Request.Cookies[key] : null; + context.Request.Cookies.TryGetValue(key, out var cookie) ? + cookie : null; } diff --git a/src/Core/MailTemplates/Handlebars/Layouts/Full.html.hbs b/src/Core/MailTemplates/Handlebars/Layouts/Full.html.hbs index 7ed9fb7d1a..bcf6be62c9 100644 --- a/src/Core/MailTemplates/Handlebars/Layouts/Full.html.hbs +++ b/src/Core/MailTemplates/Handlebars/Layouts/Full.html.hbs @@ -148,7 +148,7 @@ - + diff --git a/src/Core/MailTemplates/Handlebars/Layouts/Full.text.hbs b/src/Core/MailTemplates/Handlebars/Layouts/Full.text.hbs index bf4ec50796..72f669bf34 100644 --- a/src/Core/MailTemplates/Handlebars/Layouts/Full.text.hbs +++ b/src/Core/MailTemplates/Handlebars/Layouts/Full.text.hbs @@ -2,7 +2,7 @@ ---------------------------- -- Twitter: https://twitter.com/bitwarden +- X: https://x.com/bitwarden - Reddit: https://www.reddit.com/r/Bitwarden/ - Community Forums: https://community.bitwarden.com/ - GitHub: https://github.com/bitwarden diff --git a/src/Core/MailTemplates/Handlebars/Layouts/FullUpdated.html.hbs b/src/Core/MailTemplates/Handlebars/Layouts/FullUpdated.html.hbs index f5772d61f6..f79e5f7043 100644 --- a/src/Core/MailTemplates/Handlebars/Layouts/FullUpdated.html.hbs +++ b/src/Core/MailTemplates/Handlebars/Layouts/FullUpdated.html.hbs @@ -177,7 +177,7 @@
TwitterX Reddit CommunityForums GitHub