1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-03 10:42:21 -05:00

Merge branch 'main' into feature/phishing-detection

This commit is contained in:
Conner Turnbull 2025-04-30 07:22:31 -04:00 committed by GitHub
commit de248609f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 249 additions and 106 deletions

View File

@ -110,9 +110,14 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
IEnumerable<string> organizationOwnerEmails) IEnumerable<string> organizationOwnerEmails)
{ {
if (provider.IsBillable() && if (provider.IsBillable() &&
organization.IsValidClient() && organization.IsValidClient())
!string.IsNullOrEmpty(organization.GatewayCustomerId))
{ {
// An organization converted to a business unit will not have a Customer since it was given to the business unit.
if (string.IsNullOrEmpty(organization.GatewayCustomerId))
{
await _providerBillingService.CreateCustomerForClientOrganization(provider, organization);
}
var customer = await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions var customer = await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
{ {
Description = string.Empty, Description = string.Empty,

View File

@ -21,7 +21,6 @@ using Bit.Core.Models.Business;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities;
using CsvHelper; using CsvHelper;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -46,7 +45,6 @@ public class ProviderBillingService(
[FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy) [FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy)
: IProviderBillingService : IProviderBillingService
{ {
[RequireFeature(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)]
public async Task AddExistingOrganization( public async Task AddExistingOrganization(
Provider provider, Provider provider,
Organization organization, Organization organization,
@ -312,7 +310,6 @@ public class ProviderBillingService(
return memoryStream.ToArray(); return memoryStream.ToArray();
} }
[RequireFeature(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)]
public async Task<IEnumerable<AddableOrganization>> GetAddableOrganizations( public async Task<IEnumerable<AddableOrganization>> GetAddableOrganizations(
Provider provider, Provider provider,
Guid userId) Guid userId)

View File

@ -470,6 +470,19 @@ public class ProvidersController : Controller
[RequirePermission(Permission.Provider_Edit)] [RequirePermission(Permission.Provider_Edit)]
public async Task<IActionResult> Delete(Guid id, string providerName) public async Task<IActionResult> Delete(Guid id, string providerName)
{ {
var provider = await _providerRepository.GetByIdAsync(id);
if (provider is null)
{
return BadRequest("Provider does not exist");
}
if (provider.Status == ProviderStatusType.Pending)
{
await _providerService.DeleteAsync(provider);
return NoContent();
}
if (string.IsNullOrWhiteSpace(providerName)) if (string.IsNullOrWhiteSpace(providerName))
{ {
return BadRequest("Invalid provider name"); return BadRequest("Invalid provider name");
@ -482,13 +495,6 @@ public class ProvidersController : Controller
return BadRequest("You must unlink all clients before you can delete a provider"); return BadRequest("You must unlink all clients before you can delete a provider");
} }
var provider = await _providerRepository.GetByIdAsync(id);
if (provider is null)
{
return BadRequest("Provider does not exist");
}
if (!string.Equals(providerName.Trim(), provider.DisplayName(), StringComparison.OrdinalIgnoreCase)) if (!string.Equals(providerName.Trim(), provider.DisplayName(), StringComparison.OrdinalIgnoreCase))
{ {
return BadRequest("Invalid provider name"); return BadRequest("Invalid provider name");

View File

@ -183,6 +183,17 @@
<div class="p-3"> <div class="p-3">
<h4 class="fw-bolder" id="exampleModalLabel">Delete provider</h4> <h4 class="fw-bolder" id="exampleModalLabel">Delete provider</h4>
</div> </div>
@if (Model.Provider.Status == ProviderStatusType.Pending)
{
<div class="modal-body">
<span class="fw-light">
This action is permanent and irreversible.
</span>
</div>
}
else
{
<div class="modal-body"> <div class="modal-body">
<span class="fw-light"> <span class="fw-light">
This action is permanent and irreversible. Enter the provider name to complete deletion of the provider and associated data. This action is permanent and irreversible. Enter the provider name to complete deletion of the provider and associated data.
@ -194,6 +205,7 @@
</div> </div>
</form> </form>
</div> </div>
}
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-outline-primary btn-pill" data-bs-dismiss="modal">Cancel</button> <button type="button" class="btn btn-outline-primary btn-pill" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger btn-pill" onclick="deleteProvider('@Model.Provider.Id');">Delete provider</button> <button type="button" class="btn btn-danger btn-pill" onclick="deleteProvider('@Model.Provider.Id');">Delete provider</button>

View File

@ -1,13 +1,16 @@
using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.AdminConsole.Controllers; namespace Bit.Api.AdminConsole.Controllers;
[RequireFeature(FeatureFlagKeys.EventBasedOrganizationIntegrations)]
[Route("organizations/{organizationId:guid}/integrations/{integrationId:guid}/configurations")] [Route("organizations/{organizationId:guid}/integrations/{integrationId:guid}/configurations")]
[Authorize("Application")] [Authorize("Application")]
public class OrganizationIntegrationConfigurationController( public class OrganizationIntegrationConfigurationController(

View File

@ -1,8 +1,10 @@
using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -10,6 +12,7 @@ using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.AdminConsole.Controllers; namespace Bit.Api.AdminConsole.Controllers;
[RequireFeature(FeatureFlagKeys.EventBasedOrganizationIntegrations)]
[Route("organizations/{organizationId:guid}/integrations")] [Route("organizations/{organizationId:guid}/integrations")]
[Authorize("Application")] [Authorize("Application")]
public class OrganizationIntegrationController( public class OrganizationIntegrationController(

View File

@ -1,6 +1,5 @@
using Bit.Api.Billing.Controllers; using Bit.Api.Billing.Controllers;
using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Requests;
using Bit.Core;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
@ -17,7 +16,6 @@ namespace Bit.Api.AdminConsole.Controllers;
[Route("providers/{providerId:guid}/clients")] [Route("providers/{providerId:guid}/clients")]
public class ProviderClientsController( public class ProviderClientsController(
ICurrentContext currentContext, ICurrentContext currentContext,
IFeatureService featureService,
ILogger<BaseProviderController> logger, ILogger<BaseProviderController> logger,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IProviderBillingService providerBillingService, IProviderBillingService providerBillingService,
@ -140,11 +138,6 @@ public class ProviderClientsController(
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
public async Task<IResult> GetAddableOrganizationsAsync([FromRoute] Guid providerId) public async Task<IResult> GetAddableOrganizationsAsync([FromRoute] Guid providerId)
{ {
if (!featureService.IsEnabled(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal))
{
return Error.NotFound();
}
var (provider, result) = await TryGetBillableProviderForServiceUserOperation(providerId); var (provider, result) = await TryGetBillableProviderForServiceUserOperation(providerId);
if (provider == null) if (provider == null)

View File

@ -1,5 +1,6 @@
using System.Text.Json; using System.Text.Json;
using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -7,11 +8,13 @@ using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Integrations; using Bit.Core.Models.Data.Integrations;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.AdminConsole.Controllers; namespace Bit.Api.AdminConsole.Controllers;
[RequireFeature(FeatureFlagKeys.EventBasedOrganizationIntegrations)]
[Route("organizations/{organizationId:guid}/integrations/slack")] [Route("organizations/{organizationId:guid}/integrations/slack")]
[Authorize("Application")] [Authorize("Application")]
public class SlackIntegrationController( public class SlackIntegrationController(

View File

@ -221,8 +221,7 @@ public class MembersController : Controller
/// Remove a member. /// Remove a member.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Permanently removes a member from the organization. This cannot be undone. /// Removes a member from the organization. This cannot be undone. The user account will still remain.
/// The user account will still remain. The user is only removed from the organization.
/// </remarks> /// </remarks>
/// <param name="id">The identifier of the member to be removed.</param> /// <param name="id">The identifier of the member to be removed.</param>
[HttpDelete("{id}")] [HttpDelete("{id}")]

View File

@ -44,7 +44,7 @@ public class InviteUsersPasswordManagerValidator(
return new Invalid<PasswordManagerSubscriptionUpdate>(new PasswordManagerMustHaveSeatsError(subscriptionUpdate)); return new Invalid<PasswordManagerSubscriptionUpdate>(new PasswordManagerMustHaveSeatsError(subscriptionUpdate));
} }
if (subscriptionUpdate.MaxSeatsReached) if (subscriptionUpdate.MaxSeatsExceeded)
{ {
return new Invalid<PasswordManagerSubscriptionUpdate>( return new Invalid<PasswordManagerSubscriptionUpdate>(
new PasswordManagerSeatLimitHasBeenReachedError(subscriptionUpdate)); new PasswordManagerSeatLimitHasBeenReachedError(subscriptionUpdate));

View File

@ -48,6 +48,11 @@ public class PasswordManagerSubscriptionUpdate
/// </summary> /// </summary>
public bool MaxSeatsReached => UpdatedSeatTotal.HasValue && MaxAutoScaleSeats.HasValue && UpdatedSeatTotal.Value >= MaxAutoScaleSeats.Value; public bool MaxSeatsReached => UpdatedSeatTotal.HasValue && MaxAutoScaleSeats.HasValue && UpdatedSeatTotal.Value >= MaxAutoScaleSeats.Value;
/// <summary>
/// If the new seat total exceeds the organization's auto-scale seat limit
/// </summary>
public bool MaxSeatsExceeded => UpdatedSeatTotal.HasValue && MaxAutoScaleSeats.HasValue && UpdatedSeatTotal.Value > MaxAutoScaleSeats.Value;
public Plan.PasswordManagerPlanFeatures PasswordManagerPlan { get; } public Plan.PasswordManagerPlanFeatures PasswordManagerPlan { get; }
public InviteOrganization InviteOrganization { get; } public InviteOrganization InviteOrganization { get; }

View File

@ -41,6 +41,7 @@ public static class OrganizationLicenseConstants
public const string Refresh = nameof(Refresh); public const string Refresh = nameof(Refresh);
public const string ExpirationWithoutGracePeriod = nameof(ExpirationWithoutGracePeriod); public const string ExpirationWithoutGracePeriod = nameof(ExpirationWithoutGracePeriod);
public const string Trial = nameof(Trial); public const string Trial = nameof(Trial);
public const string UseAdminSponsoredFamilies = nameof(UseAdminSponsoredFamilies);
} }
public static class UserLicenseConstants public static class UserLicenseConstants

View File

@ -53,6 +53,7 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory<Organizati
new(nameof(OrganizationLicenseConstants.Refresh), refresh.ToString(CultureInfo.InvariantCulture)), new(nameof(OrganizationLicenseConstants.Refresh), refresh.ToString(CultureInfo.InvariantCulture)),
new(nameof(OrganizationLicenseConstants.ExpirationWithoutGracePeriod), expirationWithoutGracePeriod.ToString(CultureInfo.InvariantCulture)), new(nameof(OrganizationLicenseConstants.ExpirationWithoutGracePeriod), expirationWithoutGracePeriod.ToString(CultureInfo.InvariantCulture)),
new(nameof(OrganizationLicenseConstants.Trial), trial.ToString()), new(nameof(OrganizationLicenseConstants.Trial), trial.ToString()),
new(nameof(OrganizationLicenseConstants.UseAdminSponsoredFamilies), entity.UseAdminSponsoredFamilies.ToString()),
}; };
if (entity.Name is not null) if (entity.Name is not null)
@ -109,6 +110,7 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory<Organizati
{ {
claims.Add(new Claim(nameof(OrganizationLicenseConstants.SmServiceAccounts), entity.SmServiceAccounts.ToString())); claims.Add(new Claim(nameof(OrganizationLicenseConstants.SmServiceAccounts), entity.SmServiceAccounts.ToString()));
} }
claims.Add(new Claim(nameof(OrganizationLicenseConstants.UseAdminSponsoredFamilies), entity.UseAdminSponsoredFamilies.ToString()));
return Task.FromResult(claims); return Task.FromResult(claims);
} }

View File

@ -108,6 +108,7 @@ public static class FeatureFlagKeys
public const string PolicyRequirements = "pm-14439-policy-requirements"; public const string PolicyRequirements = "pm-14439-policy-requirements";
public const string SsoExternalIdVisibility = "pm-18630-sso-external-id-visibility"; public const string SsoExternalIdVisibility = "pm-18630-sso-external-id-visibility";
public const string ScimInviteUserOptimization = "pm-16811-optimize-invite-user-flow-to-fail-fast"; public const string ScimInviteUserOptimization = "pm-16811-optimize-invite-user-flow-to-fail-fast";
public const string EventBasedOrganizationIntegrations = "event-based-organization-integrations";
/* Auth Team */ /* Auth Team */
public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence"; public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence";
@ -142,7 +143,6 @@ public static class FeatureFlagKeys
public const string TrialPayment = "PM-8163-trial-payment"; public const string TrialPayment = "PM-8163-trial-payment";
public const string PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships"; public const string PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships";
public const string UsePricingService = "use-pricing-service"; public const string UsePricingService = "use-pricing-service";
public const string P15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal";
public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features"; public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features";
public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method"; public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method";
public const string PM19147_AutomaticTaxImprovements = "pm-19147-automatic-tax-improvements"; public const string PM19147_AutomaticTaxImprovements = "pm-19147-automatic-tax-improvements";
@ -150,6 +150,10 @@ public static class FeatureFlagKeys
public const string PM18770_EnableOrganizationBusinessUnitConversion = "pm-18770-enable-organization-business-unit-conversion"; public const string PM18770_EnableOrganizationBusinessUnitConversion = "pm-18770-enable-organization-business-unit-conversion";
public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically"; public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically";
/* Data Insights and Reporting Team */
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
/* Key Management Team */ /* Key Management Team */
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service"; public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service";
@ -186,8 +190,6 @@ public static class FeatureFlagKeys
/* Tools Team */ /* Tools Team */
public const string ItemShare = "item-share"; public const string ItemShare = "item-share";
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
public const string DesktopSendUIRefresh = "desktop-send-ui-refresh"; public const string DesktopSendUIRefresh = "desktop-send-ui-refresh";
/* Vault Team */ /* Vault Team */
@ -195,7 +197,6 @@ public static class FeatureFlagKeys
public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form"; public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form";
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss"; public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss";
public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss"; public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss";
public const string VaultBulkManagementAction = "vault-bulk-management-action";
public const string RestrictProviderAccess = "restrict-provider-access"; public const string RestrictProviderAccess = "restrict-provider-access";
public const string SecurityTasks = "security-tasks"; public const string SecurityTasks = "security-tasks";
public const string CipherKeyEncryption = "cipher-key-encryption"; public const string CipherKeyEncryption = "cipher-key-encryption";

View File

@ -23,8 +23,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" /> <PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.402.61" /> <PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.402.79" />
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.118" /> <PackageReference Include="AWSSDK.SQS" Version="3.7.400.136" />
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" /> <PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" /> <PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" /> <PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
@ -38,7 +38,7 @@
<PackageReference Include="Handlebars.Net" Version="2.1.6" /> <PackageReference Include="Handlebars.Net" Version="2.1.6" />
<PackageReference Include="MailKit" Version="4.11.0" /> <PackageReference Include="MailKit" Version="4.11.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.46.1" /> <PackageReference Include="Microsoft.Azure.Cosmos" Version="3.49.0" />
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" /> <PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.7.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.7.0" />

View File

@ -19,6 +19,34 @@ public class OrganizationLicense : ILicense
{ {
} }
/// <summary>
/// Initializes a new instance of the <see cref="OrganizationLicense"/> class.
/// </summary>
/// <remarks>
/// <para>
/// ⚠️ DEPRECATED: This constructor and the entire property-based licensing system is deprecated.
/// Do not add new properties to this constructor or extend its functionality.
/// </para>
/// <para>
/// This implementation has been replaced by a new claims-based licensing system that provides better security
/// and flexibility. The new system uses JWT claims to store and validate license information, making it more
/// secure and easier to extend without requiring changes to the license format.
/// </para>
/// <para>
/// For new license-related features or modifications:
/// 1. Use the claims-based system instead of adding properties here
/// 2. Add new claims to the license token
/// 3. Validate claims in the <see cref="CanUse"/> and <see cref="VerifyData"/> methods
/// </para>
/// <para>
/// This constructor is maintained only for backward compatibility with existing licenses.
/// </para>
/// </remarks>
/// <param name="org">The organization to create the license for.</param>
/// <param name="subscriptionInfo">Information about the organization's subscription.</param>
/// <param name="installationId">The ID of the current installation.</param>
/// <param name="licenseService">The service used to sign the license.</param>
/// <param name="version">Optional version number for the license format.</param>
public OrganizationLicense(Organization org, SubscriptionInfo subscriptionInfo, Guid installationId, public OrganizationLicense(Organization org, SubscriptionInfo subscriptionInfo, Guid installationId,
ILicensingService licenseService, int? version = null) ILicensingService licenseService, int? version = null)
{ {
@ -105,6 +133,7 @@ public class OrganizationLicense : ILicense
Trial = false; Trial = false;
} }
UseAdminSponsoredFamilies = org.UseAdminSponsoredFamilies;
Hash = Convert.ToBase64String(ComputeHash()); Hash = Convert.ToBase64String(ComputeHash());
Signature = Convert.ToBase64String(licenseService.SignLicense(this)); Signature = Convert.ToBase64String(licenseService.SignLicense(this));
} }
@ -153,6 +182,7 @@ public class OrganizationLicense : ILicense
public bool Trial { get; set; } public bool Trial { get; set; }
public LicenseType? LicenseType { get; set; } public LicenseType? LicenseType { get; set; }
public bool UseAdminSponsoredFamilies { get; set; }
public string Hash { get; set; } public string Hash { get; set; }
public string Signature { get; set; } public string Signature { get; set; }
public string Token { get; set; } public string Token { get; set; }
@ -292,13 +322,35 @@ public class OrganizationLicense : ILicense
} }
/// <summary> /// <summary>
/// Do not extend this method. It is only here for backwards compatibility with old licenses. /// Validates an obsolete license format using property-based validation.
/// Instead, extend the CanUse method using the ClaimsPrincipal.
/// </summary> /// </summary>
/// <param name="globalSettings"></param> /// <remarks>
/// <param name="licensingService"></param> /// <para>
/// <param name="exception"></param> /// ⚠️ DEPRECATED: This method is deprecated and should not be extended or modified.
/// <returns></returns> /// It is maintained only for backward compatibility with old license formats.
/// </para>
/// <para>
/// This method has been replaced by a new claims-based validation system that provides:
/// - Better security through JWT claims
/// - More flexible validation rules
/// - Easier extensibility without changing the license format
/// - Better separation of concerns
/// </para>
/// <para>
/// To add new license validation rules:
/// 1. Add new claims to the license token in the claims-based system
/// 2. Extend the <see cref="CanUse(IGlobalSettings, ILicensingService, ClaimsPrincipal, out string)"/> method
/// 3. Validate the new claims using the ClaimsPrincipal parameter
/// </para>
/// <para>
/// This method will be removed in a future version once all old licenses have been migrated
/// to the new claims-based system.
/// </para>
/// </remarks>
/// <param name="globalSettings">The global settings containing installation information.</param>
/// <param name="licensingService">The service used to verify the license signature.</param>
/// <param name="exception">When the method returns false, contains the error message explaining why the license is invalid.</param>
/// <returns>True if the license is valid, false otherwise.</returns>
private bool ObsoleteCanUse(IGlobalSettings globalSettings, ILicensingService licensingService, out string exception) private bool ObsoleteCanUse(IGlobalSettings globalSettings, ILicensingService licensingService, out string exception)
{ {
// Do not extend this method. It is only here for backwards compatibility with old licenses. // Do not extend this method. It is only here for backwards compatibility with old licenses.
@ -392,6 +444,7 @@ public class OrganizationLicense : ILicense
var usePasswordManager = claimsPrincipal.GetValue<bool>(nameof(UsePasswordManager)); var usePasswordManager = claimsPrincipal.GetValue<bool>(nameof(UsePasswordManager));
var smSeats = claimsPrincipal.GetValue<int?>(nameof(SmSeats)); var smSeats = claimsPrincipal.GetValue<int?>(nameof(SmSeats));
var smServiceAccounts = claimsPrincipal.GetValue<int?>(nameof(SmServiceAccounts)); var smServiceAccounts = claimsPrincipal.GetValue<int?>(nameof(SmServiceAccounts));
var useAdminSponsoredFamilies = claimsPrincipal.GetValue<bool>(nameof(UseAdminSponsoredFamilies));
return issued <= DateTime.UtcNow && return issued <= DateTime.UtcNow &&
expires >= DateTime.UtcNow && expires >= DateTime.UtcNow &&
@ -419,7 +472,9 @@ public class OrganizationLicense : ILicense
useSecretsManager == organization.UseSecretsManager && useSecretsManager == organization.UseSecretsManager &&
usePasswordManager == organization.UsePasswordManager && usePasswordManager == organization.UsePasswordManager &&
smSeats == organization.SmSeats && smSeats == organization.SmSeats &&
smServiceAccounts == organization.SmServiceAccounts; smServiceAccounts == organization.SmServiceAccounts &&
useAdminSponsoredFamilies == organization.UseAdminSponsoredFamilies;
} }
/// <summary> /// <summary>

View File

@ -1,4 +1,5 @@
using System.Globalization; using System.Globalization;
using Bit.Core;
using Bit.Core.AdminConsole.Services.Implementations; using Bit.Core.AdminConsole.Services.Implementations;
using Bit.Core.AdminConsole.Services.NoopImplementations; using Bit.Core.AdminConsole.Services.NoopImplementations;
using Bit.Core.Context; using Bit.Core.Context;
@ -62,33 +63,45 @@ public class Startup
{ {
services.AddSingleton<IApplicationCacheService, InMemoryApplicationCacheService>(); services.AddSingleton<IApplicationCacheService, InMemoryApplicationCacheService>();
} }
services.AddScoped<IEventService, EventService>();
if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString)) if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString))
{ {
services.AddKeyedSingleton<IEventWriteService, AzureQueueEventWriteService>("storage");
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) &&
CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName)) CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName))
{ {
services.AddSingleton<IEventWriteService, AzureServiceBusEventWriteService>(); services.AddKeyedSingleton<IEventWriteService, AzureServiceBusEventWriteService>("broadcast");
} }
else else
{ {
services.AddSingleton<IEventWriteService, AzureQueueEventWriteService>(); services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
} }
} }
else else
{ {
services.AddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("storage");
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) && if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) &&
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) && CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) &&
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) && CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) &&
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName)) CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName))
{ {
services.AddSingleton<IEventWriteService, RabbitMqEventWriteService>(); services.AddKeyedSingleton<IEventWriteService, RabbitMqEventWriteService>("broadcast");
} }
else else
{ {
services.AddSingleton<IEventWriteService, RepositoryEventWriteService>(); services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
} }
} }
services.AddScoped<IEventWriteService>(sp =>
{
var featureService = sp.GetRequiredService<IFeatureService>();
var key = featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations)
? "broadcast" : "storage";
return sp.GetRequiredKeyedService<IEventWriteService>(key);
});
services.AddScoped<IEventService, EventService>();
services.AddOptionality(); services.AddOptionality();

View File

@ -150,6 +150,8 @@ public static class DapperHelpers
os => os.LastSyncDate, os => os.LastSyncDate,
os => os.ValidUntil, os => os.ValidUntil,
os => os.ToDelete, os => os.ToDelete,
os => os.IsAdminInitiated,
os => os.Notes,
] ]
); );

View File

@ -10,7 +10,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Dapper" Version="2.1.35" /> <PackageReference Include="Dapper" Version="2.1.66" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -23,7 +23,19 @@ public class OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery : IQuery
var sponsorshipsQuery = from os in dbContext.OrganizationSponsorships var sponsorshipsQuery = from os in dbContext.OrganizationSponsorships
where os.SponsoringOrganizationId == _organizationId && where os.SponsoringOrganizationId == _organizationId &&
os.IsAdminInitiated && os.IsAdminInitiated &&
!os.ToDelete (
// Not marked for deletion - always count
(!os.ToDelete) ||
// Marked for deletion but has a valid until date in the future (RevokeWhenExpired status)
(os.ToDelete && os.ValidUntil.HasValue && os.ValidUntil.Value > DateTime.UtcNow)
) &&
(
// SENT status: When SponsoredOrganizationId is null
os.SponsoredOrganizationId == null ||
// ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future
(os.SponsoredOrganizationId != null &&
(!os.ValidUntil.HasValue || os.ValidUntil.Value > DateTime.UtcNow))
)
select new OrganizationUser select new OrganizationUser
{ {
Id = os.Id, Id = os.Id,

View File

@ -8,12 +8,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="linq2db" Version="5.4.1" /> <PackageReference Include="linq2db" Version="5.4.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="[8.0.8]" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="[8.0.8]" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="[8.0.8]" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="[8.0.4]" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="[8.0.2]" />
<PackageReference Include="linq2db.EntityFrameworkCore" Version="8.1.0" /> <PackageReference Include="linq2db.EntityFrameworkCore" Version="[8.1.0]" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -4,6 +4,7 @@ using System.Security.Claims;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using AspNetCoreRateLimit; using AspNetCoreRateLimit;
using Azure.Storage.Queues; using Azure.Storage.Queues;
using Bit.Core;
using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
@ -332,34 +333,46 @@ public static class ServiceCollectionExtensions
if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString)) if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString))
{ {
services.AddKeyedSingleton<IEventWriteService, AzureQueueEventWriteService>("storage");
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) &&
CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName)) CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName))
{ {
services.AddSingleton<IEventWriteService, AzureServiceBusEventWriteService>(); services.AddKeyedSingleton<IEventWriteService, AzureServiceBusEventWriteService>("broadcast");
} }
else else
{ {
services.AddSingleton<IEventWriteService, AzureQueueEventWriteService>(); services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
} }
} }
else if (globalSettings.SelfHosted) else if (globalSettings.SelfHosted)
{ {
services.AddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("storage");
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) && if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) &&
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) && CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) &&
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) && CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) &&
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName)) CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName))
{ {
services.AddSingleton<IEventWriteService, RabbitMqEventWriteService>(); services.AddKeyedSingleton<IEventWriteService, RabbitMqEventWriteService>("broadcast");
} }
else else
{ {
services.AddSingleton<IEventWriteService, RepositoryEventWriteService>(); services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
} }
} }
else else
{ {
services.AddSingleton<IEventWriteService, NoopEventWriteService>(); services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("storage");
services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
} }
services.AddScoped<IEventWriteService>(sp =>
{
var featureService = sp.GetRequiredService<IFeatureService>();
var key = featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations)
? "broadcast" : "storage";
return sp.GetRequiredKeyedService<IEventWriteService>(key);
});
if (CoreHelpers.SettingHasValue(globalSettings.Attachment.ConnectionString)) if (CoreHelpers.SettingHasValue(globalSettings.Attachment.ConnectionString))
{ {

View File

@ -19,5 +19,19 @@ BEGIN
FROM [dbo].[OrganizationSponsorship] FROM [dbo].[OrganizationSponsorship]
WHERE SponsoringOrganizationId = @OrganizationId WHERE SponsoringOrganizationId = @OrganizationId
AND IsAdminInitiated = 1 AND IsAdminInitiated = 1
AND (
-- Not marked for deletion - always count
(ToDelete = 0)
OR
-- Marked for deletion but has a valid until date in the future (RevokeWhenExpired status)
(ToDelete = 1 AND ValidUntil IS NOT NULL AND ValidUntil > GETUTCDATE())
)
AND (
-- SENT status: When SponsoredOrganizationId is null
SponsoredOrganizationId IS NULL
OR
-- ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future
(SponsoredOrganizationId IS NOT NULL AND (ValidUntil IS NULL OR ValidUntil > GETUTCDATE()))
)
) )
END END

View File

@ -1,9 +1,6 @@
using System.Security.Claims; using System.Security.Claims;
using System.Text.Json;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
@ -12,22 +9,6 @@ namespace Bit.Core.Test.Models.Business;
public class OrganizationLicenseTests public class OrganizationLicenseTests
{ {
/// <summary>
/// Verifies that when the license file is loaded from disk using the current OrganizationLicense class,
/// its hash does not change.
/// This guards against the risk that properties added in later versions are accidentally included in the hash,
/// or that a property is added without incrementing the version number.
/// </summary>
[Theory]
[BitAutoData(OrganizationLicense.CurrentLicenseFileVersion)] // Previous version (this property is 1 behind)
[BitAutoData(OrganizationLicense.CurrentLicenseFileVersion + 1)] // Current version
public void OrganizationLicense_LoadFromDisk_HashDoesNotChange(int licenseVersion)
{
var license = OrganizationLicenseFileFixtures.GetVersion(licenseVersion);
// Compare the hash loaded from the json to the hash generated by the current class
Assert.Equal(Convert.FromBase64String(license.Hash), license.ComputeHash());
}
/// <summary> /// <summary>
/// Verifies that when the license file is loaded from disk using the current OrganizationLicense class, /// Verifies that when the license file is loaded from disk using the current OrganizationLicense class,
@ -52,22 +33,4 @@ public class OrganizationLicenseTests
}); });
Assert.True(license.VerifyData(organization, claimsPrincipal, globalSettings)); Assert.True(license.VerifyData(organization, claimsPrincipal, globalSettings));
} }
/// <summary>
/// Helper used to generate a new json string to be added in OrganizationLicenseFileFixtures.
/// Uncomment [Fact], run the test and copy the value of the `result` variable into OrganizationLicenseFileFixtures,
/// following the instructions in that class.
/// </summary>
// [Fact]
private void GenerateLicenseFileJsonString()
{
var organization = OrganizationLicenseFileFixtures.OrganizationFactory();
var licensingService = Substitute.For<ILicensingService>();
var installationId = new Guid(OrganizationLicenseFileFixtures.InstallationId);
var license = new OrganizationLicense(organization, null, installationId, licensingService);
var result = JsonSerializer.Serialize(license, JsonHelpers.Indented).Replace("\"", "'");
// Put a break after this line, then copy and paste the value of `result` into OrganizationLicenseFileFixtures
}
} }

View File

@ -0,0 +1,41 @@
IF OBJECT_ID('[dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]
END
GO
CREATE PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]
@OrganizationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
(
SELECT COUNT(1)
FROM [dbo].[OrganizationUserView]
WHERE OrganizationId = @OrganizationId
AND Status >= 0 --Invited
) +
(
SELECT COUNT(1)
FROM [dbo].[OrganizationSponsorship]
WHERE SponsoringOrganizationId = @OrganizationId
AND IsAdminInitiated = 1
AND (
-- Not marked for deletion - always count
(ToDelete = 0)
OR
-- Marked for deletion but has a valid until date in the future (RevokeWhenExpired status)
(ToDelete = 1 AND ValidUntil IS NOT NULL AND ValidUntil > GETUTCDATE())
)
AND (
-- SENT status: When SponsoredOrganizationId is null
SponsoredOrganizationId IS NULL
OR
-- ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future
(SponsoredOrganizationId IS NOT NULL AND (ValidUntil IS NULL OR ValidUntil > GETUTCDATE()))
)
)
END
GO

View File

@ -10,7 +10,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="[8.0.8]">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@ -6,7 +6,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="[8.0.8]">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="[8.0.8]">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@ -11,7 +11,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="[8.0.8]">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>