1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-29 16:52:16 -05:00

[PM-18569]Add admin sponsored families to organization license (#5569)

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* Add `Notes` column to `OrganizationSponsorships` table

* Add feature flag to `CreateAdminInitiatedSponsorshipHandler`

* Unit tests for `CreateSponsorshipHandler`

* More tests for `CreateSponsorshipHandler`

* Forgot to add `Notes` column to `OrganizationSponsorships` table in the migration script

* `CreateAdminInitiatedSponsorshipHandler` unit tests

* Fix `CreateSponsorshipCommandTests`

* Encrypt the notes field

* Wrong business logic checking for invalid permissions.

* Wrong business logic checking for invalid permissions.

* Remove design patterns

* duplicate definition in Constants.cs

* initial commit

* Merge Change with pm-17830 and use the property

* Add the new property to download licence

* Add the new property

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>

* Remove the unsed failing test

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>

* Remove unused method

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>

---------

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>
Co-authored-by: Jonas Hendrickx <jhendrickx@bitwarden.com>
This commit is contained in:
cyprain-okeke 2025-04-28 19:21:52 +01:00 committed by GitHub
parent 12fc9dffd4
commit 07a2c0e9d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 65 additions and 44 deletions

View File

@ -41,6 +41,7 @@ public static class OrganizationLicenseConstants
public const string Refresh = nameof(Refresh);
public const string ExpirationWithoutGracePeriod = nameof(ExpirationWithoutGracePeriod);
public const string Trial = nameof(Trial);
public const string UseAdminSponsoredFamilies = nameof(UseAdminSponsoredFamilies);
}
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.ExpirationWithoutGracePeriod), expirationWithoutGracePeriod.ToString(CultureInfo.InvariantCulture)),
new(nameof(OrganizationLicenseConstants.Trial), trial.ToString()),
new(nameof(OrganizationLicenseConstants.UseAdminSponsoredFamilies), entity.UseAdminSponsoredFamilies.ToString()),
};
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.UseAdminSponsoredFamilies), entity.UseAdminSponsoredFamilies.ToString()));
return Task.FromResult(claims);
}

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,
ILicensingService licenseService, int? version = null)
{
@ -105,6 +133,7 @@ public class OrganizationLicense : ILicense
Trial = false;
}
UseAdminSponsoredFamilies = org.UseAdminSponsoredFamilies;
Hash = Convert.ToBase64String(ComputeHash());
Signature = Convert.ToBase64String(licenseService.SignLicense(this));
}
@ -153,6 +182,7 @@ public class OrganizationLicense : ILicense
public bool Trial { get; set; }
public LicenseType? LicenseType { get; set; }
public bool UseAdminSponsoredFamilies { get; set; }
public string Hash { get; set; }
public string Signature { get; set; }
public string Token { get; set; }
@ -292,13 +322,35 @@ public class OrganizationLicense : ILicense
}
/// <summary>
/// Do not extend this method. It is only here for backwards compatibility with old licenses.
/// Instead, extend the CanUse method using the ClaimsPrincipal.
/// Validates an obsolete license format using property-based validation.
/// </summary>
/// <param name="globalSettings"></param>
/// <param name="licensingService"></param>
/// <param name="exception"></param>
/// <returns></returns>
/// <remarks>
/// <para>
/// ⚠️ DEPRECATED: This method is deprecated and should not be extended or modified.
/// 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)
{
// 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 smSeats = claimsPrincipal.GetValue<int?>(nameof(SmSeats));
var smServiceAccounts = claimsPrincipal.GetValue<int?>(nameof(SmServiceAccounts));
var useAdminSponsoredFamilies = claimsPrincipal.GetValue<bool>(nameof(UseAdminSponsoredFamilies));
return issued <= DateTime.UtcNow &&
expires >= DateTime.UtcNow &&
@ -419,7 +472,9 @@ public class OrganizationLicense : ILicense
useSecretsManager == organization.UseSecretsManager &&
usePasswordManager == organization.UsePasswordManager &&
smSeats == organization.SmSeats &&
smServiceAccounts == organization.SmServiceAccounts;
smServiceAccounts == organization.SmServiceAccounts &&
useAdminSponsoredFamilies == organization.UseAdminSponsoredFamilies;
}
/// <summary>

View File

@ -1,9 +1,6 @@
using System.Security.Claims;
using System.Text.Json;
using Bit.Core.Models.Business;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
@ -12,22 +9,6 @@ namespace Bit.Core.Test.Models.Business;
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>
/// 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));
}
/// <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
}
}