mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-12-18 20:04:17 +01: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 <40631091+ConnorYoh@users.noreply.github.com> Co-authored-by: Dario Ghunney Ware <dariogware@gmail.com>
This commit is contained in:
parent
daf749e6be
commit
e6db57e031
@ -397,7 +397,8 @@ public class ProprietaryUIDataController {
|
||||
Map<Long, Date> 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<String, Date> 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);
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -62,6 +62,7 @@ public class InitialSecuritySetup {
|
||||
private void initializeUserLicenseSettings() {
|
||||
licenseSettingsService.initializeGrandfatheredCount();
|
||||
licenseSettingsService.updateLicenseMaxUsers();
|
||||
licenseSettingsService.grandfatherExistingOAuthUsers();
|
||||
}
|
||||
|
||||
private void configureJWTSettings() {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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.");
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -39,4 +39,28 @@ public interface UserRepository extends JpaRepository<User, Long> {
|
||||
long countByTeam(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")
|
||||
private String ssoProvider;
|
||||
|
||||
@Column(name = "oauth_grandfathered")
|
||||
private Boolean oauthGrandfathered = false;
|
||||
|
||||
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user")
|
||||
private Set<Authority> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -734,4 +734,48 @@ public class UserService implements UserServiceInterface {
|
||||
public void saveAll(List<User> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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> 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.
|
||||
*
|
||||
* <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
|
||||
* manually modifies the database, the grandfathering rules are still enforced.
|
||||
@ -241,12 +279,15 @@ public class UserLicenseSettingsService {
|
||||
* <p>Logic:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Grandfathered limit = max(5, existing user count at initialization)
|
||||
* <li>If premium enabled: total limit = grandfathered limit + license maxUsers
|
||||
* <li>If premium disabled: total limit = grandfathered limit
|
||||
* <li>Grandfathered limit = max(5, existing user count at V1→V2 migration)
|
||||
* <li>No license: Uses grandfathered limit only
|
||||
* <li>SERVER license (maxUsers=0): Unlimited users (Integer.MAX_VALUE)
|
||||
* <li>ENTERPRISE license (maxUsers>0): License seats only (NO grandfathering added)
|
||||
* </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() {
|
||||
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.
|
||||
*
|
||||
* <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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
|
||||
@ -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",
|
||||
"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",
|
||||
|
||||
@ -101,8 +101,8 @@ export class UpdateService {
|
||||
async getUpdateSummary(currentVersion: string, machineInfo: MachineInfo): Promise<UpdateSummary | null> {
|
||||
// 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<FullUpdateInfo | null> {
|
||||
// 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';
|
||||
}
|
||||
|
||||
@ -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';
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user