1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-04 01:22:50 -05:00

[BRE-831] Merging in main

This commit is contained in:
Andy Pixley
2025-06-13 12:28:52 -04:00
529 changed files with 16867 additions and 5239 deletions

View File

@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"swashbuckle.aspnetcore.cli": {
"version": "7.2.0",
"version": "7.3.2",
"commands": ["swagger"]
},
"dotnet-ef": {

View File

@ -12,6 +12,9 @@ on:
workflow_call:
inputs: {}
permissions:
contents: read
env:
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
_GITHUB_PR_REPO_NAME: ${{ github.event.pull_request.head.repo.full_name }}
@ -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,113 +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
run: |
has_secrets=${{ secrets.AZURE_CLIENT_ID != '' }}
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:
@ -147,6 +52,7 @@ jobs:
- project_name: Admin
base_path: ./src
dotnet: true
node: true
- project_name: Api
base_path: ./src
dotnet: true
@ -180,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
@ -190,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:
@ -201,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: Azure Login
id: azure-login
@ -273,26 +238,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: |
@ -305,7 +260,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}"
@ -335,10 +290,9 @@ jobs:
upload:
name: Upload
runs-on: ubuntu-22.04
needs: build-docker
runs-on: ubuntu-24.04
needs: build-artifacts
permissions:
contents: read
id-token: write
actions: read
steps:
@ -383,9 +337,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
@ -406,14 +360,6 @@ jobs:
- name: Azure Logout
uses: bitwarden/gh-actions/azure-logout@main
- name: Make Docker stub checksums
if: |
github.event_name != 'pull_request'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
run: |
sha256sum docker-stub-US.zip > docker-stub-US-sha256.txt
sha256sum docker-stub-EU.zip > docker-stub-EU-sha256.txt
- name: Upload Docker stub US artifact
if: |
github.event_name != 'pull_request'
@ -434,26 +380,6 @@ jobs:
path: docker-stub-EU.zip
if-no-files-found: error
- name: Upload Docker stub US checksum artifact
if: |
github.event_name != 'pull_request'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: docker-stub-US-sha256.txt
path: docker-stub-US-sha256.txt
if-no-files-found: error
- name: Upload Docker stub EU checksum artifact
if: |
github.event_name != 'pull_request'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: docker-stub-EU-sha256.txt
path: docker-stub-EU-sha256.txt
if-no-files-found: error
- name: Build Public API Swagger
run: |
cd ./src/Api
@ -521,7 +447,7 @@ jobs:
build-mssqlmigratorutility:
name: Build MSSQL migrator utility
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
needs:
- lint
defaults:
@ -577,9 +503,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
permissions:
id-token: write
steps:
@ -621,7 +547,7 @@ jobs:
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
runs-on: ubuntu-22.04
needs:
- build-docker
- build-artifacts
permissions:
id-token: write
steps:
@ -663,7 +589,6 @@ jobs:
name: Setup Ephemeral Environment
needs:
- build-artifacts
- build-docker
if: |
needs.build-artifacts.outputs.has_secrets == 'true'
&& github.event_name == 'pull_request'
@ -671,8 +596,9 @@ jobs:
uses: bitwarden/gh-actions/.github/workflows/_ephemeral_environment_manager.yml@main
with:
project: server
pull_request_number: ${{ github.event.number }}
pull_request_number: ${{ github.event.number || 0 }}
secrets: inherit
permissions: read-all
check-failures:
name: Check for failures
@ -681,7 +607,6 @@ jobs:
needs:
- lint
- build-artifacts
- build-docker
- upload
- build-mssqlmigratorutility
- self-host-build

View File

@ -17,6 +17,9 @@ on:
env:
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
permissions:
contents: read
jobs:
setup:
name: Setup
@ -65,9 +68,7 @@ jobs:
workflow_conclusion: success
branch: ${{ needs.setup.outputs.branch-name }}
artifacts: "docker-stub-US.zip,
docker-stub-US-sha256.txt,
docker-stub-EU.zip,
docker-stub-EU-sha256.txt,
swagger.json"
- name: Dry Run - Download latest release Docker stubs
@ -78,9 +79,7 @@ jobs:
workflow_conclusion: success
branch: main
artifacts: "docker-stub-US.zip,
docker-stub-US-sha256.txt,
docker-stub-EU.zip,
docker-stub-EU-sha256.txt,
swagger.json"
- name: Create release
@ -88,9 +87,7 @@ jobs:
uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0
with:
artifacts: "docker-stub-US.zip,
docker-stub-US-sha256.txt,
docker-stub-EU.zip,
docker-stub-EU-sha256.txt,
swagger.json"
commit: ${{ github.sha }}
tag: "v${{ needs.setup.outputs.release_version }}"

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>2025.5.2</Version>
<Version>2025.6.1</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
@ -69,5 +69,4 @@
</AssemblyAttribute>
</ItemGroup>
</Target>
</Project>

View File

@ -5,9 +5,6 @@
<a href="https://github.com/bitwarden/server/actions/workflows/build.yml?query=branch:main" target="_blank">
<img src="https://github.com/bitwarden/server/actions/workflows/build.yml/badge.svg?branch=main" alt="Github Workflow build on main" />
</a>
<a href="https://hub.docker.com/u/bitwarden/" target="_blank">
<img src="https://img.shields.io/docker/pulls/bitwarden/api.svg" alt="DockerHub" />
</a>
<a href="https://gitter.im/bitwarden/Lobby" target="_blank">
<img src="https://badges.gitter.im/bitwarden/Lobby.svg" alt="gitter chat" />
</a>
@ -26,12 +23,12 @@ Please refer to the [Server Setup Guide](https://contributing.bitwarden.com/gett
## Deploy
<p align="center">
<a href="https://hub.docker.com/u/bitwarden/" target="_blank">
<a href="https://github.com/orgs/bitwarden/packages" target="_blank">
<img src="https://i.imgur.com/SZc8JnH.png" alt="docker" />
</a>
</p>
You can deploy Bitwarden using Docker containers on Windows, macOS, and Linux distributions. Use the provided PowerShell and Bash scripts to get started quickly. Find all of the Bitwarden images on [Docker Hub](https://hub.docker.com/u/bitwarden/).
You can deploy Bitwarden using Docker containers on Windows, macOS, and Linux distributions. Use the provided PowerShell and Bash scripts to get started quickly. Find all of the Bitwarden images on [GitHub Container Registry](https://github.com/orgs/bitwarden/packages).
Full documentation for deploying Bitwarden with Docker can be found in our help center at: https://help.bitwarden.com/article/install-on-premise/

View File

@ -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)

View File

@ -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,

View File

@ -550,6 +550,15 @@ public class ProviderBillingService(
[
new CustomerTaxIdDataOptions { Type = taxIdType, Value = taxInfo.TaxIdNumber }
];
if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)
{
options.TaxIdData.Add(new CustomerTaxIdDataOptions
{
Type = StripeConstants.TaxIdType.EUVAT,
Value = $"ES{taxInfo.TaxIdNumber}"
});
}
}
if (!string.IsNullOrEmpty(provider.DiscountId))

View File

@ -1,4 +0,0 @@
*
!obj/build-output/publish/*
!obj/Docker/empty/
!entrypoint.sh

View File

@ -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

View File

@ -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;

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Setup
@ -19,6 +19,8 @@ then
LGID=65534
fi
if [ "$(id -u)" = "0" ]
then
# Create user and group
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
@ -35,15 +37,24 @@ 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
$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

View File

@ -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
@ -499,9 +499,9 @@ public class AccountController : Controller
// Before any user creation - if Org User doesn't exist at this point - make sure there are enough seats to add one
if (orgUser == null && organization.Seats.HasValue)
{
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
var occupiedSeats = await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
var initialSeatCount = organization.Seats.Value;
var availableSeats = initialSeatCount - occupiedSeats;
var availableSeats = initialSeatCount - occupiedSeats.Total;
if (availableSeats < 1)
{
try

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Setup
@ -19,6 +19,8 @@ then
LGID=65534
fi
if [ "$(id -u)" = "0" ]
then
# Create user and group
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
@ -29,27 +31,30 @@ mkhomedir_helper $USERNAME
# The rest...
mkdir -p /etc/bitwarden/identity
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
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
$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

View File

@ -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

View File

@ -33,6 +33,39 @@
"Name": "events-webhook-subscription"
}
]
},
{
"Name": "event-integrations",
"Subscriptions": [
{
"Name": "integration-slack-subscription",
"Rules": [
{
"Name": "slack-integration-filter",
"Properties": {
"FilterType": "Correlation",
"CorrelationFilter": {
"Label": "slack"
}
}
}
]
},
{
"Name": "integration-webhook-subscription",
"Rules": [
{
"Name": "webhook-integration-filter",
"Properties": {
"FilterType": "Correlation",
"CorrelationFilter": {
"Label": "webhook"
}
}
}
]
}
]
}
]
}

View File

@ -11,7 +11,7 @@ $corsRules = (@{
AllowedMethods = @("Get", "PUT");
});
$containers = "attachments", "sendfiles", "misc";
$queues = "event", "notifications", "reference-events", "mail";
$queues = "event", "notifications", "mail";
$tables = "event", "metadata", "installationdevice";
# End configuration

View File

@ -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"
}
}

View File

@ -1,4 +0,0 @@
*
!obj/build-output/publish/*
!obj/Docker/empty/
!entrypoint.sh

View File

@ -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<OrganizationsController> _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<OrganizationsController> 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;
@ -255,10 +242,32 @@ public class OrganizationsController : Controller
Seats = organization.Seats
};
if (model.PlanType.HasValue)
{
var freePlan = await _pricingClient.GetPlanOrThrow(model.PlanType.Value);
var isDowngradingToFree = organization.PlanType != PlanType.Free && model.PlanType.Value == PlanType.Free;
if (isDowngradingToFree)
{
if (model.Seats.HasValue && model.Seats.Value > freePlan.PasswordManager.MaxSeats)
{
TempData["Error"] = $"Organizations with more than {freePlan.PasswordManager.MaxSeats} seats cannot be downgraded to the Free plan";
return RedirectToAction("Edit", new { id });
}
if (model.MaxCollections > freePlan.PasswordManager.MaxCollections)
{
TempData["Error"] = $"Organizations with more than {freePlan.PasswordManager.MaxCollections} collections cannot be downgraded to the Free plan. Your organization currently has {organization.MaxCollections} collections.";
return RedirectToAction("Edit", new { id });
}
model.MaxStorageGb = null;
model.ExpirationDate = null;
model.Enabled = true;
}
}
UpdateOrganization(organization, model);
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
if (organization.UseSecretsManager && !plan.SupportsSecretsManager)
{
TempData["Error"] = "Plan does not support Secrets Manager";
@ -272,11 +281,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 });
}

View File

@ -44,6 +44,8 @@ public class OrganizationViewModel
orgUsers
.Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus)
.Select(u => u.Email));
OwnersDetails = orgUsers.Where(u => u.Type == OrganizationUserType.Owner && u.Status == organizationUserStatus);
AdminsDetails = orgUsers.Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus);
SecretsCount = secretsCount;
ProjectsCount = projectCount;
ServiceAccountsCount = serviceAccountsCount;
@ -70,4 +72,6 @@ public class OrganizationViewModel
public int OccupiedSmSeatsCount { get; set; }
public bool UseSecretsManager => Organization.UseSecretsManager;
public bool UseRiskInsights => Organization.UseRiskInsights;
public IEnumerable<OrganizationUserUserDetails> OwnersDetails { get; set; }
public IEnumerable<OrganizationUserUserDetails> AdminsDetails { get; set; }
}

View File

@ -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,9 +15,7 @@
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) &&
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 };

View File

@ -19,12 +19,6 @@
<span id="org-confirmed-users" title="Confirmed">@Model.UserConfirmedCount</span>)
</dd>
<dt class="col-sm-4 col-lg-3">Owners</dt>
<dd id="org-owner" class="col-sm-8 col-lg-9">@(string.IsNullOrWhiteSpace(Model.Owners) ? "None" : Model.Owners)</dd>
<dt class="col-sm-4 col-lg-3">Admins</dt>
<dd id="org-admins" class="col-sm-8 col-lg-9">@(string.IsNullOrWhiteSpace(Model.Admins) ? "None" : Model.Admins)</dd>
<dt class="col-sm-4 col-lg-3">Using 2FA</dt>
<dd id="org-2fa" class="col-sm-8 col-lg-9">@(Model.Organization.TwoFactorIsEnabled() ? "Yes" : "No")</dd>
@ -76,3 +70,49 @@
<dt class="col-sm-4 col-lg-3">Secrets Manager Seats</dt>
<dd id="sm-seat-count" class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.OccupiedSmSeatsCount: "N/A" )</dd>
</dl>
<h2>Administrators</h2>
<dl class="row">
<div class="table-responsive">
<div class="col-8">
<table class="table table-striped table-hover">
<thead>
<tr>
<th style="width: 190px;">Email</th>
<th style="width: 60px;">Role</th>
<th style="width: 40px;">Status</th>
</tr>
</thead>
<tbody>
@if(!Model.Admins.Any() && !Model.Owners.Any())
{
<tr>
<td colspan="6">No results to list.</td>
</tr>
}
else
{
@foreach(var owner in Model.OwnersDetails)
{
<tr>
<td class="align-middle">@owner.Email</td>
<td class="align-middle">Owner</td>
<td class="align-middle">@owner.Status</td>
</tr>
}
@foreach(var admin in Model.AdminsDetails)
{
<tr>
<td class="align-middle">@admin.Email</td>
<td class="align-middle">Admin</td>
<td class="align-middle">@admin.Status</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
</dl>

View File

@ -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,

View File

@ -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"]

View File

@ -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<IdentityUser>(null);

View File

@ -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;

View File

@ -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)

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Setup
@ -19,6 +19,8 @@ then
LGID=65534
fi
if [ "$(id -u)" = "0" ]
then
# Create user and group
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
@ -35,15 +37,18 @@ 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
$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

View File

@ -1,4 +0,0 @@
*
!obj/build-output/publish/*
!obj/Docker/empty/
!entrypoint.sh

View File

@ -34,7 +34,7 @@
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.2" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.1" />
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.25.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.2" />
</ItemGroup>
</Project>

View File

@ -25,7 +25,7 @@ public class UpdateTwoFactorAuthenticatorRequestModel : SecretVerificationReques
{
providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
}
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<TwoFactorProviderType, TwoFactorProvider>();
}
else if (providers.ContainsKey(TwoFactorProviderType.YubiKey))
else
{
providers.Remove(TwoFactorProviderType.YubiKey);
}
@ -228,7 +228,7 @@ public class TwoFactorEmailRequestModel : SecretVerificationRequestModel
{
providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
}
else if (providers.ContainsKey(TwoFactorProviderType.Email))
else
{
providers.Remove(TwoFactorProviderType.Email);
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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")]

View File

@ -4,7 +4,7 @@ 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.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing;
@ -25,7 +25,6 @@ namespace Bit.Api.Billing.Controllers;
public class OrganizationBillingController(
IBusinessUnitConverter businessUnitConverter,
ICurrentContext currentContext,
IFeatureService featureService,
IOrganizationBillingService organizationBillingService,
IOrganizationRepository organizationRepository,
IOrganizationWarningsQuery organizationWarningsQuery,
@ -282,17 +281,36 @@ public class OrganizationBillingController(
}
var organization = await organizationRepository.GetByIdAsync(organizationId);
if (organization == null)
{
return Error.NotFound();
}
var existingPlan = organization.PlanType;
var organizationSignup = model.ToOrganizationSignup(user);
var sale = OrganizationSale.From(organization, organizationSignup);
var plan = await pricingClient.GetPlanOrThrow(model.PlanType);
sale.Organization.PlanType = plan.Type;
sale.Organization.Plan = plan.Name;
sale.SubscriptionSetup.SkipTrial = true;
if (existingPlan == PlanType.Free && organization.GatewaySubscriptionId is not null)
{
sale.Organization.UseTotp = plan.HasTotp;
sale.Organization.UseGroups = plan.HasGroups;
sale.Organization.UseDirectory = plan.HasDirectory;
sale.Organization.SelfHost = plan.HasSelfHost;
sale.Organization.UsersGetPremium = plan.UsersGetPremium;
sale.Organization.UseEvents = plan.HasEvents;
sale.Organization.Use2fa = plan.Has2fa;
sale.Organization.UseApi = plan.HasApi;
sale.Organization.UsePolicies = plan.HasPolicies;
sale.Organization.UseSso = plan.HasSso;
sale.Organization.UseResetPassword = plan.HasResetPassword;
sale.Organization.UseKeyConnector = plan.HasKeyConnector;
sale.Organization.UseScim = plan.HasScim;
sale.Organization.UseCustomPermissions = plan.HasCustomPermissions;
sale.Organization.UseOrganizationDomains = plan.HasOrganizationDomains;
sale.Organization.MaxCollections = plan.PasswordManager.MaxCollections;
}
if (organizationSignup.PaymentMethodType == null || string.IsNullOrEmpty(organizationSignup.PaymentToken))
{
@ -318,14 +336,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)

View File

@ -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")]

View File

@ -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)

View File

@ -12,7 +12,8 @@ public record OrganizationMetadataResponse(
bool IsSubscriptionCanceled,
DateTime? InvoiceDueDate,
DateTime? InvoiceCreatedDate,
DateTime? SubPeriodEndDate)
DateTime? SubPeriodEndDate,
int OrganizationOccupiedSeats)
{
public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
=> new(
@ -25,5 +26,6 @@ public record OrganizationMetadataResponse(
metadata.IsSubscriptionCanceled,
metadata.InvoiceDueDate,
metadata.InvoiceCreatedDate,
metadata.SubPeriodEndDate);
metadata.SubPeriodEndDate,
metadata.OrganizationOccupiedSeats);
}

View File

@ -8,7 +8,7 @@ using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Tools.Controllers;
namespace Bit.Api.Dirt.Controllers;
[Route("hibp")]
[Authorize("Application")]

View File

@ -1,16 +1,16 @@
using Bit.Api.Tools.Models;
using Bit.Api.Tools.Models.Response;
using Bit.Api.Dirt.Models;
using Bit.Api.Dirt.Models.Response;
using Bit.Core.Context;
using Bit.Core.Dirt.Reports.Entities;
using Bit.Core.Dirt.Reports.Models.Data;
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Exceptions;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Models.Data;
using Bit.Core.Tools.ReportFeatures.Interfaces;
using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces;
using Bit.Core.Tools.ReportFeatures.Requests;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Tools.Controllers;
namespace Bit.Api.Dirt.Controllers;
[Route("reports")]
[Authorize("Application")]

View File

@ -1,4 +1,4 @@
namespace Bit.Api.Tools.Models;
namespace Bit.Api.Dirt.Models;
public class PasswordHealthReportApplicationModel
{

View File

@ -1,6 +1,6 @@
using Bit.Core.Tools.Models.Data;
using Bit.Core.Dirt.Reports.Models.Data;
namespace Bit.Api.Tools.Models.Response;
namespace Bit.Api.Dirt.Models.Response;
/// <summary>
/// Contains the collections and group collections a user has access to including

View File

@ -1,6 +1,6 @@
using Bit.Core.Tools.Models.Data;
using Bit.Core.Dirt.Reports.Models.Data;
namespace Bit.Api.Tools.Models.Response;
namespace Bit.Api.Dirt.Models.Response;
public class MemberCipherDetailsResponseModel
{

View File

@ -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"]

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations;
using Bit.Api.AdminConsole.Public.Models.Response;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
namespace Bit.Api.Models.Public.Response;
@ -20,6 +21,7 @@ public class CollectionResponseModel : CollectionBaseModel, IResponseModel
Id = collection.Id;
ExternalId = collection.ExternalId;
Groups = groups?.Select(c => new AssociationWithPermissionsResponseModel(c));
Type = collection.Type;
}
/// <summary>
@ -38,4 +40,8 @@ public class CollectionResponseModel : CollectionBaseModel, IResponseModel
/// The associated groups that this collection is assigned to.
/// </summary>
public IEnumerable<AssociationWithPermissionsResponseModel> Groups { get; set; }
/// <summary>
/// The type of this collection
/// </summary>
public CollectionType Type { get; set; }
}

View File

@ -1,4 +1,5 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Api;
using Bit.Core.Models.Data;
@ -18,12 +19,14 @@ public class CollectionResponseModel : ResponseModel
OrganizationId = collection.OrganizationId;
Name = collection.Name;
ExternalId = collection.ExternalId;
Type = collection.Type;
}
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }
public string Name { get; set; }
public string ExternalId { get; set; }
public CollectionType Type { get; set; }
}
/// <summary>

View File

@ -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<BaseSecretResponseModel>(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<Secret> secrets)
private async Task LogSecretsRetrievalAsync(IEnumerable<Secret> 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));
}
}
}

View File

@ -31,8 +31,8 @@ using Bit.Api.Billing;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Tools.ImportFeatures;
using Bit.Core.Tools.ReportFeatures;
using Bit.Core.Auth.Models.Api.Request;
using Bit.Core.Dirt.Reports.ReportFeatures;
using Bit.Core.Tools.SendFeatures;
#if !OSS

View File

@ -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<SendsController> _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<SendsController> 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

View File

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

View File

@ -42,7 +42,6 @@ public class CiphersController : Controller
private readonly ICurrentContext _currentContext;
private readonly ILogger<CiphersController> _logger;
private readonly GlobalSettings _globalSettings;
private readonly IFeatureService _featureService;
private readonly IOrganizationCiphersQuery _organizationCiphersQuery;
private readonly IApplicationCacheService _applicationCacheService;
private readonly ICollectionRepository _collectionRepository;
@ -57,7 +56,6 @@ public class CiphersController : Controller
ICurrentContext currentContext,
ILogger<CiphersController> logger,
GlobalSettings globalSettings,
IFeatureService featureService,
IOrganizationCiphersQuery organizationCiphersQuery,
IApplicationCacheService applicationCacheService,
ICollectionRepository collectionRepository)
@ -71,7 +69,6 @@ public class CiphersController : Controller
_currentContext = currentContext;
_logger = logger;
_globalSettings = globalSettings;
_featureService = featureService;
_organizationCiphersQuery = organizationCiphersQuery;
_applicationCacheService = applicationCacheService;
_collectionRepository = collectionRepository;
@ -375,11 +372,6 @@ public class CiphersController : Controller
private async Task<bool> CanDeleteOrRestoreCipherAsAdminAsync(Guid organizationId, IEnumerable<Guid> cipherIds)
{
if (!_featureService.IsEnabled(FeatureFlagKeys.LimitItemDeletion))
{
return await CanEditCipherAsAdminAsync(organizationId, cipherIds);
}
var org = _currentContext.GetOrganization(organizationId);
// If we're not an "admin" or if we're a provider user we don't need to check the ciphers
@ -1064,7 +1056,7 @@ public class CiphersController : Controller
[HttpPut("share")]
[HttpPost("share")]
public async Task PutShareMany([FromBody] CipherBulkShareRequestModel model)
public async Task<ListResponseModel<CipherMiniResponseModel>> PutShareMany([FromBody] CipherBulkShareRequestModel model)
{
var organizationId = new Guid(model.Ciphers.First().OrganizationId);
if (!await _currentContext.OrganizationUser(organizationId))
@ -1073,38 +1065,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 != userId)
if (cipher.EncryptedFor.HasValue && cipher.EncryptedFor.Value != userId)
{
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<CipherMiniResponseModel>(response);
}
[HttpPost("purge")]
@ -1186,14 +1181,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 +1207,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 +1360,7 @@ public class CiphersController : Controller
var cipher = await _cipherRepository.GetByIdAsync(new Guid(cipherId));
var attachments = cipher?.GetAttachments() ?? new Dictionary<string, CipherAttachment.MetaData>();
if (cipher == null || !attachments.ContainsKey(attachmentId) || attachments[attachmentId].Validated)
if (cipher == null || !attachments.TryGetValue(attachmentId, out var attachment) || attachment.Validated)
{
if (_attachmentStorageService is AzureSendFileStorageService azureFileStorageService)
{
@ -1376,7 +1370,7 @@ public class CiphersController : Controller
return;
}
await _cipherService.ValidateCipherAttachmentFile(cipher, attachments[attachmentId]);
await _cipherService.ValidateCipherAttachmentFile(cipher, attachment);
}
catch (Exception e)
{

View File

@ -1,9 +1,7 @@
using Bit.Api.Models.Response;
using Bit.Api.Vault.Models.Request;
using Bit.Api.Vault.Models.Response;
using Bit.Core;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Core.Vault.Commands.Interfaces;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Enums;
@ -15,7 +13,6 @@ namespace Bit.Api.Vault.Controllers;
[Route("tasks")]
[Authorize("Application")]
[RequireFeature(FeatureFlagKeys.SecurityTasks)]
public class SecurityTaskController : Controller
{
private readonly IUserService _userService;

View File

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

View File

@ -129,13 +129,13 @@ public class CipherDetailsResponseModel : CipherResponseModel
IDictionary<Guid, IGrouping<Guid, CollectionCipher>> 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<CollectionCipher> collectionCiphers, string obj = "cipherDetails")
: base(cipher, user, organizationAbilities, globalSettings, obj)
{
CollectionIds = collectionCiphers?.Select(c => c.CollectionId) ?? new List<Guid>();
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<Guid>();
CollectionIds = cipher.CollectionIds ?? [];
}
public IEnumerable<Guid> CollectionIds { get; set; }
@ -170,13 +170,13 @@ public class CipherMiniDetailsResponseModel : CipherMiniResponseModel
IDictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphers, bool orgUseTotp, string obj = "cipherMiniDetails")
: 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<Guid>();
CollectionIds = cipher.CollectionIds ?? [];
}
public CipherMiniDetailsResponseModel(CipherOrganizationDetailsWithCollections cipher,

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Setup
@ -19,6 +19,8 @@ then
LGID=65534
fi
if [ "$(id -u)" = "0" ]
then
# Create user and group
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
@ -35,15 +37,18 @@ 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
$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

View File

@ -1,4 +0,0 @@
*
!obj/build-output/publish/*
!obj/Docker/empty/
!entrypoint.sh

View File

@ -10,7 +10,7 @@
<ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.2" />
</ItemGroup>
</Project>

View File

@ -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();

View File

@ -51,8 +51,8 @@ public class PayPalController : Controller
[HttpPost("ipn")]
public async Task<IActionResult> 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))

View File

@ -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"]

View File

@ -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;

View File

@ -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<CustomerUpdatedHandler> _logger;
public CustomerUpdatedHandler(
IOrganizationRepository organizationRepository,
IReferenceEventService referenceEventService,
ICurrentContext currentContext,
IStripeEventService stripeEventService,
IStripeEventUtilityService stripeEventUtilityService,
ILogger<CustomerUpdatedHandler> 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));
}
}

View File

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

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Setup
@ -19,6 +19,8 @@ then
LGID=65534
fi
if [ "$(id -u)" = "0" ]
then
# Create user and group
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
@ -35,9 +37,9 @@ 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

View File

@ -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<Guid>, IStorableSubscriber, IRevisable, IReferenceable
public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable
{
private Dictionary<TwoFactorProviderType, TwoFactorProvider>? _twoFactorProviders;
@ -258,12 +257,12 @@ public class Organization : ITableObject<Guid>, 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<Guid>, 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)

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Interfaces;
using Bit.Core.Enums;
using Bit.Core.Models;
@ -9,23 +10,75 @@ using Bit.Core.Utilities;
namespace Bit.Core.Entities;
/// <summary>
/// An association table between one <see cref="User"/> and one <see cref="Organization"/>, representing that user's
/// membership in the organization. "Member" refers to the OrganizationUser object.
/// </summary>
public class OrganizationUser : ITableObject<Guid>, IExternal, IOrganizationUser
{
/// <summary>
/// A unique random identifier.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// The ID of the Organization that the user is a member of.
/// </summary>
public Guid OrganizationId { get; set; }
/// <summary>
/// The ID of the User that is the member. This is NULL if the Status is Invited (or Invited and then Revoked), because
/// it is not linked to a specific User yet.
/// </summary>
public Guid? UserId { get; set; }
/// <summary>
/// The email address of the user invited to the organization. This is NULL if the Status is not Invited (or
/// Invited and then Revoked), because in that case the OrganizationUser is linked to a User
/// and the email is stored on the User object.
/// </summary>
[MaxLength(256)]
public string? Email { get; set; }
/// <summary>
/// The Organization symmetric key encrypted with the User's public key. NULL if the user is not in a Confirmed
/// (or Confirmed and then Revoked) status.
/// </summary>
public string? Key { get; set; }
/// <summary>
/// The User's symmetric key encrypted with the Organization's public key. NULL if the OrganizationUser
/// is not enrolled in account recovery.
/// </summary>
public string? ResetPasswordKey { get; set; }
/// <inheritdoc cref="OrganizationUserStatusType"/>
public OrganizationUserStatusType Status { get; set; }
/// <summary>
/// The User's role in the Organization.
/// </summary>
public OrganizationUserType Type { get; set; }
/// <summary>
/// An ID used to identify the OrganizationUser with an external directory service. Used by Directory Connector
/// and SCIM.
/// </summary>
[MaxLength(300)]
public string? ExternalId { get; set; }
/// <summary>
/// The date the OrganizationUser was created, i.e. when the User was first invited to the Organization.
/// </summary>
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
/// <summary>
/// The last date the OrganizationUser entry was updated.
/// </summary>
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
/// <summary>
/// A json blob representing the <see cref="Bit.Core.Models.Data.Permissions"/> of the OrganizationUser if they
/// are a Custom user role (i.e. the <see cref="OrganizationUserType"/> is Custom). MAY be NULL if they are not
/// a custom user, but this is not guaranteed; do not use this to determine their role.
/// </summary>
/// <remarks>
/// Avoid using this property directly - instead use the <see cref="GetPermissions"/> and <see cref="SetPermissions"/>
/// helper methods.
/// </remarks>
public string? Permissions { get; set; }
/// <summary>
/// True if the User has access to Secrets Manager for this Organization, false otherwise.
/// </summary>
public bool AccessSecretsManager { get; set; }
public void SetNewId()

View File

@ -1,9 +1,34 @@
namespace Bit.Core.Enums;
using Bit.Core.Entities;
namespace Bit.Core.Enums;
/// <summary>
/// Represents the different stages of a member's lifecycle in an organization.
/// The <see cref="OrganizationUser"/> object is populated differently depending on their Status.
/// </summary>
public enum OrganizationUserStatusType : short
{
/// <summary>
/// The OrganizationUser entry only represents an invitation to join the organization. It is not linked to a
/// specific User yet.
/// </summary>
Invited = 0,
/// <summary>
/// The User has accepted the invitation and linked their User account to the OrganizationUser entry.
/// </summary>
Accepted = 1,
/// <summary>
/// An administrator has granted the User access to the organization. This is the final step in the User becoming
/// a "full" member of the organization, including a key exchange so that they can decrypt organization data.
/// </summary>
Confirmed = 2,
/// <summary>
/// The OrganizationUser has been revoked from the organization and cannot access organization data while in this state.
/// </summary>
/// <remarks>
/// An OrganizationUser may move into this status from any other status, and will move back to their original status
/// if restored. This allows an administrator to easily suspend and restore access without going through the
/// Invite flow again.
/// </remarks>
Revoked = -1,
}

View File

@ -17,6 +17,7 @@ public enum PolicyType : byte
AutomaticAppLogIn = 12,
FreeFamiliesSponsorshipPolicy = 13,
RemoveUnlockWithPin = 14,
RestrictedItemTypesPolicy = 15,
}
public static class PolicyTypeExtensions
@ -43,7 +44,8 @@ public static class PolicyTypeExtensions
PolicyType.ActivateAutofill => "Active auto-fill",
PolicyType.AutomaticAppLogIn => "Automatically log in users for allowed applications",
PolicyType.FreeFamiliesSponsorshipPolicy => "Remove Free Bitwarden Families sponsorship",
PolicyType.RemoveUnlockWithPin => "Remove unlock with PIN"
PolicyType.RemoveUnlockWithPin => "Remove unlock with PIN",
PolicyType.RestrictedItemTypesPolicy => "Restricted item types",
};
}
}

View File

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

View File

@ -1,4 +1,6 @@
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
#nullable enable
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
public class IntegrationHandlerResult
{

View File

@ -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<T> : 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<T> : IIntegrationMessage
DelayUntilDate = baseTime.AddSeconds(backoffSeconds + jitterSeconds);
}
public string ToJson()
public virtual string ToJson()
{
return JsonSerializer.Serialize(this);
}
}
public class IntegrationMessage<T> : IntegrationMessage
{
public required T Configuration { get; set; }
public override string ToJson()
{
return JsonSerializer.Serialize(this);
}
public static IntegrationMessage<T> FromJson(string json)
public static IntegrationMessage<T>? FromJson(string json)
{
return JsonSerializer.Deserialize<IntegrationMessage<T>>(json);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@

#nullable enable
using System.Text.Json.Serialization;
namespace Bit.Core.Models.Slack;

View File

@ -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<Guid> userIds,

View File

@ -1,4 +1,6 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
@ -27,6 +29,8 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
private readonly IUserRepository _userRepository;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
private readonly IFeatureService _featureService;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
public AcceptOrgUserCommand(
IDataProtectionProvider dataProtectionProvider,
@ -37,9 +41,10 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
IMailService mailService,
IUserRepository userRepository,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory)
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
IFeatureService featureService,
IPolicyRequirementQuery policyRequirementQuery)
{
// TODO: remove data protector when old token validation removed
_dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose);
_globalSettings = globalSettings;
@ -50,6 +55,8 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
_userRepository = userRepository;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
_featureService = featureService;
_policyRequirementQuery = policyRequirementQuery;
}
public async Task<OrganizationUser> AcceptOrgUserByEmailTokenAsync(Guid organizationUserId, User user, string emailToken,
@ -196,15 +203,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
}
// Enforce Two Factor Authentication Policy of organization user is trying to join
if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
{
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
{
throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account.");
}
}
await ValidateTwoFactorAuthenticationPolicyAsync(user, orgUser.OrganizationId);
orgUser.Status = OrganizationUserStatusType.Accepted;
orgUser.UserId = user.Id;
@ -224,4 +223,33 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
return orgUser;
}
private async Task ValidateTwoFactorAuthenticationPolicyAsync(User user, Guid organizationId)
{
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
{
if (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
{
// If the user has two-step login enabled, we skip checking the 2FA policy
return;
}
var twoFactorPolicyRequirement = await _policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id);
if (twoFactorPolicyRequirement.IsTwoFactorRequiredForOrganization(organizationId))
{
throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account.");
}
return;
}
if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
{
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == organizationId))
{
throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account.");
}
}
}
}

View File

@ -1,5 +1,7 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Enums;
@ -24,6 +26,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
private readonly IPushRegistrationService _pushRegistrationService;
private readonly IPolicyService _policyService;
private readonly IDeviceRepository _deviceRepository;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly IFeatureService _featureService;
public ConfirmOrganizationUserCommand(
IOrganizationRepository organizationRepository,
@ -35,7 +39,9 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
IPushNotificationService pushNotificationService,
IPushRegistrationService pushRegistrationService,
IPolicyService policyService,
IDeviceRepository deviceRepository)
IDeviceRepository deviceRepository,
IPolicyRequirementQuery policyRequirementQuery,
IFeatureService featureService)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -47,6 +53,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
_pushRegistrationService = pushRegistrationService;
_policyService = policyService;
_deviceRepository = deviceRepository;
_policyRequirementQuery = policyRequirementQuery;
_featureService = featureService;
}
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
@ -118,8 +126,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
}
}
var twoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled;
await CheckPoliciesAsync(organizationId, user, orgUsers, twoFactorEnabled);
var userTwoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled;
await CheckPoliciesAsync(organizationId, user, orgUsers, userTwoFactorEnabled);
orgUser.Status = OrganizationUserStatusType.Confirmed;
orgUser.Key = keys[orgUser.Id];
orgUser.Email = null;
@ -142,15 +150,10 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
}
private async Task CheckPoliciesAsync(Guid organizationId, User user,
ICollection<OrganizationUser> userOrgs, bool twoFactorEnabled)
ICollection<OrganizationUser> userOrgs, bool userTwoFactorEnabled)
{
// Enforce Two Factor Authentication Policy for this organization
var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication))
.Any(p => p.OrganizationId == organizationId);
if (orgRequiresTwoFactor && !twoFactorEnabled)
{
throw new BadRequestException("User does not have two-step login enabled.");
}
await ValidateTwoFactorAuthenticationPolicyAsync(user, organizationId, userTwoFactorEnabled);
var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId);
var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg);
@ -168,6 +171,33 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
}
}
private async Task ValidateTwoFactorAuthenticationPolicyAsync(User user, Guid organizationId, bool userTwoFactorEnabled)
{
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
{
if (userTwoFactorEnabled)
{
// If the user has two-step login enabled, we skip checking the 2FA policy
return;
}
var twoFactorPolicyRequirement = await _policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id);
if (twoFactorPolicyRequirement.IsTwoFactorRequiredForOrganization(organizationId))
{
throw new BadRequestException("User does not have two-step login enabled.");
}
return;
}
var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication))
.Any(p => p.OrganizationId == organizationId);
if (orgRequiresTwoFactor && !userTwoFactorEnabled)
{
throw new BadRequestException("User does not have two-step login enabled.");
}
}
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
{
var devices = await GetUserDeviceIdsAsync(userId);

View File

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

View File

@ -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<InviteOrganizationUsersCommand> logger,
@ -93,7 +87,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
InviteOrganization = request.InviteOrganization,
PerformedBy = request.PerformedBy,
PerformedAt = request.PerformedAt,
OccupiedPmSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(request.InviteOrganization.OrganizationId),
OccupiedPmSeats = (await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(request.InviteOrganization.OrganizationId)).Total,
OccupiedSmSeats = await organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(request.InviteOrganization.OrganizationId)
});
@ -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<InviteOrganizationUsersValidationRequest> validatedResult,
Organization organization) =>
await referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.InvitedUsers, organization, currentContext)
{
Users = validatedResult.Value.Invites.Length
});
private async Task SendInvitesAsync(IEnumerable<CreateOrganizationUser> users, Organization organization) =>
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
});
}
}
}

View File

@ -2,6 +2,7 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Utilities.Validation;
@ -83,14 +84,9 @@ public class InviteUsersPasswordManagerValidator(
return invalidEnvironment.Map(request);
}
var organizationValidationResult = await inviteUsersOrganizationValidator.ValidateAsync(request.InviteOrganization);
if (organizationValidationResult is Invalid<InviteOrganization> organizationValidation)
{
return organizationValidation.Map(request);
}
// Organizations managed by a provider need to be scaled by the provider. This needs to be checked in the event seats are increasing.
var provider = await providerRepository.GetByOrganizationIdAsync(request.InviteOrganization.OrganizationId);
if (provider is not null)
{
var providerValidationResult = InvitingUserOrganizationProviderValidator.Validate(new InviteOrganizationProvider(provider));
@ -101,6 +97,13 @@ public class InviteUsersPasswordManagerValidator(
}
}
var organizationValidationResult = await inviteUsersOrganizationValidator.ValidateAsync(request.InviteOrganization);
if (organizationValidationResult is Invalid<InviteOrganization> organizationValidation)
{
return organizationValidation.Map(request);
}
var paymentSubscription = await paymentService.GetSubscriptionAsync(
await organizationRepository.GetByIdAsync(request.InviteOrganization.OrganizationId));

View File

@ -1,10 +1,9 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
using Bit.Core.AdminConsole.Utilities.Validation;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
public static class InviteUserPaymentValidation
{

View File

@ -1,5 +1,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Enums;
@ -22,7 +24,9 @@ public class RestoreOrganizationUserCommand(
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IPolicyService policyService,
IUserRepository userRepository,
IOrganizationService organizationService) : IRestoreOrganizationUserCommand
IOrganizationService organizationService,
IFeatureService featureService,
IPolicyRequirementQuery policyRequirementQuery) : IRestoreOrganizationUserCommand
{
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId)
{
@ -66,8 +70,8 @@ public class RestoreOrganizationUserCommand(
}
var organization = await organizationRepository.GetByIdAsync(organizationUser.OrganizationId);
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
var seatCounts = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
var availableSeats = organization.Seats.GetValueOrDefault(0) - seatCounts.Total;
if (availableSeats < 1)
{
@ -159,8 +163,8 @@ public class RestoreOrganizationUserCommand(
}
var organization = await organizationRepository.GetByIdAsync(organizationId);
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
var seatCounts = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
var availableSeats = organization.Seats.GetValueOrDefault(0) - seatCounts.Total;
var newSeatsRequired = organizationUserIds.Count() - availableSeats;
await organizationService.AutoAddSeatsAsync(organization, newSeatsRequired);
@ -270,12 +274,7 @@ public class RestoreOrganizationUserCommand(
// Enforce 2FA Policy of organization user is trying to join
if (!userHasTwoFactorEnabled)
{
var invitedTwoFactorPolicies = await policyService.GetPoliciesApplicableToUserAsync(userId,
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked);
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
{
twoFactorCompliant = false;
}
twoFactorCompliant = !await IsTwoFactorRequiredForOrganizationAsync(userId, orgUser.OrganizationId);
}
var user = await userRepository.GetByIdAsync(userId);
@ -299,4 +298,17 @@ public class RestoreOrganizationUserCommand(
throw new BadRequestException(user.Email + " is not compliant with the two-step login policy");
}
}
private async Task<bool> IsTwoFactorRequiredForOrganizationAsync(Guid userId, Guid organizationId)
{
if (featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
{
var requirement = await policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(userId);
return requirement.IsTwoFactorRequiredForOrganization(organizationId);
}
var invitedTwoFactorPolicies = await policyService.GetPoliciesApplicableToUserAsync(userId,
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked);
return invitedTwoFactorPolicies.Any(p => p.OrganizationId == organizationId);
}
}

View File

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

View File

@ -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) { }
}

View File

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

View File

@ -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)

View File

@ -0,0 +1,52 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.Enums;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
/// <summary>
/// Policy requirements for the Require Two-Factor Authentication policy.
/// </summary>
public class RequireTwoFactorPolicyRequirement : IPolicyRequirement
{
private readonly IEnumerable<PolicyDetails> _policyDetails;
public RequireTwoFactorPolicyRequirement(IEnumerable<PolicyDetails> policyDetails)
{
_policyDetails = policyDetails;
}
/// <summary>
/// Checks if two-factor authentication is required for the organization due to an active policy.
/// </summary>
/// <param name="organizationId">The ID of the organization to check.</param>
/// <returns>True if two-factor authentication is required for the organization, false otherwise.</returns>
/// <remarks>
/// This should be used to check whether the member needs to have 2FA enabled before being
/// accepted, confirmed, or restored to the organization.
/// </remarks>
public bool IsTwoFactorRequiredForOrganization(Guid organizationId) =>
_policyDetails.Any(p => p.OrganizationId == organizationId);
/// <summary>
/// Returns tuples of (OrganizationId, OrganizationUserId) for active memberships where two-factor authentication is required.
/// Users should be revoked from these organizations if they disable all 2FA methods.
/// </summary>
public IEnumerable<(Guid OrganizationId, Guid OrganizationUserId)> OrganizationsRequiringTwoFactor =>
_policyDetails
.Where(p => p.OrganizationUserStatus is
OrganizationUserStatusType.Accepted or
OrganizationUserStatusType.Confirmed)
.Select(p => (p.OrganizationId, p.OrganizationUserId));
}
public class RequireTwoFactorPolicyRequirementFactory : BasePolicyRequirementFactory<RequireTwoFactorPolicyRequirement>
{
public override PolicyType PolicyType => PolicyType.TwoFactorAuthentication;
protected override IEnumerable<OrganizationUserStatusType> ExemptStatuses => [];
public override RequireTwoFactorPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
{
return new RequireTwoFactorPolicyRequirement(policyDetails);
}
}

View File

@ -36,5 +36,6 @@ public static class PolicyServiceCollectionExtensions
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, ResetPasswordPolicyRequirementFactory>();
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, PersonalOwnershipPolicyRequirementFactory>();
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireSsoPolicyRequirementFactory>();
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireTwoFactorPolicyRequirementFactory>();
}
}

View File

@ -104,8 +104,8 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
throw new BadRequestException(string.Join(", ", commandResult.ErrorMessages));
}
await Task.WhenAll(currentActiveRevocableOrganizationUsers.Select(x =>
_mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), x.Email)));
await Task.WhenAll(nonCompliantUsers.Select(nonCompliantUser =>
_mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), nonCompliantUser.user.Email)));
}
private static bool MembersWithNoMasterPasswordWillLoseAccess(

View File

@ -1,6 +1,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
#nullable enable
@ -25,4 +26,14 @@ public interface IOrganizationRepository : IRepository<Organization, Guid>
Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId);
Task<ICollection<Organization>> GetAddableToProviderByUserIdAsync(Guid userId, ProviderType providerType);
Task<ICollection<Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids);
/// <summary>
/// Returns the number of occupied seats for an organization.
/// OrganizationUsers occupy a seat, unless they are revoked.
/// As of https://bitwarden.atlassian.net/browse/PM-17772, a seat is also occupied by a Families for Enterprise sponsorship sent by an
/// organization admin, even if the user sent the invitation doesn't have a corresponding OrganizationUser in the Enterprise organization.
/// </summary>
/// <param name="organizationId">The ID of the organization to get the occupied seat count for.</param>
/// <returns>The number of occupied seats for the organization.</returns>
Task<OrganizationSeatCounts> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId);
}

View File

@ -18,16 +18,6 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
Task<ICollection<OrganizationUser>> GetManyByUserAsync(Guid userId);
Task<ICollection<OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type);
Task<int> GetCountByOrganizationAsync(Guid organizationId, string email, bool onlyRegisteredUsers);
/// <summary>
/// Returns the number of occupied seats for an organization.
/// Occupied seats are OrganizationUsers that have at least been invited.
/// As of https://bitwarden.atlassian.net/browse/PM-17772, a seat is also occupied by a Families for Enterprise sponsorship sent by an
/// organization admin, even if the user sent the invitation doesn't have a corresponding OrganizationUser in the Enterprise organization.
/// </summary>
/// <param name="organizationId">The ID of the organization to get the occupied seat count for.</param>
/// <returns>The number of occupied seats for the organization.</returns>
Task<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId);
Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails, bool onlyRegisteredUsers);
Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId);
Task<Tuple<OrganizationUser?, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id);

View File

@ -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<EventLoggingListenerService> _logger;
protected EventLoggingListenerService(IEventMessageHandler handler)
protected EventLoggingListenerService(IEventMessageHandler handler, ILogger<EventLoggingListenerService> 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<IEnumerable<EventMessage>>();
await _handler.HandleManyEventsAsync(eventMessages);
}
else if (root.ValueKind == JsonValueKind.Object)
{
var eventMessage = root.Deserialize<EventMessage>();
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"
);
}
}
}
}

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