From e6db57e0312935eb6ee87649b22ae1535cf6bfb2 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Tue, 25 Nov 2025 21:23:32 +0000 Subject: [PATCH] grandfather users (#4984) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Co-authored-by: Dario Ghunney Ware --- .../api/ProprietaryUIDataController.java | 6 +- .../security/CustomLogoutSuccessHandler.java | 6 + .../security/InitialSecuritySetup.java | 1 + .../config/PremiumEndpointAspect.java | 2 +- .../configuration/SecurityConfiguration.java | 15 +- .../configuration/ee/EEAppConfig.java | 3 +- .../ee/KeygenLicenseVerifier.java | 90 +++++--- .../configuration/ee/LicenseKeyChecker.java | 4 +- .../controller/api/UserController.java | 13 ++ .../database/repository/UserRepository.java | 24 +++ .../proprietary/security/model/User.java | 11 + ...tomOAuth2AuthenticationSuccessHandler.java | 27 ++- ...stomSaml2AuthenticationSuccessHandler.java | 27 ++- .../security/service/UserService.java | 44 ++++ .../service/ServerCertificateService.java | 2 +- .../service/UserLicenseSettingsService.java | 129 +++++++++-- .../CustomLogoutSuccessHandlerTest.java | 9 + .../ee/LicenseKeyCheckerTest.java | 4 +- .../UserLicenseSettingsServiceTest.java | 201 ++++++++++++++++++ .../public/locales/en-GB/translation.json | 3 + frontend/src/core/services/updateService.ts | 8 +- .../proprietary/services/licenseService.ts | 6 +- 22 files changed, 564 insertions(+), 71 deletions(-) create mode 100644 app/proprietary/src/test/java/stirling/software/proprietary/service/UserLicenseSettingsServiceTest.java diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/ProprietaryUIDataController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/ProprietaryUIDataController.java index 5c2c9502f..d335eb9c2 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/ProprietaryUIDataController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/ProprietaryUIDataController.java @@ -397,7 +397,8 @@ public class ProprietaryUIDataController { Map teamLastRequest = new HashMap<>(); for (Object[] result : teamActivities) { Long teamId = (Long) result[0]; - Date lastActivity = (Date) result[1]; + Instant instant = (Instant) result[1]; + Date lastActivity = instant != null ? Date.from(instant) : null; teamLastRequest.put(teamId, lastActivity); } @@ -441,7 +442,8 @@ public class ProprietaryUIDataController { Map userLastRequest = new HashMap<>(); for (Object[] result : userSessions) { String username = (String) result[0]; - Date lastRequest = (Date) result[1]; + Instant instant = (Instant) result[1]; + Date lastRequest = instant != null ? Date.from(instant) : null; userLastRequest.put(username, lastRequest); } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomLogoutSuccessHandler.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomLogoutSuccessHandler.java index 5901a5168..16272b37f 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomLogoutSuccessHandler.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomLogoutSuccessHandler.java @@ -238,6 +238,12 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { path = "errorOAuth=oAuth2AutoCreateDisabled"; } else if (request.getParameter("oAuth2AdminBlockedUser") != null) { path = "errorOAuth=oAuth2AdminBlockedUser"; + } else if (request.getParameter("oAuth2RequiresLicense") != null) { + path = "errorOAuth=oAuth2RequiresLicense"; + } else if (request.getParameter("saml2RequiresLicense") != null) { + path = "errorOAuth=saml2RequiresLicense"; + } else if (request.getParameter("maxUsersReached") != null) { + path = "errorOAuth=maxUsersReached"; } else if (request.getParameter("userIsDisabled") != null) { path = "errorOAuth=userIsDisabled"; } else if ((errorMessage = request.getParameter("error")) != null) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/InitialSecuritySetup.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/InitialSecuritySetup.java index 24326f31e..8d9ec5a01 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/InitialSecuritySetup.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/InitialSecuritySetup.java @@ -62,6 +62,7 @@ public class InitialSecuritySetup { private void initializeUserLicenseSettings() { licenseSettingsService.initializeGrandfatheredCount(); licenseSettingsService.updateLicenseMaxUsers(); + licenseSettingsService.grandfatherExistingOAuthUsers(); } private void configureJWTSettings() { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/config/PremiumEndpointAspect.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/config/PremiumEndpointAspect.java index 1ee25574b..9b26e5b55 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/config/PremiumEndpointAspect.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/config/PremiumEndpointAspect.java @@ -23,7 +23,7 @@ public class PremiumEndpointAspect { public Object checkPremiumAccess(ProceedingJoinPoint joinPoint) throws Throwable { if (!runningProOrHigher) { throw new ResponseStatusException( - HttpStatus.FORBIDDEN, "This endpoint requires a Pro or higher license"); + HttpStatus.FORBIDDEN, "This endpoint requires a Server or Enterprise license"); } return joinPoint.proceed(); } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java index bd6acc1b9..e683fa8dc 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java @@ -84,6 +84,8 @@ public class SecurityConfiguration { private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper; private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations; private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver; + private final stirling.software.proprietary.service.UserLicenseSettingsService + licenseSettingsService; public SecurityConfiguration( PersistentLoginRepository persistentLoginRepository, @@ -103,7 +105,9 @@ public class SecurityConfiguration { @Autowired(required = false) RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations, @Autowired(required = false) - OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver) { + OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver, + stirling.software.proprietary.service.UserLicenseSettingsService + licenseSettingsService) { this.userDetailsService = userDetailsService; this.userService = userService; this.loginEnabledValue = loginEnabledValue; @@ -120,10 +124,11 @@ public class SecurityConfiguration { this.oAuth2userAuthoritiesMapper = oAuth2userAuthoritiesMapper; this.saml2RelyingPartyRegistrations = saml2RelyingPartyRegistrations; this.saml2AuthenticationRequestResolver = saml2AuthenticationRequestResolver; + this.licenseSettingsService = licenseSettingsService; } @Bean - public PasswordEncoder passwordEncoder() { + public static PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @@ -354,7 +359,8 @@ public class SecurityConfiguration { loginAttemptService, securityProperties.getOauth2(), userService, - jwtService)) + jwtService, + licenseSettingsService)) .failureHandler(new CustomOAuth2AuthenticationFailureHandler()) // Add existing Authorities from the database .userInfoEndpoint( @@ -395,7 +401,8 @@ public class SecurityConfiguration { loginAttemptService, securityProperties.getSaml2(), userService, - jwtService)) + jwtService, + licenseSettingsService)) .failureHandler( new CustomSaml2AuthenticationFailureHandler()) .authenticationRequestResolver( diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ee/EEAppConfig.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ee/EEAppConfig.java index 2e71b670d..85a0a91db 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ee/EEAppConfig.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ee/EEAppConfig.java @@ -32,7 +32,8 @@ public class EEAppConfig { @Bean(name = "runningProOrHigher") @Primary public boolean runningProOrHigher() { - return licenseKeyChecker.getPremiumLicenseEnabledResult() != License.NORMAL; + License license = licenseKeyChecker.getPremiumLicenseEnabledResult(); + return license == License.SERVER || license == License.ENTERPRISE; } @Profile("security") diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ee/KeygenLicenseVerifier.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ee/KeygenLicenseVerifier.java index d5902bcfa..a10f395b4 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ee/KeygenLicenseVerifier.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ee/KeygenLicenseVerifier.java @@ -30,7 +30,7 @@ public class KeygenLicenseVerifier { public enum License { NORMAL, - PRO, + SERVER, ENTERPRISE } @@ -76,7 +76,7 @@ public class KeygenLicenseVerifier { log.info("Detected certificate-based license. Processing..."); boolean isValid = verifyCertificateLicense(licenseKeyOrCert, context); if (isValid) { - license = context.isEnterpriseLicense ? License.ENTERPRISE : License.PRO; + license = context.isEnterpriseLicense ? License.ENTERPRISE : License.SERVER; } else { license = License.NORMAL; } @@ -84,7 +84,7 @@ public class KeygenLicenseVerifier { log.info("Detected JWT-style license key. Processing..."); boolean isValid = verifyJWTLicense(licenseKeyOrCert, context); if (isValid) { - license = context.isEnterpriseLicense ? License.ENTERPRISE : License.PRO; + license = context.isEnterpriseLicense ? License.ENTERPRISE : License.SERVER; } else { license = License.NORMAL; } @@ -92,7 +92,7 @@ public class KeygenLicenseVerifier { log.info("Detected standard license key. Processing..."); boolean isValid = verifyStandardLicense(licenseKeyOrCert, context); if (isValid) { - license = context.isEnterpriseLicense ? License.ENTERPRISE : License.PRO; + license = context.isEnterpriseLicense ? License.ENTERPRISE : License.SERVER; } else { license = License.NORMAL; } @@ -261,10 +261,23 @@ public class KeygenLicenseVerifier { // Extract metadata JSONObject metadataObj = attributesObj.optJSONObject("metadata"); if (metadataObj != null) { - int users = metadataObj.optInt("users", 1); - applicationProperties.getPremium().setMaxUsers(users); - log.info("License allows for {} users", users); + // Check if this is an old license (no planType) with isEnterprise flag context.isEnterpriseLicense = metadataObj.optBoolean("isEnterprise", false); + + // Extract user count - default based on license type + // Old licenses: Only had isEnterprise flag + // New licenses: Have planType field with "server" or "enterprise" + int users = metadataObj.optInt("users", context.isEnterpriseLicense ? 1 : 0); + + // SERVER license (isEnterprise=false, users=0) = unlimited + // ENTERPRISE license (isEnterprise=true, users>0) = limited seats + if (!context.isEnterpriseLicense && users == 0) { + log.info("SERVER license detected with unlimited users"); + } else if (context.isEnterpriseLicense && users > 0) { + log.info("ENTERPRISE license allows for {} users", users); + } + + applicationProperties.getPremium().setMaxUsers(users); } // Check license status if available @@ -431,28 +444,31 @@ public class KeygenLicenseVerifier { } // Extract max users and isEnterprise from policy or metadata - int users = policyObj.optInt("users", 1); context.isEnterpriseLicense = policyObj.optBoolean("isEnterprise", false); + int users = policyObj.optInt("users", -1); - if (users > 0) { - applicationProperties.getPremium().setMaxUsers(users); - log.info("License allows for {} users", users); - } else { - // Try to get users from metadata if present + if (users == -1) { + // Try to get users from metadata if not at policy level Object metadataObj = policyObj.opt("metadata"); if (metadataObj instanceof JSONObject metadata) { - users = metadata.optInt("users", 1); - applicationProperties.getPremium().setMaxUsers(users); - log.info("License allows for {} users (from metadata)", users); - - // Check for isEnterprise flag in metadata - context.isEnterpriseLicense = metadata.optBoolean("isEnterprise", false); + context.isEnterpriseLicense = + metadata.optBoolean("isEnterprise", context.isEnterpriseLicense); + users = metadata.optInt("users", context.isEnterpriseLicense ? 1 : 0); } else { - // Default value - applicationProperties.getPremium().setMaxUsers(1); - log.info("Using default of 1 user for license"); + // Default based on license type + users = context.isEnterpriseLicense ? 1 : 0; } } + + // SERVER license (isEnterprise=false, users=0) = unlimited + // ENTERPRISE license (isEnterprise=true, users>0) = limited seats + if (!context.isEnterpriseLicense && users == 0) { + log.info("SERVER license detected with unlimited users"); + } else if (context.isEnterpriseLicense && users > 0) { + log.info("ENTERPRISE license allows for {} users", users); + } + + applicationProperties.getPremium().setMaxUsers(users); } return true; @@ -584,17 +600,7 @@ public class KeygenLicenseVerifier { context.maxMachines); } - // Extract user count, default to 1 if not specified - int users = - jsonResponse - .path("data") - .path("attributes") - .path("metadata") - .path("users") - .asInt(1); - applicationProperties.getPremium().setMaxUsers(users); - - // Extract isEnterprise flag + // Extract isEnterprise flag first context.isEnterpriseLicense = jsonResponse .path("data") @@ -603,6 +609,24 @@ public class KeygenLicenseVerifier { .path("isEnterprise") .asBoolean(false); + // Extract user count - default based on license type + int users = + jsonResponse + .path("data") + .path("attributes") + .path("metadata") + .path("users") + .asInt(context.isEnterpriseLicense ? 1 : 0); + + // SERVER license (isEnterprise=false, users=0) = unlimited + // ENTERPRISE license (isEnterprise=true, users>0) = limited seats + if (!context.isEnterpriseLicense && users == 0) { + log.info("SERVER license detected with unlimited users"); + } else if (context.isEnterpriseLicense && users > 0) { + log.info("ENTERPRISE license allows for {} users", users); + } + + applicationProperties.getPremium().setMaxUsers(users); log.debug(applicationProperties.toString()); } else { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ee/LicenseKeyChecker.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ee/LicenseKeyChecker.java index 050a565c2..3e9a8a413 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ee/LicenseKeyChecker.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ee/LicenseKeyChecker.java @@ -68,8 +68,8 @@ public class LicenseKeyChecker { premiumEnabledResult = licenseService.verifyLicense(licenseKey); if (License.ENTERPRISE == premiumEnabledResult) { log.info("License key is Enterprise."); - } else if (License.PRO == premiumEnabledResult) { - log.info("License key is Pro."); + } else if (License.SERVER == premiumEnabledResult) { + log.info("License key is Server."); } else { log.info("License key is invalid, defaulting to non pro license."); } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java index 9f2ce9456..9e31ee69c 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java @@ -87,6 +87,19 @@ public class UserController { .body(Map.of("error", "Password is required")); } + if (licenseSettingsService.wouldExceedLimit(1)) { + long availableSlots = licenseSettingsService.getAvailableUserSlots(); + int maxAllowed = licenseSettingsService.calculateMaxAllowedUsers(); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body( + Map.of( + "error", + "Maximum number of users reached. Allowed: " + + maxAllowed + + ", Available slots: " + + availableSlots)); + } + Team team = teamRepository.findByName(TeamService.DEFAULT_TEAM_NAME).orElse(null); User user = userService.saveUser( diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/database/repository/UserRepository.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/database/repository/UserRepository.java index 36fa23a6a..1a8b51bca 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/database/repository/UserRepository.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/database/repository/UserRepository.java @@ -39,4 +39,28 @@ public interface UserRepository extends JpaRepository { long countByTeam(Team team); List findAllByTeam(Team team); + + // OAuth grandfathering queries + long countBySsoProviderIsNotNull(); + + long countByOauthGrandfatheredTrue(); + + List findAllBySsoProviderIsNotNull(); + + /** + * Finds all SSO users - those with sso_provider set OR authenticationType is sso/oauth2/saml2. + * This catches V1 users who were created via SSO but never signed in (sso_provider is null). + */ + @Query( + "SELECT u FROM User u WHERE u.ssoProvider IS NOT NULL " + + "OR LOWER(u.authenticationType) IN ('sso', 'oauth2', 'saml2')") + List findAllSsoUsers(); + + /** + * Counts all SSO users - those with sso_provider set OR authenticationType is sso/oauth2/saml2. + */ + @Query( + "SELECT COUNT(u) FROM User u WHERE u.ssoProvider IS NOT NULL " + + "OR LOWER(u.authenticationType) IN ('sso', 'oauth2', 'saml2')") + long countSsoUsers(); } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/model/User.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/model/User.java index 8f64d3187..8207eae28 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/model/User.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/model/User.java @@ -71,6 +71,9 @@ public class User implements UserDetails, Serializable { @Column(name = "sso_provider") private String ssoProvider; + @Column(name = "oauth_grandfathered") + private Boolean oauthGrandfathered = false; + @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user") private Set authorities = new HashSet<>(); @@ -135,4 +138,12 @@ public class User implements UserDetails, Serializable { public boolean hasPassword() { return this.password != null && !this.password.isEmpty(); } + + public boolean isOauthGrandfathered() { + return oauthGrandfathered != null && oauthGrandfathered; + } + + public void setOauthGrandfathered(boolean oauthGrandfathered) { + this.oauthGrandfathered = oauthGrandfathered; + } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java index d2e03a04e..e1e670394 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java @@ -50,6 +50,8 @@ public class CustomOAuth2AuthenticationSuccessHandler private final ApplicationProperties.Security.OAUTH2 oauth2Properties; private final UserService userService; private final JwtServiceInterface jwtService; + private final stirling.software.proprietary.service.UserLicenseSettingsService + licenseSettingsService; @Override @Audited(type = AuditEventType.USER_LOGIN, level = AuditLevel.BASIC) @@ -66,6 +68,25 @@ public class CustomOAuth2AuthenticationSuccessHandler username = detailsUser.getUsername(); } + boolean userExists = userService.usernameExistsIgnoreCase(username); + + // Check if user is eligible for OAuth (grandfathered or system has paid license) + if (userExists) { + stirling.software.proprietary.security.model.User user = + userService.findByUsernameIgnoreCase(username).orElse(null); + + if (user != null && !licenseSettingsService.isOAuthEligible(user)) { + // User is not grandfathered and no paid license - block OAuth login + response.sendRedirect( + request.getContextPath() + "/logout?oAuth2RequiresLicense=true"); + return; + } + } else if (!licenseSettingsService.isOAuthEligible(null)) { + // No existing user and no paid license -> block auto creation + response.sendRedirect(request.getContextPath() + "/logout?oAuth2RequiresLicense=true"); + return; + } + // Get the saved request HttpSession session = request.getSession(false); String contextPath = request.getContextPath(); @@ -91,7 +112,7 @@ public class CustomOAuth2AuthenticationSuccessHandler .sendRedirect(request, response, "/logout?userIsDisabled=true"); return; } - if (userService.usernameExistsIgnoreCase(username) + if (userExists && userService.hasPassword(username) && (!userService.isAuthenticationTypeByUsername(username, SSO) || !userService.isAuthenticationTypeByUsername(username, OAUTH2)) @@ -106,6 +127,10 @@ public class CustomOAuth2AuthenticationSuccessHandler response.sendRedirect(contextPath + "/logout?oAuth2AdminBlockedUser=true"); return; } + if (!userExists && licenseSettingsService.wouldExceedLimit(1)) { + response.sendRedirect(contextPath + "/logout?maxUsersReached=true"); + return; + } if (principal instanceof OAuth2User oAuth2User) { // Extract SSO provider information from OAuth2User String ssoProviderId = oAuth2User.getAttribute("sub"); // OIDC ID diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticationSuccessHandler.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticationSuccessHandler.java index af6d284cf..0f350d7b4 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticationSuccessHandler.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticationSuccessHandler.java @@ -49,6 +49,8 @@ public class CustomSaml2AuthenticationSuccessHandler private ApplicationProperties.Security.SAML2 saml2Properties; private UserService userService; private final JwtServiceInterface jwtService; + private final stirling.software.proprietary.service.UserLicenseSettingsService + licenseSettingsService; @Override @Audited(type = AuditEventType.USER_LOGIN, level = AuditLevel.BASIC) @@ -63,6 +65,26 @@ public class CustomSaml2AuthenticationSuccessHandler String username = saml2Principal.name(); log.debug("Authenticated principal found for user: {}", username); + boolean userExists = userService.usernameExistsIgnoreCase(username); + + // Check if user is eligible for SAML (grandfathered or system has paid license) + if (userExists) { + stirling.software.proprietary.security.model.User user = + userService.findByUsernameIgnoreCase(username).orElse(null); + + if (user != null && !licenseSettingsService.isOAuthEligible(user)) { + // User is not grandfathered and no paid license - block SAML login + response.sendRedirect( + request.getContextPath() + "/logout?saml2RequiresLicense=true"); + return; + } + } else if (!licenseSettingsService.isOAuthEligible(null)) { + // No existing user and no paid license -> block auto creation + response.sendRedirect( + request.getContextPath() + "/logout?saml2RequiresLicense=true"); + return; + } + HttpSession session = request.getSession(false); String contextPath = request.getContextPath(); SavedRequest savedRequest = @@ -96,7 +118,6 @@ public class CustomSaml2AuthenticationSuccessHandler "Your account has been locked due to too many failed login attempts."); } - boolean userExists = userService.usernameExistsIgnoreCase(username); boolean hasPassword = userExists && userService.hasPassword(username); boolean isSSOUser = userExists && userService.isAuthenticationTypeByUsername(username, SSO); @@ -129,6 +150,10 @@ public class CustomSaml2AuthenticationSuccessHandler contextPath + "/login?errorOAuth=oAuth2AdminBlockedUser"); return; } + if (!userExists && licenseSettingsService.wouldExceedLimit(1)) { + response.sendRedirect(contextPath + "/logout?maxUsersReached=true"); + return; + } // Extract SSO provider information from SAML2 assertion String ssoProviderId = saml2Principal.nameId(); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java index 4772368f8..d131eb2bd 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java @@ -734,4 +734,48 @@ public class UserService implements UserServiceInterface { public void saveAll(List users) { userRepository.saveAll(users); } + + /** + * Counts the number of OAuth/SAML users. Includes users with sso_provider set OR + * authenticationType is sso/oauth2/saml2 (catches V1 users who never signed in). + * + * @return Count of OAuth users + */ + public long countOAuthUsers() { + return userRepository.countSsoUsers(); + } + + /** + * Counts the number of OAuth users who are grandfathered. + * + * @return Count of grandfathered OAuth users + */ + public long countGrandfatheredOAuthUsers() { + return userRepository.countByOauthGrandfatheredTrue(); + } + + /** + * Grandfathers all existing OAuth/SAML users. This marks all users with an SSO provider as + * grandfathered, allowing them to keep OAuth access even without a paid license. + * + * @return Number of users updated + */ + @Transactional + public int grandfatherAllOAuthUsers() { + List ssoUsers = userRepository.findAllSsoUsers(); + int updated = 0; + + for (User user : ssoUsers) { + if (!user.isOauthGrandfathered()) { + user.setOauthGrandfathered(true); + updated++; + } + } + + if (updated > 0) { + userRepository.saveAll(ssoUsers); + } + + return updated; + } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/ServerCertificateService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/ServerCertificateService.java index aca98eb75..01db79a5d 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/service/ServerCertificateService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/ServerCertificateService.java @@ -69,7 +69,7 @@ public class ServerCertificateService implements ServerCertificateServiceInterfa private boolean hasProOrEnterpriseAccess() { License license = licenseKeyChecker.getPremiumLicenseEnabledResult(); - return license == License.PRO || license == License.ENTERPRISE; + return license == License.SERVER || license == License.ENTERPRISE; } public boolean isEnabled() { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/UserLicenseSettingsService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/UserLicenseSettingsService.java index 44efce960..352ce0ed2 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/service/UserLicenseSettingsService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/UserLicenseSettingsService.java @@ -10,6 +10,7 @@ import java.util.UUID; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -18,6 +19,8 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.ApplicationProperties; import stirling.software.proprietary.model.UserLicenseSettings; +import stirling.software.proprietary.security.configuration.ee.KeygenLicenseVerifier.License; +import stirling.software.proprietary.security.configuration.ee.LicenseKeyChecker; import stirling.software.proprietary.security.repository.UserLicenseSettingsRepository; import stirling.software.proprietary.security.service.UserService; @@ -45,6 +48,7 @@ public class UserLicenseSettingsService { private final UserLicenseSettingsRepository settingsRepository; private final UserService userService; private final ApplicationProperties applicationProperties; + private final ObjectProvider licenseKeyChecker; /** * Gets the current user license settings, creating them if they don't exist. @@ -151,7 +155,7 @@ public class UserLicenseSettingsService { UserLicenseSettings settings = getOrCreateSettings(); int licenseMaxUsers = 0; - if (applicationProperties.getPremium().isEnabled()) { + if (hasPaidLicense()) { licenseMaxUsers = applicationProperties.getPremium().getMaxUsers(); } @@ -162,6 +166,40 @@ public class UserLicenseSettingsService { } } + /** + * Grandfathers existing OAuth users on first run. This is a one-time migration that marks all + * existing OAuth/SAML users as grandfathered, allowing them to keep OAuth access even without a + * paid license. + * + *

New users created after this migration will NOT be grandfathered and will require a paid + * license to use OAuth. + */ + @Transactional + public void grandfatherExistingOAuthUsers() { + UserLicenseSettings settings = getOrCreateSettings(); + + // Check if we've already run this migration + if (settings.getId() != null && settings.isGrandfatheringLocked()) { + // Migration should happen at the same time as grandfathering is locked + long oauthUsersCount = userService.countOAuthUsers(); + long grandfatheredCount = userService.countGrandfatheredOAuthUsers(); + + if (oauthUsersCount > 0 && grandfatheredCount == 0) { + // We have OAuth users but none are grandfathered - this is first run after upgrade + int updated = userService.grandfatherAllOAuthUsers(); + log.warn( + "OAuth GRANDFATHERING: Marked {} existing OAuth/SAML users as grandfathered. " + + "They will retain OAuth access even without a paid license. " + + "New users will require a paid license for OAuth.", + updated); + } else if (grandfatheredCount > 0) { + log.debug( + "OAuth grandfathering already completed: {} users grandfathered", + grandfatheredCount); + } + } + } + /** * Validates and enforces the integrity of license settings. This ensures that even if someone * manually modifies the database, the grandfathering rules are still enforced. @@ -241,12 +279,15 @@ public class UserLicenseSettingsService { *

Logic: * *

    - *
  • Grandfathered limit = max(5, existing user count at initialization) - *
  • If premium enabled: total limit = grandfathered limit + license maxUsers - *
  • If premium disabled: total limit = grandfathered limit + *
  • Grandfathered limit = max(5, existing user count at V1→V2 migration) + *
  • No license: Uses grandfathered limit only + *
  • SERVER license (maxUsers=0): Unlimited users (Integer.MAX_VALUE) + *
  • ENTERPRISE license (maxUsers>0): License seats only (NO grandfathering added) *
* - * @return Maximum number of users allowed + *

IMPORTANT: Paid licenses REPLACE the limit, they don't add to grandfathering. + * + * @return Maximum number of users allowed (Integer.MAX_VALUE for unlimited) */ public int calculateMaxAllowedUsers() { validateSettingsIntegrity(); @@ -259,20 +300,52 @@ public class UserLicenseSettingsService { grandfatheredLimit = DEFAULT_USER_LIMIT; } - int totalLimit = grandfatheredLimit; - - if (applicationProperties.getPremium().isEnabled()) { - totalLimit = grandfatheredLimit + settings.getLicenseMaxUsers(); + // No license: use grandfathered limit + if (!hasPaidLicense()) { + log.debug("No license: using grandfathered limit of {}", grandfatheredLimit); + return grandfatheredLimit; } - log.debug( - "Calculated max allowed users: {} (grandfathered: {}, license: {}, premium enabled: {})", - totalLimit, - grandfatheredLimit, - settings.getLicenseMaxUsers(), - applicationProperties.getPremium().isEnabled()); + int licenseMaxUsers = settings.getLicenseMaxUsers(); - return totalLimit; + // 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; + } + + /** + * Checks if a user is eligible to use OAuth/SAML authentication. + * + *

A user is eligible if: + * + *

    + *
  • They are grandfathered for OAuth (existing user before policy change), OR + *
  • The system has an ENTERPRISE license (SSO is enterprise-only) + *
+ * + * @param user The user to check + * @return true if the user can use OAuth/SAML + */ + public boolean isOAuthEligible(stirling.software.proprietary.security.model.User user) { + // Grandfathered users always have OAuth access + if (user != null && user.isOauthGrandfathered()) { + log.debug("User {} is grandfathered for OAuth", user.getUsername()); + return true; + } + + // Users can use OAuth/SAML only if system has ENTERPRISE license + boolean hasEnterpriseLicense = hasEnterpriseLicense(); + log.debug("OAuth eligibility check: hasEnterpriseLicense={}", hasEnterpriseLicense); + return hasEnterpriseLicense; } /** @@ -408,4 +481,28 @@ public class UserLicenseSettingsService { throw new IllegalStateException("Invalid key for grandfathered user signature", e); } } + + private boolean hasPaidLicense() { + LicenseKeyChecker checker = licenseKeyChecker.getIfAvailable(); + if (checker == null) { + return false; + } + License license = checker.getPremiumLicenseEnabledResult(); + return license == License.SERVER || license == License.ENTERPRISE; + } + + /** + * Checks if the system has an ENTERPRISE license. Used for enterprise-only features like SSO + * (OAuth/SAML). + * + * @return true if ENTERPRISE license is active + */ + private boolean hasEnterpriseLicense() { + LicenseKeyChecker checker = licenseKeyChecker.getIfAvailable(); + if (checker == null) { + return false; + } + License license = checker.getPremiumLicenseEnabledResult(); + return license == License.ENTERPRISE; + } } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/security/CustomLogoutSuccessHandlerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/security/CustomLogoutSuccessHandlerTest.java index c6b6d17e7..0a8a2fbe0 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/security/CustomLogoutSuccessHandlerTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/security/CustomLogoutSuccessHandlerTest.java @@ -103,6 +103,9 @@ class CustomLogoutSuccessHandlerTest { when(request.getParameter("errorOAuth")).thenReturn(null); when(request.getParameter("oAuth2AutoCreateDisabled")).thenReturn(null); when(request.getParameter("oAuth2AdminBlockedUser")).thenReturn(null); + when(request.getParameter("oAuth2RequiresLicense")).thenReturn(null); + when(request.getParameter("saml2RequiresLicense")).thenReturn(null); + when(request.getParameter("maxUsersReached")).thenReturn(null); when(request.getParameter(error)).thenReturn("true"); when(request.getScheme()).thenReturn("http"); when(request.getServerName()).thenReturn("localhost"); @@ -208,6 +211,9 @@ class CustomLogoutSuccessHandlerTest { when(request.getParameter("errorOAuth")).thenReturn(null); when(request.getParameter("oAuth2AutoCreateDisabled")).thenReturn(null); when(request.getParameter("oAuth2AdminBlockedUser")).thenReturn(null); + when(request.getParameter("oAuth2RequiresLicense")).thenReturn(null); + when(request.getParameter("saml2RequiresLicense")).thenReturn(null); + when(request.getParameter("maxUsersReached")).thenReturn(null); when(request.getParameter("userIsDisabled")).thenReturn(null); when(request.getParameter("error")).thenReturn("!@$!@£" + error + "£$%^*$"); when(request.getScheme()).thenReturn("http"); @@ -237,6 +243,9 @@ class CustomLogoutSuccessHandlerTest { when(request.getParameter("errorOAuth")).thenReturn(null); when(request.getParameter("oAuth2AutoCreateDisabled")).thenReturn(null); when(request.getParameter("oAuth2AdminBlockedUser")).thenReturn(null); + when(request.getParameter("oAuth2RequiresLicense")).thenReturn(null); + when(request.getParameter("saml2RequiresLicense")).thenReturn(null); + when(request.getParameter("maxUsersReached")).thenReturn(null); when(request.getParameter("userIsDisabled")).thenReturn(null); when(request.getParameter("error")).thenReturn(null); when(request.getParameter(error)).thenReturn("true"); diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/security/configuration/ee/LicenseKeyCheckerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/security/configuration/ee/LicenseKeyCheckerTest.java index 136bec9e5..70dd809e4 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/security/configuration/ee/LicenseKeyCheckerTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/security/configuration/ee/LicenseKeyCheckerTest.java @@ -44,13 +44,13 @@ class LicenseKeyCheckerTest { ApplicationProperties props = new ApplicationProperties(); props.getPremium().setEnabled(true); props.getPremium().setKey("abc"); - when(verifier.verifyLicense("abc")).thenReturn(License.PRO); + when(verifier.verifyLicense("abc")).thenReturn(License.SERVER); LicenseKeyChecker checker = new LicenseKeyChecker(verifier, props, userLicenseSettingsService); checker.init(); - assertEquals(License.PRO, checker.getPremiumLicenseEnabledResult()); + assertEquals(License.SERVER, checker.getPremiumLicenseEnabledResult()); verify(verifier).verifyLicense("abc"); } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/service/UserLicenseSettingsServiceTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/service/UserLicenseSettingsServiceTest.java new file mode 100644 index 000000000..c819055e4 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/service/UserLicenseSettingsServiceTest.java @@ -0,0 +1,201 @@ +package stirling.software.proprietary.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.beans.factory.ObjectProvider; + +import stirling.software.common.model.ApplicationProperties; +import stirling.software.proprietary.model.UserLicenseSettings; +import stirling.software.proprietary.security.configuration.ee.KeygenLicenseVerifier.License; +import stirling.software.proprietary.security.configuration.ee.LicenseKeyChecker; +import stirling.software.proprietary.security.repository.UserLicenseSettingsRepository; +import stirling.software.proprietary.security.service.UserService; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class UserLicenseSettingsServiceTest { + + @Mock private UserLicenseSettingsRepository settingsRepository; + @Mock private UserService userService; + @Mock private ApplicationProperties applicationProperties; + @Mock private ApplicationProperties.Premium premium; + @Mock private LicenseKeyChecker licenseKeyChecker; + @Mock private ObjectProvider licenseKeyCheckerProvider; + + private UserLicenseSettingsService service; + private UserLicenseSettings mockSettings; + + @BeforeEach + void setUp() { + mockSettings = new UserLicenseSettings(); + mockSettings.setId(1L); + mockSettings.setGrandfatheredUserCount(80); + mockSettings.setGrandfatheringLocked(true); + mockSettings.setIntegritySalt("test-salt"); + mockSettings.setGrandfatheredUserSignature("80:test-signature"); + + when(applicationProperties.getPremium()).thenReturn(premium); + when(settingsRepository.findSettings()).thenReturn(Optional.of(mockSettings)); + when(userService.getTotalUsersCount()).thenReturn(80L); + when(settingsRepository.save(any(UserLicenseSettings.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.NORMAL); + when(licenseKeyCheckerProvider.getIfAvailable()).thenReturn(licenseKeyChecker); + + // Create service with overridden validateSettingsIntegrity to bypass signature validation + service = + new UserLicenseSettingsService( + settingsRepository, + userService, + applicationProperties, + licenseKeyCheckerProvider) { + @Override + public void validateSettingsIntegrity() { + // Override to do nothing in tests - avoid HMAC signature validation + // complexity + } + }; + } + + @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 + when(premium.isEnabled()).thenReturn(true); + when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.SERVER); + mockSettings.setLicenseMaxUsers(0); + + int result = service.calculateMaxAllowedUsers(); + + 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 + mockSettings.setGrandfatheredUserCount(5); + when(premium.isEnabled()).thenReturn(true); + when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.SERVER); + mockSettings.setLicenseMaxUsers(0); + + int result = service.calculateMaxAllowedUsers(); + + assertEquals( + Integer.MAX_VALUE, + result, + "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"); + } +} diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 2361b5235..e4329b857 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -3473,6 +3473,9 @@ "ssoSignIn": "Login via Single Sign-on", "oAuth2AutoCreateDisabled": "OAUTH2 Auto-Create User Disabled", "oAuth2AdminBlockedUser": "Registration or logging in of non-registered users is currently blocked. Please contact the administrator.", + "oAuth2RequiresLicense": "OAuth/SSO login requires a paid license (Server or Enterprise). Please contact the administrator to upgrade your plan.", + "saml2RequiresLicense": "SAML login requires a paid license (Server or Enterprise). Please contact the administrator to upgrade your plan.", + "maxUsersReached": "Maximum number of users reached for your current license. Please contact the administrator to upgrade your plan or add more seats.", "oauth2RequestNotFound": "Authorization request not found", "oauth2InvalidUserInfoResponse": "Invalid User Info Response", "oauth2invalidRequest": "Invalid Request", diff --git a/frontend/src/core/services/updateService.ts b/frontend/src/core/services/updateService.ts index 343898232..e83b222e1 100644 --- a/frontend/src/core/services/updateService.ts +++ b/frontend/src/core/services/updateService.ts @@ -101,8 +101,8 @@ export class UpdateService { async getUpdateSummary(currentVersion: string, machineInfo: MachineInfo): Promise { // Map Java License enum to API types let type = 'normal'; - if (machineInfo.licenseType === 'PRO') { - type = 'pro'; + if (machineInfo.licenseType === 'SERVER') { + type = 'server'; } else if (machineInfo.licenseType === 'ENTERPRISE') { type = 'enterprise'; } @@ -133,8 +133,8 @@ export class UpdateService { async getFullUpdateInfo(currentVersion: string, machineInfo: MachineInfo): Promise { // Map Java License enum to API types let type = 'normal'; - if (machineInfo.licenseType === 'PRO') { - type = 'pro'; + if (machineInfo.licenseType === 'SERVER') { + type = 'server'; } else if (machineInfo.licenseType === 'ENTERPRISE') { type = 'enterprise'; } diff --git a/frontend/src/proprietary/services/licenseService.ts b/frontend/src/proprietary/services/licenseService.ts index 819cbff70..867567977 100644 --- a/frontend/src/proprietary/services/licenseService.ts +++ b/frontend/src/proprietary/services/licenseService.ts @@ -70,7 +70,7 @@ export interface LicenseKeyResponse { } export interface LicenseInfo { - licenseType: 'NORMAL' | 'PRO' | 'ENTERPRISE'; + licenseType: 'NORMAL' | 'SERVER' | 'ENTERPRISE'; enabled: boolean; maxUsers: number; hasKey: boolean; @@ -496,8 +496,8 @@ export const mapLicenseToTier = (licenseInfo: LicenseInfo | null): 'free' | 'ser return 'free'; } - // PRO type (no seats) = Server tier - if (licenseInfo.licenseType === 'PRO') { + // SERVER type (unlimited users) = Server tier + if (licenseInfo.licenseType === 'SERVER') { return 'server'; }