diff --git a/build.gradle b/build.gradle index 9598eecce..34a55b286 100644 --- a/build.gradle +++ b/build.gradle @@ -397,6 +397,9 @@ dependencies { if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") { + implementation 'org.springframework.security:spring-security-ldap' + implementation 'org.springframework.ldap:spring-ldap-core' + implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' diff --git a/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java b/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java index f1df7d340..fe7cfe0c3 100644 --- a/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java +++ b/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java @@ -43,6 +43,7 @@ public class EEAppConfig { return applicationProperties.getPremium().getProFeatures().isSsoAutoLogin(); } + // TODO: Remove post migration public void migrateEnterpriseSettingsToPremium(ApplicationProperties applicationProperties) { EnterpriseEdition enterpriseEdition = applicationProperties.getEnterpriseEdition(); diff --git a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java index a4c10d1ae..e011eebd0 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -8,6 +8,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Lazy; +import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; @@ -63,7 +64,8 @@ public class SecurityConfiguration { private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper; private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations; private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver; - + private final AuthenticationProvider ldapAuthenticationProvider; + public SecurityConfiguration( PersistentLoginRepository persistentLoginRepository, CustomUserDetailsService userDetailsService, @@ -79,7 +81,7 @@ public class SecurityConfiguration { @Autowired(required = false) RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations, @Autowired(required = false) - OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver) { + OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver, @Autowired(required = false) AuthenticationProvider ldapAuthenticationProvider) { this.userDetailsService = userDetailsService; this.userService = userService; this.loginEnabledValue = loginEnabledValue; @@ -93,6 +95,7 @@ public class SecurityConfiguration { this.oAuth2userAuthoritiesMapper = oAuth2userAuthoritiesMapper; this.saml2RelyingPartyRegistrations = saml2RelyingPartyRegistrations; this.saml2AuthenticationRequestResolver = saml2AuthenticationRequestResolver; + this.ldapAuthenticationProvider = ldapAuthenticationProvider; } @Bean @@ -284,10 +287,20 @@ public class SecurityConfiguration { } }); } + + } else { log.debug("SAML 2 login is not enabled. Using default."); http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll()); } + + if (applicationProperties.getSecurity().getLdap().getEnabled()) { + if (ldapAuthenticationProvider != null) { + http.authenticationProvider(ldapAuthenticationProvider); + } + } + + return http.build(); } diff --git a/src/main/java/stirling/software/SPDF/config/security/ldap/LDAPConfiguration.java b/src/main/java/stirling/software/SPDF/config/security/ldap/LDAPConfiguration.java new file mode 100644 index 000000000..8e48cadab --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/ldap/LDAPConfiguration.java @@ -0,0 +1,86 @@ +package stirling.software.SPDF.config.security.ldap; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.ldap.core.support.LdapContextSource; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.ldap.authentication.BindAuthenticator; +import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; +import org.springframework.security.ldap.authentication.LdapAuthenticator; +import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; +import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator; +import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; + +import stirling.software.SPDF.model.ApplicationProperties; + +@Configuration +@ConditionalOnProperty(name = "security.ldap.enabled", havingValue = "true") +public class LDAPConfiguration { + + private final ApplicationProperties applicationProperties; + + public LDAPConfiguration(ApplicationProperties applicationProperties) { + this.applicationProperties = applicationProperties; + } + + @Bean + public LdapContextSource ldapContextSource() { + LdapContextSource contextSource = new LdapContextSource(); + contextSource.setUrl(applicationProperties.getSecurity().getLdap().getUrl()); + contextSource.setBase(applicationProperties.getSecurity().getLdap().getBaseDn()); + + String managerDn = applicationProperties.getSecurity().getLdap().getManagerDn(); + String managerPassword = applicationProperties.getSecurity().getLdap().getManagerPassword(); + + if (managerDn != null && !managerDn.isEmpty() && managerPassword != null) { + contextSource.setUserDn(managerDn); + contextSource.setPassword(managerPassword); + } + + contextSource.afterPropertiesSet(); + return contextSource; + } + + @Bean + public LdapAuthoritiesPopulator ldapAuthoritiesPopulator(LdapContextSource contextSource) { + DefaultLdapAuthoritiesPopulator authoritiesPopulator = new DefaultLdapAuthoritiesPopulator( + contextSource, + applicationProperties.getSecurity().getLdap().getGroupSearchBase() + ); + authoritiesPopulator.setGroupSearchFilter(applicationProperties.getSecurity().getLdap().getGroupSearchFilter()); + authoritiesPopulator.setRolePrefix("ROLE_"); + return authoritiesPopulator; + } + + @Bean + public LdapAuthenticator ldapAuthenticator(LdapContextSource contextSource) { + BindAuthenticator authenticator = new BindAuthenticator(contextSource); + String userDnPattern = applicationProperties.getSecurity().getLdap().getUserDnPattern(); + + if (userDnPattern != null && !userDnPattern.isEmpty()) { + authenticator.setUserDnPatterns(new String[]{userDnPattern}); + } else { + String userSearchBase = applicationProperties.getSecurity().getLdap().getUserSearchBase(); + String userSearchFilter = applicationProperties.getSecurity().getLdap().getUserSearchFilter(); + if (userSearchBase != null && userSearchFilter != null) { + FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch( + userSearchBase, + userSearchFilter, + contextSource + ); + authenticator.setUserSearch(userSearch); + } else { + throw new IllegalStateException("LDAP configuration requires either userDnPattern or userSearchBase and userSearchFilter."); + } + } + return authenticator; + } + + @Bean + public AuthenticationProvider ldapAuthenticationProvider( + LdapAuthenticator authenticator, + LdapAuthoritiesPopulator authoritiesPopulator) { + return new LdapAuthenticationProvider(authenticator, authoritiesPopulator); + } +} \ No newline at end of file diff --git a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java index c70560e3f..a4b2cc962 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java @@ -122,7 +122,8 @@ public class OtherWebController { return Arrays.stream(files) .filter(file -> file.getName().endsWith(".traineddata")) .map(file -> file.getName().replace(".traineddata", "")) - .filter(lang -> !lang.equalsIgnoreCase("osd")).sorted() + .filter(lang -> !lang.equalsIgnoreCase("osd")) + .sorted() .toList(); } diff --git a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java index 36f6f82b7..0b6347665 100644 --- a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java +++ b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java @@ -108,6 +108,7 @@ public class ApplicationProperties { private InitialLogin initialLogin = new InitialLogin(); private OAUTH2 oauth2 = new OAUTH2(); private SAML2 saml2 = new SAML2(); + private LDAP ldap = new LDAP(); private int loginAttemptCount; private long loginResetTimeMinutes; private String loginMethod = "all"; @@ -218,6 +219,25 @@ public class ApplicationProperties { } } + @Data + @ToString + public static class LDAP { + private Boolean enabled = false; + private String url; + private String baseDn; + private String userDnPattern; + private String groupSearchBase; + private String managerDn; + @ToString.Exclude private String managerPassword; + + public boolean isSettingsValid() { + return enabled + && !isStringEmpty(url) + && !isStringEmpty(baseDn) + && !isStringEmpty(userDnPattern); + } + } + @Data public static class OAUTH2 { private Boolean enabled = false; diff --git a/src/main/resources/settings.yml.template b/src/main/resources/settings.yml.template index b7306861c..1f42885c3 100644 --- a/src/main/resources/settings.yml.template +++ b/src/main/resources/settings.yml.template @@ -60,7 +60,14 @@ security: idpCert: classpath:okta.cert # The certificate your Provider will use to authenticate your app's SAML authentication requests. Provided by your Provider privateKey: classpath:saml-private-key.key # Your private key. Generated from your keypair spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair - + ldap: + enabled: true + url: ldap://your-ldap-server:389 + base-dn: dc=example,dc=com + user-dn-pattern: uid={0},ou=people + group-search-base: ou=groups + manager-dn: cn=admin,dc=example,dc=com # If needed + manager-password: admin-password # If needed premium: key: 00000000-0000-0000-0000-000000000000 @@ -70,9 +77,10 @@ premium: CustomMetadata: autoUpdateMetadata: false author: username - creator: Stirling-PDF - producer: Stirling-PDF - + creator: Team1 + producer: + + legal: termsAndConditions: https://www.stirlingpdf.com/terms-and-conditions # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder privacyPolicy: https://www.stirlingpdf.com/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy). Empty string to disable or filename to load from local file in static folder