diff options
16 files changed, 348 insertions, 1022 deletions
diff --git a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/JakartaToJavaxRequestWrapper.java b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/JakartaToJavaxRequestWrapper.java deleted file mode 100644 index 89ede705811..00000000000 --- a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/JakartaToJavaxRequestWrapper.java +++ /dev/null @@ -1,409 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.auth.saml; - -import jakarta.servlet.http.HttpServletRequest; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.security.Principal; -import java.util.Collection; -import java.util.Enumeration; -import java.util.Locale; -import java.util.Map; -import javax.servlet.AsyncContext; -import javax.servlet.DispatcherType; -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.ServletInputStream; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import javax.servlet.http.HttpUpgradeHandler; -import javax.servlet.http.Part; - -/** - * This class is needed only due to the fact that OneLogin Java SAML needs javax HttpServletRequest. - * It wraps a jakarta.servlet.http.HttpServletRequest and adapts it to javax.servlet.http.HttpServletRequest. - */ -class JakartaToJavaxRequestWrapper implements javax.servlet.http.HttpServletRequest { - public static final String NOT_IMPLEMENTED = "Not implemented"; - private final HttpServletRequest delegate; - - public JakartaToJavaxRequestWrapper(HttpServletRequest delegate) { - this.delegate = delegate; - } - - @Override - public String getAuthType() { - return delegate.getAuthType(); - } - - @Override - public Cookie[] getCookies() { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public long getDateHeader(String s) { - return delegate.getDateHeader(s); - } - - @Override - public String getHeader(String s) { - return delegate.getHeader(s); - } - - @Override - public Enumeration<String> getHeaders(String s) { - return delegate.getHeaders(s); - } - - @Override - public Enumeration<String> getHeaderNames() { - return delegate.getHeaderNames(); - } - - @Override - public int getIntHeader(String s) { - return delegate.getIntHeader(s); - } - - @Override - public String getMethod() { - return delegate.getMethod(); - } - - @Override - public String getPathInfo() { - return delegate.getPathInfo(); - } - - @Override - public String getPathTranslated() { - return delegate.getPathTranslated(); - } - - @Override - public String getContextPath() { - return delegate.getContextPath(); - } - - @Override - public String getQueryString() { - return delegate.getQueryString(); - } - - @Override - public String getRemoteUser() { - return delegate.getRemoteUser(); - } - - @Override - public boolean isUserInRole(String s) { - return delegate.isUserInRole(s); - } - - @Override - public Principal getUserPrincipal() { - return delegate.getUserPrincipal(); - } - - @Override - public String getRequestedSessionId() { - return delegate.getSession().getId(); - } - - @Override - public String getRequestURI() { - return delegate.getRequestURI(); - } - - @Override - public StringBuffer getRequestURL() { - return delegate.getRequestURL(); - } - - @Override - public String getServletPath() { - return delegate.getServletPath(); - } - - @Override - public HttpSession getSession(boolean b) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public HttpSession getSession() { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public String changeSessionId() { - return delegate.changeSessionId(); - } - - @Override - public boolean isRequestedSessionIdValid() { - return delegate.isRequestedSessionIdValid(); - } - - @Override - public boolean isRequestedSessionIdFromCookie() { - return delegate.isRequestedSessionIdFromCookie(); - } - - @Override - public boolean isRequestedSessionIdFromURL() { - return delegate.isRequestedSessionIdFromURL(); - } - - @Override - public boolean isRequestedSessionIdFromUrl() { - return delegate.isRequestedSessionIdFromURL(); - } - - @Override - public boolean authenticate(HttpServletResponse httpServletResponse) throws IOException, ServletException { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public void login(String s, String s1) throws ServletException { - try { - delegate.login(s, s1); - } catch (jakarta.servlet.ServletException e) { - throw new ServletException(e); - } - } - - @Override - public void logout() throws ServletException { - try { - delegate.logout(); - } catch (jakarta.servlet.ServletException e) { - throw new ServletException(e); - } - } - - @Override - public Collection<Part> getParts() throws IOException, ServletException { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public Part getPart(String s) throws IOException, ServletException { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public <T extends HttpUpgradeHandler> T upgrade(Class<T> aClass) throws IOException, ServletException { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public Object getAttribute(String s) { - return delegate.getAttribute(s); - } - - @Override - public Enumeration<String> getAttributeNames() { - return delegate.getAttributeNames(); - } - - @Override - public String getCharacterEncoding() { - return delegate.getCharacterEncoding(); - } - - @Override - public void setCharacterEncoding(String s) throws UnsupportedEncodingException { - delegate.setCharacterEncoding(s); - } - - @Override - public int getContentLength() { - return delegate.getContentLength(); - } - - @Override - public long getContentLengthLong() { - return delegate.getContentLengthLong(); - } - - @Override - public String getContentType() { - return delegate.getContentType(); - } - - @Override - public ServletInputStream getInputStream() throws IOException { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public String getParameter(String s) { - return delegate.getParameter(s); - } - - @Override - public Enumeration<String> getParameterNames() { - return delegate.getParameterNames(); - } - - @Override - public String[] getParameterValues(String s) { - return delegate.getParameterValues(s); - } - - @Override - public Map<String, String[]> getParameterMap() { - return delegate.getParameterMap(); - } - - @Override - public String getProtocol() { - return delegate.getProtocol(); - } - - @Override - public String getScheme() { - return delegate.getScheme(); - } - - @Override - public String getServerName() { - return delegate.getServerName(); - } - - @Override - public int getServerPort() { - return delegate.getServerPort(); - } - - @Override - public BufferedReader getReader() throws IOException { - return delegate.getReader(); - } - - @Override - public String getRemoteAddr() { - return delegate.getRemoteAddr(); - } - - @Override - public String getRemoteHost() { - return delegate.getRemoteHost(); - } - - @Override - public void setAttribute(String s, Object o) { - delegate.setAttribute(s, o); - } - - @Override - public void removeAttribute(String s) { - delegate.removeAttribute(s); - } - - @Override - public Locale getLocale() { - return delegate.getLocale(); - } - - @Override - public Enumeration<Locale> getLocales() { - return delegate.getLocales(); - } - - @Override - public boolean isSecure() { - return delegate.isSecure(); - } - - @Override - public RequestDispatcher getRequestDispatcher(String s) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public String getRealPath(String s) { - return delegate.getServletContext().getRealPath(s); - } - - @Override - public int getRemotePort() { - return delegate.getRemotePort(); - } - - @Override - public String getLocalName() { - return delegate.getLocalName(); - } - - @Override - public String getLocalAddr() { - return delegate.getLocalAddr(); - } - - @Override - public int getLocalPort() { - return delegate.getLocalPort(); - } - - @Override - public ServletContext getServletContext() { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public AsyncContext startAsync() throws IllegalStateException { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public boolean isAsyncStarted() { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public boolean isAsyncSupported() { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public AsyncContext getAsyncContext() { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public DispatcherType getDispatcherType() { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } -} diff --git a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/JakartaToJavaxResponseWrapper.java b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/JakartaToJavaxResponseWrapper.java deleted file mode 100644 index 65f1ffe2249..00000000000 --- a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/JakartaToJavaxResponseWrapper.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.auth.saml; - -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.io.PrintWriter; -import java.util.Collection; -import java.util.Locale; -import javax.servlet.ServletOutputStream; -import javax.servlet.http.Cookie; - -/** - * This class is needed only due to the fact that OneLogin Java SAML needs javax HttpServletResponse. - * It wraps a jakarta.servlet.http.HttpServletResponse and adapts it to javax.servlet.http.HttpServletResponse. - */ -class JakartaToJavaxResponseWrapper implements javax.servlet.http.HttpServletResponse { - public static final String NOT_IMPLEMENTED = "Not implemented"; - private final HttpServletResponse delegate; - - public JakartaToJavaxResponseWrapper(HttpServletResponse delegate) { - this.delegate = delegate; - } - - @Override - public void addCookie(Cookie cookie) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public boolean containsHeader(String s) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public String encodeURL(String s) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public String encodeRedirectURL(String s) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public String encodeUrl(String s) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public String encodeRedirectUrl(String s) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public void sendError(int i, String s) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public void sendError(int i) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public void sendRedirect(String s) throws IOException { - delegate.sendRedirect(s); - } - - @Override - public void setDateHeader(String s, long l) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public void addDateHeader(String s, long l) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public void setHeader(String s, String s1) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public void addHeader(String s, String s1) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public void setIntHeader(String s, int i) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public void addIntHeader(String s, int i) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public void setStatus(int i) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public void setStatus(int i, String s) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public int getStatus() { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public String getHeader(String s) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public Collection<String> getHeaders(String s) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public Collection<String> getHeaderNames() { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public String getCharacterEncoding() { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public String getContentType() { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public ServletOutputStream getOutputStream() { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public PrintWriter getWriter() throws IOException { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public void setCharacterEncoding(String s) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public void setContentLength(int i) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public void setContentLengthLong(long l) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public void setContentType(String s) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public void setBufferSize(int i) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public int getBufferSize() { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public void flushBuffer() { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public void resetBuffer() { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public boolean isCommitted() { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public void reset() { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public void setLocale(Locale locale) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } - - @Override - public Locale getLocale() { - throw new UnsupportedOperationException(NOT_IMPLEMENTED); - } -} diff --git a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/RedirectToUrlProvider.java b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/RedirectToUrlProvider.java index c3ce97a7a0f..7121c595163 100644 --- a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/RedirectToUrlProvider.java +++ b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/RedirectToUrlProvider.java @@ -35,7 +35,7 @@ public class RedirectToUrlProvider { private final RelyingPartyRegistrationRepositoryProvider relyingPartyRegistrationRepositoryProvider; - public RedirectToUrlProvider(RelyingPartyRegistrationRepositoryProvider relyingPartyRegistrationRepositoryProvider) { + RedirectToUrlProvider(RelyingPartyRegistrationRepositoryProvider relyingPartyRegistrationRepositoryProvider) { this.relyingPartyRegistrationRepositoryProvider = relyingPartyRegistrationRepositoryProvider; } diff --git a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/RelyingPartyRegistrationRepositoryProvider.java b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/RelyingPartyRegistrationRepositoryProvider.java index 2b7d917454c..e04d342ed50 100644 --- a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/RelyingPartyRegistrationRepositoryProvider.java +++ b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/RelyingPartyRegistrationRepositoryProvider.java @@ -27,13 +27,17 @@ import org.springframework.security.saml2.provider.service.registration.RelyingP public class RelyingPartyRegistrationRepositoryProvider { private final SamlSettings samlSettings; + private final SamlCertificateConverter samlCertificateConverter; + private final SamlPrivateKeyConverter samlPrivateKeyConverter; - public RelyingPartyRegistrationRepositoryProvider(SamlSettings samlSettings) { + public RelyingPartyRegistrationRepositoryProvider(SamlSettings samlSettings, SamlCertificateConverter samlCertificateConverter, SamlPrivateKeyConverter samlPrivateKeyConverter) { this.samlSettings = samlSettings; + this.samlCertificateConverter = samlCertificateConverter; + this.samlPrivateKeyConverter = samlPrivateKeyConverter; } RelyingPartyRegistrationRepository provide(@Nullable String callbackUrl) { - return new SonarqubeRelyingPartyRegistrationRepository(samlSettings, callbackUrl); + return new SonarqubeRelyingPartyRegistrationRepository(samlSettings, samlCertificateConverter, samlPrivateKeyConverter, callbackUrl); } } diff --git a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlCertificateConverter.java b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlCertificateConverter.java new file mode 100644 index 00000000000..bcc4d768027 --- /dev/null +++ b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlCertificateConverter.java @@ -0,0 +1,31 @@ +package org.sonar.auth.saml; + +import java.io.ByteArrayInputStream; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Base64; +import org.sonar.api.server.ServerSide; + +@ServerSide +class SamlCertificateConverter { + + X509Certificate toX509Certificate(String certificateString) { + String cleanedCertificateString = sanitizeCertificateString(certificateString); + + byte[] decoded = Base64.getDecoder().decode(cleanedCertificateString); + try { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(decoded)); + } catch (CertificateException e) { + throw new RuntimeException(e); + } + } + + private static String sanitizeCertificateString(String certificateString) { + return certificateString + .replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replaceAll("\\s+", ""); + } +} diff --git a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlModule.java b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlModule.java index 1690e9a2447..81c5e38dcab 100644 --- a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlModule.java +++ b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlModule.java @@ -32,8 +32,10 @@ public class SamlModule extends Module { RelyingPartyRegistrationRepositoryProvider.class, SamlAuthenticator.class, SamlConfiguration.class, + SamlCertificateConverter.class, SamlIdentityProvider.class, SamlMessageIdChecker.class, + SamlPrivateKeyConverter.class, SamlResponseAuthenticator.class, SamlSettings.class, RedirectToUrlProvider.class, diff --git a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlPrivateKeyConverter.java b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlPrivateKeyConverter.java new file mode 100644 index 00000000000..50ccb152e96 --- /dev/null +++ b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlPrivateKeyConverter.java @@ -0,0 +1,32 @@ +package org.sonar.auth.saml; + +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import org.sonar.api.server.ServerSide; + +@ServerSide +class SamlPrivateKeyConverter { + PrivateKey toPrivateKey(String privateKeyString) { + String cleanedPrivateKeyString = sanitizePrivateKeyString(privateKeyString); + + byte[] decoded = Base64.getDecoder().decode(cleanedPrivateKeyString); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decoded); + try { + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePrivate(keySpec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new RuntimeException(e); + } + } + + private static String sanitizePrivateKeyString(String privateKeyString) { + return privateKeyString + .replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s+", ""); + } +} diff --git a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlResponseAuthenticator.java b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlResponseAuthenticator.java index 284baf1d777..a20e41080df 100644 --- a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlResponseAuthenticator.java +++ b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlResponseAuthenticator.java @@ -30,12 +30,12 @@ import org.springframework.security.saml2.provider.service.authentication.Saml2A import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter; @ServerSide -public class SamlResponseAuthenticator { +class SamlResponseAuthenticator { private final OpenSaml4AuthenticationProvider openSaml4AuthenticationProvider; private final RelyingPartyRegistrationRepositoryProvider relyingPartyRegistrationRepositoryProvider; - public SamlResponseAuthenticator(OpenSaml4AuthenticationProvider openSaml4AuthenticationProvider, + SamlResponseAuthenticator(OpenSaml4AuthenticationProvider openSaml4AuthenticationProvider, RelyingPartyRegistrationRepositoryProvider relyingPartyRegistrationRepositoryProvider) { this.openSaml4AuthenticationProvider = openSaml4AuthenticationProvider; this.relyingPartyRegistrationRepositoryProvider = relyingPartyRegistrationRepositoryProvider; diff --git a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SonarqubeRelyingPartyRegistrationRepository.java b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SonarqubeRelyingPartyRegistrationRepository.java index 85295ef6eed..f37321903ee 100644 --- a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SonarqubeRelyingPartyRegistrationRepository.java +++ b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SonarqubeRelyingPartyRegistrationRepository.java @@ -19,16 +19,9 @@ */ package org.sonar.auth.saml; -import java.io.ByteArrayInputStream; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; +import com.google.common.annotations.VisibleForTesting; import java.security.PrivateKey; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import java.util.Base64; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.springframework.security.saml2.core.Saml2X509Credential; @@ -41,17 +34,23 @@ public class SonarqubeRelyingPartyRegistrationRepository implements RelyingParty private static final String ANY_URL = "http://anyurl"; private final SamlSettings samlSettings; + private final SamlCertificateConverter samlCertificateConverter; + private final SamlPrivateKeyConverter samlPrivateKeyConverter; @CheckForNull private final String callbackUrl; - public SonarqubeRelyingPartyRegistrationRepository(SamlSettings samlSettings, @Nullable String callbackUrl) { + SonarqubeRelyingPartyRegistrationRepository(SamlSettings samlSettings, SamlCertificateConverter samlCertificateConverter, SamlPrivateKeyConverter samlPrivateKeyConverter, + @Nullable String callbackUrl) { this.samlSettings = samlSettings; + this.samlCertificateConverter = samlCertificateConverter; + this.samlPrivateKeyConverter = samlPrivateKeyConverter; this.callbackUrl = callbackUrl; } @Override public RelyingPartyRegistration findByRegistrationId(String registrationId) { + X509Certificate x509Certificate = samlCertificateConverter.toX509Certificate(samlSettings.getCertificate()); RelyingPartyRegistration.Builder builder = RelyingPartyRegistration.withRegistrationId("saml") .assertionConsumerServiceLocation(callbackUrl != null ? callbackUrl : ANY_URL) .assertionConsumerServiceBinding(Saml2MessageBinding.POST) @@ -59,72 +58,33 @@ public class SonarqubeRelyingPartyRegistrationRepository implements RelyingParty .assertingPartyMetadata(metadata -> metadata .entityId(samlSettings.getProviderId()) .singleSignOnServiceLocation(samlSettings.getLoginUrl()) - .verificationX509Credentials(c -> c.add(convertStringToSaml2X509Credential(samlSettings.getCertificate()))) + .verificationX509Credentials(c -> c.add(Saml2X509Credential.verification(x509Certificate))) .wantAuthnRequestsSigned(samlSettings.isSignRequestsEnabled()) ); - - if(samlSettings.isSignRequestsEnabled()) { - builder - .signingX509Credentials(c -> c.add(convertStringToSaml2X509Credential(samlSettings.getServiceProviderCertificate(), - samlSettings.getServiceProviderPrivateKey().get(), Saml2X509Credential.Saml2X509CredentialType.SIGNING))) - .decryptionX509Credentials(c -> c.add(convertStringToSaml2X509Credential(samlSettings.getServiceProviderCertificate(), - samlSettings.getServiceProviderPrivateKey().get(), Saml2X509Credential.Saml2X509CredentialType.DECRYPTION))); - } + addSignRequestFieldsIfNecessary(builder); return builder.build(); } - public Saml2X509Credential convertStringToSaml2X509Credential(String certificateString, String privateKey, Saml2X509Credential.Saml2X509CredentialType type){ - return new Saml2X509Credential(convertStringToPrivateKey(privateKey), getX509Certificate(certificateString), type); - } - - public Saml2X509Credential convertStringToSaml2X509Credential(String certificateString){ - X509Certificate certificate = getX509Certificate(certificateString); - - // Create and return the Saml2X509Credential - return Saml2X509Credential.verification(certificate); - } - - - public static PrivateKey convertStringToPrivateKey(String privateKeyString){ - // Remove the "BEGIN" and "END" lines and any whitespace - String cleanedPrivateKeyString = privateKeyString - .replace("-----BEGIN PRIVATE KEY-----", "") - .replace("-----END PRIVATE KEY-----", "") - .replaceAll("\\s+", ""); - - // Decode the base64 encoded string - byte[] decoded = Base64.getDecoder().decode(cleanedPrivateKeyString); - - // Create a PrivateKey from the decoded bytes - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decoded); - KeyFactory keyFactory = null; - try { - keyFactory = KeyFactory.getInstance("RSA"); - return keyFactory.generatePrivate(keySpec); - } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { - throw new RuntimeException(e); + private void addSignRequestFieldsIfNecessary(RelyingPartyRegistration.Builder builder) { + if (!samlSettings.isSignRequestsEnabled()) { + return; } + String privateKeyString = samlSettings.getServiceProviderPrivateKey().orElseThrow(() -> new IllegalStateException("Sign requests is enabled but private key is missing")); + String serviceProviderCertificateString = samlSettings.getServiceProviderCertificate(); + PrivateKey privateKey = samlPrivateKeyConverter.toPrivateKey(privateKeyString); + X509Certificate spX509Certificate = samlCertificateConverter.toX509Certificate(serviceProviderCertificateString); + builder + .signingX509Credentials(c -> c.add(Saml2X509Credential.signing(privateKey, spX509Certificate))) + .decryptionX509Credentials(c -> c.add(Saml2X509Credential.decryption(privateKey, spX509Certificate))); } + @VisibleForTesting + SamlSettings getSamlSettings() { + return samlSettings; + } - private static X509Certificate getX509Certificate(String certificateString) { - String cleanedCertificateString = certificateString - .replace("-----BEGIN CERTIFICATE-----", "") - .replace("-----END CERTIFICATE-----", "") - .replaceAll("\\s+", ""); - - // Decode the base64 encoded string - byte[] decoded = Base64.getDecoder().decode(cleanedCertificateString); - - // Create an X509Certificate from the decoded bytes - CertificateFactory factory; - X509Certificate certificate; - try { - factory = CertificateFactory.getInstance("X.509"); - certificate = (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(decoded)); - } catch (CertificateException e) { - throw new RuntimeException(e); - } - return certificate; + @VisibleForTesting + String getCallbackUrl() { + return callbackUrl; } } diff --git a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SonarqubeRelyingPartyRegistrationResolver.java b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SonarqubeRelyingPartyRegistrationResolver.java index 61e2812bfd7..1c7a776e85f 100644 --- a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SonarqubeRelyingPartyRegistrationResolver.java +++ b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SonarqubeRelyingPartyRegistrationResolver.java @@ -26,13 +26,13 @@ import org.springframework.security.saml2.provider.service.registration.RelyingP import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver; import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; -public class SonarqubeRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver { +class SonarqubeRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver { private final RelyingPartyRegistrationRepositoryProvider relyingPartyRegistrationRepositoryProvider; @Nullable private final String callbackUrl; - public SonarqubeRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepositoryProvider relyingPartyRegistrationRepositoryProvider, + SonarqubeRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepositoryProvider relyingPartyRegistrationRepositoryProvider, @Nullable String callbackUrl) { this.relyingPartyRegistrationRepositoryProvider = relyingPartyRegistrationRepositoryProvider; this.callbackUrl = callbackUrl; diff --git a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SonarqubeSaml2ResponseValidator.java b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SonarqubeSaml2ResponseValidator.java index d24c2a1428b..98b4d5697c5 100644 --- a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SonarqubeSaml2ResponseValidator.java +++ b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SonarqubeSaml2ResponseValidator.java @@ -29,7 +29,7 @@ import static org.springframework.security.saml2.core.Saml2ErrorCodes.INVALID_IN import static org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken; import static org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.createDefaultResponseValidator; -public class SonarqubeSaml2ResponseValidator implements Converter<ResponseToken, Saml2ResponseValidatorResult> { +class SonarqubeSaml2ResponseValidator implements Converter<ResponseToken, Saml2ResponseValidatorResult> { private final Converter<ResponseToken, Saml2ResponseValidatorResult> delegate = createDefaultResponseValidator(); diff --git a/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/JakartaToJavaxRequestWrapperTest.java b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/JakartaToJavaxRequestWrapperTest.java deleted file mode 100644 index ae12a2cd612..00000000000 --- a/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/JakartaToJavaxRequestWrapperTest.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.auth.saml; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpSession; -import java.io.BufferedReader; -import java.io.IOException; -import java.security.Principal; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import javax.servlet.ServletException; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThatException; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; - -class JakartaToJavaxRequestWrapperTest { - - @Test - void delegate_methods() throws IOException, ServletException, jakarta.servlet.ServletException { - - HttpServletRequest delegateRequest = getDelegateRequest(); - - JakartaToJavaxRequestWrapper underTest = new JakartaToJavaxRequestWrapper(delegateRequest); - - assertThat(underTest.getServerPort()).isEqualTo(80); - assertThat(underTest.getScheme()).isEqualTo("https"); - assertThat(underTest.getServerName()).isEqualTo("hostname"); - assertThat(underTest.getRequestURL()).hasToString("https://hostname:80/path"); - assertThat(underTest.getRequestURI()).isEqualTo("/request-uri"); - assertThat(underTest.getQueryString()).isEqualTo("param1=value1"); - assertThat(underTest.getContextPath()).isEqualTo("/context-path"); - assertThat(underTest.getMethod()).isEqualTo("POST"); - assertThat(underTest.getParameter("param1")).isEqualTo("value1"); - assertThat(underTest.getParameterValues("param1")).containsExactly("value1"); - assertThat(underTest.getHeader("header1")).isEqualTo("hvalue1"); - assertThat(underTest.getHeaders("header1")).isEqualTo(delegateRequest.getHeaders("header1")); - assertThat(underTest.getHeaderNames()).isEqualTo(delegateRequest.getHeaderNames()); - assertThat(underTest.getRemoteAddr()).isEqualTo("192.168.0.1"); - assertThat(underTest.getRemoteHost()).isEqualTo("remoteHost"); - assertThat(underTest.getRemotePort()).isEqualTo(80); - assertThat(underTest.getServletPath()).isEqualTo("/servlet-path"); - assertThat(underTest.getReader()).isEqualTo(delegateRequest.getReader()); - assertThat(underTest.getAuthType()).isEqualTo("authType"); - assertThat(underTest.getDateHeader("header1")).isEqualTo(1L); - assertThat(underTest.getIntHeader("header1")).isEqualTo(1); - assertThat(underTest.getPathInfo()).isEqualTo("/path-info"); - assertThat(underTest.getPathTranslated()).isEqualTo("/path-translated"); - assertThat(underTest.getRemoteUser()).isEqualTo("remoteUser"); - assertThat(underTest.isUserInRole("role")).isFalse(); - assertThat(underTest.getRequestedSessionId()).isEqualTo("sessionId"); - assertThat(underTest.getProtocol()).isEqualTo("protocol"); - assertThat(underTest.getContentType()).isEqualTo("content-type"); - assertThat(underTest.getContentLength()).isEqualTo(1); - assertThat(underTest.getContentLengthLong()).isEqualTo(1L); - assertThat(underTest.getParameterNames()).isEqualTo(delegateRequest.getParameterNames()); - assertThat(underTest.getLocale()).isEqualTo(Locale.ENGLISH); - assertThat(underTest.getLocales()).isEqualTo(delegateRequest.getLocales()); - assertThat(underTest.getUserPrincipal()).isEqualTo(delegateRequest.getUserPrincipal()); - assertThat(underTest.getLocalName()).isEqualTo("localName"); - assertThat(underTest.getLocalAddr()).isEqualTo("localAddress"); - assertThat(underTest.getLocalPort()).isEqualTo(80); - assertThat(underTest.getParameterMap()).isEqualTo(delegateRequest.getParameterMap()); - assertThat(underTest.getAttribute("key")).isEqualTo("value"); - assertThat(underTest.getAttributeNames()).isEqualTo(delegateRequest.getAttributeNames()); - assertThat(underTest.getCharacterEncoding()).isEqualTo("encoding"); - - assertTrue(underTest.isRequestedSessionIdValid()); - assertTrue(underTest.isRequestedSessionIdFromCookie()); - assertTrue(underTest.isRequestedSessionIdFromURL()); - assertTrue(underTest.isRequestedSessionIdFromUrl()); - assertTrue(underTest.isSecure()); - - underTest.changeSessionId(); - verify(delegateRequest).changeSessionId(); - - underTest.setAttribute("name", "value"); - verify(delegateRequest).setAttribute("name", "value"); - - underTest.setCharacterEncoding("encoding"); - verify(delegateRequest).setCharacterEncoding("encoding"); - - underTest.removeAttribute("name"); - verify(delegateRequest).removeAttribute("name"); - - underTest.login("name", "password"); - verify(delegateRequest).login("name", "password"); - - underTest.logout(); - verify(delegateRequest).logout(); - } - - @Test - void methodsNotImplemented_throwUnsupportedOperationException() throws IOException { - HttpServletRequest delegateRequest = getDelegateRequest(); - - JakartaToJavaxRequestWrapper underTest = new JakartaToJavaxRequestWrapper(delegateRequest); - - assertThatException().isThrownBy(underTest::getInputStream).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::getParts).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::getCookies).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::getSession).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::getAsyncContext).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::getDispatcherType).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::isAsyncSupported).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::isAsyncStarted).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::startAsync).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::getServletContext).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::getInputStream).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.upgrade(null)).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.getSession(false)).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.authenticate(null)).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.getPart(null)).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.getRequestDispatcher(null)).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.getRequestDispatcher(null)).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.startAsync(null, null)).isInstanceOf(UnsupportedOperationException.class); - - verifyNoInteractions(delegateRequest); - } - - HttpServletRequest getDelegateRequest() throws IOException { - HttpServletRequest delegateRequest = mock(HttpServletRequest.class); - Enumeration<String> stringEnumeration = Collections.enumeration(Collections.emptySet()); - Enumeration<Locale> localeEnumeration = Collections.enumeration(Collections.emptySet()); - - Map<String, String[]> paramterMap = new HashMap<>(); - when(delegateRequest.getParameterMap()).thenReturn(paramterMap); - - HttpSession httpSession = mock(HttpSession.class); - when(httpSession.getId()).thenReturn("sessionId"); - - Principal principal = mock(Principal.class); - when(principal.getName()).thenReturn("name"); - when(delegateRequest.getUserPrincipal()).thenReturn(principal); - - when(delegateRequest.getSession()).thenReturn(httpSession); - - when(delegateRequest.getAuthType()).thenReturn("authType"); - when(delegateRequest.getCookies()).thenReturn(new jakarta.servlet.http.Cookie[0]); - when(delegateRequest.getDateHeader("header1")).thenReturn(1L); - when(delegateRequest.getHeader("header1")).thenReturn("hvalue1"); - when(delegateRequest.getHeaders("header1")).thenReturn(stringEnumeration); - when(delegateRequest.getHeaderNames()).thenReturn(stringEnumeration); - when(delegateRequest.getIntHeader("header1")).thenReturn(1); - when(delegateRequest.getMethod()).thenReturn("POST"); - when(delegateRequest.getPathInfo()).thenReturn("/path-info"); - when(delegateRequest.getPathTranslated()).thenReturn("/path-translated"); - when(delegateRequest.getContextPath()).thenReturn("/context-path"); - when(delegateRequest.getQueryString()).thenReturn("param1=value1"); - when(delegateRequest.getRemoteUser()).thenReturn("remoteUser"); - when(delegateRequest.getRequestURI()).thenReturn("/request-uri"); - when(delegateRequest.getRequestURL()).thenReturn(new StringBuffer("https://hostname:80/path")); - when(delegateRequest.getServletPath()).thenReturn("/servlet-path"); - when(delegateRequest.getServerName()).thenReturn("hostname"); - when(delegateRequest.getServerPort()).thenReturn(80); - when(delegateRequest.isSecure()).thenReturn(true); - when(delegateRequest.getRemoteHost()).thenReturn("remoteHost"); - when(delegateRequest.getLocale()).thenReturn(Locale.ENGLISH); - when(delegateRequest.getLocales()).thenReturn(localeEnumeration); - when(delegateRequest.getRemoteAddr()).thenReturn("192.168.0.1"); - when(delegateRequest.getRemotePort()).thenReturn(80); - BufferedReader bufferedReader = mock(BufferedReader.class); - when(delegateRequest.getReader()).thenReturn(bufferedReader); - jakarta.servlet.http.Cookie[] cookies = new jakarta.servlet.http.Cookie[0]; - when(delegateRequest.getCookies()).thenReturn(cookies); - when(delegateRequest.getScheme()).thenReturn("https"); - when(delegateRequest.getParameter("param1")).thenReturn("value1"); - when(delegateRequest.getParameterValues("param1")).thenReturn(new String[]{"value1"}); - when(delegateRequest.getHeader("header1")).thenReturn("hvalue1"); - Enumeration<String> headers = mock(Enumeration.class); - when(delegateRequest.getHeaders("header1")).thenReturn(headers); - when(delegateRequest.isRequestedSessionIdValid()).thenReturn(true); - when(delegateRequest.isRequestedSessionIdFromCookie()).thenReturn(true); - when(delegateRequest.isRequestedSessionIdFromURL()).thenReturn(true); - when(delegateRequest.getProtocol()).thenReturn("protocol"); - when(delegateRequest.getContentType()).thenReturn("content-type"); - when(delegateRequest.getContentLength()).thenReturn(1); - when(delegateRequest.getContentLengthLong()).thenReturn(1L); - when(delegateRequest.getParameterNames()).thenReturn(stringEnumeration); - when(delegateRequest.getLocalName()).thenReturn("localName"); - when(delegateRequest.getLocalAddr()).thenReturn("localAddress"); - when(delegateRequest.getLocalPort()).thenReturn(80); - when(delegateRequest.getAttribute("key")).thenReturn("value"); - when(delegateRequest.getAttributeNames()).thenReturn(stringEnumeration); - when(delegateRequest.getCharacterEncoding()).thenReturn("encoding"); - - return delegateRequest; - } - -} diff --git a/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/JakartaToJavaxResponseWrapperTest.java b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/JakartaToJavaxResponseWrapperTest.java deleted file mode 100644 index d07bf0c70bf..00000000000 --- a/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/JakartaToJavaxResponseWrapperTest.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.auth.saml; - -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThatException; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; - -class JakartaToJavaxResponseWrapperTest { - - private final HttpServletResponse delegate = getResponse(); - - private HttpServletResponse getResponse() { - HttpServletResponse mockedResponse = mock(HttpServletResponse.class); - when(mockedResponse.containsHeader(anyString())).thenReturn(false); - return mockedResponse; - } - - private final JakartaToJavaxResponseWrapper underTest = new JakartaToJavaxResponseWrapper(delegate); - - @Test - void sendRedirectIsDelegated() throws IOException { - underTest.sendRedirect("redirectUrl"); - - verify(delegate).sendRedirect("redirectUrl"); - } - - @Test - void methodsNotImplemented_throwUnsupportedOperationException() { - assertThatException().isThrownBy(() -> underTest.setBufferSize(0)).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::resetBuffer).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::flushBuffer).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.setLocale(null)).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.setContentType("type")).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.setContentLengthLong(0L)).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.setContentLength(0)).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.setCharacterEncoding(null)).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.setStatus(0, "")).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.setStatus(0)).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.addIntHeader("",0)).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.setIntHeader("",0)).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.addDateHeader("",0l)).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.setDateHeader("",0l)).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.addHeader("","")).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.setHeader("","")).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.sendError(0)).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.sendError(0, "")).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.addCookie(null)).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.encodeURL("")).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.encodeRedirectURL("")).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.encodeUrl("")).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.encodeRedirectUrl("")).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::getCharacterEncoding).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::getContentType).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::getStatus).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.getHeader("")).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::getBufferSize).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::getOutputStream).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::getWriter).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::getLocale).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.getHeaders("")).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::getHeaderNames).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(() -> underTest.containsHeader("")).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::isCommitted).isInstanceOf(UnsupportedOperationException.class); - assertThatException().isThrownBy(underTest::reset).isInstanceOf(UnsupportedOperationException.class); - - verifyNoInteractions(delegate); - } - - -} diff --git a/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/RelyingPartyRegistrationRepositoryProviderTest.java b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/RelyingPartyRegistrationRepositoryProviderTest.java new file mode 100644 index 00000000000..9172f53d328 --- /dev/null +++ b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/RelyingPartyRegistrationRepositoryProviderTest.java @@ -0,0 +1,58 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.saml; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class RelyingPartyRegistrationRepositoryProviderTest { + + @Mock + private SamlSettings samlSettings; + + @InjectMocks + private RelyingPartyRegistrationRepositoryProvider relyingPartyRegistrationRepositoryProvider; + + @Test + void provide_whenNullCallback_returnsRelyingPartyRegistrationRepository() { + SonarqubeRelyingPartyRegistrationRepository relyingPartyRegistrationRepository = (SonarqubeRelyingPartyRegistrationRepository) relyingPartyRegistrationRepositoryProvider.provide(null); + + assertThat(relyingPartyRegistrationRepository).isNotNull(); + assertThat(relyingPartyRegistrationRepository.getSamlSettings()).isEqualTo(samlSettings); + assertThat(relyingPartyRegistrationRepository.getCallbackUrl()).isNull(); + } + + @Test + void provide_whenCallbackSet_returnsRelyingPartyRegistrationRepository() { + SonarqubeRelyingPartyRegistrationRepository relyingPartyRegistrationRepository = + (SonarqubeRelyingPartyRegistrationRepository) relyingPartyRegistrationRepositoryProvider.provide("callback"); + + assertThat(relyingPartyRegistrationRepository).isNotNull(); + assertThat(relyingPartyRegistrationRepository.getSamlSettings()).isEqualTo(samlSettings); + assertThat(relyingPartyRegistrationRepository.getCallbackUrl()).isEqualTo("callback"); + } + +} diff --git a/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlModuleTest.java b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlModuleTest.java index e40790d85ac..fc432a50648 100644 --- a/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlModuleTest.java +++ b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlModuleTest.java @@ -30,6 +30,6 @@ public class SamlModuleTest { public void verify_count_of_added_components() { ListContainer container = new ListContainer(); new SamlModule().configure(container); - assertThat(container.getAddedObjects()).hasSize(17); + assertThat(container.getAddedObjects()).hasSize(25); } } diff --git a/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SonarqubeRelyingPartyRegistrationRepositoryTest.java b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SonarqubeRelyingPartyRegistrationRepositoryTest.java new file mode 100644 index 00000000000..4af992cc598 --- /dev/null +++ b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SonarqubeRelyingPartyRegistrationRepositoryTest.java @@ -0,0 +1,185 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.saml; + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Optional; +import javax.annotation.Nullable; +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.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class SonarqubeRelyingPartyRegistrationRepositoryTest { + + private static final String VALID_CERTIFICATE_STRING = """ + -----BEGIN CERTIFICATE----- + MIIDqDCCApCgAwIBAgIGAYcJtZATMA0GCSqGSIb3DQEBCwUAMIGUMQswCQYDVQQGEwJVUzETMBEG + A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU + MBIGA1UECwwLU1NPUHJvdmlkZXIxFTATBgNVBAMMDGRldi05ODIxMjUzNzEcMBoGCSqGSIb3DQEJ + ARYNaW5mb0Bva3RhLmNvbTAeFw0yMzAzMjIxNDI0MDZaFw0zMzAzMjIxNDI1MDZaMIGUMQswCQYD + VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsG + A1UECgwET2t0YTEUMBIGA1UECwwLU1NPUHJvdmlkZXIxFTATBgNVBAMMDGRldi05ODIxMjUzNzEc + MBoGCSqGSIb3DQEJARYNaW5mb0Bva3RhLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC + ggEBALNloIuL4r7mUDkZcD7hdYrUw335hlRGWFjMV1OjRFhI/hMw2NUq+KgQjyte6zpE7a9nMlWw + lRqkEVAuCW7ZcjO/Wk1TECiKwS2nYN3InPuuF6TCk0/gJSFZuiKXdtUUDod5viNJyEXb0Ol8rtIl + TRffbSRiaWPvPykhtDZVObS0QDpBo4wVK1C+G+3e0/P/YCD6g4+zJWFYT4sbY6Ee97xhVwcdO6ZS + jfba6lYtmUCUwRPRLQPkM9xAjKinVu5mmNPY8sXuxIRs/yEvhxnhTOnbvnU5oNU5DWI28vAiMOlD + SpQTUQZjqLDa9AHyvkWT/j0WU5AI1IFgLqB5gg6dY8UCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA + DRAEvjil9vPgCMJSYl3x5i83is4JlZ6SeN8mXxJfj35pQb+sLa+XrnITnAk6fnYX4NYYEwDGD+Vq + AnSIRsJEeMYTnQWMGLp5er88IDltDlfIMSs8WgVxWkJ6R66BVGFQRVo9IJQRVuBXrPTahL43ZBn1 + SynJxMl9tceAb6Q18ncyK9DpsLfrgpkerPcLjhjWiCl9iEpfUEzGEeLzin9OyfSwTtMWcPLrqgUb + nWiSEIvNnGzGVQunZaUF4cLxlstgWJzsWLcuzr0cdSO7eIsAtAMVDqXY1ESpewRYqzDeXmj+eKso + k5X4rDjQIGfE0XskScXfKyY7CVklfmW1dCuzdw== + -----END CERTIFICATE----- + """; + public static final String APPLICATION_ID = "applicationId"; + public static final String PROVIDER_ID = "providerId"; + public static final String SSO_URL = "ssoUrl"; + public static final String CERTIF_STRING = "certifString"; + private static final String CERTIF_SP_STRING = "certifSpString"; + public static final String PRIVATE_KEY = "privateKey"; + public static final String CALLBACK_URL = "callback/"; + + @Mock + private SamlSettings samlSettings; + @Mock + private SamlCertificateConverter samlCertificateConverter; + @Mock + private SamlPrivateKeyConverter samlPrivateKeyConverter; + + @Test + void findByRegistrationId_whenSignRequestsIsDisabledAndAllFieldSet_succeeds() { + when(samlSettings.getApplicationId()).thenReturn(APPLICATION_ID); + when(samlSettings.getProviderId()).thenReturn(PROVIDER_ID); + when(samlSettings.getLoginUrl()).thenReturn(SSO_URL); + when(samlSettings.getCertificate()).thenReturn(CERTIF_STRING); + + X509Certificate mockedCertificate = mockCertificate(); + + RelyingPartyRegistration registration = findRegistration(CALLBACK_URL); + + assertCommonFields(registration, mockedCertificate, CALLBACK_URL); + + assertThat(registration.getAssertingPartyMetadata().getWantAuthnRequestsSigned()).isFalse(); + assertThat(registration.getSigningX509Credentials()).isEmpty(); + assertThat(registration.getDecryptionX509Credentials()).isEmpty(); + + verifyNoInteractions(samlPrivateKeyConverter); + } + + @Test + void findByRegistrationId_whenCallbackUrlIsNull_succeeds() { + when(samlSettings.getApplicationId()).thenReturn(APPLICATION_ID); + when(samlSettings.getProviderId()).thenReturn(PROVIDER_ID); + when(samlSettings.getLoginUrl()).thenReturn(SSO_URL); + when(samlSettings.getCertificate()).thenReturn(CERTIF_STRING); + + X509Certificate mockedCertificate = mockCertificate(); + + RelyingPartyRegistration registration = findRegistration(null); + + assertCommonFields(registration, mockedCertificate, "http://anyurl"); + } + + @Test + void findByRegistrationId_whenSignRequestIsEnabledAndAllFieldSet_succeeds() { + when(samlSettings.getApplicationId()).thenReturn(APPLICATION_ID); + when(samlSettings.getProviderId()).thenReturn(PROVIDER_ID); + when(samlSettings.getLoginUrl()).thenReturn(SSO_URL); + when(samlSettings.getCertificate()).thenReturn(CERTIF_STRING); + when(samlSettings.isSignRequestsEnabled()).thenReturn(true); + when(samlSettings.getServiceProviderPrivateKey()).thenReturn(Optional.of(PRIVATE_KEY)); + when(samlSettings.getServiceProviderCertificate()).thenReturn(CERTIF_SP_STRING); + + X509Certificate certificate = mockCertificate(); + X509Certificate serviceProviderCertificate = mockServiceProviderCertificate(); + PrivateKey privateKey = mockPrivateKey(); + + RelyingPartyRegistration registration = findRegistration(CALLBACK_URL); + + assertCommonFields(registration, certificate, CALLBACK_URL); + + assertThat(registration.getAssertingPartyMetadata().getWantAuthnRequestsSigned()).isTrue(); + assertThat(registration.getSigningX509Credentials()).containsExactly(Saml2X509Credential.signing(privateKey, serviceProviderCertificate)); + assertThat(registration.getDecryptionX509Credentials()).containsExactly(Saml2X509Credential.decryption(privateKey, serviceProviderCertificate)); + } + + @Test + void findByRegistrationId_whenSignRequestIsEnabledAndPrivateKeyEmpty_throws() { + when(samlSettings.getApplicationId()).thenReturn(APPLICATION_ID); + when(samlSettings.getProviderId()).thenReturn(PROVIDER_ID); + when(samlSettings.getLoginUrl()).thenReturn(SSO_URL); + when(samlSettings.getCertificate()).thenReturn(CERTIF_STRING); + when(samlSettings.isSignRequestsEnabled()).thenReturn(true); + when(samlSettings.getServiceProviderPrivateKey()).thenReturn(Optional.empty()); + mockCertificate(); + + assertThatIllegalStateException() + .isThrownBy(() -> findRegistration(CALLBACK_URL)) + .withMessage("Sign requests is enabled but private key is missing"); + } + + private static void assertCommonFields(RelyingPartyRegistration registration, X509Certificate certificate, String callbackUrl) { + assertThat(registration.getRegistrationId()).isEqualTo("saml"); + assertThat(registration.getAssertionConsumerServiceLocation()).isEqualTo(callbackUrl); + assertThat(registration.getAssertionConsumerServiceBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(registration.getEntityId()).isEqualTo(APPLICATION_ID); + assertThat(registration.getAssertingPartyMetadata().getEntityId()).isEqualTo(PROVIDER_ID); + assertThat(registration.getAssertingPartyMetadata().getSingleSignOnServiceLocation()).isEqualTo(SSO_URL); + assertThat(registration.getAssertingPartyMetadata().getVerificationX509Credentials()).containsExactly(Saml2X509Credential.verification(certificate)); + } + + private X509Certificate mockCertificate() { + X509Certificate mockedCertificate = mock(); + when(samlCertificateConverter.toX509Certificate(CERTIF_STRING)).thenReturn(mockedCertificate); + return mockedCertificate; + } + + private X509Certificate mockServiceProviderCertificate() { + X509Certificate mockedCertificate = mock(); + when(samlCertificateConverter.toX509Certificate(CERTIF_SP_STRING)).thenReturn(mockedCertificate); + return mockedCertificate; + } + + private PrivateKey mockPrivateKey() { + PrivateKey privateKey = mock(); + when(samlPrivateKeyConverter.toPrivateKey(PRIVATE_KEY)).thenReturn(privateKey); + return privateKey; + } + + private RelyingPartyRegistration findRegistration(@Nullable String callbackUrl) { + SonarqubeRelyingPartyRegistrationRepository relyingPartyRegistrationRepository = new SonarqubeRelyingPartyRegistrationRepository(samlSettings, samlCertificateConverter, + samlPrivateKeyConverter, callbackUrl); + return relyingPartyRegistrationRepository.findByRegistrationId("registrationId"); + } + +} |