From 7a431f509e58de581647bf6571621bd7082b9a5f Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Thu, 9 Jan 2025 13:05:58 +0000 Subject: [PATCH] format and cleanups --- .../software/SPDF/EE/EEAppConfig.java | 2 +- .../security/SecurityConfiguration.java | 329 +----------------- .../security/database/DatabaseConfig.java | 4 +- .../security/oauth2/OAuth2Configuration.java | 213 ++++++++++++ .../security/saml2/SAML2Configuration.java | 136 ++++++++ .../api/security/RedactController.java | 44 ++- .../api/security/ManualRedactPdfRequest.java | 1 + .../model/api/security/RedactionArea.java | 3 + .../StringToArrayListPropertyEditor.java | 4 +- test.sh | 10 +- 10 files changed, 409 insertions(+), 337 deletions(-) create mode 100644 src/main/java/stirling/software/SPDF/config/security/oauth2/OAuth2Configuration.java create mode 100644 src/main/java/stirling/software/SPDF/config/security/saml2/SAML2Configuration.java diff --git a/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java b/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java index 4f14be90..4648c033 100644 --- a/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java +++ b/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java @@ -27,7 +27,7 @@ public class EEAppConfig { public boolean runningEnterpriseEdition() { return licenseKeyChecker.getEnterpriseEnabledResult(); } - + @Bean(name = "SSOAutoLogin") public boolean ssoAutoLogin() { return applicationProperties.getEnterpriseEdition().isSsoAutoLogin(); 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 94422283..7fbc4436 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -1,39 +1,24 @@ package stirling.software.SPDF.config.security; -import java.security.cert.X509Certificate; import java.util.*; -import org.opensaml.saml.saml2.core.AuthnRequest; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 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.core.io.Resource; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.registration.ClientRegistrations; -import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; -import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; -import org.springframework.security.saml2.core.Saml2X509Credential; -import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType; import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; -import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; -import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -43,24 +28,16 @@ import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; import org.springframework.security.web.savedrequest.NullRequestCache; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService; -import stirling.software.SPDF.config.security.saml2.CertificateUtils; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationFailureHandler; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationSuccessHandler; import stirling.software.SPDF.config.security.saml2.CustomSaml2ResponseAuthenticationConverter; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.model.ApplicationProperties; -import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; -import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client; -import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2; import stirling.software.SPDF.model.User; -import stirling.software.SPDF.model.provider.GithubProvider; -import stirling.software.SPDF.model.provider.GoogleProvider; -import stirling.software.SPDF.model.provider.KeycloakProvider; import stirling.software.SPDF.repository.JPATokenRepositoryImpl; import stirling.software.SPDF.repository.PersistentLoginRepository; @@ -72,7 +49,7 @@ import stirling.software.SPDF.repository.PersistentLoginRepository; public class SecurityConfiguration { private final CustomUserDetailsService userDetailsService; - @Lazy private final UserService userService; + private final UserService userService; @Qualifier("loginEnabled") private final boolean loginEnabledValue; @@ -86,16 +63,10 @@ public class SecurityConfiguration { private final FirstLoginFilter firstLoginFilter; private final SessionPersistentRegistry sessionRegistry; private final PersistentLoginRepository persistentLoginRepository; + private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper; + private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations; + private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver; - // // Only Dev test - // @Bean - // public WebSecurityCustomizer webSecurityCustomizer() { - // return (web) -> - // web.ignoring() - // .requestMatchers( - // "/css/**", "/images/**", "/js/**", "/**.svg", - // "/pdfjs-legacy/**"); - // } public SecurityConfiguration( PersistentLoginRepository persistentLoginRepository, CustomUserDetailsService userDetailsService, @@ -106,7 +77,12 @@ public class SecurityConfiguration { UserAuthenticationFilter userAuthenticationFilter, LoginAttemptService loginAttemptService, FirstLoginFilter firstLoginFilter, - SessionPersistentRegistry sessionRegistry) { + SessionPersistentRegistry sessionRegistry, + @Autowired(required = false) GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper, + @Autowired(required = false) + RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations, + @Autowired(required = false) + OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver) { this.userDetailsService = userDetailsService; this.userService = userService; this.loginEnabledValue = loginEnabledValue; @@ -117,6 +93,9 @@ public class SecurityConfiguration { this.firstLoginFilter = firstLoginFilter; this.sessionRegistry = sessionRegistry; this.persistentLoginRepository = persistentLoginRepository; + this.oAuth2userAuthoritiesMapper = oAuth2userAuthoritiesMapper; + this.saml2RelyingPartyRegistrations = saml2RelyingPartyRegistrations; + this.saml2AuthenticationRequestResolver = saml2AuthenticationRequestResolver; } @Bean @@ -274,7 +253,7 @@ public class SecurityConfiguration { userService, loginAttemptService)) .userAuthoritiesMapper( - userAuthoritiesMapper())) + oAuth2userAuthoritiesMapper)) .permitAll()); } // Handle SAML @@ -291,7 +270,7 @@ public class SecurityConfiguration { try { saml2.loginPage("/saml2") .relyingPartyRegistrationRepository( - relyingPartyRegistrations()) + saml2RelyingPartyRegistrations) .authenticationManager( new ProviderManager(authenticationProvider)) .successHandler( @@ -302,8 +281,7 @@ public class SecurityConfiguration { .failureHandler( new CustomSaml2AuthenticationFailureHandler()) .authenticationRequestResolver( - authenticationRequestResolver( - relyingPartyRegistrations())); + saml2AuthenticationRequestResolver); } catch (Exception e) { log.error("Error configuring SAML2 login", e); throw new RuntimeException(e); @@ -311,244 +289,11 @@ public class SecurityConfiguration { }); } } else { - // if (!applicationProperties.getSecurity().getCsrfDisabled()) { - // CookieCsrfTokenRepository cookieRepo = - // CookieCsrfTokenRepository.withHttpOnlyFalse(); - // CsrfTokenRequestAttributeHandler requestHandler = - // new CsrfTokenRequestAttributeHandler(); - // requestHandler.setCsrfRequestAttributeName(null); - // http.csrf( - // csrf -> - // csrf.csrfTokenRepository(cookieRepo) - // .csrfTokenRequestHandler(requestHandler)); - // } http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll()); } return http.build(); } - @Bean - @ConditionalOnProperty( - value = "security.oauth2.enabled", - havingValue = "true", - matchIfMissing = false) - public ClientRegistrationRepository clientRegistrationRepository() { - List registrations = new ArrayList<>(); - githubClientRegistration().ifPresent(registrations::add); - oidcClientRegistration().ifPresent(registrations::add); - googleClientRegistration().ifPresent(registrations::add); - keycloakClientRegistration().ifPresent(registrations::add); - if (registrations.isEmpty()) { - log.error("At least one OAuth2 provider must be configured"); - System.exit(1); - } - return new InMemoryClientRegistrationRepository(registrations); - } - - private Optional googleClientRegistration() { - OAUTH2 oauth = applicationProperties.getSecurity().getOauth2(); - if (oauth == null || !oauth.getEnabled()) { - return Optional.empty(); - } - Client client = oauth.getClient(); - if (client == null) { - return Optional.empty(); - } - GoogleProvider google = client.getGoogle(); - return google != null && google.isSettingsValid() - ? Optional.of( - ClientRegistration.withRegistrationId(google.getName()) - .clientId(google.getClientId()) - .clientSecret(google.getClientSecret()) - .scope(google.getScopes()) - .authorizationUri(google.getAuthorizationuri()) - .tokenUri(google.getTokenuri()) - .userInfoUri(google.getUserinfouri()) - .userNameAttributeName(google.getUseAsUsername()) - .clientName(google.getClientName()) - .redirectUri("{baseUrl}/login/oauth2/code/" + google.getName()) - .authorizationGrantType( - org.springframework.security.oauth2.core - .AuthorizationGrantType.AUTHORIZATION_CODE) - .build()) - : Optional.empty(); - } - - private Optional keycloakClientRegistration() { - OAUTH2 oauth = applicationProperties.getSecurity().getOauth2(); - if (oauth == null || !oauth.getEnabled()) { - return Optional.empty(); - } - Client client = oauth.getClient(); - if (client == null) { - return Optional.empty(); - } - KeycloakProvider keycloak = client.getKeycloak(); - return keycloak != null && keycloak.isSettingsValid() - ? Optional.of( - ClientRegistrations.fromIssuerLocation(keycloak.getIssuer()) - .registrationId(keycloak.getName()) - .clientId(keycloak.getClientId()) - .clientSecret(keycloak.getClientSecret()) - .scope(keycloak.getScopes()) - .userNameAttributeName(keycloak.getUseAsUsername()) - .clientName(keycloak.getClientName()) - .build()) - : Optional.empty(); - } - - private Optional githubClientRegistration() { - OAUTH2 oauth = applicationProperties.getSecurity().getOauth2(); - if (oauth == null || !oauth.getEnabled()) { - return Optional.empty(); - } - Client client = oauth.getClient(); - if (client == null) { - return Optional.empty(); - } - GithubProvider github = client.getGithub(); - return github != null && github.isSettingsValid() - ? Optional.of( - ClientRegistration.withRegistrationId(github.getName()) - .clientId(github.getClientId()) - .clientSecret(github.getClientSecret()) - .scope(github.getScopes()) - .authorizationUri(github.getAuthorizationuri()) - .tokenUri(github.getTokenuri()) - .userInfoUri(github.getUserinfouri()) - .userNameAttributeName(github.getUseAsUsername()) - .clientName(github.getClientName()) - .redirectUri("{baseUrl}/login/oauth2/code/" + github.getName()) - .authorizationGrantType( - org.springframework.security.oauth2.core - .AuthorizationGrantType.AUTHORIZATION_CODE) - .build()) - : Optional.empty(); - } - - private Optional oidcClientRegistration() { - OAUTH2 oauth = applicationProperties.getSecurity().getOauth2(); - if (oauth == null - || oauth.getIssuer() == null - || oauth.getIssuer().isEmpty() - || oauth.getClientId() == null - || oauth.getClientId().isEmpty() - || oauth.getClientSecret() == null - || oauth.getClientSecret().isEmpty() - || oauth.getScopes() == null - || oauth.getScopes().isEmpty() - || oauth.getUseAsUsername() == null - || oauth.getUseAsUsername().isEmpty()) { - return Optional.empty(); - } - return Optional.of( - ClientRegistrations.fromIssuerLocation(oauth.getIssuer()) - .registrationId("oidc") - .clientId(oauth.getClientId()) - .clientSecret(oauth.getClientSecret()) - .scope(oauth.getScopes()) - .userNameAttributeName(oauth.getUseAsUsername()) - .clientName("OIDC") - .build()); - } - - @Bean - @ConditionalOnProperty( - name = "security.saml2.enabled", - havingValue = "true", - matchIfMissing = false) - public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception { - SAML2 samlConf = applicationProperties.getSecurity().getSaml2(); - X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert()); - Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert); - Resource privateKeyResource = samlConf.getPrivateKey(); - Resource certificateResource = samlConf.getSpCert(); - Saml2X509Credential signingCredential = - new Saml2X509Credential( - CertificateUtils.readPrivateKey(privateKeyResource), - CertificateUtils.readCertificate(certificateResource), - Saml2X509CredentialType.SIGNING); - RelyingPartyRegistration rp = - RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId()) - .signingX509Credentials(c -> c.add(signingCredential)) - .assertingPartyMetadata( - metadata -> - metadata.entityId(samlConf.getIdpIssuer()) - .singleSignOnServiceLocation( - samlConf.getIdpSingleLoginUrl()) - .verificationX509Credentials( - c -> c.add(verificationCredential)) - .singleSignOnServiceBinding( - Saml2MessageBinding.POST) - .wantAuthnRequestsSigned(true)) - .build(); - return new InMemoryRelyingPartyRegistrationRepository(rp); - } - - @Bean - @ConditionalOnProperty( - name = "security.saml2.enabled", - havingValue = "true", - matchIfMissing = false) - public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver( - RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) { - OpenSaml4AuthenticationRequestResolver resolver = - new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository); - resolver.setAuthnRequestCustomizer( - customizer -> { - log.debug("Customizing SAML Authentication request"); - AuthnRequest authnRequest = customizer.getAuthnRequest(); - log.debug("AuthnRequest ID: {}", authnRequest.getID()); - if (authnRequest.getID() == null) { - authnRequest.setID("ARQ" + UUID.randomUUID().toString()); - } - log.debug("AuthnRequest new ID after set: {}", authnRequest.getID()); - log.debug("AuthnRequest IssueInstant: {}", authnRequest.getIssueInstant()); - log.debug( - "AuthnRequest Issuer: {}", - authnRequest.getIssuer() != null - ? authnRequest.getIssuer().getValue() - : "null"); - HttpServletRequest request = customizer.getRequest(); - // Log HTTP request details - log.debug("HTTP Request Method: {}", request.getMethod()); - log.debug("Request URI: {}", request.getRequestURI()); - log.debug("Request URL: {}", request.getRequestURL().toString()); - log.debug("Query String: {}", request.getQueryString()); - log.debug("Remote Address: {}", request.getRemoteAddr()); - // Log headers - Collections.list(request.getHeaderNames()) - .forEach( - headerName -> { - log.debug( - "Header - {}: {}", - headerName, - request.getHeader(headerName)); - }); - // Log SAML specific parameters - log.debug("SAML Request Parameters:"); - log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest")); - log.debug("RelayState: {}", request.getParameter("RelayState")); - // Log session debugrmation if exists - if (request.getSession(false) != null) { - log.debug("Session ID: {}", request.getSession().getId()); - } - // Log any assertions consumer service details if present - if (authnRequest.getAssertionConsumerServiceURL() != null) { - log.debug( - "AssertionConsumerServiceURL: {}", - authnRequest.getAssertionConsumerServiceURL()); - } - // Log NameID policy if present - if (authnRequest.getNameIDPolicy() != null) { - log.debug( - "NameIDPolicy Format: {}", - authnRequest.getNameIDPolicy().getFormat()); - } - }); - return resolver; - } - public DaoAuthenticationProvider daoAuthenticationProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(userDetailsService); @@ -556,46 +301,6 @@ public class SecurityConfiguration { return provider; } - /* - This following function is to grant Authorities to the OAUTH2 user from the values stored in the database. - This is required for the internal; 'hasRole()' function to give out the correct role. - */ - @Bean - @ConditionalOnProperty( - value = "security.oauth2.enabled", - havingValue = "true", - matchIfMissing = false) - GrantedAuthoritiesMapper userAuthoritiesMapper() { - return (authorities) -> { - Set mappedAuthorities = new HashSet<>(); - authorities.forEach( - authority -> { - // Add existing OAUTH2 Authorities - mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority())); - // Add Authorities from database for existing user, if user is present. - if (authority instanceof OAuth2UserAuthority oauth2Auth) { - String useAsUsername = - applicationProperties - .getSecurity() - .getOauth2() - .getUseAsUsername(); - Optional userOpt = - userService.findByUsernameIgnoreCase( - (String) oauth2Auth.getAttributes().get(useAsUsername)); - if (userOpt.isPresent()) { - User user = userOpt.get(); - if (user != null) { - mappedAuthorities.add( - new SimpleGrantedAuthority( - userService.findRole(user).getAuthority())); - } - } - } - }); - return mappedAuthorities; - }; - } - @Bean public IPRateLimitingFilter rateLimitingFilter() { // Example limit TODO add config level diff --git a/src/main/java/stirling/software/SPDF/config/security/database/DatabaseConfig.java b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseConfig.java index 66ab1881..124aee5c 100644 --- a/src/main/java/stirling/software/SPDF/config/security/database/DatabaseConfig.java +++ b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseConfig.java @@ -27,7 +27,9 @@ public class DatabaseConfig { private final ApplicationProperties applicationProperties; private final boolean runningEE; - public DatabaseConfig(ApplicationProperties applicationProperties, @Qualifier("runningEE") boolean runningEE) { + public DatabaseConfig( + ApplicationProperties applicationProperties, + @Qualifier("runningEE") boolean runningEE) { this.applicationProperties = applicationProperties; this.runningEE = runningEE; } diff --git a/src/main/java/stirling/software/SPDF/config/security/oauth2/OAuth2Configuration.java b/src/main/java/stirling/software/SPDF/config/security/oauth2/OAuth2Configuration.java new file mode 100644 index 00000000..b7571796 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/oauth2/OAuth2Configuration.java @@ -0,0 +1,213 @@ +package stirling.software.SPDF.config.security.oauth2; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.ClientRegistrations; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; +import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; + +import lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.config.security.UserService; +import stirling.software.SPDF.model.ApplicationProperties; +import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; +import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client; +import stirling.software.SPDF.model.User; +import stirling.software.SPDF.model.provider.GithubProvider; +import stirling.software.SPDF.model.provider.GoogleProvider; +import stirling.software.SPDF.model.provider.KeycloakProvider; + +@Configuration +@Slf4j +@ConditionalOnProperty( + value = "security.oauth2.enabled", + havingValue = "true", + matchIfMissing = false) +public class OAuth2Configuration { + + private final ApplicationProperties applicationProperties; + @Lazy private final UserService userService; + + public OAuth2Configuration( + ApplicationProperties applicationProperties, @Lazy UserService userService) { + this.userService = userService; + this.applicationProperties = applicationProperties; + } + + @Bean + @ConditionalOnProperty( + value = "security.oauth2.enabled", + havingValue = "true", + matchIfMissing = false) + public ClientRegistrationRepository clientRegistrationRepository() { + List registrations = new ArrayList<>(); + githubClientRegistration().ifPresent(registrations::add); + oidcClientRegistration().ifPresent(registrations::add); + googleClientRegistration().ifPresent(registrations::add); + keycloakClientRegistration().ifPresent(registrations::add); + if (registrations.isEmpty()) { + log.error("At least one OAuth2 provider must be configured"); + System.exit(1); + } + return new InMemoryClientRegistrationRepository(registrations); + } + + private Optional googleClientRegistration() { + OAUTH2 oauth = applicationProperties.getSecurity().getOauth2(); + if (oauth == null || !oauth.getEnabled()) { + return Optional.empty(); + } + Client client = oauth.getClient(); + if (client == null) { + return Optional.empty(); + } + GoogleProvider google = client.getGoogle(); + return google != null && google.isSettingsValid() + ? Optional.of( + ClientRegistration.withRegistrationId(google.getName()) + .clientId(google.getClientId()) + .clientSecret(google.getClientSecret()) + .scope(google.getScopes()) + .authorizationUri(google.getAuthorizationuri()) + .tokenUri(google.getTokenuri()) + .userInfoUri(google.getUserinfouri()) + .userNameAttributeName(google.getUseAsUsername()) + .clientName(google.getClientName()) + .redirectUri("{baseUrl}/login/oauth2/code/" + google.getName()) + .authorizationGrantType( + org.springframework.security.oauth2.core + .AuthorizationGrantType.AUTHORIZATION_CODE) + .build()) + : Optional.empty(); + } + + private Optional keycloakClientRegistration() { + OAUTH2 oauth = applicationProperties.getSecurity().getOauth2(); + if (oauth == null || !oauth.getEnabled()) { + return Optional.empty(); + } + Client client = oauth.getClient(); + if (client == null) { + return Optional.empty(); + } + KeycloakProvider keycloak = client.getKeycloak(); + return keycloak != null && keycloak.isSettingsValid() + ? Optional.of( + ClientRegistrations.fromIssuerLocation(keycloak.getIssuer()) + .registrationId(keycloak.getName()) + .clientId(keycloak.getClientId()) + .clientSecret(keycloak.getClientSecret()) + .scope(keycloak.getScopes()) + .userNameAttributeName(keycloak.getUseAsUsername()) + .clientName(keycloak.getClientName()) + .build()) + : Optional.empty(); + } + + private Optional githubClientRegistration() { + OAUTH2 oauth = applicationProperties.getSecurity().getOauth2(); + if (oauth == null || !oauth.getEnabled()) { + return Optional.empty(); + } + Client client = oauth.getClient(); + if (client == null) { + return Optional.empty(); + } + GithubProvider github = client.getGithub(); + return github != null && github.isSettingsValid() + ? Optional.of( + ClientRegistration.withRegistrationId(github.getName()) + .clientId(github.getClientId()) + .clientSecret(github.getClientSecret()) + .scope(github.getScopes()) + .authorizationUri(github.getAuthorizationuri()) + .tokenUri(github.getTokenuri()) + .userInfoUri(github.getUserinfouri()) + .userNameAttributeName(github.getUseAsUsername()) + .clientName(github.getClientName()) + .redirectUri("{baseUrl}/login/oauth2/code/" + github.getName()) + .authorizationGrantType( + org.springframework.security.oauth2.core + .AuthorizationGrantType.AUTHORIZATION_CODE) + .build()) + : Optional.empty(); + } + + private Optional oidcClientRegistration() { + OAUTH2 oauth = applicationProperties.getSecurity().getOauth2(); + if (oauth == null + || oauth.getIssuer() == null + || oauth.getIssuer().isEmpty() + || oauth.getClientId() == null + || oauth.getClientId().isEmpty() + || oauth.getClientSecret() == null + || oauth.getClientSecret().isEmpty() + || oauth.getScopes() == null + || oauth.getScopes().isEmpty() + || oauth.getUseAsUsername() == null + || oauth.getUseAsUsername().isEmpty()) { + return Optional.empty(); + } + return Optional.of( + ClientRegistrations.fromIssuerLocation(oauth.getIssuer()) + .registrationId("oidc") + .clientId(oauth.getClientId()) + .clientSecret(oauth.getClientSecret()) + .scope(oauth.getScopes()) + .userNameAttributeName(oauth.getUseAsUsername()) + .clientName("OIDC") + .build()); + } + + /* + This following function is to grant Authorities to the OAUTH2 user from the values stored in the database. + This is required for the internal; 'hasRole()' function to give out the correct role. + */ + @Bean + @ConditionalOnProperty( + value = "security.oauth2.enabled", + havingValue = "true", + matchIfMissing = false) + GrantedAuthoritiesMapper userAuthoritiesMapper() { + return (authorities) -> { + Set mappedAuthorities = new HashSet<>(); + authorities.forEach( + authority -> { + // Add existing OAUTH2 Authorities + mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority())); + // Add Authorities from database for existing user, if user is present. + if (authority instanceof OAuth2UserAuthority oauth2Auth) { + String useAsUsername = + applicationProperties + .getSecurity() + .getOauth2() + .getUseAsUsername(); + Optional userOpt = + userService.findByUsernameIgnoreCase( + (String) oauth2Auth.getAttributes().get(useAsUsername)); + if (userOpt.isPresent()) { + User user = userOpt.get(); + if (user != null) { + mappedAuthorities.add( + new SimpleGrantedAuthority( + userService.findRole(user).getAuthority())); + } + } + } + }); + return mappedAuthorities; + }; + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/saml2/SAML2Configuration.java b/src/main/java/stirling/software/SPDF/config/security/saml2/SAML2Configuration.java new file mode 100644 index 00000000..0e4b83d1 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/saml2/SAML2Configuration.java @@ -0,0 +1,136 @@ +package stirling.software.SPDF.config.security.saml2; + +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.UUID; + +import org.opensaml.saml.saml2.core.AuthnRequest; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType; +import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.model.ApplicationProperties; +import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2; + +@Configuration +@Slf4j +@ConditionalOnProperty( + value = "security.saml2.enabled", + havingValue = "true", + matchIfMissing = false) +public class SAML2Configuration { + + private final ApplicationProperties applicationProperties; + + public SAML2Configuration(ApplicationProperties applicationProperties) { + + this.applicationProperties = applicationProperties; + } + + @Bean + @ConditionalOnProperty( + name = "security.saml2.enabled", + havingValue = "true", + matchIfMissing = false) + public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception { + SAML2 samlConf = applicationProperties.getSecurity().getSaml2(); + X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert()); + Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert); + Resource privateKeyResource = samlConf.getPrivateKey(); + Resource certificateResource = samlConf.getSpCert(); + Saml2X509Credential signingCredential = + new Saml2X509Credential( + CertificateUtils.readPrivateKey(privateKeyResource), + CertificateUtils.readCertificate(certificateResource), + Saml2X509CredentialType.SIGNING); + RelyingPartyRegistration rp = + RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId()) + .signingX509Credentials(c -> c.add(signingCredential)) + .assertingPartyMetadata( + metadata -> + metadata.entityId(samlConf.getIdpIssuer()) + .singleSignOnServiceLocation( + samlConf.getIdpSingleLoginUrl()) + .verificationX509Credentials( + c -> c.add(verificationCredential)) + .singleSignOnServiceBinding( + Saml2MessageBinding.POST) + .wantAuthnRequestsSigned(true)) + .build(); + return new InMemoryRelyingPartyRegistrationRepository(rp); + } + + @Bean + @ConditionalOnProperty( + name = "security.saml2.enabled", + havingValue = "true", + matchIfMissing = false) + public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver( + RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) { + OpenSaml4AuthenticationRequestResolver resolver = + new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository); + resolver.setAuthnRequestCustomizer( + customizer -> { + log.debug("Customizing SAML Authentication request"); + AuthnRequest authnRequest = customizer.getAuthnRequest(); + log.debug("AuthnRequest ID: {}", authnRequest.getID()); + if (authnRequest.getID() == null) { + authnRequest.setID("ARQ" + UUID.randomUUID().toString()); + } + log.debug("AuthnRequest new ID after set: {}", authnRequest.getID()); + log.debug("AuthnRequest IssueInstant: {}", authnRequest.getIssueInstant()); + log.debug( + "AuthnRequest Issuer: {}", + authnRequest.getIssuer() != null + ? authnRequest.getIssuer().getValue() + : "null"); + HttpServletRequest request = customizer.getRequest(); + // Log HTTP request details + log.debug("HTTP Request Method: {}", request.getMethod()); + log.debug("Request URI: {}", request.getRequestURI()); + log.debug("Request URL: {}", request.getRequestURL().toString()); + log.debug("Query String: {}", request.getQueryString()); + log.debug("Remote Address: {}", request.getRemoteAddr()); + // Log headers + Collections.list(request.getHeaderNames()) + .forEach( + headerName -> { + log.debug( + "Header - {}: {}", + headerName, + request.getHeader(headerName)); + }); + // Log SAML specific parameters + log.debug("SAML Request Parameters:"); + log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest")); + log.debug("RelayState: {}", request.getParameter("RelayState")); + // Log session debugrmation if exists + if (request.getSession(false) != null) { + log.debug("Session ID: {}", request.getSession().getId()); + } + // Log any assertions consumer service details if present + if (authnRequest.getAssertionConsumerServiceURL() != null) { + log.debug( + "AssertionConsumerServiceURL: {}", + authnRequest.getAssertionConsumerServiceURL()); + } + // Log NameID policy if present + if (authnRequest.getNameIDPolicy() != null) { + log.debug( + "NameIDPolicy Format: {}", + authnRequest.getNameIDPolicy().getFormat()); + } + }); + return resolver; + } +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java b/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java index d123e2ef..19d54b0b 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java @@ -26,7 +26,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; - import stirling.software.SPDF.model.PDFText; import stirling.software.SPDF.model.api.security.ManualRedactPdfRequest; import stirling.software.SPDF.model.api.security.RedactPdfRequest; @@ -53,12 +52,17 @@ public class RedactController { @InitBinder public void initBinder(WebDataBinder binder) { - binder.registerCustomEditor(List.class, "redactions", new StringToArrayListPropertyEditor()); + binder.registerCustomEditor( + List.class, "redactions", new StringToArrayListPropertyEditor()); } @PostMapping(value = "/redact", consumes = "multipart/form-data") - @Operation(summary = "Redacts areas and pages in a PDF document", description = "This operation takes an input PDF file with a list of areas, page number(s)/range(s)/function(s) to redact. Input:PDF, Output:PDF, Type:SISO") - public ResponseEntity redactPDF(@ModelAttribute ManualRedactPdfRequest request) throws IOException { + @Operation( + summary = "Redacts areas and pages in a PDF document", + description = + "This operation takes an input PDF file with a list of areas, page number(s)/range(s)/function(s) to redact. Input:PDF, Output:PDF, Type:SISO") + public ResponseEntity redactPDF(@ModelAttribute ManualRedactPdfRequest request) + throws IOException { MultipartFile file = request.getFileInput(); List redactionAreas = request.getRedactions(); @@ -86,18 +90,22 @@ public class RedactController { + "_redacted.pdf"); } - private void redactAreas(List redactionAreas, PDDocument document, PDPageTree allPages) + private void redactAreas( + List redactionAreas, PDDocument document, PDPageTree allPages) throws IOException { Color redactColor = null; for (RedactionArea redactionArea : redactionAreas) { - if (redactionArea.getPage() == null || redactionArea.getPage() <= 0 - || redactionArea.getHeight() == null || redactionArea.getHeight() <= 0.0D - || redactionArea.getWidth() == null || redactionArea.getWidth() <= 0.0D) - continue; + if (redactionArea.getPage() == null + || redactionArea.getPage() <= 0 + || redactionArea.getHeight() == null + || redactionArea.getHeight() <= 0.0D + || redactionArea.getWidth() == null + || redactionArea.getWidth() <= 0.0D) continue; PDPage page = allPages.get(redactionArea.getPage() - 1); - PDPageContentStream contentStream = new PDPageContentStream( - document, page, PDPageContentStream.AppendMode.APPEND, true, true); + PDPageContentStream contentStream = + new PDPageContentStream( + document, page, PDPageContentStream.AppendMode.APPEND, true, true); redactColor = decodeOrDefault(redactionArea.getColor(), Color.BLACK); contentStream.setNonStrokingColor(redactColor); @@ -114,15 +122,17 @@ public class RedactController { } } - private void redactPages(ManualRedactPdfRequest request, PDDocument document, PDPageTree allPages) + private void redactPages( + ManualRedactPdfRequest request, PDDocument document, PDPageTree allPages) throws IOException { Color redactColor = decodeOrDefault(request.getPageRedactionColor(), Color.BLACK); List pageNumbers = getPageNumbers(request, allPages.getCount()); for (Integer pageNumber : pageNumbers) { PDPage page = allPages.get(pageNumber); - PDPageContentStream contentStream = new PDPageContentStream( - document, page, PDPageContentStream.AppendMode.APPEND, true, true); + PDPageContentStream contentStream = + new PDPageContentStream( + document, page, PDPageContentStream.AppendMode.APPEND, true, true); contentStream.setNonStrokingColor(redactColor); PDRectangle box = page.getBBox(); @@ -146,8 +156,10 @@ public class RedactController { private List getPageNumbers(ManualRedactPdfRequest request, int pagesCount) { String pageNumbersInput = request.getPageNumbers(); - String[] parsedPageNumbers = pageNumbersInput != null ? pageNumbersInput.split(",") : new String[0]; - List pageNumbers = GeneralUtils.parsePageList(parsedPageNumbers, pagesCount, false); + String[] parsedPageNumbers = + pageNumbersInput != null ? pageNumbersInput.split(",") : new String[0]; + List pageNumbers = + GeneralUtils.parsePageList(parsedPageNumbers, pagesCount, false); Collections.sort(pageNumbers); return pageNumbers; } diff --git a/src/main/java/stirling/software/SPDF/model/api/security/ManualRedactPdfRequest.java b/src/main/java/stirling/software/SPDF/model/api/security/ManualRedactPdfRequest.java index cb98499e..0bd2d41d 100644 --- a/src/main/java/stirling/software/SPDF/model/api/security/ManualRedactPdfRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/security/ManualRedactPdfRequest.java @@ -3,6 +3,7 @@ package stirling.software.SPDF.model.api.security; import java.util.List; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFWithPageNums; diff --git a/src/main/java/stirling/software/SPDF/model/api/security/RedactionArea.java b/src/main/java/stirling/software/SPDF/model/api/security/RedactionArea.java index 5157d415..a8d2a61a 100644 --- a/src/main/java/stirling/software/SPDF/model/api/security/RedactionArea.java +++ b/src/main/java/stirling/software/SPDF/model/api/security/RedactionArea.java @@ -1,17 +1,20 @@ package stirling.software.SPDF.model.api.security; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; @Data public class RedactionArea { @Schema(description = "The left edge point of the area to be redacted.") private Double x; + @Schema(description = "The top edge point of the area to be redacted.") private Double y; @Schema(description = "The height of the area to be redacted.") private Double height; + @Schema(description = "The width of the area to be redacted.") private Double width; diff --git a/src/main/java/stirling/software/SPDF/utils/propertyeditor/StringToArrayListPropertyEditor.java b/src/main/java/stirling/software/SPDF/utils/propertyeditor/StringToArrayListPropertyEditor.java index cbcb6172..d4ec7acc 100644 --- a/src/main/java/stirling/software/SPDF/utils/propertyeditor/StringToArrayListPropertyEditor.java +++ b/src/main/java/stirling/software/SPDF/utils/propertyeditor/StringToArrayListPropertyEditor.java @@ -24,8 +24,8 @@ public class StringToArrayListPropertyEditor extends PropertyEditorSupport { } try { objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); - TypeReference> typeRef = new TypeReference>() { - }; + TypeReference> typeRef = + new TypeReference>() {}; List list = objectMapper.readValue(text, typeRef); setValue(list); } catch (Exception e) { diff --git a/test.sh b/test.sh index d789c6be..820d8082 100644 --- a/test.sh +++ b/test.sh @@ -73,15 +73,15 @@ main() { # Building Docker images - # docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest -f ./Dockerfile . - # docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite . + # docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest -f ./Dockerfile . + docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite . # Test each configuration - #run_tests "Stirling-PDF-Ultra-Lite" "./exampleYmlFiles/docker-compose-latest-ultra-lite.yml" - #docker-compose -f "./exampleYmlFiles/docker-compose-latest-ultra-lite.yml" down + run_tests "Stirling-PDF-Ultra-Lite" "./exampleYmlFiles/docker-compose-latest-ultra-lite.yml" + docker-compose -f "./exampleYmlFiles/docker-compose-latest-ultra-lite.yml" down - # run_tests "Stirling-PDF" "./exampleYmlFiles/docker-compose-latest.yml" + #run_tests "Stirling-PDF" "./exampleYmlFiles/docker-compose-latest.yml" #docker-compose -f "./exampleYmlFiles/docker-compose-latest.yml" down export DOCKER_ENABLE_SECURITY=true