Browse Source

Merge branch 'main' into BRE-1037/update-rc-deploy-trigger

BRE-1037/update-rc-deploy-trigger
AJ Mabry 5 months ago
parent
commit
3983498319
No known key found for this signature in database
GPG Key ID: 959B72506842FAE9
  1. 1
      .github/renovate.json5
  2. 2
      Directory.Build.props
  3. 22
      bitwarden_license/src/Scim/Dockerfile
  4. 2
      bitwarden_license/src/Scim/entrypoint.sh
  5. 22
      bitwarden_license/src/Sso/Dockerfile
  6. 2
      bitwarden_license/src/Sso/entrypoint.sh
  7. 45
      src/Admin/Dockerfile
  8. 2
      src/Admin/entrypoint.sh
  9. 14
      src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs
  10. 22
      src/Api/Dockerfile
  11. 8
      src/Api/Models/Response/DomainsResponseModel.cs
  12. 23
      src/Api/Vault/Models/Response/SyncResponseModel.cs
  13. 2
      src/Api/entrypoint.sh
  14. 20
      src/Billing/Dockerfile
  15. 2
      src/Billing/entrypoint.sh
  16. 12
      src/Core/AdminConsole/Models/Data/Organizations/Policies/MasterPasswordPolicyData.cs
  17. 55
      src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordPolicyRequirement.cs
  18. 1
      src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs
  19. 23
      src/Core/AdminConsole/Services/Implementations/PolicyService.cs
  20. 10
      src/Core/Auth/Models/Api/Response/UserDecryptionOptions.cs
  21. 6
      src/Core/Billing/Commands/BillingCommandResult.cs
  22. 20
      src/Core/KeyManagement/Models/Response/MasterPasswordUnlockResponseModel.cs
  23. 9
      src/Core/KeyManagement/Models/Response/UserDecryptionResponseModel.cs
  24. 36
      src/Core/Utilities/LoggerFactoryExtensions.cs
  25. 22
      src/Events/Dockerfile
  26. 2
      src/Events/entrypoint.sh
  27. 20
      src/EventsProcessor/Dockerfile
  28. 2
      src/EventsProcessor/entrypoint.sh
  29. 21
      src/Icons/Dockerfile
  30. 2
      src/Icons/entrypoint.sh
  31. 22
      src/Identity/Dockerfile
  32. 47
      src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs
  33. 2
      src/Identity/entrypoint.sh
  34. 20
      src/Notifications/Dockerfile
  35. 2
      src/Notifications/entrypoint.sh
  36. 100
      test/Api.IntegrationTest/Vault/Controllers/SyncControllerTests.cs
  37. 14
      test/Api.Test/Controllers/PoliciesControllerTests.cs
  38. 49
      test/Api.Test/Vault/Controllers/SyncControllerTests.cs
  39. 75
      test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordPolicyRequirementTests.cs
  40. 38
      test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs
  41. 195
      test/Core.Test/Utilities/LoggerFactoryExtensionsTests.cs
  42. 38
      test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs
  43. 31
      test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs
  44. 102
      test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs
  45. 30
      test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs
  46. 20
      util/Attachments/Dockerfile
  47. 12
      util/Attachments/entrypoint.sh
  48. 13
      util/MsSqlMigratorUtility/Dockerfile
  49. 10
      util/Nginx/Dockerfile
  50. 40
      util/Nginx/Dockerfile-k8s
  51. 2
      util/Nginx/entrypoint.sh
  52. 11
      util/Nginx/setup-bwuser.sh
  53. 20
      util/Setup/Dockerfile
  54. 2
      util/Setup/Helpers.cs
  55. 2
      util/Setup/entrypoint.sh

1
.github/renovate.json5

@ -84,7 +84,6 @@ @@ -84,7 +84,6 @@
"Serilog.AspNetCore",
"Serilog.Extensions.Logging",
"Serilog.Extensions.Logging.File",
"Serilog.Sinks.AzureCosmosDB",
"Serilog.Sinks.SyslogMessages",
"Stripe.net",
"Swashbuckle.AspNetCore",

2
Directory.Build.props

@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>2025.7.1</Version>
<Version>2025.7.2</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>

22
bitwarden_license/src/Scim/Dockerfile

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
###############################################
# Build stage #
###############################################
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build
# Docker buildx supplies the value for this arg
ARG TARGETPLATFORM
@ -9,11 +9,11 @@ ARG TARGETPLATFORM @@ -9,11 +9,11 @@ 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 ; \
RID=linux-musl-x64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
RID=linux-arm64 ; \
RID=linux-musl-arm64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
RID=linux-arm ; \
RID=linux-musl-arm ; \
fi \
&& echo "RID=$RID" > /tmp/rid.txt
@ -37,21 +37,21 @@ RUN . /tmp/rid.txt && dotnet publish \ @@ -37,21 +37,21 @@ RUN . /tmp/rid.txt && dotnet publish \
###############################################
# App stage #
###############################################
FROM mcr.microsoft.com/dotnet/aspnet:8.0
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21
ARG TARGETPLATFORM
LABEL com.bitwarden.product="bitwarden"
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:5000
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
EXPOSE 5000
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gosu \
curl \
krb5-user \
&& rm -rf /var/lib/apt/lists/*
RUN apk add --no-cache curl \
krb5 \
icu-libs \
shadow \
&& apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu
# Copy app from the build stage
WORKDIR /app

2
bitwarden_license/src/Scim/entrypoint.sh

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/sh
# Setup

22
bitwarden_license/src/Sso/Dockerfile

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
###############################################
# Build stage #
###############################################
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build
# Docker buildx supplies the value for this arg
ARG TARGETPLATFORM
@ -9,11 +9,11 @@ ARG TARGETPLATFORM @@ -9,11 +9,11 @@ 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 ; \
RID=linux-musl-x64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
RID=linux-arm64 ; \
RID=linux-musl-arm64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
RID=linux-arm ; \
RID=linux-musl-arm ; \
fi \
&& echo "RID=$RID" > /tmp/rid.txt
@ -37,21 +37,21 @@ RUN . /tmp/rid.txt && dotnet publish \ @@ -37,21 +37,21 @@ RUN . /tmp/rid.txt && dotnet publish \
###############################################
# App stage #
###############################################
FROM mcr.microsoft.com/dotnet/aspnet:8.0
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21
ARG TARGETPLATFORM
LABEL com.bitwarden.product="bitwarden"
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:5000
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
EXPOSE 5000
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gosu \
curl \
krb5-user \
&& rm -rf /var/lib/apt/lists/*
RUN apk add --no-cache curl \
krb5 \
icu-libs \
shadow \
&& apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu
# Copy app from the build stage
WORKDIR /app

2
bitwarden_license/src/Sso/entrypoint.sh

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/sh
# Setup

45
src/Admin/Dockerfile

@ -1,40 +1,41 @@ @@ -1,40 +1,41 @@
###############################################
# Node.js build stage #
###############################################
FROM node:20-alpine3.21 AS node-build
WORKDIR /app
COPY src/Admin/package*.json ./
COPY /src/Admin/ .
RUN npm ci
RUN npm run build
###############################################
# Build stage #
###############################################
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build
# Docker buildx supplies the value for this arg
ARG TARGETPLATFORM
# Determine proper runtime value for .NET
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
RID=linux-x64 ; \
RID=linux-musl-x64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
RID=linux-arm64 ; \
RID=linux-musl-arm64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
RID=linux-arm ; \
RID=linux-musl-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 \
@ -46,25 +47,27 @@ RUN . /tmp/rid.txt && dotnet publish \ @@ -46,25 +47,27 @@ RUN . /tmp/rid.txt && dotnet publish \
###############################################
# App stage #
###############################################
FROM mcr.microsoft.com/dotnet/aspnet:8.0
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21
ARG TARGETPLATFORM
LABEL com.bitwarden.product="bitwarden"
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:5000
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
EXPOSE 5000
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gosu \
curl \
krb5-user \
&& rm -rf /var/lib/apt/lists/*
RUN apk add --no-cache curl \
icu-libs \
tzdata \
krb5 \
shadow \
&& apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu
# Copy app from the build stage
WORKDIR /app
COPY --from=build /source/src/Admin/out /app
COPY --from=node-build /app/wwwroot /app/wwwroot
COPY ./src/Admin/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1

2
src/Admin/entrypoint.sh

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/sh
# Setup

14
src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
#nullable enable
using Bit.Api.Billing.Attributes;
using Bit.Api.Billing.Attributes;
using Bit.Api.Billing.Models.Requests.Payment;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Payment.Commands;
using Bit.Core.Billing.Payment.Queries;
using Bit.Core.Utilities;
@ -19,6 +19,7 @@ public class ProviderBillingVNextController( @@ -19,6 +19,7 @@ public class ProviderBillingVNextController(
IGetBillingAddressQuery getBillingAddressQuery,
IGetCreditQuery getCreditQuery,
IGetPaymentMethodQuery getPaymentMethodQuery,
IProviderService providerService,
IUpdateBillingAddressCommand updateBillingAddressCommand,
IUpdatePaymentMethodCommand updatePaymentMethodCommand,
IVerifyBankAccountCommand verifyBankAccountCommand) : BaseBillingController
@ -82,6 +83,15 @@ public class ProviderBillingVNextController( @@ -82,6 +83,15 @@ public class ProviderBillingVNextController(
{
var (paymentMethod, billingAddress) = request.ToDomain();
var result = await updatePaymentMethodCommand.Run(provider, paymentMethod, billingAddress);
// TODO: Temporary until we can send Provider notifications from the Billing API
if (!provider.Enabled)
{
await result.TapAsync(async _ =>
{
provider.Enabled = true;
await providerService.UpdateAsync(provider);
});
}
return Handle(result);
}

22
src/Api/Dockerfile

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
###############################################
# Build stage #
###############################################
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build
# Docker buildx supplies the value for this arg
ARG TARGETPLATFORM
@ -9,11 +9,11 @@ ARG TARGETPLATFORM @@ -9,11 +9,11 @@ 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 ; \
RID=linux-musl-x64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
RID=linux-arm64 ; \
RID=linux-musl-arm64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
RID=linux-arm ; \
RID=linux-musl-arm ; \
fi \
&& echo "RID=$RID" > /tmp/rid.txt
@ -37,21 +37,21 @@ RUN . /tmp/rid.txt && dotnet publish \ @@ -37,21 +37,21 @@ RUN . /tmp/rid.txt && dotnet publish \
###############################################
# App stage #
###############################################
FROM mcr.microsoft.com/dotnet/aspnet:8.0
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21
ARG TARGETPLATFORM
LABEL com.bitwarden.product="bitwarden"
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:5000
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
EXPOSE 5000
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gosu \
curl \
krb5-user \
&& rm -rf /var/lib/apt/lists/*
RUN apk add --no-cache curl \
krb5 \
icu-libs \
shadow \
&& apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu
# Copy app from the build stage
WORKDIR /app

8
src/Api/Models/Response/DomainsResponseModel.cs

@ -8,10 +8,10 @@ using Bit.Core.Models.Api; @@ -8,10 +8,10 @@ using Bit.Core.Models.Api;
namespace Bit.Api.Models.Response;
public class DomainsResponseModel : ResponseModel
public class DomainsResponseModel() : ResponseModel("domains")
{
public DomainsResponseModel(User user, bool excluded = true)
: base("domains")
: this()
{
if (user == null)
{
@ -38,13 +38,13 @@ public class DomainsResponseModel : ResponseModel @@ -38,13 +38,13 @@ public class DomainsResponseModel : ResponseModel
public IEnumerable<GlobalDomains> GlobalEquivalentDomains { get; set; }
public class GlobalDomains
public class GlobalDomains()
{
public GlobalDomains(
GlobalEquivalentDomainsType globalDomain,
IEnumerable<string> domains,
IEnumerable<GlobalEquivalentDomainsType> excludedDomains,
bool excluded)
bool excluded) : this()
{
Type = (byte)globalDomain;
Domains = domains;

23
src/Api/Vault/Models/Response/SyncResponseModel.cs

@ -7,6 +7,7 @@ using Bit.Api.Tools.Models.Response; @@ -7,6 +7,7 @@ using Bit.Api.Tools.Models.Response;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Response;
using Bit.Core.Models.Api;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations;
@ -18,7 +19,7 @@ using Bit.Core.Vault.Models.Data; @@ -18,7 +19,7 @@ using Bit.Core.Vault.Models.Data;
namespace Bit.Api.Vault.Models.Response;
public class SyncResponseModel : ResponseModel
public class SyncResponseModel() : ResponseModel("sync")
{
public SyncResponseModel(
GlobalSettings globalSettings,
@ -37,7 +38,7 @@ public class SyncResponseModel : ResponseModel @@ -37,7 +38,7 @@ public class SyncResponseModel : ResponseModel
bool excludeDomains,
IEnumerable<Policy> policies,
IEnumerable<Send> sends)
: base("sync")
: this()
{
Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingingUser);
@ -54,6 +55,23 @@ public class SyncResponseModel : ResponseModel @@ -54,6 +55,23 @@ public class SyncResponseModel : ResponseModel
Domains = excludeDomains ? null : new DomainsResponseModel(user, false);
Policies = policies?.Select(p => new PolicyResponseModel(p)) ?? new List<PolicyResponseModel>();
Sends = sends.Select(s => new SendResponseModel(s, globalSettings));
UserDecryption = new UserDecryptionResponseModel
{
MasterPasswordUnlock = user.HasMasterPassword()
? new MasterPasswordUnlockResponseModel
{
Kdf = new MasterPasswordUnlockKdfResponseModel
{
KdfType = user.Kdf,
Iterations = user.KdfIterations,
Memory = user.KdfMemory,
Parallelism = user.KdfParallelism
},
MasterKeyEncryptedUserKey = user.Key!,
Salt = user.Email.ToLowerInvariant()
}
: null
};
}
public ProfileResponseModel Profile { get; set; }
@ -63,4 +81,5 @@ public class SyncResponseModel : ResponseModel @@ -63,4 +81,5 @@ public class SyncResponseModel : ResponseModel
public DomainsResponseModel Domains { get; set; }
public IEnumerable<PolicyResponseModel> Policies { get; set; }
public IEnumerable<SendResponseModel> Sends { get; set; }
public UserDecryptionResponseModel UserDecryption { get; set; }
}

2
src/Api/entrypoint.sh

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/sh
# Setup

20
src/Billing/Dockerfile

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
###############################################
# Build stage #
###############################################
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build
# Docker buildx supplies the value for this arg
ARG TARGETPLATFORM
@ -9,11 +9,11 @@ ARG TARGETPLATFORM @@ -9,11 +9,11 @@ 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 ; \
RID=linux-musl-x64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
RID=linux-arm64 ; \
RID=linux-musl-arm64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
RID=linux-arm ; \
RID=linux-musl-arm ; \
fi \
&& echo "RID=$RID" > /tmp/rid.txt
@ -37,20 +37,20 @@ RUN . /tmp/rid.txt && dotnet publish \ @@ -37,20 +37,20 @@ RUN . /tmp/rid.txt && dotnet publish \
###############################################
# App stage #
###############################################
FROM mcr.microsoft.com/dotnet/aspnet:8.0
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21
ARG TARGETPLATFORM
LABEL com.bitwarden.product="bitwarden"
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:5000
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
EXPOSE 5000
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gosu \
curl \
&& rm -rf /var/lib/apt/lists/*
RUN apk add --no-cache curl \
icu-libs \
shadow \
&& apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu
# Copy app from the build stage
WORKDIR /app

2
src/Billing/entrypoint.sh

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/sh
# Setup

12
src/Core/AdminConsole/Models/Data/Organizations/Policies/MasterPasswordPolicyData.cs

@ -1,20 +1,28 @@ @@ -1,20 +1,28 @@
namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using System.Text.Json.Serialization;
namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
public class MasterPasswordPolicyData : IPolicyDataModel
{
[JsonPropertyName("minComplexity")]
public int? MinComplexity { get; set; }
[JsonPropertyName("minLength")]
public int? MinLength { get; set; }
[JsonPropertyName("requireLower")]
public bool? RequireLower { get; set; }
[JsonPropertyName("requireUpper")]
public bool? RequireUpper { get; set; }
[JsonPropertyName("requireNumbers")]
public bool? RequireNumbers { get; set; }
[JsonPropertyName("requireSpecial")]
public bool? RequireSpecial { get; set; }
[JsonPropertyName("enforceOnLogin")]
public bool? EnforceOnLogin { get; set; }
/// <summary>
/// Combine the other policy data with this instance, taking the most secure options
/// </summary>
/// <param name="other">The other policy instance to combine with this</param>
public void CombineWith(MasterPasswordPolicyData other)
public void CombineWith(MasterPasswordPolicyData? other)
{
if (other == null)
{

55
src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordPolicyRequirement.cs

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
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 Master Password Requirements policy.
/// </summary>
public class MasterPasswordPolicyRequirement : IPolicyRequirement
{
/// <summary>
/// Indicates whether MasterPassword requirements are enabled for the user.
/// </summary>
public bool Enabled { get; init; }
/// <summary>
/// Master Password Policy data model associated with this Policy
/// </summary>
public MasterPasswordPolicyData? EnforcedOptions { get; init; }
}
public class MasterPasswordPolicyRequirementFactory : BasePolicyRequirementFactory<MasterPasswordPolicyRequirement>
{
public override PolicyType PolicyType => PolicyType.MasterPassword;
protected override bool ExemptProviders => false;
protected override IEnumerable<OrganizationUserType> ExemptRoles => [];
protected override IEnumerable<OrganizationUserStatusType> ExemptStatuses =>
[OrganizationUserStatusType.Accepted,
OrganizationUserStatusType.Invited,
OrganizationUserStatusType.Revoked,
];
public override MasterPasswordPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
{
var result = policyDetails
.Select(p => p.GetDataModel<MasterPasswordPolicyData>())
.Aggregate(
new MasterPasswordPolicyRequirement(),
(result, data) =>
{
data.CombineWith(result.EnforcedOptions);
return new MasterPasswordPolicyRequirement
{
Enabled = true,
EnforcedOptions = data
};
});
return result;
}
}

1
src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs

@ -37,5 +37,6 @@ public static class PolicyServiceCollectionExtensions @@ -37,5 +37,6 @@ public static class PolicyServiceCollectionExtensions
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, OrganizationDataOwnershipPolicyRequirementFactory>();
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireSsoPolicyRequirementFactory>();
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireTwoFactorPolicyRequirementFactory>();
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, MasterPasswordPolicyRequirementFactory>();
}
}

23
src/Core/AdminConsole/Services/Implementations/PolicyService.cs

@ -3,6 +3,8 @@ @@ -3,6 +3,8 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
@ -19,21 +21,39 @@ public class PolicyService : IPolicyService @@ -19,21 +21,39 @@ public class PolicyService : IPolicyService
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IPolicyRepository _policyRepository;
private readonly GlobalSettings _globalSettings;
private readonly IFeatureService _featureService;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
public PolicyService(
IApplicationCacheService applicationCacheService,
IOrganizationUserRepository organizationUserRepository,
IPolicyRepository policyRepository,
GlobalSettings globalSettings)
GlobalSettings globalSettings,
IFeatureService featureService,
IPolicyRequirementQuery policyRequirementQuery)
{
_applicationCacheService = applicationCacheService;
_organizationUserRepository = organizationUserRepository;
_policyRepository = policyRepository;
_globalSettings = globalSettings;
_featureService = featureService;
_policyRequirementQuery = policyRequirementQuery;
}
public async Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user)
{
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
{
var masterPaswordPolicy = (await _policyRequirementQuery.GetAsync<MasterPasswordPolicyRequirement>(user.Id));
if (!masterPaswordPolicy.Enabled)
{
return null;
}
return masterPaswordPolicy.EnforcedOptions;
}
var policies = (await _policyRepository.GetManyByUserIdAsync(user.Id))
.Where(p => p.Type == PolicyType.MasterPassword && p.Enabled)
.ToList();
@ -51,6 +71,7 @@ public class PolicyService : IPolicyService @@ -51,6 +71,7 @@ public class PolicyService : IPolicyService
}
return enforcedOptions;
}
public async Task<ICollection<OrganizationUserPolicyDetails>> GetPoliciesApplicableToUserAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted)

10
src/Core/Auth/Models/Api/Response/UserDecryptionOptions.cs

@ -1,8 +1,7 @@ @@ -1,8 +1,7 @@
using System.Text.Json.Serialization;
using Bit.Core.KeyManagement.Models.Response;
using Bit.Core.Models.Api;
#nullable enable
namespace Bit.Core.Auth.Models.Api.Response;
public class UserDecryptionOptions : ResponseModel
@ -14,8 +13,15 @@ public class UserDecryptionOptions : ResponseModel @@ -14,8 +13,15 @@ public class UserDecryptionOptions : ResponseModel
/// <summary>
/// Gets or sets whether the current user has a master password that can be used to decrypt their vault.
/// </summary>
[Obsolete("Use MasterPasswordUnlock instead. This will be removed in a future version.")]
public bool HasMasterPassword { get; set; }
/// <summary>
/// Gets or sets whether the current user has master password unlock data available.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public MasterPasswordUnlockResponseModel? MasterPasswordUnlock { get; set; }
/// <summary>
/// Gets or sets the WebAuthn PRF decryption keys.
/// </summary>

6
src/Core/Billing/Commands/BillingCommandResult.cs

@ -28,4 +28,10 @@ public class BillingCommandResult<T> : OneOfBase<T, BadRequest, Conflict, Unhand @@ -28,4 +28,10 @@ public class BillingCommandResult<T> : OneOfBase<T, BadRequest, Conflict, Unhand
public static implicit operator BillingCommandResult<T>(BadRequest badRequest) => new(badRequest);
public static implicit operator BillingCommandResult<T>(Conflict conflict) => new(conflict);
public static implicit operator BillingCommandResult<T>(Unhandled unhandled) => new(unhandled);
public Task TapAsync(Func<T, Task> f) => Match(
f,
_ => Task.CompletedTask,
_ => Task.CompletedTask,
_ => Task.CompletedTask);
}

20
src/Core/KeyManagement/Models/Response/MasterPasswordUnlockResponseModel.cs

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
using Bit.Core.Utilities;
namespace Bit.Core.KeyManagement.Models.Response;
public class MasterPasswordUnlockResponseModel
{
public required MasterPasswordUnlockKdfResponseModel Kdf { get; init; }
[EncryptedString] public required string MasterKeyEncryptedUserKey { get; init; }
[StringLength(256)] public required string Salt { get; init; }
}
public class MasterPasswordUnlockKdfResponseModel
{
public required KdfType KdfType { get; init; }
public required int Iterations { get; init; }
public int? Memory { get; init; }
public int? Parallelism { get; init; }
}

9
src/Core/KeyManagement/Models/Response/UserDecryptionResponseModel.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
namespace Bit.Core.KeyManagement.Models.Response;
public class UserDecryptionResponseModel
{
/// <summary>
/// Returns the unlock data when the user has a master password that can be used to decrypt their vault.
/// </summary>
public MasterPasswordUnlockResponseModel? MasterPasswordUnlock { get; set; }
}

36
src/Core/Utilities/LoggerFactoryExtensions.cs

@ -1,7 +1,4 @@ @@ -1,7 +1,4 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.X509Certificates;
using Bit.Core.Settings;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@ -33,7 +30,7 @@ public static class LoggerFactoryExtensions @@ -33,7 +30,7 @@ public static class LoggerFactoryExtensions
public static ILoggingBuilder AddSerilog(
this ILoggingBuilder builder,
WebHostBuilderContext context,
Func<LogEvent, IGlobalSettings, bool> filter = null)
Func<LogEvent, IGlobalSettings, bool>? filter = null)
{
var globalSettings = new GlobalSettings();
ConfigurationBinder.Bind(context.Configuration.GetSection("GlobalSettings"), globalSettings);
@ -57,19 +54,27 @@ public static class LoggerFactoryExtensions @@ -57,19 +54,27 @@ public static class LoggerFactoryExtensions
return filter(e, globalSettings);
}
var logSentryWarning = false;
var logSyslogWarning = false;
// Path format is the only required option for file logging, we will use that as
// the keystone for if they have configured the new location.
var newPathFormat = context.Configuration["Logging:PathFormat"];
var config = new LoggerConfiguration()
.MinimumLevel.Verbose()
.Enrich.FromLogContext()
.Filter.ByIncludingOnly(inclusionPredicate);
if (CoreHelpers.SettingHasValue(globalSettings?.Sentry.Dsn))
if (CoreHelpers.SettingHasValue(globalSettings.Sentry.Dsn))
{
config.WriteTo.Sentry(globalSettings.Sentry.Dsn)
.Enrich.FromLogContext()
.Enrich.WithProperty("Project", globalSettings.ProjectName);
}
else if (CoreHelpers.SettingHasValue(globalSettings?.Syslog.Destination))
else if (CoreHelpers.SettingHasValue(globalSettings.Syslog.Destination))
{
logSyslogWarning = true;
// appending sitename to project name to allow easier identification in syslog.
var appName = $"{globalSettings.SiteName}-{globalSettings.ProjectName}";
if (globalSettings.Syslog.Destination.Equals("local", StringComparison.OrdinalIgnoreCase))
@ -107,10 +112,14 @@ public static class LoggerFactoryExtensions @@ -107,10 +112,14 @@ public static class LoggerFactoryExtensions
certProvider: new CertificateFileProvider(globalSettings.Syslog.CertificatePath,
globalSettings.Syslog?.CertificatePassword ?? string.Empty));
}
}
}
}
else if (!string.IsNullOrEmpty(newPathFormat))
{
// Use new location
builder.AddFile(context.Configuration.GetSection("Logging"));
}
else if (CoreHelpers.SettingHasValue(globalSettings.LogDirectory))
{
if (globalSettings.LogRollBySizeLimit.HasValue)
@ -138,6 +147,17 @@ public static class LoggerFactoryExtensions @@ -138,6 +147,17 @@ public static class LoggerFactoryExtensions
}
var serilog = config.CreateLogger();
if (logSentryWarning)
{
serilog.Warning("Sentry for logging has been deprecated. Read more: https://btwrdn.com/log-deprecation");
}
if (logSyslogWarning)
{
serilog.Warning("Syslog for logging has been deprecated. Read more: https://btwrdn.com/log-deprecation");
}
builder.AddSerilog(serilog);
return builder;

22
src/Events/Dockerfile

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
###############################################
# Build stage #
###############################################
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build
# Docker buildx supplies the value for this arg
ARG TARGETPLATFORM
@ -9,11 +9,11 @@ ARG TARGETPLATFORM @@ -9,11 +9,11 @@ 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 ; \
RID=linux-musl-x64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
RID=linux-arm64 ; \
RID=linux-musl-arm64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
RID=linux-arm ; \
RID=linux-musl-arm ; \
fi \
&& echo "RID=$RID" > /tmp/rid.txt
@ -37,21 +37,21 @@ RUN . /tmp/rid.txt && dotnet publish \ @@ -37,21 +37,21 @@ RUN . /tmp/rid.txt && dotnet publish \
###############################################
# App stage #
###############################################
FROM mcr.microsoft.com/dotnet/aspnet:8.0
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21
ARG TARGETPLATFORM
LABEL com.bitwarden.product="bitwarden"
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:5000
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
EXPOSE 5000
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gosu \
curl \
krb5-user \
&& rm -rf /var/lib/apt/lists/*
RUN apk add --no-cache curl \
icu-libs \
krb5 \
shadow \
&& apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu
# Copy app from the build stage
WORKDIR /app

2
src/Events/entrypoint.sh

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/sh
# Setup

20
src/EventsProcessor/Dockerfile

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
###############################################
# Build stage #
###############################################
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build
# Docker buildx supplies the value for this arg
ARG TARGETPLATFORM
@ -9,11 +9,11 @@ ARG TARGETPLATFORM @@ -9,11 +9,11 @@ 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 ; \
RID=linux-musl-x64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
RID=linux-arm64 ; \
RID=linux-musl-arm64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
RID=linux-arm ; \
RID=linux-musl-arm ; \
fi \
&& echo "RID=$RID" > /tmp/rid.txt
@ -37,20 +37,20 @@ RUN . /tmp/rid.txt && dotnet publish \ @@ -37,20 +37,20 @@ RUN . /tmp/rid.txt && dotnet publish \
###############################################
# App stage #
###############################################
FROM mcr.microsoft.com/dotnet/aspnet:8.0
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21
ARG TARGETPLATFORM
LABEL com.bitwarden.product="bitwarden"
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:5000
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
EXPOSE 5000
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gosu \
curl \
&& rm -rf /var/lib/apt/lists/*
RUN apk add --no-cache curl \
icu-libs \
shadow \
&& apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu
# Copy app from the build stage
WORKDIR /app

2
src/EventsProcessor/entrypoint.sh

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/sh
# Setup

21
src/Icons/Dockerfile

@ -1,18 +1,18 @@ @@ -1,18 +1,18 @@
###############################################
# Build stage #
###############################################
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build
# Docker buildx supplies the value for this arg
ARG TARGETPLATFORM
# Determine proper runtime value for .NET
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
RID=linux-x64 ; \
RID=linux-musl-x64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
RID=linux-arm64 ; \
RID=linux-musl-arm64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
RID=linux-arm ; \
RID=linux-musl-arm ; \
fi \
&& echo "RID=$RID" > /tmp/rid.txt
@ -36,20 +36,21 @@ RUN . /tmp/rid.txt && dotnet publish \ @@ -36,20 +36,21 @@ RUN . /tmp/rid.txt && dotnet publish \
###############################################
# App stage #
###############################################
FROM mcr.microsoft.com/dotnet/aspnet:8.0
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21
ARG TARGETPLATFORM
LABEL com.bitwarden.product="bitwarden"
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:5000
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
EXPOSE 5000
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gosu \
curl \
&& rm -rf /var/lib/apt/lists/*
RUN apk add --no-cache curl \
krb5 \
icu-libs \
shadow \
&& apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu
# Copy app from the build stage
WORKDIR /app

2
src/Icons/entrypoint.sh

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/sh
# Setup

22
src/Identity/Dockerfile

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
###############################################
# Build stage #
###############################################
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build
# Docker buildx supplies the value for this arg
ARG TARGETPLATFORM
@ -9,11 +9,11 @@ ARG TARGETPLATFORM @@ -9,11 +9,11 @@ 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 ; \
RID=linux-musl-x64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
RID=linux-arm64 ; \
RID=linux-musl-arm64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
RID=linux-arm ; \
RID=linux-musl-arm ; \
fi \
&& echo "RID=$RID" > /tmp/rid.txt
@ -37,21 +37,21 @@ RUN . /tmp/rid.txt && dotnet publish \ @@ -37,21 +37,21 @@ RUN . /tmp/rid.txt && dotnet publish \
###############################################
# App stage #
###############################################
FROM mcr.microsoft.com/dotnet/aspnet:8.0
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21
ARG TARGETPLATFORM
LABEL com.bitwarden.product="bitwarden"
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:5000
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
EXPOSE 5000
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gosu \
curl \
krb5-user \
&& rm -rf /var/lib/apt/lists/*
RUN apk add --no-cache curl \
krb5 \
icu-libs \
shadow \
&& apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu
# Copy app from the build stage
WORKDIR /app

47
src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs

@ -5,6 +5,7 @@ using Bit.Core.Auth.Utilities; @@ -5,6 +5,7 @@ using Bit.Core.Auth.Utilities;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Response;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Bit.Identity.Utilities;
@ -25,7 +26,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder @@ -25,7 +26,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
private readonly ILoginApprovingClientTypes _loginApprovingClientTypes;
private UserDecryptionOptions _options = new UserDecryptionOptions();
private User? _user;
private User _user = null!;
private SsoConfig? _ssoConfig;
private Device? _device;
@ -44,7 +45,6 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder @@ -44,7 +45,6 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
public IUserDecryptionOptionsBuilder ForUser(User user)
{
_options.HasMasterPassword = user.HasMasterPassword();
_user = user;
return this;
}
@ -72,6 +72,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder @@ -72,6 +72,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
public async Task<UserDecryptionOptions> BuildAsync()
{
BuildMasterPasswordUnlock();
BuildKeyConnectorOptions();
await BuildTrustedDeviceOptions();
@ -101,7 +102,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder @@ -101,7 +102,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
}
var isTdeActive = _ssoConfig.GetData() is { MemberDecryptionType: MemberDecryptionType.TrustedDeviceEncryption };
var isTdeOffboarding = _user != null && !_user.HasMasterPassword() && _device != null && _device.IsTrusted() && !isTdeActive;
var isTdeOffboarding = !_user.HasMasterPassword() && _device != null && _device.IsTrusted() && !isTdeActive;
if (!isTdeActive && !isTdeOffboarding)
{
return;
@ -116,7 +117,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder @@ -116,7 +117,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
}
var hasLoginApprovingDevice = false;
if (_device != null && _user != null)
if (_device != null)
{
var allDevices = await _deviceRepository.GetManyByUserIdAsync(_user.Id);
// Checks if the current user has any devices that are capable of approving login with device requests except for
@ -134,16 +135,12 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder @@ -134,16 +135,12 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
hasManageResetPasswordPermission = await _currentContext.ManageResetPassword(_ssoConfig!.OrganizationId);
}
var hasAdminApproval = false;
if (_user != null)
{
// If sso configuration data is not null then I know for sure that ssoConfiguration isn't null
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(_ssoConfig.OrganizationId, _user.Id);
// If sso configuration data is not null then I know for sure that ssoConfiguration isn't null
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(_ssoConfig.OrganizationId, _user.Id);
hasManageResetPasswordPermission |= organizationUser != null && (organizationUser.Type == OrganizationUserType.Owner || organizationUser.Type == OrganizationUserType.Admin);
// They are only able to be approved by an admin if they have enrolled is reset password
hasAdminApproval = organizationUser != null && !string.IsNullOrEmpty(organizationUser.ResetPasswordKey);
}
hasManageResetPasswordPermission |= organizationUser != null && (organizationUser.Type == OrganizationUserType.Owner || organizationUser.Type == OrganizationUserType.Admin);
// They are only able to be approved by an admin if they have enrolled is reset password
var hasAdminApproval = organizationUser != null && !string.IsNullOrEmpty(organizationUser.ResetPasswordKey);
_options.TrustedDeviceOption = new TrustedDeviceUserDecryptionOption(
hasAdminApproval,
@ -153,4 +150,28 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder @@ -153,4 +150,28 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
encryptedPrivateKey,
encryptedUserKey);
}
private void BuildMasterPasswordUnlock()
{
if (_user.HasMasterPassword())
{
_options.HasMasterPassword = true;
_options.MasterPasswordUnlock = new MasterPasswordUnlockResponseModel
{
Kdf = new MasterPasswordUnlockKdfResponseModel
{
KdfType = _user.Kdf,
Iterations = _user.KdfIterations,
Memory = _user.KdfMemory,
Parallelism = _user.KdfParallelism
},
MasterKeyEncryptedUserKey = _user.Key!,
Salt = _user.Email.ToLowerInvariant()
};
}
else
{
_options.HasMasterPassword = false;
}
}
}

2
src/Identity/entrypoint.sh

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/sh
# Setup

20
src/Notifications/Dockerfile

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
###############################################
# Build stage #
###############################################
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build
# Docker buildx supplies the value for this arg
ARG TARGETPLATFORM
@ -9,11 +9,11 @@ ARG TARGETPLATFORM @@ -9,11 +9,11 @@ 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 ; \
RID=linux-musl-x64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
RID=linux-arm64 ; \
RID=linux-musl-arm64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
RID=linux-arm ; \
RID=linux-musl-arm ; \
fi \
&& echo "RID=$RID" > /tmp/rid.txt
@ -37,20 +37,20 @@ RUN . /tmp/rid.txt && dotnet publish \ @@ -37,20 +37,20 @@ RUN . /tmp/rid.txt && dotnet publish \
###############################################
# App stage #
###############################################
FROM mcr.microsoft.com/dotnet/aspnet:8.0
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21
ARG TARGETPLATFORM
LABEL com.bitwarden.product="bitwarden"
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:5000
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
EXPOSE 5000
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gosu \
curl \
&& rm -rf /var/lib/apt/lists/*
RUN apk add --no-cache curl \
icu-libs \
shadow \
&& apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu
# Copy app from the build stage
WORKDIR /app

2
src/Notifications/entrypoint.sh

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/sh
# Setup

100
test/Api.IntegrationTest/Vault/Controllers/SyncControllerTests.cs

@ -0,0 +1,100 @@ @@ -0,0 +1,100 @@
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.Vault.Models.Response;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Api.IntegrationTest.Vault.Controllers;
public class SyncControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
{
private readonly HttpClient _client;
private readonly ApiApplicationFactory _factory;
private readonly LoginHelper _loginHelper;
private readonly IUserRepository _userRepository;
private string _ownerEmail = null!;
public SyncControllerTests(ApiApplicationFactory factory)
{
_factory = factory;
_client = factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
_userRepository = _factory.GetService<IUserRepository>();
}
public async Task InitializeAsync()
{
_ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(_ownerEmail);
}
public Task DisposeAsync()
{
_client.Dispose();
return Task.CompletedTask;
}
[Fact]
// [BitAutoData]
public async Task Get_HaveNoMasterPassword_UserDecryptionMasterPasswordUnlockIsNull()
{
var tempEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(tempEmail);
await _loginHelper.LoginAsync(tempEmail);
// Remove user's password.
var user = await _userRepository.GetByEmailAsync(tempEmail);
Assert.NotNull(user);
user.MasterPassword = null;
await _userRepository.UpsertAsync(user);
var response = await _client.GetAsync("/sync");
response.EnsureSuccessStatusCode();
var syncResponseModel = await response.Content.ReadFromJsonAsync<SyncResponseModel>();
Assert.NotNull(syncResponseModel);
Assert.NotNull(syncResponseModel.UserDecryption);
Assert.Null(syncResponseModel.UserDecryption.MasterPasswordUnlock);
}
[Theory]
[BitAutoData(KdfType.PBKDF2_SHA256, 654_321, null, null)]
[BitAutoData(KdfType.Argon2id, 11, 128, 5)]
public async Task Get_HaveMasterPassword_UserDecryptionMasterPasswordUnlockNotNull(
KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)
{
var tempEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(tempEmail);
await _loginHelper.LoginAsync(tempEmail);
// Change KDF settings
var user = await _userRepository.GetByEmailAsync(tempEmail);
Assert.NotNull(user);
user.Kdf = kdfType;
user.KdfIterations = kdfIterations;
user.KdfMemory = kdfMemory;
user.KdfParallelism = kdfParallelism;
await _userRepository.UpsertAsync(user);
var response = await _client.GetAsync("/sync");
response.EnsureSuccessStatusCode();
var syncResponseModel = await response.Content.ReadFromJsonAsync<SyncResponseModel>();
Assert.NotNull(syncResponseModel);
Assert.NotNull(syncResponseModel.UserDecryption);
Assert.NotNull(syncResponseModel.UserDecryption.MasterPasswordUnlock);
Assert.NotNull(syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf);
Assert.Equal(kdfType, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.KdfType);
Assert.Equal(kdfIterations, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.Iterations);
Assert.Equal(kdfMemory, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.Memory);
Assert.Equal(kdfParallelism, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.Parallelism);
Assert.Equal(user.Key, syncResponseModel.UserDecryption.MasterPasswordUnlock.MasterKeyEncryptedUserKey);
Assert.Equal(user.Email.ToLower(), syncResponseModel.UserDecryption.MasterPasswordUnlock.Salt);
}
}

14
test/Api.Test/Controllers/PoliciesControllerTests.cs

@ -73,13 +73,13 @@ public class PoliciesControllerTests @@ -73,13 +73,13 @@ public class PoliciesControllerTests
// Assert that the data is deserialized correctly into a Dictionary<string, object>
// for all MasterPasswordPolicyData properties
Assert.Equal(mpPolicyData.MinComplexity, ((JsonElement)result.Data["MinComplexity"]).GetInt32());
Assert.Equal(mpPolicyData.MinLength, ((JsonElement)result.Data["MinLength"]).GetInt32());
Assert.Equal(mpPolicyData.RequireLower, ((JsonElement)result.Data["RequireLower"]).GetBoolean());
Assert.Equal(mpPolicyData.RequireUpper, ((JsonElement)result.Data["RequireUpper"]).GetBoolean());
Assert.Equal(mpPolicyData.RequireNumbers, ((JsonElement)result.Data["RequireNumbers"]).GetBoolean());
Assert.Equal(mpPolicyData.RequireSpecial, ((JsonElement)result.Data["RequireSpecial"]).GetBoolean());
Assert.Equal(mpPolicyData.EnforceOnLogin, ((JsonElement)result.Data["EnforceOnLogin"]).GetBoolean());
Assert.Equal(mpPolicyData.MinComplexity, ((JsonElement)result.Data["minComplexity"]).GetInt32());
Assert.Equal(mpPolicyData.MinLength, ((JsonElement)result.Data["minLength"]).GetInt32());
Assert.Equal(mpPolicyData.RequireLower, ((JsonElement)result.Data["requireLower"]).GetBoolean());
Assert.Equal(mpPolicyData.RequireUpper, ((JsonElement)result.Data["requireUpper"]).GetBoolean());
Assert.Equal(mpPolicyData.RequireNumbers, ((JsonElement)result.Data["requireNumbers"]).GetBoolean());
Assert.Equal(mpPolicyData.RequireSpecial, ((JsonElement)result.Data["requireSpecial"]).GetBoolean());
Assert.Equal(mpPolicyData.EnforceOnLogin, ((JsonElement)result.Data["enforceOnLogin"]).GetBoolean());
}

49
test/Api.Test/Vault/Controllers/SyncControllerTests.cs

@ -317,6 +317,55 @@ public class SyncControllerTests @@ -317,6 +317,55 @@ public class SyncControllerTests
}
}
[Theory]
[BitAutoData]
public async Task Get_HaveNoMasterPassword_UserDecryptionMasterPasswordUnlockIsNull(
User user, SutProvider<SyncController> sutProvider)
{
user.EquivalentDomains = null;
user.ExcludedGlobalEquivalentDomains = null;
user.MasterPassword = null;
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
var result = await sutProvider.Sut.Get();
Assert.Null(result.UserDecryption.MasterPasswordUnlock);
}
[Theory]
[BitAutoData(KdfType.PBKDF2_SHA256, 654_321, null, null)]
[BitAutoData(KdfType.Argon2id, 11, 128, 5)]
public async Task Get_HaveMasterPassword_UserDecryptionMasterPasswordUnlockNotNull(
KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism,
User user, SutProvider<SyncController> sutProvider)
{
user.EquivalentDomains = null;
user.ExcludedGlobalEquivalentDomains = null;
user.Key = "test-key";
user.MasterPassword = "test-master-password";
user.Kdf = kdfType;
user.KdfIterations = kdfIterations;
user.KdfMemory = kdfMemory;
user.KdfParallelism = kdfParallelism;
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
var result = await sutProvider.Sut.Get();
Assert.NotNull(result.UserDecryption.MasterPasswordUnlock);
Assert.NotNull(result.UserDecryption.MasterPasswordUnlock.Kdf);
Assert.Equal(kdfType, result.UserDecryption.MasterPasswordUnlock.Kdf.KdfType);
Assert.Equal(kdfIterations, result.UserDecryption.MasterPasswordUnlock.Kdf.Iterations);
Assert.Equal(kdfMemory, result.UserDecryption.MasterPasswordUnlock.Kdf.Memory);
Assert.Equal(kdfParallelism, result.UserDecryption.MasterPasswordUnlock.Kdf.Parallelism);
Assert.Equal(user.Key, result.UserDecryption.MasterPasswordUnlock.MasterKeyEncryptedUserKey);
Assert.Equal(user.Email.ToLower(), result.UserDecryption.MasterPasswordUnlock.Salt);
}
private async Task AssertMethodsCalledAsync(IUserService userService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,

75
test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordPolicyRequirementTests.cs

@ -0,0 +1,75 @@ @@ -0,0 +1,75 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
[SutProviderCustomize]
public class MasterPasswordPolicyRequirementFactoryTests
{
[Theory, BitAutoData]
public void MasterPasswordPolicyData_CombineWith_Joins_Policy_Options(SutProvider<MasterPasswordPolicyRequirementFactory> sutProvider)
{
var mpd1 = JsonSerializer.Serialize(new MasterPasswordPolicyData { MinLength = 20, RequireLower = false, RequireSpecial = false });
var mpd2 = JsonSerializer.Serialize(new MasterPasswordPolicyData { RequireLower = true });
var mpd3 = JsonSerializer.Serialize(new MasterPasswordPolicyData { RequireSpecial = true });
var policyDetails1 = new PolicyDetails
{
PolicyType = PolicyType.MasterPassword,
PolicyData = mpd1
};
var policyDetails2 = new PolicyDetails
{
PolicyType = PolicyType.MasterPassword,
PolicyData = mpd2
};
var policyDetails3 = new PolicyDetails
{
PolicyType = PolicyType.MasterPassword,
PolicyData = mpd3
};
var actual = sutProvider.Sut.Create([policyDetails1, policyDetails2, policyDetails3]);
Assert.NotNull(actual);
Assert.True(actual.Enabled);
Assert.True(actual.EnforcedOptions.RequireLower);
Assert.True(actual.EnforcedOptions.RequireSpecial);
Assert.Equal(20, actual.EnforcedOptions.MinLength);
}
[Theory, BitAutoData]
public void MasterPassword_IsFalse_IfNoPolicies(SutProvider<MasterPasswordPolicyRequirementFactory> sutProvider)
{
var actual = sutProvider.Sut.Create([]);
Assert.False(actual.Enabled);
Assert.Null(actual.EnforcedOptions);
}
[Theory, BitAutoData]
public void MasterPassword_IsTrue_IfAnyDisableSendPolicies(
[PolicyDetails(PolicyType.MasterPassword)] PolicyDetails[] policies,
SutProvider<MasterPasswordPolicyRequirementFactory> sutProvider)
{
var actual = sutProvider.Sut.Create(policies);
Assert.True(actual.Enabled);
Assert.NotNull(actual.EnforcedOptions);
Assert.NotNull(actual.EnforcedOptions.EnforceOnLogin);
Assert.NotNull(actual.EnforcedOptions.RequireLower);
Assert.NotNull(actual.EnforcedOptions.RequireNumbers);
Assert.NotNull(actual.EnforcedOptions.RequireSpecial);
Assert.NotNull(actual.EnforcedOptions.RequireUpper);
Assert.Null(actual.EnforcedOptions.MinComplexity);
Assert.Null(actual.EnforcedOptions.MinLength);
}
}

38
test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs

@ -1,9 +1,15 @@ @@ -1,9 +1,15 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services.Implementations;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
@ -117,6 +123,38 @@ public class PolicyServiceTests @@ -117,6 +123,38 @@ public class PolicyServiceTests
Assert.True(result);
}
[Theory, BitAutoData]
public async Task GetMasterPasswordPolicyForUserAsync_WithFeatureFlagEnabled_EvaluatesPolicyRequirement(User user, SutProvider<PolicyService> sutProvider)
{
SetupUserPolicies(user.Id, sutProvider);
var policyRequirement = new MasterPasswordPolicyRequirement
{
Enabled = true,
EnforcedOptions = new MasterPasswordPolicyData()
};
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<MasterPasswordPolicyRequirement>(user.Id).Returns(policyRequirement);
var result = await sutProvider.Sut.GetMasterPasswordPolicyForUserAsync(user);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.PolicyRequirements);
await sutProvider.GetDependency<IPolicyRepository>().DidNotReceive().GetManyByUserIdAsync(user.Id);
await sutProvider.GetDependency<IPolicyRequirementQuery>().Received(1).GetAsync<MasterPasswordPolicyRequirement>(user.Id);
}
[Theory, BitAutoData]
public async Task GetMasterPasswordPolicyForUserAsync_WithFeatureFlagDisabled_EvaluatesPolicyDetails(User user, SutProvider<PolicyService> sutProvider)
{
SetupUserPolicies(user.Id, sutProvider);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(false);
var result = await sutProvider.Sut.GetMasterPasswordPolicyForUserAsync(user);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.PolicyRequirements);
await sutProvider.GetDependency<IPolicyRepository>().Received(1).GetManyByUserIdAsync(user.Id);
await sutProvider.GetDependency<IPolicyRequirementQuery>().DidNotReceive().GetAsync<MasterPasswordPolicyRequirement>(user.Id);
}
private static void SetupOrg(SutProvider<PolicyService> sutProvider, Guid organizationId, Organization organization)
{
sutProvider.GetDependency<IOrganizationRepository>()

195
test/Core.Test/Utilities/LoggerFactoryExtensionsTests.cs

@ -0,0 +1,195 @@ @@ -0,0 +1,195 @@
using System.Net;
using System.Net.Sockets;
using System.Text;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Serilog;
using Serilog.Extensions.Logging;
using Xunit;
namespace Bit.Core.Test.Utilities;
public class LoggerFactoryExtensionsTests
{
[Fact]
public void AddSerilog_IsDevelopment_AddsNoProviders()
{
var providers = GetProviders([], "Development");
Assert.Empty(providers);
}
[Fact]
public void AddSerilog_IsDevelopment_DevLoggingEnabled_AddsSerilog()
{
var providers = GetProviders(new Dictionary<string, string?>
{
{ "GlobalSettings:EnableDevLogging", "true" },
}, "Development");
var provider = Assert.Single(providers);
Assert.IsAssignableFrom<SerilogLoggerProvider>(provider);
}
[Fact]
public void AddSerilog_IsProduction_AddsSerilog()
{
var providers = GetProviders([]);
var provider = Assert.Single(providers);
Assert.IsAssignableFrom<SerilogLoggerProvider>(provider);
}
[Fact]
public async Task AddSerilog_FileLogging_Old_Works()
{
var tempDir = Directory.CreateTempSubdirectory();
var providers = GetProviders(new Dictionary<string, string?>
{
{ "GlobalSettings:ProjectName", "Test" },
{ "GlobalSetting:LogDirectoryByProject", "true" },
{ "GlobalSettings:LogDirectory", tempDir.FullName },
});
var provider = Assert.Single(providers);
Assert.IsAssignableFrom<SerilogLoggerProvider>(provider);
var logger = provider.CreateLogger("Test");
logger.LogWarning("This is a test");
var logFile = Assert.Single(tempDir.EnumerateFiles("Test/*.txt"));
var logFileContents = await File.ReadAllTextAsync(logFile.FullName);
Assert.Contains(
"This is a test",
logFileContents
);
}
[Fact]
public async Task AddSerilog_FileLogging_New_Works()
{
var tempDir = Directory.CreateTempSubdirectory();
var provider = GetServiceProvider(new Dictionary<string, string?>
{
{ "Logging:PathFormat", $"{tempDir}/Logs/log-{{Date}}.log" },
}, "Production");
var logger = provider
.GetRequiredService<ILoggerFactory>()
.CreateLogger("Test");
logger.LogWarning("This is a test");
// Writing to the file is buffered, give it a little time to flush
await Task.Delay(5);
var logFile = Assert.Single(tempDir.EnumerateFiles("Logs/*.log"));
var logFileContents = await File.ReadAllTextAsync(logFile.FullName);
Assert.DoesNotContain(
"This configuration location for file logging has been deprecated.",
logFileContents
);
Assert.Contains(
"This is a test",
logFileContents
);
}
[Fact(Skip = "Only for local development.")]
public async Task AddSerilog_SyslogConfigured_Warns()
{
// Setup a fake syslog server
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
using var listener = new TcpListener(IPAddress.Parse("127.0.0.1"), 25000);
listener.Start();
var provider = GetServiceProvider(new Dictionary<string, string?>
{
{ "GlobalSettings:SysLog:Destination", "tcp://127.0.0.1:25000" },
{ "GlobalSettings:SiteName", "TestSite" },
{ "GlobalSettings:ProjectName", "TestProject" },
}, "Production");
var loggerFactory = provider.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("Test");
logger.LogWarning("This is a test");
// Look in syslog for data
using var socket = await listener.AcceptSocketAsync(cts.Token);
// This is rather lazy as opposed to implementing smarter syslog message
// reading but thats not what this test about, so instead just give
// the sink time to finish its work in the background
List<string> messages = [];
while (true)
{
var buffer = new byte[1024];
var received = await socket.ReceiveAsync(buffer, SocketFlags.None, cts.Token);
if (received == 0)
{
break;
}
var response = Encoding.ASCII.GetString(buffer, 0, received);
messages.Add(response);
if (messages.Count == 2)
{
break;
}
}
Assert.Collection(
messages,
(firstMessage) => Assert.Contains("Syslog for logging has been deprecated", firstMessage),
(secondMessage) => Assert.Contains("This is a test", secondMessage)
);
}
private static IEnumerable<ILoggerProvider> GetProviders(Dictionary<string, string?> initialData, string environment = "Production")
{
var provider = GetServiceProvider(initialData, environment);
return provider.GetServices<ILoggerProvider>();
}
private static IServiceProvider GetServiceProvider(Dictionary<string, string?> initialData, string environment)
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(initialData)
.Build();
var hostingEnvironment = Substitute.For<IWebHostEnvironment>();
hostingEnvironment
.EnvironmentName
.Returns(environment);
var context = new WebHostBuilderContext
{
HostingEnvironment = hostingEnvironment,
Configuration = config,
};
var services = new ServiceCollection();
services.AddLogging(builder =>
{
builder.AddSerilog(context);
});
return services.BuildServiceProvider();
}
}

38
test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs

@ -36,7 +36,15 @@ public class IdentityServerSsoTests @@ -36,7 +36,15 @@ public class IdentityServerSsoTests
public async Task Test_MasterPassword_DecryptionType()
{
// Arrange
using var responseBody = await RunSuccessTestAsync(MemberDecryptionType.MasterPassword);
User? expectedUser = null;
using var responseBody = await RunSuccessTestAsync(async factory =>
{
var database = factory.GetDatabaseContext();
expectedUser = await database.Users.SingleAsync(u => u.Email == TestEmail);
Assert.NotNull(expectedUser);
}, MemberDecryptionType.MasterPassword);
Assert.NotNull(expectedUser);
// Assert
// If the organization has a member decryption type of MasterPassword that should be the only option in the reply
@ -47,13 +55,33 @@ public class IdentityServerSsoTests @@ -47,13 +55,33 @@ public class IdentityServerSsoTests
// Expected to look like:
// "UserDecryptionOptions": {
// "Object": "userDecryptionOptions"
// "HasMasterPassword": true
// "HasMasterPassword": true,
// "MasterPasswordUnlock": {
// "Kdf": {
// "KdfType": 0,
// "Iterations": 600000
// },
// "MasterKeyEncryptedUserKey": "2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==",
// "Salt": "sso_user@email.com"
// }
// }
AssertHelper.AssertJsonProperty(userDecryptionOptions, "HasMasterPassword", JsonValueKind.True);
// One property for the Object and one for master password
Assert.Equal(2, userDecryptionOptions.EnumerateObject().Count());
var objectString = AssertHelper.AssertJsonProperty(userDecryptionOptions, "Object", JsonValueKind.String).ToString();
Assert.Equal("userDecryptionOptions", objectString);
var masterPasswordUnlock = AssertHelper.AssertJsonProperty(userDecryptionOptions, "MasterPasswordUnlock", JsonValueKind.Object);
// MasterPasswordUnlock.Kdf
var kdf = AssertHelper.AssertJsonProperty(masterPasswordUnlock, "Kdf", JsonValueKind.Object);
var kdfType = AssertHelper.AssertJsonProperty(kdf, "KdfType", JsonValueKind.Number).GetInt32();
Assert.Equal((int)expectedUser.Kdf, kdfType);
var kdfIterations = AssertHelper.AssertJsonProperty(kdf, "Iterations", JsonValueKind.Number).GetInt32();
Assert.Equal(expectedUser.KdfIterations, kdfIterations);
// MasterPasswordUnlock.MasterKeyEncryptedUserKey
var masterKeyEncryptedUserKey = AssertHelper.AssertJsonProperty(masterPasswordUnlock, "MasterKeyEncryptedUserKey", JsonValueKind.String).ToString();
Assert.Equal(expectedUser.Key, masterKeyEncryptedUserKey);
// MasterPasswordUnlock.Salt
var salt = AssertHelper.AssertJsonProperty(masterPasswordUnlock, "Salt", JsonValueKind.String).ToString();
Assert.Equal(TestEmail, salt);
}
[Fact]

31
test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs

@ -67,7 +67,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory> @@ -67,7 +67,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
Assert.Equal(0, kdf);
var kdfIterations = AssertHelper.AssertJsonProperty(root, "KdfIterations", JsonValueKind.Number).GetInt32();
Assert.Equal(AuthConstants.PBKDF2_ITERATIONS.Default, kdfIterations);
AssertUserDecryptionOptions(root);
AssertUserDecryptionOptions(root, user);
}
[Theory, RegisterFinishRequestModelCustomize]
@ -601,14 +601,27 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory> @@ -601,14 +601,27 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
Assert.StartsWith("sso authentication", errorDescription.ToLowerInvariant());
}
private static void AssertUserDecryptionOptions(JsonElement tokenResponse)
{
var userDecryptionOptions = AssertHelper.AssertJsonProperty(tokenResponse, "UserDecryptionOptions", JsonValueKind.Object)
.EnumerateObject();
Assert.Collection(userDecryptionOptions,
(prop) => { Assert.Equal("HasMasterPassword", prop.Name); Assert.Equal(JsonValueKind.True, prop.Value.ValueKind); },
(prop) => { Assert.Equal("Object", prop.Name); Assert.Equal("userDecryptionOptions", prop.Value.GetString()); });
private static void AssertUserDecryptionOptions(JsonElement tokenResponse, User expectedUser)
{
var userDecryptionOptions =
AssertHelper.AssertJsonProperty(tokenResponse, "UserDecryptionOptions", JsonValueKind.Object);
AssertHelper.AssertJsonProperty(userDecryptionOptions, "HasMasterPassword", JsonValueKind.True);
var objectString = AssertHelper.AssertJsonProperty(userDecryptionOptions, "Object", JsonValueKind.String).ToString();
Assert.Equal("userDecryptionOptions", objectString);
var masterPasswordUnlock = AssertHelper.AssertJsonProperty(userDecryptionOptions, "MasterPasswordUnlock", JsonValueKind.Object);
// MasterPasswordUnlock.Kdf
var kdf = AssertHelper.AssertJsonProperty(masterPasswordUnlock, "Kdf", JsonValueKind.Object);
var kdfType = AssertHelper.AssertJsonProperty(kdf, "KdfType", JsonValueKind.Number).GetInt32();
Assert.Equal((int)expectedUser.Kdf, kdfType);
var kdfIterations = AssertHelper.AssertJsonProperty(kdf, "Iterations", JsonValueKind.Number).GetInt32();
Assert.Equal(expectedUser.KdfIterations, kdfIterations);
// MasterPasswordUnlock.MasterKeyEncryptedUserKey
var masterKeyEncryptedUserKey = AssertHelper.AssertJsonProperty(masterPasswordUnlock, "MasterKeyEncryptedUserKey", JsonValueKind.String).ToString();
Assert.Equal(expectedUser.Key, masterKeyEncryptedUserKey);
// MasterPasswordUnlock.Salt
var salt = AssertHelper.AssertJsonProperty(masterPasswordUnlock, "Salt", JsonValueKind.String).ToString();
Assert.Equal(expectedUser.Email.ToLower(), salt);
}
private void ReinitializeDbForTests(IdentityApplicationFactory factory)

102
test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs

@ -3,10 +3,13 @@ using Bit.Core.AdminConsole.Entities; @@ -3,10 +3,13 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Api.Response;
using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Response;
using Bit.Core.Models.Api;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -27,6 +30,9 @@ namespace Bit.Identity.Test.IdentityServer; @@ -27,6 +30,9 @@ namespace Bit.Identity.Test.IdentityServer;
public class BaseRequestValidatorTests
{
private static readonly string _mockEncryptedString =
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
private UserManager<User> _userManager;
private readonly IUserService _userService;
private readonly IEventService _eventService;
@ -377,6 +383,102 @@ public class BaseRequestValidatorTests @@ -377,6 +383,102 @@ public class BaseRequestValidatorTests
Assert.Equal(expectedMessage, errorResponse.Message);
}
[Theory, BitAutoData]
public async Task ValidateAsync_CustomResponse_NoMasterPassword_ShouldSetUserDecryptionOptions(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
_userDecryptionOptionsBuilder.ForUser(Arg.Any<User>()).Returns(_userDecryptionOptionsBuilder);
_userDecryptionOptionsBuilder.WithDevice(Arg.Any<Device>()).Returns(_userDecryptionOptionsBuilder);
_userDecryptionOptionsBuilder.WithSso(Arg.Any<SsoConfig>()).Returns(_userDecryptionOptionsBuilder);
_userDecryptionOptionsBuilder.WithWebAuthnLoginCredential(Arg.Any<WebAuthnCredential>()).Returns(_userDecryptionOptionsBuilder);
_userDecryptionOptionsBuilder.BuildAsync().Returns(Task.FromResult(new UserDecryptionOptions
{
HasMasterPassword = false,
MasterPasswordUnlock = null
}));
var context = CreateContext(tokenRequest, requestContext, grantResult);
_sut.isValid = true;
_twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
.Returns(Task.FromResult(true));
// Act
await _sut.ValidateAsync(context);
// Assert
Assert.False(context.GrantResult.IsError);
var customResponse = context.GrantResult.CustomResponse;
Assert.Contains("UserDecryptionOptions", customResponse);
Assert.IsType<UserDecryptionOptions>(customResponse["UserDecryptionOptions"]);
var userDecryptionOptions = (UserDecryptionOptions)customResponse["UserDecryptionOptions"];
Assert.False(userDecryptionOptions.HasMasterPassword);
Assert.Null(userDecryptionOptions.MasterPasswordUnlock);
}
[Theory]
[BitAutoData(KdfType.PBKDF2_SHA256, 654_321, null, null)]
[BitAutoData(KdfType.Argon2id, 11, 128, 5)]
public async Task ValidateAsync_CustomResponse_MasterPassword_ShouldSetUserDecryptionOptions(
KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
_userDecryptionOptionsBuilder.ForUser(Arg.Any<User>()).Returns(_userDecryptionOptionsBuilder);
_userDecryptionOptionsBuilder.WithDevice(Arg.Any<Device>()).Returns(_userDecryptionOptionsBuilder);
_userDecryptionOptionsBuilder.WithSso(Arg.Any<SsoConfig>()).Returns(_userDecryptionOptionsBuilder);
_userDecryptionOptionsBuilder.WithWebAuthnLoginCredential(Arg.Any<WebAuthnCredential>()).Returns(_userDecryptionOptionsBuilder);
_userDecryptionOptionsBuilder.BuildAsync().Returns(Task.FromResult(new UserDecryptionOptions
{
HasMasterPassword = true,
MasterPasswordUnlock = new MasterPasswordUnlockResponseModel
{
Kdf = new MasterPasswordUnlockKdfResponseModel
{
KdfType = kdfType,
Iterations = kdfIterations,
Memory = kdfMemory,
Parallelism = kdfParallelism
},
MasterKeyEncryptedUserKey = _mockEncryptedString,
Salt = "test@example.com"
}
}));
var context = CreateContext(tokenRequest, requestContext, grantResult);
_sut.isValid = true;
_twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
.Returns(Task.FromResult(true));
// Act
await _sut.ValidateAsync(context);
// Assert
Assert.False(context.GrantResult.IsError);
var customResponse = context.GrantResult.CustomResponse;
Assert.Contains("UserDecryptionOptions", customResponse);
Assert.IsType<UserDecryptionOptions>(customResponse["UserDecryptionOptions"]);
var userDecryptionOptions = (UserDecryptionOptions)customResponse["UserDecryptionOptions"];
Assert.True(userDecryptionOptions.HasMasterPassword);
Assert.NotNull(userDecryptionOptions.MasterPasswordUnlock);
Assert.Equal(kdfType, userDecryptionOptions.MasterPasswordUnlock.Kdf.KdfType);
Assert.Equal(kdfIterations, userDecryptionOptions.MasterPasswordUnlock.Kdf.Iterations);
Assert.Equal(kdfMemory, userDecryptionOptions.MasterPasswordUnlock.Kdf.Memory);
Assert.Equal(kdfParallelism, userDecryptionOptions.MasterPasswordUnlock.Kdf.Parallelism);
Assert.Equal(_mockEncryptedString, userDecryptionOptions.MasterPasswordUnlock.MasterKeyEncryptedUserKey);
Assert.Equal("test@example.com", userDecryptionOptions.MasterPasswordUnlock.Salt);
}
private BaseRequestValidationContextFake CreateContext(
ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext,

30
test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs

@ -28,6 +28,8 @@ public class UserDecryptionOptionsBuilderTests @@ -28,6 +28,8 @@ public class UserDecryptionOptionsBuilderTests
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
_loginApprovingClientTypes = Substitute.For<ILoginApprovingClientTypes>();
_builder = new UserDecryptionOptionsBuilder(_currentContext, _deviceRepository, _organizationUserRepository, _loginApprovingClientTypes);
var user = new User();
_builder.ForUser(user);
}
[Theory]
@ -285,4 +287,32 @@ public class UserDecryptionOptionsBuilderTests @@ -285,4 +287,32 @@ public class UserDecryptionOptionsBuilderTests
Assert.True(result.TrustedDeviceOption?.HasAdminApproval);
}
[Theory, BitAutoData]
public async Task Build_WhenUserHasNoMasterPassword_ShouldReturnNoMasterPasswordUnlock(User user)
{
user.MasterPassword = null;
var result = await _builder.ForUser(user).BuildAsync();
Assert.False(result.HasMasterPassword);
Assert.Null(result.MasterPasswordUnlock);
}
[Theory, BitAutoData]
public async Task Build_WhenUserHasMasterPassword_ShouldReturnMasterPasswordUnlock(User user)
{
user.Email = "test@example.COM";
var result = await _builder.ForUser(user).BuildAsync();
Assert.True(result.HasMasterPassword);
Assert.NotNull(result.MasterPasswordUnlock);
Assert.Equal(user.Kdf, result.MasterPasswordUnlock.Kdf.KdfType);
Assert.Equal(user.KdfIterations, result.MasterPasswordUnlock.Kdf.Iterations);
Assert.Equal(user.KdfMemory, result.MasterPasswordUnlock.Kdf.Memory);
Assert.Equal(user.KdfParallelism, result.MasterPasswordUnlock.Kdf.Parallelism);
Assert.Equal("test@example.com", result.MasterPasswordUnlock.Salt);
Assert.Equal(user.Key, result.MasterPasswordUnlock.MasterKeyEncryptedUserKey);
}
}

20
util/Attachments/Dockerfile

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
###############################################
# Build stage #
###############################################
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build
# Docker buildx supplies the value for this arg
ARG TARGETPLATFORM
@ -9,11 +9,11 @@ ARG TARGETPLATFORM @@ -9,11 +9,11 @@ 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 ; \
RID=linux-musl-x64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
RID=linux-arm64 ; \
RID=linux-musl-arm64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
RID=linux-arm ; \
RID=linux-musl-arm ; \
fi \
&& echo "RID=$RID" > /tmp/rid.txt
@ -38,20 +38,20 @@ RUN . /tmp/rid.txt && dotnet publish \ @@ -38,20 +38,20 @@ RUN . /tmp/rid.txt && dotnet publish \
###############################################
# App stage #
###############################################
FROM mcr.microsoft.com/dotnet/aspnet:8.0
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21
ARG TARGETPLATFORM
LABEL com.bitwarden.product="bitwarden"
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:5000
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
EXPOSE 5000
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gosu \
curl \
&& rm -rf /var/lib/apt/lists/*
RUN apk add --no-cache curl \
icu-libs \
shadow \
&& apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu
# Copy app from the build stage
WORKDIR /bitwarden_server

12
util/Attachments/entrypoint.sh

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/sh
# Setup
@ -23,11 +23,11 @@ if [ "$(id -u)" = "0" ] @@ -23,11 +23,11 @@ if [ "$(id -u)" = "0" ]
then
# Create user and group
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1
useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||
usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1
mkhomedir_helper $USERNAME
addgroup -g "$LGID" -S "$GROUPNAME" 2>/dev/null || true
adduser -u "$LUID" -G "$GROUPNAME" -S -D -H "$USERNAME" 2>/dev/null || true
mkdir -p /home/$USERNAME
chown $USERNAME:$GROUPNAME /home/$USERNAME
# The rest...

13
util/MsSqlMigratorUtility/Dockerfile

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
###############################################
# Build stage #
###############################################
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build
# Docker buildx supplies the value for this arg
ARG TARGETPLATFORM
@ -9,11 +9,11 @@ ARG TARGETPLATFORM @@ -9,11 +9,11 @@ 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 ; \
RID=linux-musl-x64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
RID=linux-arm64 ; \
RID=linux-musl-arm64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
RID=linux-arm ; \
RID=linux-musl-arm ; \
fi \
&& echo "RID=$RID" > /tmp/rid.txt
@ -38,15 +38,18 @@ RUN . /tmp/rid.txt && dotnet publish \ @@ -38,15 +38,18 @@ RUN . /tmp/rid.txt && dotnet publish \
###############################################
# App stage #
###############################################
FROM mcr.microsoft.com/dotnet/aspnet:8.0
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 AS app
ARG TARGETPLATFORM
LABEL com.bitwarden.product="bitwarden"
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
# Copy app from the build stage
WORKDIR /app
COPY --from=build /source/util/MsSqlMigratorUtility/out /app
RUN apk add --no-cache icu-libs
ENTRYPOINT ["sh", "-c", "/app/MsSqlMigratorUtility \"${MSSQL_CONN_STRING}\" ${@}", "--" ]

10
util/Nginx/Dockerfile

@ -1,15 +1,13 @@ @@ -1,15 +1,13 @@
FROM --platform=$BUILDPLATFORM nginx:stable
FROM --platform=$BUILDPLATFORM nginx:stable-alpine3.21
ARG TARGETPLATFORM
LABEL com.bitwarden.product="bitwarden"
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gosu \
curl \
&& rm -rf /var/lib/apt/lists/*
RUN apk add --no-cache curl \
shadow \
&& apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu
COPY util/Nginx/nginx.conf /etc/nginx
COPY util/Nginx/proxy.conf /etc/nginx

40
util/Nginx/Dockerfile-k8s

@ -1,40 +0,0 @@ @@ -1,40 +0,0 @@
FROM nginx:stable
LABEL com.bitwarden.product="bitwarden"
ENV USERNAME="bitwarden"
ENV GROUPNAME="bitwarden"
RUN apt-get update && \
apt-get install -y --no-install-recommends \
gosu \
curl && \
rm -rf /var/lib/apt/lists/*
COPY nginx.conf /etc/nginx/nginx.conf
COPY proxy.conf /etc/nginx/proxy.conf
COPY mime.types /etc/nginx/mime.types
COPY security-headers.conf /etc/nginx/security-headers.conf
COPY security-headers-ssl.conf /etc/nginx/security-headers.conf
COPY setup-bwuser.sh /
EXPOSE 8000
EXPOSE 8080
EXPOSE 8443
RUN chmod +x /setup-bwuser.sh
RUN ./setup-bwuser.sh $USERNAME $GROUPNAME
RUN mkdir -p /var/run/nginx && \
touch /var/run/nginx/nginx.pid
RUN chown -R $USERNAME:$GROUPNAME /var/run/nginx && \
chown -R $USERNAME:$GROUPNAME /var/cache/nginx && \
chown -R $USERNAME:$GROUPNAME /var/log/nginx
HEALTHCHECK CMD curl --insecure -Lfs https://localhost:8443/alive || curl -Lfs http://localhost:8080/alive || exit 1
USER bitwarden

2
util/Nginx/entrypoint.sh

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/sh
# Setup

11
util/Nginx/setup-bwuser.sh

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/sh
# Setup
@ -32,8 +32,7 @@ fi @@ -32,8 +32,7 @@ fi
# Create user and group
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1
useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||
usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1
mkhomedir_helper $USERNAME
addgroup -g "$LGID" -S "$GROUPNAME" 2>/dev/null || true
adduser -u "$LUID" -G "$GROUPNAME" -S -D -H "$USERNAME" 2>/dev/null || true
mkdir -p /home/$USERNAME
chown $USERNAME:$GROUPNAME /home/$USERNAME

20
util/Setup/Dockerfile

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
###############################################
# Build stage #
###############################################
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
# Docker buildx supplies the value for this arg
ARG TARGETPLATFORM
@ -9,11 +9,11 @@ ARG TARGETPLATFORM @@ -9,11 +9,11 @@ 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 ; \
RID=linux-musl-x64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
RID=linux-arm64 ; \
RID=linux-musl-arm64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
RID=linux-arm ; \
RID=linux-musl-arm ; \
fi \
&& echo "RID=$RID" > /tmp/rid.txt
@ -38,18 +38,18 @@ RUN . /tmp/rid.txt && dotnet publish \ @@ -38,18 +38,18 @@ RUN . /tmp/rid.txt && dotnet publish \
###############################################
# App stage #
###############################################
FROM mcr.microsoft.com/dotnet/aspnet:8.0
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
ARG TARGETPLATFORM
LABEL com.bitwarden.product="bitwarden" com.bitwarden.project="setup"
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
RUN apk add --no-cache curl \
openssl \
gosu \
&& rm -rf /var/lib/apt/lists/*
icu-libs \
shadow \
&& apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu
# Copy app from the build stage
WORKDIR /app

2
util/Setup/Helpers.cs

@ -128,7 +128,7 @@ public static class Helpers @@ -128,7 +128,7 @@ public static class Helpers
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var escapedArgs = cmd.Replace("\"", "\\\"");
process.StartInfo.FileName = "/bin/bash";
process.StartInfo.FileName = "/bin/sh";
process.StartInfo.Arguments = $"-c \"{escapedArgs}\"";
}
else

2
util/Setup/entrypoint.sh

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/sh
# Setup

Loading…
Cancel
Save