Skip to content

Commit 98172cb

Browse files
authored
Merge branch 'main' into ac/pm-35351/post-public-members-no-longer-works-on-self-hosts
2 parents 8a994d8 + 26f0702 commit 98172cb

41 files changed

Lines changed: 887 additions & 266 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/renovate.json5

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@
125125
"AutoMapper.Extensions.Microsoft.DependencyInjection",
126126
"AWSSDK.SimpleEmail",
127127
"AWSSDK.SQS",
128+
"Azure.Storage.Blobs.Batch",
128129
"Handlebars.Net",
129130
"MailKit",
130131
"Microsoft.Azure.NotificationHubs",

.github/workflows/publish.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,13 @@ jobs:
9595
- project_name: Sso
9696
steps:
9797
- name: Print environment
98+
env:
99+
DRY_RUN: ${{ inputs.dry_run }}
98100
run: |
99101
whoami
100102
echo "GitHub ref: $GITHUB_REF"
101103
echo "GitHub event: $GITHUB_EVENT"
102-
echo "Dry Run: ${{ inputs.dry_run }}"
104+
echo "Dry Run: $DRY_RUN"
103105
104106
- name: Set up project name
105107
id: setup

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<TargetFramework>net8.0</TargetFramework>
55

6-
<Version>2026.4.0</Version>
6+
<Version>2026.4.1</Version>
77

88
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
99
<ImplicitUsings>enable</ImplicitUsings>

src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDeleteCommand.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Bit.Core.Exceptions;
88
using Bit.Core.Repositories;
99
using Bit.Core.Services;
10+
using Bit.Core.Tools.Services;
1011
using Bit.Core.Vault.Services;
1112
using Microsoft.Extensions.Logging;
1213

@@ -21,6 +22,7 @@ public class OrganizationDeleteCommand : IOrganizationDeleteCommand
2122
private readonly ICipherService _cipherService;
2223
private readonly ISubscriberService _subscriberService;
2324
private readonly IFeatureService _featureService;
25+
private readonly ISendFileStorageService _sendFileStorageService;
2426
private readonly ILogger<OrganizationDeleteCommand> _logger;
2527

2628
public OrganizationDeleteCommand(
@@ -31,6 +33,7 @@ public OrganizationDeleteCommand(
3133
ICipherService cipherService,
3234
ISubscriberService subscriberService,
3335
IFeatureService featureService,
36+
ISendFileStorageService sendFileStorageService,
3437
ILogger<OrganizationDeleteCommand> logger)
3538
{
3639
_applicationCacheService = applicationCacheService;
@@ -40,6 +43,7 @@ public OrganizationDeleteCommand(
4043
_cipherService = cipherService;
4144
_subscriberService = subscriberService;
4245
_featureService = featureService;
46+
_sendFileStorageService = sendFileStorageService;
4347
_logger = logger;
4448
}
4549

@@ -70,6 +74,7 @@ public async Task DeleteAsync(Organization organization)
7074
}
7175
}
7276

77+
await _sendFileStorageService.DeleteFilesForOrganizationAsync(organization.Id);
7378
await _cipherService.DeleteAttachmentsForOrganizationAsync(organization.Id);
7479
await _organizationRepository.DeleteAsync(organization);
7580
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);

src/Core/Billing/Services/Implementations/LicensingService.cs

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ namespace Bit.Core.Billing.Services;
3131

3232
public class LicensingService : ILicensingService
3333
{
34-
private readonly X509Certificate2 _certificate;
34+
private const string _productionCertThumbprint = "‎B34876439FCDA2846505B2EFBBA6C4A951313EBE";
35+
private const string _developmentCertThumbprint = "207E64A231E8AA32AAF68A61037C075EBEBD553F";
36+
private readonly X509Certificate2 _creationCertificate;
37+
private readonly HashSet<X509Certificate2> _verificationCertificates;
3538
private readonly IGlobalSettings _globalSettings;
3639
private readonly IUserRepository _userRepository;
3740
private readonly IOrganizationRepository _organizationRepository;
@@ -63,31 +66,63 @@ public LicensingService(
6366
_userLicenseClaimsFactory = userLicenseClaimsFactory;
6467
_pushNotificationService = pushNotificationService;
6568

66-
var certThumbprint = environment.IsDevelopment() ?
67-
"207E64A231E8AA32AAF68A61037C075EBEBD553F" :
68-
"‎B34876439FCDA2846505B2EFBBA6C4A951313EBE";
69+
70+
// Load license creation cert
71+
var creationCertThumbprint = environment.IsDevelopment() ? _developmentCertThumbprint : _productionCertThumbprint;
72+
_verificationCertificates = new HashSet<X509Certificate2>();
6973
if (_globalSettings.SelfHosted)
7074
{
71-
_certificate = CoreHelpers.GetEmbeddedCertificateAsync(environment.IsDevelopment() ? "licensing_dev.cer" : "licensing.cer", null)
72-
.GetAwaiter().GetResult();
75+
X509Certificate2 devCert = null;
76+
X509Certificate2 prodCert = CoreHelpers.GetEmbeddedCertificateAsync("licensing.cer", null).GetAwaiter().GetResult();
77+
78+
if (environment.IsDevelopment())
79+
{
80+
devCert = CoreHelpers.GetEmbeddedCertificateAsync("licensing_dev.cer", null).GetAwaiter().GetResult();
81+
_creationCertificate = devCert;
82+
// All self host envs accept prod cert. Creation cert added below to handle dev self-hosts
83+
_verificationCertificates.Add(prodCert);
84+
}
85+
else
86+
{
87+
_creationCertificate = prodCert;
88+
}
89+
90+
// non-production environments can use dev cert-generated licenses
91+
if (!environment.IsProduction())
92+
{
93+
devCert ??= CoreHelpers.GetEmbeddedCertificateAsync("licensing_dev.cer", null).GetAwaiter().GetResult();
94+
_verificationCertificates.Add(devCert);
95+
}
7396
}
7497
else if (CoreHelpers.SettingHasValue(_globalSettings.Storage?.ConnectionString) &&
7598
CoreHelpers.SettingHasValue(_globalSettings.LicenseCertificatePassword))
7699
{
77-
_certificate = CoreHelpers.GetBlobCertificateAsync(globalSettings.Storage.ConnectionString, "certificates",
100+
_creationCertificate = CoreHelpers.GetBlobCertificateAsync(globalSettings.Storage.ConnectionString, "certificates",
78101
"licensing.pfx", _globalSettings.LicenseCertificatePassword)
79102
.GetAwaiter().GetResult();
80103
}
81104
else
82105
{
83-
_certificate = CoreHelpers.GetCertificate(certThumbprint);
106+
_creationCertificate = CoreHelpers.GetCertificate(creationCertThumbprint);
84107
}
108+
// Creation cert can always be used to verify
109+
_verificationCertificates.Add(_creationCertificate);
85110

86-
if (_certificate == null || !_certificate.Thumbprint.Equals(CoreHelpers.CleanCertificateThumbprint(certThumbprint),
111+
if (_creationCertificate == null || !_creationCertificate.Thumbprint.Equals(CoreHelpers.CleanCertificateThumbprint(creationCertThumbprint),
87112
StringComparison.InvariantCultureIgnoreCase))
88113
{
89114
throw new Exception("Invalid licensing certificate.");
90115
}
116+
var allowedThumbprints = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
117+
{
118+
CoreHelpers.CleanCertificateThumbprint(_productionCertThumbprint),
119+
CoreHelpers.CleanCertificateThumbprint(_developmentCertThumbprint)
120+
};
121+
if (_verificationCertificates is null || _verificationCertificates.Count == 0
122+
|| _verificationCertificates.Any(c => !allowedThumbprints.Contains(c.Thumbprint)))
123+
{
124+
throw new Exception("Invalid license verifying certificate.");
125+
}
91126

92127
if (_globalSettings.SelfHosted && !CoreHelpers.SettingHasValue(_globalSettings.LicenseDirectory))
93128
{
@@ -132,7 +167,7 @@ public async Task ValidateOrganizationsAsync()
132167
continue;
133168
}
134169

135-
if (string.IsNullOrWhiteSpace(license.Token) && !license.VerifySignature(_certificate))
170+
if (string.IsNullOrWhiteSpace(license.Token) && !_verificationCertificates.Any(c => license.VerifySignature(c)))
136171
{
137172
await DisableOrganizationAsync(org, license, "Invalid signature.");
138173
continue;
@@ -231,7 +266,7 @@ private async Task<bool> ProcessUserValidationAsync(User user)
231266
return false;
232267
}
233268

234-
if (string.IsNullOrWhiteSpace(license.Token) && !license.VerifySignature(_certificate))
269+
if (string.IsNullOrWhiteSpace(license.Token) && !_verificationCertificates.Any(c => license.VerifySignature(c)))
235270
{
236271
await DisablePremiumAsync(user, license, "Invalid signature.");
237272
return false;
@@ -271,7 +306,7 @@ public bool VerifyLicense(ILicense license)
271306
{
272307
if (string.IsNullOrWhiteSpace(license.Token))
273308
{
274-
return license.VerifySignature(_certificate);
309+
return _verificationCertificates.Any((c) => license.VerifySignature(c));
275310
}
276311

277312
try
@@ -288,12 +323,12 @@ public bool VerifyLicense(ILicense license)
288323

289324
public byte[] SignLicense(ILicense license)
290325
{
291-
if (_globalSettings.SelfHosted || !_certificate.HasPrivateKey)
326+
if (_globalSettings.SelfHosted || !_creationCertificate.HasPrivateKey)
292327
{
293328
throw new InvalidOperationException("Cannot sign licenses.");
294329
}
295330

296-
return license.Sign(_certificate);
331+
return license.Sign(_creationCertificate);
297332
}
298333

299334
private UserLicense ReadUserLicense(User user)
@@ -341,7 +376,7 @@ public ClaimsPrincipal GetClaimsPrincipalFromLicense(ILicense license)
341376
var validationParameters = new TokenValidationParameters
342377
{
343378
ValidateIssuerSigningKey = true,
344-
IssuerSigningKey = new X509SecurityKey(_certificate),
379+
IssuerSigningKeys = _verificationCertificates.Select(c => new X509SecurityKey(c)),
345380
ValidateIssuer = true,
346381
ValidIssuer = "bitwarden",
347382
ValidateAudience = true,
@@ -393,7 +428,7 @@ private string GenerateToken(List<Claim> claims, string audience)
393428
claims.Add(new Claim(JwtClaimTypes.JwtId, Guid.NewGuid().ToString()));
394429
}
395430

396-
var securityKey = new RsaSecurityKey(_certificate.GetRSAPrivateKey());
431+
var securityKey = new RsaSecurityKey(_creationCertificate.GetRSAPrivateKey());
397432
var tokenDescriptor = new SecurityTokenDescriptor
398433
{
399434
Subject = new ClaimsIdentity(claims),

src/Core/Core.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,14 @@
3232
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
3333
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.20.1" />
3434
<PackageReference Include="Azure.Storage.Blobs" Version="12.26.0" />
35+
<PackageReference Include="Azure.Storage.Blobs.Batch" Version="12.23.0" />
3536
<PackageReference Include="Azure.Storage.Queues" Version="12.24.0" />
3637
<PackageReference Include="BitPay.Light" Version="1.0.1907" />
3738
<PackageReference Include="DuoUniversal" Version="1.3.1" />
3839
<PackageReference Include="DnsClient" Version="1.8.0" />
3940
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
4041
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
41-
<PackageReference Include="MailKit" Version="4.15.0" />
42+
<PackageReference Include="MailKit" Version="4.16.0" />
4243
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
4344
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.52.0" />
4445
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />

src/Core/Services/Implementations/UserService.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
using Bit.Core.Platform.Push;
3131
using Bit.Core.Repositories;
3232
using Bit.Core.Settings;
33+
using Bit.Core.Tools.Services;
3334
using Bit.Core.Utilities;
3435
using Microsoft.AspNetCore.Identity;
3536
using Microsoft.Extensions.Caching.Distributed;
@@ -69,6 +70,7 @@ public class UserService : UserManager<User>, IUserService
6970
private readonly IPricingClient _pricingClient;
7071
private readonly IHasPremiumAccessQuery _hasPremiumAccessQuery;
7172
private readonly ISubscriberService _subscriberService;
73+
private readonly ISendFileStorageService _sendFileStorageService;
7274

7375
public UserService(
7476
IUserRepository userRepository,
@@ -102,7 +104,8 @@ public UserService(
102104
IPolicyRequirementQuery policyRequirementQuery,
103105
IPricingClient pricingClient,
104106
IHasPremiumAccessQuery hasPremiumAccessQuery,
105-
ISubscriberService subscriberService)
107+
ISubscriberService subscriberService,
108+
ISendFileStorageService sendFileStorageService)
106109
: base(
107110
store,
108111
optionsAccessor,
@@ -141,6 +144,7 @@ public UserService(
141144
_pricingClient = pricingClient;
142145
_hasPremiumAccessQuery = hasPremiumAccessQuery;
143146
_subscriberService = subscriberService;
147+
_sendFileStorageService = sendFileStorageService;
144148
}
145149

146150
public Guid? GetProperUserId(ClaimsPrincipal principal)
@@ -237,6 +241,7 @@ public override async Task<IdentityResult> DeleteAsync(User user)
237241
var orgCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(org.Id);
238242
if (orgCount <= 1)
239243
{
244+
await _sendFileStorageService.DeleteFilesForUserAsync(user.Id);
240245
await _organizationRepository.DeleteAsync(org);
241246
deletedOrg = true;
242247
}
@@ -281,6 +286,7 @@ await _subscriberService.CancelSubscription(
281286
catch (BillingException) { }
282287
}
283288

289+
await _sendFileStorageService.DeleteFilesForUserAsync(user.Id);
284290
await _userRepository.DeleteAsync(user);
285291
await _pushService.PushLogOutAsync(user.Id);
286292
return IdentityResult.Success;

src/Core/Services/Play/Implementations/PlayIdService.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ public class PlayIdService(IHostEnvironment hostEnvironment) : IPlayIdService
88
public bool InPlay(out string playId)
99
{
1010
playId = PlayId ?? string.Empty;
11-
return !string.IsNullOrEmpty(PlayId) && hostEnvironment.IsDevelopment();
11+
var hasPlayId = !string.IsNullOrEmpty(playId);
12+
var isNotProd = !hostEnvironment.IsProduction();
13+
return hasPlayId && isNotProd;
1214
}
1315
}

src/Core/Services/Play/Implementations/PlayIdSingletonService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public string? PlayId
3535

3636
public bool InPlay(out string playId)
3737
{
38-
if (hostEnvironment.IsDevelopment())
38+
if (!hostEnvironment.IsProduction())
3939
{
4040
return Current.InPlay(out playId);
4141
}

src/Core/Tools/Repositories/ISendRepository.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ public interface ISendRepository : IRepository<Send, Guid>
2323
/// </returns>
2424
Task<ICollection<Send>> GetManyByUserIdAsync(Guid userId);
2525

26+
/// <summary>
27+
/// Loads all <see cref="Send"/>s owned by an organization.
28+
/// </summary>
29+
/// <param name="organizationId">
30+
/// Identifies the organization.
31+
/// </param>
32+
/// <returns>
33+
/// A task that completes once the <see cref="Send"/>s have been loaded.
34+
/// The task's result contains the loaded <see cref="Send"/>s.
35+
/// </returns>
36+
Task<ICollection<Send>> GetManyByOrganizationIdAsync(Guid organizationId);
37+
2638
/// <summary>
2739
/// Loads <see cref="Send"/>s scheduled for deletion.
2840
/// </summary>
@@ -35,6 +47,30 @@ public interface ISendRepository : IRepository<Send, Guid>
3547
/// </returns>
3648
Task<ICollection<Send>> GetManyByDeletionDateAsync(DateTime deletionDateBefore);
3749

50+
/// <summary>
51+
/// Loads file-type <see cref="Send"/>s created by a user.
52+
/// </summary>
53+
/// <param name="userId">
54+
/// Identifies the user.
55+
/// </param>
56+
/// <returns>
57+
/// A task that completes once the <see cref="Send"/>s have been loaded.
58+
/// The task's result contains the loaded file-type <see cref="Send"/>s.
59+
/// </returns>
60+
Task<ICollection<Send>> GetManyFileSendsByUserIdAsync(Guid userId);
61+
62+
/// <summary>
63+
/// Loads file-type <see cref="Send"/>s owned by an organization.
64+
/// </summary>
65+
/// <param name="organizationId">
66+
/// Identifies the organization.
67+
/// </param>
68+
/// <returns>
69+
/// A task that completes once the <see cref="Send"/>s have been loaded.
70+
/// The task's result contains the loaded file-type <see cref="Send"/>s.
71+
/// </returns>
72+
Task<ICollection<Send>> GetManyFileSendsByOrganizationIdAsync(Guid organizationId);
73+
3874
/// <summary>
3975
/// Updates encrypted data for sends during a key rotation
4076
/// </summary>

0 commit comments

Comments
 (0)