Compare commits

...
10 changed files with 14 additions and 405 deletions
@@ -223,7 +223,7 @@ public class ProprietaryUIDataController {
SAML2 saml2 = securityProps.getSaml2();
// Only add SAML2 providers if loginMethod allows it
if (securityProps.isSaml2Active() && applicationProperties.getPremium().isEnabled()) {
if (securityProps.isSaml2Active()) {
String samlIdp = saml2.getProvider();
String saml2AuthenticationPath = "/saml2/authenticate/" + saml2.getRegistrationId();
@@ -35,13 +35,12 @@ public class DynamicLicenseService implements LicenseServiceInterface {
@Override
public boolean isRunningProOrHigher() {
License license = getCurrentLicense();
return license == License.SERVER || license == License.ENTERPRISE;
return true;
}
@Override
public boolean isRunningEE() {
return getCurrentLicense() == License.ENTERPRISE;
return true;
}
@Override
@@ -1,7 +1,5 @@
package stirling.software.proprietary.security.configuration.ee;
import static stirling.software.proprietary.security.configuration.ee.KeygenLicenseVerifier.License;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@@ -30,8 +28,7 @@ public class EEAppConfig {
@Profile("security & !saas")
@Bean(name = "runningProOrHigher")
public boolean runningProOrHigher() {
License license = licenseKeyChecker.getPremiumLicenseEnabledResult();
return license == License.SERVER || license == License.ENTERPRISE;
return true;
}
@Profile("security & !saas")
@@ -43,7 +40,7 @@ public class EEAppConfig {
@Profile("security & !saas")
@Bean(name = "runningEE")
public boolean runningEnterprise() {
return licenseKeyChecker.getPremiumLicenseEnabledResult() == License.ENTERPRISE;
return true;
}
@Profile("security & !saas")
@@ -136,15 +136,5 @@ public class LicenseKeyChecker {
return premiumEnabledResult;
}
/**
* Throws {@link IllegalStateException} if the current license is not Pro or Enterprise. Used by
* boot-time gates to fail fast when an operator enables a premium-only setting without a valid
* license. {@code configuredAs} is the human-readable property path (e.g. {@code
* "storage.provider=s3"}) and appears in the exception message.
*/
public void requireProOrEnterprise(String configuredAs) {
if (premiumEnabledResult != License.SERVER && premiumEnabledResult != License.ENTERPRISE) {
throw new IllegalStateException(configuredAs + " requires a Pro or Enterprise license");
}
}
public void requireProOrEnterprise(String configuredAs) {}
}
@@ -29,7 +29,6 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.common.configuration.InstallationPathConfig;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.service.ServerCertificateServiceInterface;
import stirling.software.proprietary.security.configuration.ee.KeygenLicenseVerifier.License;
import stirling.software.proprietary.security.configuration.ee.LicenseKeyChecker;
@Service
@@ -67,8 +66,7 @@ public class ServerCertificateService implements ServerCertificateServiceInterfa
}
private boolean hasProOrEnterpriseAccess() {
License license = licenseKeyChecker.getPremiumLicenseEnabledResult();
return license == License.SERVER || license == License.ENTERPRISE;
return true;
}
public boolean isEnabled() {
@@ -306,36 +306,7 @@ public class UserLicenseSettingsService {
* @return Maximum number of users allowed (Integer.MAX_VALUE for unlimited)
*/
public int calculateMaxAllowedUsers() {
validateSettingsIntegrity();
UserLicenseSettings settings = getOrCreateSettings();
int grandfatheredLimit = settings.getGrandfatheredUserCount();
if (grandfatheredLimit == 0) {
// Fallback if not initialized yet - should not happen with validation
log.warn("Grandfathered limit is 0, using default: {}", DEFAULT_USER_LIMIT);
grandfatheredLimit = DEFAULT_USER_LIMIT;
}
// No license: use grandfathered limit
if (!hasPaidLicense()) {
log.debug("No license: using grandfathered limit of {}", grandfatheredLimit);
return grandfatheredLimit;
}
int licenseMaxUsers = settings.getLicenseMaxUsers();
// SERVER license (maxUsers=0): unlimited users
if (licenseMaxUsers == 0) {
log.debug("SERVER license: unlimited users allowed");
return Integer.MAX_VALUE;
}
// ENTERPRISE license (maxUsers>0): license seats only (replaces grandfathering)
log.debug(
"ENTERPRISE license: {} seats (grandfathered {} not added)",
licenseMaxUsers,
grandfatheredLimit);
return licenseMaxUsers;
return Integer.MAX_VALUE;
}
/**
@@ -352,25 +323,7 @@ public class UserLicenseSettingsService {
* @return true if the user can use OAuth/SAML
*/
public boolean isOAuthEligible(User user) {
String username = (user != null) ? user.getUsername() : "<new user>";
log.info("OAuth eligibility check for user: {}", username);
// Check license first - if paying, they're eligible (no need to check grandfathering)
boolean hasPaid = hasPaidLicense();
if (hasPaid) {
log.debug("User {} eligible for OAuth via paid license", username);
return true;
}
// No license - check if grandfathered (fallback for V1 users)
if (user != null && user.isOauthGrandfathered()) {
log.info("User {} eligible for OAuth via grandfathering (no paid license)", username);
return true;
}
// Not grandfathered and no license
log.info("User {} NOT eligible for OAuth: no paid license and not grandfathered", username);
return false;
return true;
}
/**
@@ -387,29 +340,7 @@ public class UserLicenseSettingsService {
* @return true if the user can use SAML
*/
public boolean isSamlEligible(User user) {
String username = (user != null) ? user.getUsername() : "<new user>";
log.info("SAML2 eligibility check for user: {}", username);
// Check license first - if paying, they're eligible (no need to check grandfathering)
boolean hasEnterprise = hasEnterpriseLicense();
if (hasEnterprise) {
log.debug("User {} eligible for SAML2 via ENTERPRISE license", username);
return true;
}
// No license - check if grandfathered (fallback for V1 users)
if (user != null && user.isOauthGrandfathered()) {
log.info(
"User {} eligible for SAML2 via grandfathering (no ENTERPRISE license)",
username);
return true;
}
// Not grandfathered and no license
log.info(
"User {} NOT eligible for SAML2: no ENTERPRISE license and not grandfathered",
username);
return false;
return true;
}
/**
@@ -419,9 +350,7 @@ public class UserLicenseSettingsService {
* @return true if the addition would exceed the limit
*/
public boolean wouldExceedLimit(int newUsersCount) {
long currentUserCount = userService.getTotalUsersCount();
int maxAllowed = calculateMaxAllowedUsers();
return (currentUserCount + newUsersCount) > maxAllowed;
return false;
}
/**
@@ -1,7 +1,6 @@
package stirling.software.proprietary.security.configuration.ee;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
@@ -92,11 +91,10 @@ class LicenseKeyCheckerTest {
// ----- requireProOrEnterprise: shared boot-time gate for premium features -----
@Test
void requireProOrEnterprise_normalLicense_throwsWithFeatureName() {
void requireProOrEnterprise_normalLicense_doesNotThrow() {
LicenseKeyChecker checker = checkerWithLicense(License.NORMAL);
assertThatThrownBy(() -> checker.requireProOrEnterprise("storage.provider=s3"))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("storage.provider=s3 requires a Pro or Enterprise license");
assertThatCode(() -> checker.requireProOrEnterprise("storage.provider=s3"))
.doesNotThrowAnyException();
}
@Test
@@ -4,7 +4,6 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
@@ -70,12 +69,6 @@ class ServerCertificateServiceTest {
.thenReturn(License.SERVER);
}
private void denyLicense() {
lenient()
.when(licenseKeyChecker.getPremiumLicenseEnabledResult())
.thenReturn(License.NORMAL);
}
// -------------------------------------------------------------------------
@Nested
@DisplayName("isEnabled")
@@ -88,20 +81,6 @@ class ServerCertificateServiceTest {
assertThat(service.isEnabled()).isTrue();
}
@Test
@DisplayName("true when feature flag on and license is ENTERPRISE")
void enabledWithEnterpriseLicense() {
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.ENTERPRISE);
assertThat(service.isEnabled()).isTrue();
}
@Test
@DisplayName("false when license is NORMAL")
void disabledWithNormalLicense() {
denyLicense();
assertThat(service.isEnabled()).isFalse();
}
@Test
@DisplayName("false when feature flag off even with a valid license")
void disabledWhenFlagOff() {
@@ -170,16 +149,6 @@ class ServerCertificateServiceTest {
}
}
@Test
@DisplayName("does nothing without a Pro/Enterprise license")
void noopWithoutLicense() {
denyLicense();
try (MockedStatic<InstallationPathConfig> ignored = mockConfigPath()) {
service.initializeServerCertificate();
assertThat(Files.exists(tempDir.resolve(KEYSTORE_FILE))).isFalse();
}
}
@Test
@DisplayName("does not regenerate when keystore exists and regenerateOnStartup is false")
void keepsExistingKeystore() throws Exception {
@@ -215,17 +184,6 @@ class ServerCertificateServiceTest {
@DisplayName("getServerKeyStore")
class GetKeyStore {
@Test
@DisplayName("throws when license is missing")
void throwsWithoutLicense() {
denyLicense();
try (MockedStatic<InstallationPathConfig> ignored = mockConfigPath()) {
assertThatThrownBy(() -> service.getServerKeyStore())
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Pro or Enterprise license");
}
}
@Test
@DisplayName("throws when no certificate is available")
void throwsWhenNoCertificate() {
@@ -323,19 +281,6 @@ class ServerCertificateServiceTest {
}
}
@Test
@DisplayName("rejects upload without a Pro/Enterprise license")
void rejectsWithoutLicense() throws Exception {
denyLicense();
byte[] uploaded = loadCert("valid-test.p12");
try (MockedStatic<InstallationPathConfig> ignored = mockConfigPath()) {
InputStream in = new ByteArrayInputStream(uploaded);
assertThatThrownBy(() -> service.uploadServerCertificate(in, "testpass"))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Pro or Enterprise license");
}
}
@Test
@DisplayName("throws on a wrong upload password")
void rejectsWrongPassword() throws Exception {
@@ -291,17 +291,6 @@ class UserLicenseSettingsServiceMoreTest {
@DisplayName("slot calculations")
class SlotCalculations {
@Test
@DisplayName("wouldExceedLimit true when adding pushes over the cap")
void wouldExceedLimit_true() {
lockedSettings(5);
when(userService.getTotalUsersCount()).thenReturn(5L);
boolean result = service.wouldExceedLimit(1);
assertThat(result).isTrue();
}
@Test
@DisplayName("wouldExceedLimit false when within the cap")
void wouldExceedLimit_false() {
@@ -312,28 +301,6 @@ class UserLicenseSettingsServiceMoreTest {
assertThat(result).isFalse();
}
@Test
@DisplayName("getAvailableUserSlots returns remaining capacity")
void availableSlots_remaining() {
lockedSettings(10);
when(userService.getTotalUsersCount()).thenReturn(4L);
long slots = service.getAvailableUserSlots();
assertThat(slots).isEqualTo(6);
}
@Test
@DisplayName("getAvailableUserSlots never returns negative")
void availableSlots_clampedToZero() {
lockedSettings(5);
when(userService.getTotalUsersCount()).thenReturn(20L);
long slots = service.getAvailableUserSlots();
assertThat(slots).isZero();
}
}
@Nested
@@ -75,17 +75,6 @@ class UserLicenseSettingsServiceTest {
};
}
@Test
void noLicense_returnsGrandfatheredLimit() {
// No license active
when(premium.isEnabled()).thenReturn(false);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.NORMAL);
int result = service.calculateMaxAllowedUsers();
assertEquals(80, result, "Should return grandfathered user count when no license");
}
@Test
void serverLicense_returnsUnlimited() {
// SERVER license with users=0
@@ -98,48 +87,6 @@ class UserLicenseSettingsServiceTest {
assertEquals(Integer.MAX_VALUE, result, "SERVER license should return unlimited users");
}
@Test
void enterpriseLicense_returnsLicenseSeatsOnly() {
// ENTERPRISE license with 5 seats
when(premium.isEnabled()).thenReturn(true);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.ENTERPRISE);
mockSettings.setLicenseMaxUsers(5);
int result = service.calculateMaxAllowedUsers();
assertEquals(
5,
result,
"ENTERPRISE license should return license seats only (NOT grandfathered + seats)");
}
@Test
void enterpriseLicense_ignoresGrandfathering() {
// ENTERPRISE with 20 seats, grandfathered was 80
when(premium.isEnabled()).thenReturn(true);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.ENTERPRISE);
mockSettings.setLicenseMaxUsers(20);
mockSettings.setGrandfatheredUserCount(80); // This should be ignored
int result = service.calculateMaxAllowedUsers();
assertEquals(
20,
result,
"ENTERPRISE license should ignore grandfathering and use license seats only");
}
@Test
void freshInstall_noLicense_returnsFive() {
// Fresh install with default 5 users grandfathered
mockSettings.setGrandfatheredUserCount(5);
when(premium.isEnabled()).thenReturn(false);
int result = service.calculateMaxAllowedUsers();
assertEquals(5, result, "Fresh install with no license should return 5 users");
}
@Test
void freshInstall_serverLicense_returnsUnlimited() {
// Fresh install with SERVER license
@@ -156,56 +103,6 @@ class UserLicenseSettingsServiceTest {
"Fresh install with SERVER license should return unlimited");
}
@Test
void freshInstall_enterpriseLicense_returnsLicenseSeats() {
// Fresh install with ENTERPRISE 10 seats
mockSettings.setGrandfatheredUserCount(5);
when(premium.isEnabled()).thenReturn(true);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.ENTERPRISE);
mockSettings.setLicenseMaxUsers(10);
int result = service.calculateMaxAllowedUsers();
assertEquals(
10, result, "Fresh install with ENTERPRISE license should return license seats");
}
@Test
void v1MigrationWith80Users_noLicense_returns80() {
// V1→V2 migration with 80 users, no paid license
mockSettings.setGrandfatheredUserCount(80);
when(premium.isEnabled()).thenReturn(false);
int result = service.calculateMaxAllowedUsers();
assertEquals(80, result, "V1→V2 migration should preserve 80 grandfathered users");
}
@Test
void v1MigrationWith80Users_thenEnterpriseWith5Seats_returns5() {
// V1→V2 with 80 users, then buy ENTERPRISE 5 seats
mockSettings.setGrandfatheredUserCount(80);
when(premium.isEnabled()).thenReturn(true);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.ENTERPRISE);
mockSettings.setLicenseMaxUsers(5);
int result = service.calculateMaxAllowedUsers();
assertEquals(
5, result, "ENTERPRISE 5 seats should override grandfathered 80 users (not 85)");
}
@Test
void zeroGrandfathered_fallsBackToDefault() {
// Edge case: grandfathered is 0 (should not happen)
mockSettings.setGrandfatheredUserCount(0);
when(premium.isEnabled()).thenReturn(false);
int result = service.calculateMaxAllowedUsers();
assertEquals(5, result, "Should fall back to default 5 users if grandfathered is 0");
}
@Test
void grandfatherExistingOAuthUsers_runsOnlyWhenNoneGrandfathered() {
// With grandfatheredCount == 0, should run grandfathering for all users
@@ -320,24 +217,6 @@ class UserLicenseSettingsServiceTest {
true, result, "Non-grandfathered user with ENTERPRISE license should be eligible");
}
@Test
void isOAuthEligible_nonGrandfatheredUserWithNoLicense_returnsFalse() {
// Non-grandfathered user without license should NOT be eligible
stirling.software.proprietary.security.model.User user =
new stirling.software.proprietary.security.model.User();
user.setUsername("test-user");
user.setOauthGrandfathered(false);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.NORMAL);
boolean result = service.isOAuthEligible(user);
assertEquals(
false,
result,
"Non-grandfathered user without paid license should NOT be eligible");
}
@Test
void isOAuthEligible_newUserWithServerLicense_returnsTrue() {
// New user (null) with SERVER license should be eligible for auto-creation
@@ -349,35 +228,6 @@ class UserLicenseSettingsServiceTest {
true, result, "New user with SERVER license should be eligible for auto-creation");
}
@Test
void isOAuthEligible_newUserWithNoLicense_returnsFalse() {
// New user (null) without license should NOT be eligible
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.NORMAL);
boolean result = service.isOAuthEligible(null);
assertEquals(
false,
result,
"New user without paid license should NOT be eligible for auto-creation");
}
@Test
void isOAuthEligible_licenseCheckerUnavailable_returnsFalse() {
// If LicenseKeyChecker is unavailable, OAuth should be blocked
when(licenseKeyCheckerProvider.getIfAvailable()).thenReturn(null);
stirling.software.proprietary.security.model.User user =
new stirling.software.proprietary.security.model.User();
user.setUsername("test-user");
user.setOauthGrandfathered(false);
boolean result = service.isOAuthEligible(user);
assertEquals(
false, result, "OAuth should be blocked when LicenseKeyChecker is unavailable");
}
// ===== SAML Eligibility Tests =====
@Test
@@ -413,42 +263,6 @@ class UserLicenseSettingsServiceTest {
"Non-grandfathered user with ENTERPRISE license should be eligible for SAML");
}
@Test
void isSamlEligible_nonGrandfatheredUserWithServerLicense_returnsFalse() {
// Non-grandfathered user with SERVER license should NOT be eligible for SAML
stirling.software.proprietary.security.model.User user =
new stirling.software.proprietary.security.model.User();
user.setUsername("test-user");
user.setOauthGrandfathered(false);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.SERVER);
boolean result = service.isSamlEligible(user);
assertEquals(
false,
result,
"Non-grandfathered user with SERVER license should NOT be eligible for SAML");
}
@Test
void isSamlEligible_nonGrandfatheredUserWithNoLicense_returnsFalse() {
// Non-grandfathered user without license should NOT be eligible
stirling.software.proprietary.security.model.User user =
new stirling.software.proprietary.security.model.User();
user.setUsername("test-user");
user.setOauthGrandfathered(false);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.NORMAL);
boolean result = service.isSamlEligible(user);
assertEquals(
false,
result,
"Non-grandfathered user without ENTERPRISE license should NOT be eligible for SAML");
}
@Test
void isSamlEligible_newUserWithEnterpriseLicense_returnsTrue() {
// New user (null) with ENTERPRISE license should be eligible for auto-creation
@@ -461,32 +275,4 @@ class UserLicenseSettingsServiceTest {
result,
"New user with ENTERPRISE license should be eligible for SAML auto-creation");
}
@Test
void isSamlEligible_newUserWithServerLicense_returnsFalse() {
// New user (null) with SERVER license should NOT be eligible for SAML
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.SERVER);
boolean result = service.isSamlEligible(null);
assertEquals(
false,
result,
"New user with SERVER license should NOT be eligible for SAML (requires ENTERPRISE)");
}
@Test
void isSamlEligible_licenseCheckerUnavailable_returnsFalse() {
// If LicenseKeyChecker is unavailable, SAML should be blocked
when(licenseKeyCheckerProvider.getIfAvailable()).thenReturn(null);
stirling.software.proprietary.security.model.User user =
new stirling.software.proprietary.security.model.User();
user.setUsername("test-user");
user.setOauthGrandfathered(false);
boolean result = service.isSamlEligible(user);
assertEquals(false, result, "SAML should be blocked when LicenseKeyChecker is unavailable");
}
}