mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
grandfather users (#4984)
# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## 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 <[email protected]> Co-authored-by: Dario Ghunney Ware <[email protected]>
This commit is contained in:
co-authored by
ConnorYoh
Dario Ghunney Ware
parent
daf749e6be
commit
e6db57e031
+4
-2
@@ -397,7 +397,8 @@ public class ProprietaryUIDataController {
|
|||||||
Map<Long, Date> teamLastRequest = new HashMap<>();
|
Map<Long, Date> teamLastRequest = new HashMap<>();
|
||||||
for (Object[] result : teamActivities) {
|
for (Object[] result : teamActivities) {
|
||||||
Long teamId = (Long) result[0];
|
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);
|
teamLastRequest.put(teamId, lastActivity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +442,8 @@ public class ProprietaryUIDataController {
|
|||||||
Map<String, Date> userLastRequest = new HashMap<>();
|
Map<String, Date> userLastRequest = new HashMap<>();
|
||||||
for (Object[] result : userSessions) {
|
for (Object[] result : userSessions) {
|
||||||
String username = (String) result[0];
|
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);
|
userLastRequest.put(username, lastRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
@@ -238,6 +238,12 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
|||||||
path = "errorOAuth=oAuth2AutoCreateDisabled";
|
path = "errorOAuth=oAuth2AutoCreateDisabled";
|
||||||
} else if (request.getParameter("oAuth2AdminBlockedUser") != null) {
|
} else if (request.getParameter("oAuth2AdminBlockedUser") != null) {
|
||||||
path = "errorOAuth=oAuth2AdminBlockedUser";
|
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) {
|
} else if (request.getParameter("userIsDisabled") != null) {
|
||||||
path = "errorOAuth=userIsDisabled";
|
path = "errorOAuth=userIsDisabled";
|
||||||
} else if ((errorMessage = request.getParameter("error")) != null) {
|
} else if ((errorMessage = request.getParameter("error")) != null) {
|
||||||
|
|||||||
+1
@@ -62,6 +62,7 @@ public class InitialSecuritySetup {
|
|||||||
private void initializeUserLicenseSettings() {
|
private void initializeUserLicenseSettings() {
|
||||||
licenseSettingsService.initializeGrandfatheredCount();
|
licenseSettingsService.initializeGrandfatheredCount();
|
||||||
licenseSettingsService.updateLicenseMaxUsers();
|
licenseSettingsService.updateLicenseMaxUsers();
|
||||||
|
licenseSettingsService.grandfatherExistingOAuthUsers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void configureJWTSettings() {
|
private void configureJWTSettings() {
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ public class PremiumEndpointAspect {
|
|||||||
public Object checkPremiumAccess(ProceedingJoinPoint joinPoint) throws Throwable {
|
public Object checkPremiumAccess(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||||
if (!runningProOrHigher) {
|
if (!runningProOrHigher) {
|
||||||
throw new ResponseStatusException(
|
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();
|
return joinPoint.proceed();
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-4
@@ -84,6 +84,8 @@ public class SecurityConfiguration {
|
|||||||
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
|
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
|
||||||
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
|
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
|
||||||
private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver;
|
private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver;
|
||||||
|
private final stirling.software.proprietary.service.UserLicenseSettingsService
|
||||||
|
licenseSettingsService;
|
||||||
|
|
||||||
public SecurityConfiguration(
|
public SecurityConfiguration(
|
||||||
PersistentLoginRepository persistentLoginRepository,
|
PersistentLoginRepository persistentLoginRepository,
|
||||||
@@ -103,7 +105,9 @@ public class SecurityConfiguration {
|
|||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
|
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
|
||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver) {
|
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver,
|
||||||
|
stirling.software.proprietary.service.UserLicenseSettingsService
|
||||||
|
licenseSettingsService) {
|
||||||
this.userDetailsService = userDetailsService;
|
this.userDetailsService = userDetailsService;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
this.loginEnabledValue = loginEnabledValue;
|
this.loginEnabledValue = loginEnabledValue;
|
||||||
@@ -120,10 +124,11 @@ public class SecurityConfiguration {
|
|||||||
this.oAuth2userAuthoritiesMapper = oAuth2userAuthoritiesMapper;
|
this.oAuth2userAuthoritiesMapper = oAuth2userAuthoritiesMapper;
|
||||||
this.saml2RelyingPartyRegistrations = saml2RelyingPartyRegistrations;
|
this.saml2RelyingPartyRegistrations = saml2RelyingPartyRegistrations;
|
||||||
this.saml2AuthenticationRequestResolver = saml2AuthenticationRequestResolver;
|
this.saml2AuthenticationRequestResolver = saml2AuthenticationRequestResolver;
|
||||||
|
this.licenseSettingsService = licenseSettingsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public static PasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +359,8 @@ public class SecurityConfiguration {
|
|||||||
loginAttemptService,
|
loginAttemptService,
|
||||||
securityProperties.getOauth2(),
|
securityProperties.getOauth2(),
|
||||||
userService,
|
userService,
|
||||||
jwtService))
|
jwtService,
|
||||||
|
licenseSettingsService))
|
||||||
.failureHandler(new CustomOAuth2AuthenticationFailureHandler())
|
.failureHandler(new CustomOAuth2AuthenticationFailureHandler())
|
||||||
// Add existing Authorities from the database
|
// Add existing Authorities from the database
|
||||||
.userInfoEndpoint(
|
.userInfoEndpoint(
|
||||||
@@ -395,7 +401,8 @@ public class SecurityConfiguration {
|
|||||||
loginAttemptService,
|
loginAttemptService,
|
||||||
securityProperties.getSaml2(),
|
securityProperties.getSaml2(),
|
||||||
userService,
|
userService,
|
||||||
jwtService))
|
jwtService,
|
||||||
|
licenseSettingsService))
|
||||||
.failureHandler(
|
.failureHandler(
|
||||||
new CustomSaml2AuthenticationFailureHandler())
|
new CustomSaml2AuthenticationFailureHandler())
|
||||||
.authenticationRequestResolver(
|
.authenticationRequestResolver(
|
||||||
|
|||||||
+2
-1
@@ -32,7 +32,8 @@ public class EEAppConfig {
|
|||||||
@Bean(name = "runningProOrHigher")
|
@Bean(name = "runningProOrHigher")
|
||||||
@Primary
|
@Primary
|
||||||
public boolean runningProOrHigher() {
|
public boolean runningProOrHigher() {
|
||||||
return licenseKeyChecker.getPremiumLicenseEnabledResult() != License.NORMAL;
|
License license = licenseKeyChecker.getPremiumLicenseEnabledResult();
|
||||||
|
return license == License.SERVER || license == License.ENTERPRISE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Profile("security")
|
@Profile("security")
|
||||||
|
|||||||
+57
-33
@@ -30,7 +30,7 @@ public class KeygenLicenseVerifier {
|
|||||||
|
|
||||||
public enum License {
|
public enum License {
|
||||||
NORMAL,
|
NORMAL,
|
||||||
PRO,
|
SERVER,
|
||||||
ENTERPRISE
|
ENTERPRISE
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ public class KeygenLicenseVerifier {
|
|||||||
log.info("Detected certificate-based license. Processing...");
|
log.info("Detected certificate-based license. Processing...");
|
||||||
boolean isValid = verifyCertificateLicense(licenseKeyOrCert, context);
|
boolean isValid = verifyCertificateLicense(licenseKeyOrCert, context);
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
license = context.isEnterpriseLicense ? License.ENTERPRISE : License.PRO;
|
license = context.isEnterpriseLicense ? License.ENTERPRISE : License.SERVER;
|
||||||
} else {
|
} else {
|
||||||
license = License.NORMAL;
|
license = License.NORMAL;
|
||||||
}
|
}
|
||||||
@@ -84,7 +84,7 @@ public class KeygenLicenseVerifier {
|
|||||||
log.info("Detected JWT-style license key. Processing...");
|
log.info("Detected JWT-style license key. Processing...");
|
||||||
boolean isValid = verifyJWTLicense(licenseKeyOrCert, context);
|
boolean isValid = verifyJWTLicense(licenseKeyOrCert, context);
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
license = context.isEnterpriseLicense ? License.ENTERPRISE : License.PRO;
|
license = context.isEnterpriseLicense ? License.ENTERPRISE : License.SERVER;
|
||||||
} else {
|
} else {
|
||||||
license = License.NORMAL;
|
license = License.NORMAL;
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@ public class KeygenLicenseVerifier {
|
|||||||
log.info("Detected standard license key. Processing...");
|
log.info("Detected standard license key. Processing...");
|
||||||
boolean isValid = verifyStandardLicense(licenseKeyOrCert, context);
|
boolean isValid = verifyStandardLicense(licenseKeyOrCert, context);
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
license = context.isEnterpriseLicense ? License.ENTERPRISE : License.PRO;
|
license = context.isEnterpriseLicense ? License.ENTERPRISE : License.SERVER;
|
||||||
} else {
|
} else {
|
||||||
license = License.NORMAL;
|
license = License.NORMAL;
|
||||||
}
|
}
|
||||||
@@ -261,10 +261,23 @@ public class KeygenLicenseVerifier {
|
|||||||
// Extract metadata
|
// Extract metadata
|
||||||
JSONObject metadataObj = attributesObj.optJSONObject("metadata");
|
JSONObject metadataObj = attributesObj.optJSONObject("metadata");
|
||||||
if (metadataObj != null) {
|
if (metadataObj != null) {
|
||||||
int users = metadataObj.optInt("users", 1);
|
// Check if this is an old license (no planType) with isEnterprise flag
|
||||||
applicationProperties.getPremium().setMaxUsers(users);
|
|
||||||
log.info("License allows for {} users", users);
|
|
||||||
context.isEnterpriseLicense = metadataObj.optBoolean("isEnterprise", false);
|
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
|
// Check license status if available
|
||||||
@@ -431,28 +444,31 @@ public class KeygenLicenseVerifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract max users and isEnterprise from policy or metadata
|
// Extract max users and isEnterprise from policy or metadata
|
||||||
int users = policyObj.optInt("users", 1);
|
|
||||||
context.isEnterpriseLicense = policyObj.optBoolean("isEnterprise", false);
|
context.isEnterpriseLicense = policyObj.optBoolean("isEnterprise", false);
|
||||||
|
int users = policyObj.optInt("users", -1);
|
||||||
|
|
||||||
if (users > 0) {
|
if (users == -1) {
|
||||||
applicationProperties.getPremium().setMaxUsers(users);
|
// Try to get users from metadata if not at policy level
|
||||||
log.info("License allows for {} users", users);
|
|
||||||
} else {
|
|
||||||
// Try to get users from metadata if present
|
|
||||||
Object metadataObj = policyObj.opt("metadata");
|
Object metadataObj = policyObj.opt("metadata");
|
||||||
if (metadataObj instanceof JSONObject metadata) {
|
if (metadataObj instanceof JSONObject metadata) {
|
||||||
users = metadata.optInt("users", 1);
|
context.isEnterpriseLicense =
|
||||||
applicationProperties.getPremium().setMaxUsers(users);
|
metadata.optBoolean("isEnterprise", context.isEnterpriseLicense);
|
||||||
log.info("License allows for {} users (from metadata)", users);
|
users = metadata.optInt("users", context.isEnterpriseLicense ? 1 : 0);
|
||||||
|
|
||||||
// Check for isEnterprise flag in metadata
|
|
||||||
context.isEnterpriseLicense = metadata.optBoolean("isEnterprise", false);
|
|
||||||
} else {
|
} else {
|
||||||
// Default value
|
// Default based on license type
|
||||||
applicationProperties.getPremium().setMaxUsers(1);
|
users = context.isEnterpriseLicense ? 1 : 0;
|
||||||
log.info("Using default of 1 user for license");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
return true;
|
||||||
@@ -584,17 +600,7 @@ public class KeygenLicenseVerifier {
|
|||||||
context.maxMachines);
|
context.maxMachines);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract user count, default to 1 if not specified
|
// Extract isEnterprise flag first
|
||||||
int users =
|
|
||||||
jsonResponse
|
|
||||||
.path("data")
|
|
||||||
.path("attributes")
|
|
||||||
.path("metadata")
|
|
||||||
.path("users")
|
|
||||||
.asInt(1);
|
|
||||||
applicationProperties.getPremium().setMaxUsers(users);
|
|
||||||
|
|
||||||
// Extract isEnterprise flag
|
|
||||||
context.isEnterpriseLicense =
|
context.isEnterpriseLicense =
|
||||||
jsonResponse
|
jsonResponse
|
||||||
.path("data")
|
.path("data")
|
||||||
@@ -603,6 +609,24 @@ public class KeygenLicenseVerifier {
|
|||||||
.path("isEnterprise")
|
.path("isEnterprise")
|
||||||
.asBoolean(false);
|
.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());
|
log.debug(applicationProperties.toString());
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+2
-2
@@ -68,8 +68,8 @@ public class LicenseKeyChecker {
|
|||||||
premiumEnabledResult = licenseService.verifyLicense(licenseKey);
|
premiumEnabledResult = licenseService.verifyLicense(licenseKey);
|
||||||
if (License.ENTERPRISE == premiumEnabledResult) {
|
if (License.ENTERPRISE == premiumEnabledResult) {
|
||||||
log.info("License key is Enterprise.");
|
log.info("License key is Enterprise.");
|
||||||
} else if (License.PRO == premiumEnabledResult) {
|
} else if (License.SERVER == premiumEnabledResult) {
|
||||||
log.info("License key is Pro.");
|
log.info("License key is Server.");
|
||||||
} else {
|
} else {
|
||||||
log.info("License key is invalid, defaulting to non pro license.");
|
log.info("License key is invalid, defaulting to non pro license.");
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -87,6 +87,19 @@ public class UserController {
|
|||||||
.body(Map.of("error", "Password is required"));
|
.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);
|
Team team = teamRepository.findByName(TeamService.DEFAULT_TEAM_NAME).orElse(null);
|
||||||
User user =
|
User user =
|
||||||
userService.saveUser(
|
userService.saveUser(
|
||||||
|
|||||||
+24
@@ -39,4 +39,28 @@ public interface UserRepository extends JpaRepository<User, Long> {
|
|||||||
long countByTeam(Team team);
|
long countByTeam(Team team);
|
||||||
|
|
||||||
List<User> findAllByTeam(Team team);
|
List<User> findAllByTeam(Team team);
|
||||||
|
|
||||||
|
// OAuth grandfathering queries
|
||||||
|
long countBySsoProviderIsNotNull();
|
||||||
|
|
||||||
|
long countByOauthGrandfatheredTrue();
|
||||||
|
|
||||||
|
List<User> 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<User> 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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ public class User implements UserDetails, Serializable {
|
|||||||
@Column(name = "sso_provider")
|
@Column(name = "sso_provider")
|
||||||
private String ssoProvider;
|
private String ssoProvider;
|
||||||
|
|
||||||
|
@Column(name = "oauth_grandfathered")
|
||||||
|
private Boolean oauthGrandfathered = false;
|
||||||
|
|
||||||
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user")
|
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user")
|
||||||
private Set<Authority> authorities = new HashSet<>();
|
private Set<Authority> authorities = new HashSet<>();
|
||||||
|
|
||||||
@@ -135,4 +138,12 @@ public class User implements UserDetails, Serializable {
|
|||||||
public boolean hasPassword() {
|
public boolean hasPassword() {
|
||||||
return this.password != null && !this.password.isEmpty();
|
return this.password != null && !this.password.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isOauthGrandfathered() {
|
||||||
|
return oauthGrandfathered != null && oauthGrandfathered;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOauthGrandfathered(boolean oauthGrandfathered) {
|
||||||
|
this.oauthGrandfathered = oauthGrandfathered;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-1
@@ -50,6 +50,8 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
private final ApplicationProperties.Security.OAUTH2 oauth2Properties;
|
private final ApplicationProperties.Security.OAUTH2 oauth2Properties;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final JwtServiceInterface jwtService;
|
private final JwtServiceInterface jwtService;
|
||||||
|
private final stirling.software.proprietary.service.UserLicenseSettingsService
|
||||||
|
licenseSettingsService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Audited(type = AuditEventType.USER_LOGIN, level = AuditLevel.BASIC)
|
@Audited(type = AuditEventType.USER_LOGIN, level = AuditLevel.BASIC)
|
||||||
@@ -66,6 +68,25 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
username = detailsUser.getUsername();
|
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
|
// Get the saved request
|
||||||
HttpSession session = request.getSession(false);
|
HttpSession session = request.getSession(false);
|
||||||
String contextPath = request.getContextPath();
|
String contextPath = request.getContextPath();
|
||||||
@@ -91,7 +112,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
.sendRedirect(request, response, "/logout?userIsDisabled=true");
|
.sendRedirect(request, response, "/logout?userIsDisabled=true");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (userService.usernameExistsIgnoreCase(username)
|
if (userExists
|
||||||
&& userService.hasPassword(username)
|
&& userService.hasPassword(username)
|
||||||
&& (!userService.isAuthenticationTypeByUsername(username, SSO)
|
&& (!userService.isAuthenticationTypeByUsername(username, SSO)
|
||||||
|| !userService.isAuthenticationTypeByUsername(username, OAUTH2))
|
|| !userService.isAuthenticationTypeByUsername(username, OAUTH2))
|
||||||
@@ -106,6 +127,10 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
response.sendRedirect(contextPath + "/logout?oAuth2AdminBlockedUser=true");
|
response.sendRedirect(contextPath + "/logout?oAuth2AdminBlockedUser=true");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!userExists && licenseSettingsService.wouldExceedLimit(1)) {
|
||||||
|
response.sendRedirect(contextPath + "/logout?maxUsersReached=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (principal instanceof OAuth2User oAuth2User) {
|
if (principal instanceof OAuth2User oAuth2User) {
|
||||||
// Extract SSO provider information from OAuth2User
|
// Extract SSO provider information from OAuth2User
|
||||||
String ssoProviderId = oAuth2User.getAttribute("sub"); // OIDC ID
|
String ssoProviderId = oAuth2User.getAttribute("sub"); // OIDC ID
|
||||||
|
|||||||
+26
-1
@@ -49,6 +49,8 @@ public class CustomSaml2AuthenticationSuccessHandler
|
|||||||
private ApplicationProperties.Security.SAML2 saml2Properties;
|
private ApplicationProperties.Security.SAML2 saml2Properties;
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
private final JwtServiceInterface jwtService;
|
private final JwtServiceInterface jwtService;
|
||||||
|
private final stirling.software.proprietary.service.UserLicenseSettingsService
|
||||||
|
licenseSettingsService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Audited(type = AuditEventType.USER_LOGIN, level = AuditLevel.BASIC)
|
@Audited(type = AuditEventType.USER_LOGIN, level = AuditLevel.BASIC)
|
||||||
@@ -63,6 +65,26 @@ public class CustomSaml2AuthenticationSuccessHandler
|
|||||||
String username = saml2Principal.name();
|
String username = saml2Principal.name();
|
||||||
log.debug("Authenticated principal found for user: {}", username);
|
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);
|
HttpSession session = request.getSession(false);
|
||||||
String contextPath = request.getContextPath();
|
String contextPath = request.getContextPath();
|
||||||
SavedRequest savedRequest =
|
SavedRequest savedRequest =
|
||||||
@@ -96,7 +118,6 @@ public class CustomSaml2AuthenticationSuccessHandler
|
|||||||
"Your account has been locked due to too many failed login attempts.");
|
"Your account has been locked due to too many failed login attempts.");
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean userExists = userService.usernameExistsIgnoreCase(username);
|
|
||||||
boolean hasPassword = userExists && userService.hasPassword(username);
|
boolean hasPassword = userExists && userService.hasPassword(username);
|
||||||
boolean isSSOUser =
|
boolean isSSOUser =
|
||||||
userExists && userService.isAuthenticationTypeByUsername(username, SSO);
|
userExists && userService.isAuthenticationTypeByUsername(username, SSO);
|
||||||
@@ -129,6 +150,10 @@ public class CustomSaml2AuthenticationSuccessHandler
|
|||||||
contextPath + "/login?errorOAuth=oAuth2AdminBlockedUser");
|
contextPath + "/login?errorOAuth=oAuth2AdminBlockedUser");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!userExists && licenseSettingsService.wouldExceedLimit(1)) {
|
||||||
|
response.sendRedirect(contextPath + "/logout?maxUsersReached=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Extract SSO provider information from SAML2 assertion
|
// Extract SSO provider information from SAML2 assertion
|
||||||
String ssoProviderId = saml2Principal.nameId();
|
String ssoProviderId = saml2Principal.nameId();
|
||||||
|
|||||||
+44
@@ -734,4 +734,48 @@ public class UserService implements UserServiceInterface {
|
|||||||
public void saveAll(List<User> users) {
|
public void saveAll(List<User> users) {
|
||||||
userRepository.saveAll(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<User> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@ public class ServerCertificateService implements ServerCertificateServiceInterfa
|
|||||||
|
|
||||||
private boolean hasProOrEnterpriseAccess() {
|
private boolean hasProOrEnterpriseAccess() {
|
||||||
License license = licenseKeyChecker.getPremiumLicenseEnabledResult();
|
License license = licenseKeyChecker.getPremiumLicenseEnabledResult();
|
||||||
return license == License.PRO || license == License.ENTERPRISE;
|
return license == License.SERVER || license == License.ENTERPRISE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
|
|||||||
+113
-16
@@ -10,6 +10,7 @@ import java.util.UUID;
|
|||||||
import javax.crypto.Mac;
|
import javax.crypto.Mac;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -18,6 +19,8 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.proprietary.model.UserLicenseSettings;
|
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.repository.UserLicenseSettingsRepository;
|
||||||
import stirling.software.proprietary.security.service.UserService;
|
import stirling.software.proprietary.security.service.UserService;
|
||||||
|
|
||||||
@@ -45,6 +48,7 @@ public class UserLicenseSettingsService {
|
|||||||
private final UserLicenseSettingsRepository settingsRepository;
|
private final UserLicenseSettingsRepository settingsRepository;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
private final ObjectProvider<LicenseKeyChecker> licenseKeyChecker;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current user license settings, creating them if they don't exist.
|
* Gets the current user license settings, creating them if they don't exist.
|
||||||
@@ -151,7 +155,7 @@ public class UserLicenseSettingsService {
|
|||||||
UserLicenseSettings settings = getOrCreateSettings();
|
UserLicenseSettings settings = getOrCreateSettings();
|
||||||
|
|
||||||
int licenseMaxUsers = 0;
|
int licenseMaxUsers = 0;
|
||||||
if (applicationProperties.getPremium().isEnabled()) {
|
if (hasPaidLicense()) {
|
||||||
licenseMaxUsers = applicationProperties.getPremium().getMaxUsers();
|
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.
|
||||||
|
*
|
||||||
|
* <p>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
|
* Validates and enforces the integrity of license settings. This ensures that even if someone
|
||||||
* manually modifies the database, the grandfathering rules are still enforced.
|
* manually modifies the database, the grandfathering rules are still enforced.
|
||||||
@@ -241,12 +279,15 @@ public class UserLicenseSettingsService {
|
|||||||
* <p>Logic:
|
* <p>Logic:
|
||||||
*
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Grandfathered limit = max(5, existing user count at initialization)
|
* <li>Grandfathered limit = max(5, existing user count at V1→V2 migration)
|
||||||
* <li>If premium enabled: total limit = grandfathered limit + license maxUsers
|
* <li>No license: Uses grandfathered limit only
|
||||||
* <li>If premium disabled: total limit = grandfathered limit
|
* <li>SERVER license (maxUsers=0): Unlimited users (Integer.MAX_VALUE)
|
||||||
|
* <li>ENTERPRISE license (maxUsers>0): License seats only (NO grandfathering added)
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @return Maximum number of users allowed
|
* <p>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() {
|
public int calculateMaxAllowedUsers() {
|
||||||
validateSettingsIntegrity();
|
validateSettingsIntegrity();
|
||||||
@@ -259,20 +300,52 @@ public class UserLicenseSettingsService {
|
|||||||
grandfatheredLimit = DEFAULT_USER_LIMIT;
|
grandfatheredLimit = DEFAULT_USER_LIMIT;
|
||||||
}
|
}
|
||||||
|
|
||||||
int totalLimit = grandfatheredLimit;
|
// No license: use grandfathered limit
|
||||||
|
if (!hasPaidLicense()) {
|
||||||
if (applicationProperties.getPremium().isEnabled()) {
|
log.debug("No license: using grandfathered limit of {}", grandfatheredLimit);
|
||||||
totalLimit = grandfatheredLimit + settings.getLicenseMaxUsers();
|
return grandfatheredLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug(
|
int licenseMaxUsers = settings.getLicenseMaxUsers();
|
||||||
"Calculated max allowed users: {} (grandfathered: {}, license: {}, premium enabled: {})",
|
|
||||||
totalLimit,
|
|
||||||
grandfatheredLimit,
|
|
||||||
settings.getLicenseMaxUsers(),
|
|
||||||
applicationProperties.getPremium().isEnabled());
|
|
||||||
|
|
||||||
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.
|
||||||
|
*
|
||||||
|
* <p>A user is eligible if:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>They are grandfathered for OAuth (existing user before policy change), OR
|
||||||
|
* <li>The system has an ENTERPRISE license (SSO is enterprise-only)
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @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);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -103,6 +103,9 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
when(request.getParameter("errorOAuth")).thenReturn(null);
|
when(request.getParameter("errorOAuth")).thenReturn(null);
|
||||||
when(request.getParameter("oAuth2AutoCreateDisabled")).thenReturn(null);
|
when(request.getParameter("oAuth2AutoCreateDisabled")).thenReturn(null);
|
||||||
when(request.getParameter("oAuth2AdminBlockedUser")).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.getParameter(error)).thenReturn("true");
|
||||||
when(request.getScheme()).thenReturn("http");
|
when(request.getScheme()).thenReturn("http");
|
||||||
when(request.getServerName()).thenReturn("localhost");
|
when(request.getServerName()).thenReturn("localhost");
|
||||||
@@ -208,6 +211,9 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
when(request.getParameter("errorOAuth")).thenReturn(null);
|
when(request.getParameter("errorOAuth")).thenReturn(null);
|
||||||
when(request.getParameter("oAuth2AutoCreateDisabled")).thenReturn(null);
|
when(request.getParameter("oAuth2AutoCreateDisabled")).thenReturn(null);
|
||||||
when(request.getParameter("oAuth2AdminBlockedUser")).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("userIsDisabled")).thenReturn(null);
|
||||||
when(request.getParameter("error")).thenReturn("!@$!@£" + error + "£$%^*$");
|
when(request.getParameter("error")).thenReturn("!@$!@£" + error + "£$%^*$");
|
||||||
when(request.getScheme()).thenReturn("http");
|
when(request.getScheme()).thenReturn("http");
|
||||||
@@ -237,6 +243,9 @@ class CustomLogoutSuccessHandlerTest {
|
|||||||
when(request.getParameter("errorOAuth")).thenReturn(null);
|
when(request.getParameter("errorOAuth")).thenReturn(null);
|
||||||
when(request.getParameter("oAuth2AutoCreateDisabled")).thenReturn(null);
|
when(request.getParameter("oAuth2AutoCreateDisabled")).thenReturn(null);
|
||||||
when(request.getParameter("oAuth2AdminBlockedUser")).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("userIsDisabled")).thenReturn(null);
|
||||||
when(request.getParameter("error")).thenReturn(null);
|
when(request.getParameter("error")).thenReturn(null);
|
||||||
when(request.getParameter(error)).thenReturn("true");
|
when(request.getParameter(error)).thenReturn("true");
|
||||||
|
|||||||
+2
-2
@@ -44,13 +44,13 @@ class LicenseKeyCheckerTest {
|
|||||||
ApplicationProperties props = new ApplicationProperties();
|
ApplicationProperties props = new ApplicationProperties();
|
||||||
props.getPremium().setEnabled(true);
|
props.getPremium().setEnabled(true);
|
||||||
props.getPremium().setKey("abc");
|
props.getPremium().setKey("abc");
|
||||||
when(verifier.verifyLicense("abc")).thenReturn(License.PRO);
|
when(verifier.verifyLicense("abc")).thenReturn(License.SERVER);
|
||||||
|
|
||||||
LicenseKeyChecker checker =
|
LicenseKeyChecker checker =
|
||||||
new LicenseKeyChecker(verifier, props, userLicenseSettingsService);
|
new LicenseKeyChecker(verifier, props, userLicenseSettingsService);
|
||||||
checker.init();
|
checker.init();
|
||||||
|
|
||||||
assertEquals(License.PRO, checker.getPremiumLicenseEnabledResult());
|
assertEquals(License.SERVER, checker.getPremiumLicenseEnabledResult());
|
||||||
verify(verifier).verifyLicense("abc");
|
verify(verifier).verifyLicense("abc");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+201
@@ -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<LicenseKeyChecker> 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3473,6 +3473,9 @@
|
|||||||
"ssoSignIn": "Login via Single Sign-on",
|
"ssoSignIn": "Login via Single Sign-on",
|
||||||
"oAuth2AutoCreateDisabled": "OAUTH2 Auto-Create User Disabled",
|
"oAuth2AutoCreateDisabled": "OAUTH2 Auto-Create User Disabled",
|
||||||
"oAuth2AdminBlockedUser": "Registration or logging in of non-registered users is currently blocked. Please contact the administrator.",
|
"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",
|
"oauth2RequestNotFound": "Authorization request not found",
|
||||||
"oauth2InvalidUserInfoResponse": "Invalid User Info Response",
|
"oauth2InvalidUserInfoResponse": "Invalid User Info Response",
|
||||||
"oauth2invalidRequest": "Invalid Request",
|
"oauth2invalidRequest": "Invalid Request",
|
||||||
|
|||||||
@@ -101,8 +101,8 @@ export class UpdateService {
|
|||||||
async getUpdateSummary(currentVersion: string, machineInfo: MachineInfo): Promise<UpdateSummary | null> {
|
async getUpdateSummary(currentVersion: string, machineInfo: MachineInfo): Promise<UpdateSummary | null> {
|
||||||
// Map Java License enum to API types
|
// Map Java License enum to API types
|
||||||
let type = 'normal';
|
let type = 'normal';
|
||||||
if (machineInfo.licenseType === 'PRO') {
|
if (machineInfo.licenseType === 'SERVER') {
|
||||||
type = 'pro';
|
type = 'server';
|
||||||
} else if (machineInfo.licenseType === 'ENTERPRISE') {
|
} else if (machineInfo.licenseType === 'ENTERPRISE') {
|
||||||
type = 'enterprise';
|
type = 'enterprise';
|
||||||
}
|
}
|
||||||
@@ -133,8 +133,8 @@ export class UpdateService {
|
|||||||
async getFullUpdateInfo(currentVersion: string, machineInfo: MachineInfo): Promise<FullUpdateInfo | null> {
|
async getFullUpdateInfo(currentVersion: string, machineInfo: MachineInfo): Promise<FullUpdateInfo | null> {
|
||||||
// Map Java License enum to API types
|
// Map Java License enum to API types
|
||||||
let type = 'normal';
|
let type = 'normal';
|
||||||
if (machineInfo.licenseType === 'PRO') {
|
if (machineInfo.licenseType === 'SERVER') {
|
||||||
type = 'pro';
|
type = 'server';
|
||||||
} else if (machineInfo.licenseType === 'ENTERPRISE') {
|
} else if (machineInfo.licenseType === 'ENTERPRISE') {
|
||||||
type = 'enterprise';
|
type = 'enterprise';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export interface LicenseKeyResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LicenseInfo {
|
export interface LicenseInfo {
|
||||||
licenseType: 'NORMAL' | 'PRO' | 'ENTERPRISE';
|
licenseType: 'NORMAL' | 'SERVER' | 'ENTERPRISE';
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
maxUsers: number;
|
maxUsers: number;
|
||||||
hasKey: boolean;
|
hasKey: boolean;
|
||||||
@@ -496,8 +496,8 @@ export const mapLicenseToTier = (licenseInfo: LicenseInfo | null): 'free' | 'ser
|
|||||||
return 'free';
|
return 'free';
|
||||||
}
|
}
|
||||||
|
|
||||||
// PRO type (no seats) = Server tier
|
// SERVER type (unlimited users) = Server tier
|
||||||
if (licenseInfo.licenseType === 'PRO') {
|
if (licenseInfo.licenseType === 'SERVER') {
|
||||||
return 'server';
|
return 'server';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user