using System.Security.Claims;
using System.Text;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Licenses.Attributes;
using Bit.Core.Billing.Licenses.Extensions;
using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Core.Settings;
namespace Bit.Core.Models.Business;
public class OrganizationLicense : BaseLicense
{
public OrganizationLicense()
{
}
///
/// Initializes a new instance of the class.
///
///
///
/// ⚠️ DEPRECATED: This constructor and the entire property-based licensing system is deprecated.
/// Do not add new properties to this constructor or extend its functionality.
///
///
/// 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.
///
///
/// 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 and methods
///
///
/// This constructor is maintained only for backward compatibility with existing licenses.
///
///
/// The organization to create the license for.
/// Information about the organization's subscription.
/// The ID of the current installation.
/// The service used to sign the license.
/// Optional version number for the license format.
public OrganizationLicense(Organization org, SubscriptionInfo subscriptionInfo, Guid installationId,
ILicensingService licenseService, int? version = null)
{
Version = version.GetValueOrDefault(CurrentLicenseFileVersion); // TODO: Remember to change the constant
LicenseType = Enums.LicenseType.Organization;
LicenseKey = org.LicenseKey;
InstallationId = installationId;
Id = org.Id;
Name = org.Name;
BillingEmail = org.BillingEmail;
BusinessName = org.BusinessName;
Enabled = org.Enabled;
Plan = org.Plan;
PlanType = org.PlanType;
Seats = org.Seats;
MaxCollections = org.MaxCollections;
UsePolicies = org.UsePolicies;
UseSso = org.UseSso;
UseKeyConnector = org.UseKeyConnector;
UseScim = org.UseScim;
UseGroups = org.UseGroups;
UseEvents = org.UseEvents;
UseDirectory = org.UseDirectory;
UseTotp = org.UseTotp;
Use2fa = org.Use2fa;
UseApi = org.UseApi;
UseResetPassword = org.UseResetPassword;
MaxStorageGb = org.MaxStorageGb;
SelfHost = org.SelfHost;
UsersGetPremium = org.UsersGetPremium;
UseCustomPermissions = org.UseCustomPermissions;
Issued = DateTime.UtcNow;
UsePasswordManager = org.UsePasswordManager;
UseSecretsManager = org.UseSecretsManager;
SmSeats = org.SmSeats;
SmServiceAccounts = org.SmServiceAccounts;
UseRiskInsights = org.UseRiskInsights;
UseOrganizationDomains = org.UseOrganizationDomains;
// Deprecated. Left for backwards compatibility with old license versions.
LimitCollectionCreationDeletion = org.LimitCollectionCreation || org.LimitCollectionDeletion;
AllowAdminAccessToAllCollectionItems = org.AllowAdminAccessToAllCollectionItems;
//
if (subscriptionInfo?.Subscription == null)
{
if (org.PlanType == PlanType.Custom && org.ExpirationDate.HasValue)
{
Expires = Refresh = org.ExpirationDate.Value;
Trial = false;
}
else
{
Expires = Refresh = Issued.AddDays(7);
Trial = true;
}
}
else if (subscriptionInfo.Subscription.TrialEndDate.HasValue &&
subscriptionInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow)
{
Expires = Refresh = subscriptionInfo.Subscription.TrialEndDate.Value;
Trial = true;
}
else
{
if (org.ExpirationDate.HasValue && org.ExpirationDate.Value < DateTime.UtcNow)
{
// expired
Expires = Refresh = org.ExpirationDate.Value;
}
else if (subscriptionInfo?.Subscription?.PeriodDuration != null &&
subscriptionInfo.Subscription.PeriodDuration > TimeSpan.FromDays(180))
{
Refresh = DateTime.UtcNow.AddDays(30);
Expires = subscriptionInfo.Subscription.PeriodEndDate?.AddDays(Constants
.OrganizationSelfHostSubscriptionGracePeriodDays);
ExpirationWithoutGracePeriod = subscriptionInfo.Subscription.PeriodEndDate;
}
else
{
Expires = org.ExpirationDate.HasValue ? org.ExpirationDate.Value.AddMonths(11) : Issued.AddYears(1);
Refresh = DateTime.UtcNow - Expires > TimeSpan.FromDays(30) ? DateTime.UtcNow.AddDays(30) : Expires;
}
Trial = false;
}
UseAdminSponsoredFamilies = org.UseAdminSponsoredFamilies;
Hash = Convert.ToBase64String(ComputeHash());
Signature = Convert.ToBase64String(licenseService.SignLicense(this));
}
///
/// Represents the current version of the license format. Should be updated whenever new fields are added.
///
/// Intentionally set one version behind to allow self hosted users some time to update before
/// getting out of date license errors
///
public const int CurrentLicenseFileVersion = 15;
[LicenseVersion(1)]
public Guid InstallationId { get; set; }
[LicenseVersion(1)]
public int? Seats { get; set; }
[LicenseVersion(1)]
public short? MaxCollections { get; set; }
[LicenseVersion(1)]
public short? MaxStorageGb { get; set; }
[LicenseVersion(1)]
public bool Enabled { get; set; }
[LicenseVersion(1)]
public bool SelfHost { get; set; }
[LicenseVersion(1)]
public bool UseDirectory { get; set; }
[LicenseVersion(1)]
public bool UseGroups { get; set; }
[LicenseVersion(1)]
public bool UseTotp { get; set; }
[LicenseVersion(1)]
public string BillingEmail { get; set; }
[LicenseVersion(1)]
public string BusinessName { get; set; }
[LicenseVersion(1)]
public string Plan { get; set; }
[LicenseVersion(1)]
public PlanType PlanType { get; set; }
[LicenseVersion(2)]
public bool UsersGetPremium { get; set; }
[LicenseVersion(3)]
public bool UseEvents { get; set; }
[LicenseVersion(4)]
public bool Use2fa { get; set; }
[LicenseVersion(5)]
public bool UseApi { get; set; }
[LicenseVersion(6)]
public bool UsePolicies { get; set; }
[LicenseVersion(7)]
public bool UseSso { get; set; }
[LicenseVersion(8)]
public bool UseResetPassword { get; set; }
[LicenseVersion(9)]
public bool UseKeyConnector { get; set; }
[LicenseVersion(10)]
public bool UseScim { get; set; }
[LicenseVersion(11)]
public bool UseCustomPermissions { get; set; }
[LicenseVersion(12)]
public DateTime? ExpirationWithoutGracePeriod { get; set; }
[LicenseVersion(13)]
public bool UsePasswordManager { get; set; }
[LicenseVersion(13)]
public bool UseSecretsManager { get; set; }
[LicenseVersion(13)]
public int? SmSeats { get; set; }
[LicenseVersion(13)]
public int? SmServiceAccounts { get; set; }
// Deprecated. Left for backwards compatibility with old license versions.
[LicenseVersion(14)]
public bool LimitCollectionCreationDeletion { get; set; } = true;
[LicenseVersion(15)]
public bool AllowAdminAccessToAllCollectionItems { get; set; } = true;
//
[LicenseVersion(16)]
public bool UseOrganizationDomains { get; set; }
[LicenseIgnore]
public bool UseAdminSponsoredFamilies { get; set; }
[LicenseIgnore]
public bool UseRiskInsights { get; set; }
private bool ValidLicenseVersion
{
get => Version is >= 1 and <= 16;
}
public override byte[] GetDataBytes(bool forHash = false)
{
if (!ValidLicenseVersion)
{
throw new NotSupportedException($"Version {Version} is not supported.");
}
return this.GetDataBytesWithAttributes(forHash);
}
public bool CanUse(
IGlobalSettings globalSettings,
ILicensingService licensingService,
ClaimsPrincipal claimsPrincipal,
out string exception)
{
if (string.IsNullOrWhiteSpace(Token) || claimsPrincipal is null)
{
return ObsoleteCanUse(globalSettings, licensingService, out exception);
}
var errorMessages = new StringBuilder();
var enabled = claimsPrincipal.GetValue(nameof(Enabled));
if (!enabled)
{
errorMessages.AppendLine("Your cloud-hosted organization is currently disabled.");
}
var installationId = claimsPrincipal.GetValue(nameof(InstallationId));
if (installationId != globalSettings.Installation.Id)
{
errorMessages.AppendLine("The installation ID does not match the current installation.");
}
var selfHost = claimsPrincipal.GetValue(nameof(SelfHost));
if (!selfHost)
{
errorMessages.AppendLine("The license does not allow for on-premise hosting of organizations.");
}
var licenseType = claimsPrincipal.GetValue(nameof(LicenseType));
if (licenseType != Enums.LicenseType.Organization)
{
errorMessages.AppendLine("Premium licenses cannot be applied to an organization. " +
"Upload this license from your personal account settings page.");
}
if (errorMessages.Length > 0)
{
exception = $"Invalid license. {errorMessages.ToString().TrimEnd()}";
return false;
}
exception = "";
return true;
}
///
/// Validates an obsolete license format using property-based validation.
///
///
///
/// ⚠️ DEPRECATED: This method is deprecated and should not be extended or modified.
/// It is maintained only for backward compatibility with old license formats.
///
///
/// 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
///
///
/// To add new license validation rules:
/// 1. Add new claims to the license token in the claims-based system
/// 2. Extend the method
/// 3. Validate the new claims using the ClaimsPrincipal parameter
///
///
/// This method will be removed in a future version once all old licenses have been migrated
/// to the new claims-based system.
///
///
/// The global settings containing installation information.
/// The service used to verify the license signature.
/// When the method returns false, contains the error message explaining why the license is invalid.
/// True if the license is valid, false otherwise.
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.
var errorMessages = new StringBuilder();
if (!Enabled)
{
errorMessages.AppendLine("Your cloud-hosted organization is currently disabled.");
}
if (Issued > DateTime.UtcNow)
{
errorMessages.AppendLine("The license hasn't been issued yet.");
}
if (Expires < DateTime.UtcNow)
{
errorMessages.AppendLine("The license has expired.");
}
if (!ValidLicenseVersion)
{
errorMessages.AppendLine($"Version {Version} is not supported.");
}
if (InstallationId != globalSettings.Installation.Id)
{
errorMessages.AppendLine("The installation ID does not match the current installation.");
}
if (!SelfHost)
{
errorMessages.AppendLine("The license does not allow for on-premise hosting of organizations.");
}
if (LicenseType != LicenseType.Organization)
{
errorMessages.AppendLine("Premium licenses cannot be applied to an organization. " +
"Upload this license from your personal account settings page.");
}
if (!licensingService.VerifyLicense(this))
{
errorMessages.AppendLine("The license verification failed.");
}
if (errorMessages.Length > 0)
{
exception = $"Invalid license. {errorMessages.ToString().TrimEnd()}";
return false;
}
exception = "";
return true;
}
public bool VerifyData(
Organization organization,
ClaimsPrincipal claimsPrincipal,
IGlobalSettings globalSettings)
{
if (string.IsNullOrWhiteSpace(Token))
{
return ObsoleteVerifyData(organization, globalSettings);
}
var issued = claimsPrincipal.GetValue(nameof(Issued));
var expires = claimsPrincipal.GetValue(nameof(Expires));
var installationId = claimsPrincipal.GetValue(nameof(InstallationId));
var licenseKey = claimsPrincipal.GetValue(nameof(LicenseKey));
var enabled = claimsPrincipal.GetValue(nameof(Enabled));
var planType = claimsPrincipal.GetValue(nameof(PlanType));
var seats = claimsPrincipal.GetValue(nameof(Seats));
var maxCollections = claimsPrincipal.GetValue(nameof(MaxCollections));
var useGroups = claimsPrincipal.GetValue(nameof(UseGroups));
var useDirectory = claimsPrincipal.GetValue(nameof(UseDirectory));
var useTotp = claimsPrincipal.GetValue(nameof(UseTotp));
var selfHost = claimsPrincipal.GetValue(nameof(SelfHost));
var name = claimsPrincipal.GetValue(nameof(Name));
var usersGetPremium = claimsPrincipal.GetValue(nameof(UsersGetPremium));
var useEvents = claimsPrincipal.GetValue(nameof(UseEvents));
var use2fa = claimsPrincipal.GetValue(nameof(Use2fa));
var useApi = claimsPrincipal.GetValue(nameof(UseApi));
var usePolicies = claimsPrincipal.GetValue(nameof(UsePolicies));
var useSso = claimsPrincipal.GetValue(nameof(UseSso));
var useResetPassword = claimsPrincipal.GetValue(nameof(UseResetPassword));
var useKeyConnector = claimsPrincipal.GetValue(nameof(UseKeyConnector));
var useScim = claimsPrincipal.GetValue(nameof(UseScim));
var useCustomPermissions = claimsPrincipal.GetValue(nameof(UseCustomPermissions));
var useSecretsManager = claimsPrincipal.GetValue(nameof(UseSecretsManager));
var usePasswordManager = claimsPrincipal.GetValue(nameof(UsePasswordManager));
var smSeats = claimsPrincipal.GetValue(nameof(SmSeats));
var smServiceAccounts = claimsPrincipal.GetValue(nameof(SmServiceAccounts));
var useAdminSponsoredFamilies = claimsPrincipal.GetValue(nameof(UseAdminSponsoredFamilies));
var useOrganizationDomains = claimsPrincipal.GetValue(nameof(UseOrganizationDomains));
return issued <= DateTime.UtcNow &&
expires >= DateTime.UtcNow &&
installationId == globalSettings.Installation.Id &&
licenseKey == organization.LicenseKey &&
enabled == organization.Enabled &&
planType == organization.PlanType &&
seats == organization.Seats &&
maxCollections == organization.MaxCollections &&
useGroups == organization.UseGroups &&
useDirectory == organization.UseDirectory &&
useTotp == organization.UseTotp &&
selfHost == organization.SelfHost &&
name == organization.Name &&
usersGetPremium == organization.UsersGetPremium &&
useEvents == organization.UseEvents &&
use2fa == organization.Use2fa &&
useApi == organization.UseApi &&
usePolicies == organization.UsePolicies &&
useSso == organization.UseSso &&
useResetPassword == organization.UseResetPassword &&
useKeyConnector == organization.UseKeyConnector &&
useScim == organization.UseScim &&
useCustomPermissions == organization.UseCustomPermissions &&
useSecretsManager == organization.UseSecretsManager &&
usePasswordManager == organization.UsePasswordManager &&
smSeats == organization.SmSeats &&
smServiceAccounts == organization.SmServiceAccounts &&
useAdminSponsoredFamilies == organization.UseAdminSponsoredFamilies &&
useOrganizationDomains == organization.UseOrganizationDomains;
}
///
/// Do not extend this method. It is only here for backwards compatibility with old licenses.
/// Instead, extend the VerifyData method using the ClaimsPrincipal.
///
///
///
///
///
private bool ObsoleteVerifyData(Organization organization, IGlobalSettings globalSettings)
{
// Do not extend this method. It is only here for backwards compatibility with old licenses.
if (Issued > DateTime.UtcNow || Expires < DateTime.UtcNow)
{
return false;
}
if (!ValidLicenseVersion)
{
throw new NotSupportedException($"Version {Version} is not supported.");
}
var valid =
globalSettings.Installation.Id == InstallationId &&
organization.LicenseKey != null && organization.LicenseKey.Equals(LicenseKey) &&
organization.Enabled == Enabled &&
organization.PlanType == PlanType &&
organization.Seats == Seats &&
organization.MaxCollections == MaxCollections &&
organization.UseGroups == UseGroups &&
organization.UseDirectory == UseDirectory &&
organization.UseTotp == UseTotp &&
organization.SelfHost == SelfHost &&
organization.Name.Equals(Name);
if (valid && Version >= 2)
{
valid = organization.UsersGetPremium == UsersGetPremium;
}
if (valid && Version >= 3)
{
valid = organization.UseEvents == UseEvents;
}
if (valid && Version >= 4)
{
valid = organization.Use2fa == Use2fa;
}
if (valid && Version >= 5)
{
valid = organization.UseApi == UseApi;
}
if (valid && Version >= 6)
{
valid = organization.UsePolicies == UsePolicies;
}
if (valid && Version >= 7)
{
valid = organization.UseSso == UseSso;
}
if (valid && Version >= 8)
{
valid = organization.UseResetPassword == UseResetPassword;
}
if (valid && Version >= 9)
{
valid = organization.UseKeyConnector == UseKeyConnector;
}
if (valid && Version >= 10)
{
valid = organization.UseScim == UseScim;
}
if (valid && Version >= 11)
{
valid = organization.UseCustomPermissions == UseCustomPermissions;
}
/*Version 12 added ExpirationWithoutDatePeriod, but that property is informational only and is not saved
to the Organization object. It's validated as part of the hash but does not need to be validated here.
*/
if (valid && Version >= 13)
{
valid = organization.UseSecretsManager == UseSecretsManager &&
organization.UsePasswordManager == UsePasswordManager &&
organization.SmSeats == SmSeats &&
organization.SmServiceAccounts == SmServiceAccounts;
}
/*
* Version 14 added LimitCollectionCreationDeletion and Version
* 15 added AllowAdminAccessToAllCollectionItems, however they
* are no longer used and are intentionally excluded from
* validation.
*/
if (valid && Version >= 16)
{
valid = organization.UseOrganizationDomains;
}
return valid;
}
}