@@ -17,6 +17,7 @@ dependencies { | |||
compileOnly 'com.google.code.findbugs:jsr305' | |||
compileOnly 'com.squareup.okhttp3:okhttp' | |||
compileOnly 'javax.servlet:javax.servlet-api' | |||
compileOnly 'org.json:json' | |||
compileOnly project(':server:sonar-db-dao') | |||
compileOnly project(':sonar-core') | |||
@@ -0,0 +1,86 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 com.google.common.io.Resources; | |||
import java.io.IOException; | |||
import java.net.URL; | |||
import java.nio.charset.StandardCharsets; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.stream.Collectors; | |||
import javax.annotation.Nullable; | |||
import org.json.JSONObject; | |||
public class SamlAuthStatusPageGenerator { | |||
private static final String WEB_CONTEXT = "%WEB_CONTEXT%"; | |||
private static final String STATUS = "%STATUS%"; | |||
private static final String ERRORS = "%ERRORS%"; | |||
private static final String WARNINGS = "%WARNINGS%"; | |||
private static final String AVAILABLE_ATTRIBUTES = "%AVAILABLE_ATTRIBUTES%"; | |||
private static final String ATTRIBUTE_MAPPINGS = "%ATTRIBUTE_MAPPINGS%"; | |||
private static final String HTML_TEMPLATE_NAME = "samlAuthResult.html"; | |||
private SamlAuthStatusPageGenerator() { | |||
throw new IllegalStateException("This Utility class cannot be instantiated"); | |||
} | |||
public static String getSamlAuthStatusHtml(SamlAuthenticationStatus samlAuthenticationStatus) { | |||
Map<String, String> substitutionsMap = getSubstitutionsMap(samlAuthenticationStatus); | |||
String htmlTemplate = getPlainTemplate(); | |||
return substitutionsMap | |||
.keySet() | |||
.stream() | |||
.reduce(htmlTemplate, (accumulator, pattern) -> accumulator.replace(pattern, substitutionsMap.get(pattern))); | |||
} | |||
private static Map<String, String> getSubstitutionsMap(SamlAuthenticationStatus samlAuthenticationStatus) { | |||
return Map.of( | |||
WEB_CONTEXT, "", | |||
STATUS, toJsString(samlAuthenticationStatus.getStatus()), | |||
ERRORS, toJsArrayFromList(samlAuthenticationStatus.getErrors()), | |||
WARNINGS, toJsArrayFromList(samlAuthenticationStatus.getWarnings()), | |||
AVAILABLE_ATTRIBUTES, new JSONObject(samlAuthenticationStatus.getAvailableAttributes()).toString(), | |||
ATTRIBUTE_MAPPINGS, new JSONObject(samlAuthenticationStatus.getMappedAttributes()).toString()); | |||
} | |||
private static String toJsString(@Nullable String inputString) { | |||
return String.format("'%s'", inputString != null ? inputString.replace("'", "\\'") : ""); | |||
} | |||
private static String toJsArrayFromList(List<String> inputArray) { | |||
return "[" + inputArray.stream() | |||
.map(SamlAuthStatusPageGenerator::toJsString) | |||
.collect(Collectors.joining(",")) + "]"; | |||
} | |||
private static String getPlainTemplate() { | |||
URL url = Resources.getResource(HTML_TEMPLATE_NAME); | |||
try { | |||
return Resources.toString(url, StandardCharsets.UTF_8); | |||
} catch (IOException e) { | |||
throw new IllegalStateException("Cannot read the template " + HTML_TEMPLATE_NAME); | |||
} | |||
} | |||
} |
@@ -0,0 +1,79 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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.util.ArrayList; | |||
import java.util.Collection; | |||
import java.util.HashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
public class SamlAuthenticationStatus { | |||
private String status = ""; | |||
private Map<String, List<String>> availableAttributes = new HashMap<>(); | |||
private Map<String, Collection<String>> mappedAttributes = new HashMap<>(); | |||
private List<String> errors = new ArrayList<>(); | |||
private List<String> warnings = new ArrayList<>(); | |||
public String getStatus() { | |||
return status; | |||
} | |||
public void setStatus(String status) { | |||
this.status = status; | |||
} | |||
public Map<String, List<String>> getAvailableAttributes() { | |||
return availableAttributes; | |||
} | |||
public void setAvailableAttributes(Map<String, List<String>> availableAttributes) { | |||
this.availableAttributes = availableAttributes; | |||
} | |||
public Map<String, Collection<String>> getMappedAttributes() { | |||
return mappedAttributes; | |||
} | |||
public void setMappedAttributes(Map<String, Collection<String>> mappedAttributes) { | |||
this.mappedAttributes = mappedAttributes; | |||
} | |||
public List<String> getErrors() { | |||
return errors; | |||
} | |||
public void setErrors(List<String> errors) { | |||
this.errors = errors; | |||
} | |||
public List<String> getWarnings() { | |||
return warnings; | |||
} | |||
public void setWarnings(List<String> warnings) { | |||
this.warnings = warnings; | |||
} | |||
} |
@@ -0,0 +1,208 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 com.onelogin.saml2.Auth; | |||
import com.onelogin.saml2.exception.SettingsException; | |||
import com.onelogin.saml2.settings.Saml2Settings; | |||
import com.onelogin.saml2.settings.SettingsBuilder; | |||
import java.io.IOException; | |||
import java.util.Collection; | |||
import java.util.HashMap; | |||
import java.util.HashSet; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import javax.servlet.http.HttpServletRequest; | |||
import javax.servlet.http.HttpServletResponse; | |||
import org.sonar.api.server.authentication.OAuth2IdentityProvider; | |||
import org.sonar.api.server.authentication.UnauthorizedException; | |||
import org.sonar.api.server.authentication.UserIdentity; | |||
import org.sonar.api.utils.log.Logger; | |||
import org.sonar.api.utils.log.Loggers; | |||
import static java.util.Collections.emptySet; | |||
import static java.util.Objects.requireNonNull; | |||
import static org.sonar.auth.saml.SamlIdentityProvider.RSA_SHA_256_URL; | |||
public class SamlAuthenticator { | |||
private static final Logger LOGGER = Loggers.get(SamlAuthenticator.class); | |||
private static final String ANY_URL = "http://anyurl"; | |||
private static final String STATE_REQUEST_PARAMETER = "RelayState"; | |||
private final SamlSettings samlSettings; | |||
private final SamlMessageIdChecker samlMessageIdChecker; | |||
public SamlAuthenticator(SamlSettings samlSettings, SamlMessageIdChecker samlMessageIdChecker) { | |||
this.samlSettings = samlSettings; | |||
this.samlMessageIdChecker = samlMessageIdChecker; | |||
} | |||
public UserIdentity buildUserIdentity(OAuth2IdentityProvider.CallbackContext context, HttpServletRequest processedRequest) { | |||
Auth auth = this.initSamlAuth(processedRequest, context.getResponse()); | |||
processResponse(auth); | |||
context.verifyCsrfState(STATE_REQUEST_PARAMETER); | |||
LOGGER.trace("Name ID : {}", getNameId(auth)); | |||
checkAuthentication(auth); | |||
this.checkMessageId(auth); | |||
LOGGER.trace("Attributes received : {}", getAttributes(auth)); | |||
String login = this.getLogin(auth); | |||
UserIdentity.Builder userIdentityBuilder = UserIdentity.builder() | |||
.setProviderLogin(login) | |||
.setName(this.getName(auth)); | |||
this.getEmail(auth).ifPresent(userIdentityBuilder::setEmail); | |||
userIdentityBuilder.setGroups(this.getGroups(auth)); | |||
return userIdentityBuilder.build(); | |||
} | |||
public void initLogin(String callbackUrl, String relayState, HttpServletRequest request, HttpServletResponse response) { | |||
Auth auth = this.initSamlAuth(callbackUrl, request, response); | |||
login(auth, relayState); | |||
} | |||
private Auth initSamlAuth(HttpServletRequest request, HttpServletResponse response) { | |||
return initSamlAuth(null, request, response); | |||
} | |||
private Auth initSamlAuth(@Nullable String callbackUrl, HttpServletRequest request, HttpServletResponse response) { | |||
try { | |||
return new Auth(initSettings(callbackUrl), request, response); | |||
} catch (SettingsException e) { | |||
throw new IllegalStateException("Failed to create a SAML Auth", e); | |||
} | |||
} | |||
private Saml2Settings initSettings(@Nullable String callbackUrl) { | |||
Map<String, Object> samlData = new HashMap<>(); | |||
samlData.put("onelogin.saml2.strict", true); | |||
samlData.put("onelogin.saml2.idp.entityid", samlSettings.getProviderId()); | |||
samlData.put("onelogin.saml2.idp.single_sign_on_service.url", samlSettings.getLoginUrl()); | |||
samlData.put("onelogin.saml2.idp.x509cert", samlSettings.getCertificate()); | |||
// Service Provider configuration | |||
samlData.put("onelogin.saml2.sp.entityid", samlSettings.getApplicationId()); | |||
if (samlSettings.isSignRequestsEnabled()) { | |||
samlData.put("onelogin.saml2.security.authnrequest_signed", true); | |||
samlData.put("onelogin.saml2.security.logoutrequest_signed", true); | |||
samlData.put("onelogin.saml2.security.logoutresponse_signed", true); | |||
samlData.put("onelogin.saml2.sp.x509cert", samlSettings.getServiceProviderCertificate()); | |||
samlData.put("onelogin.saml2.sp.privatekey", | |||
samlSettings.getServiceProviderPrivateKey().orElseThrow(() -> new IllegalArgumentException("Service provider private key is missing"))); | |||
} else { | |||
samlSettings.getServiceProviderPrivateKey().ifPresent(privateKey -> samlData.put("onelogin.saml2.sp.privatekey", privateKey)); | |||
} | |||
samlData.put("onelogin.saml2.security.signature_algorithm", RSA_SHA_256_URL); | |||
// During callback, the callback URL is by definition not needed, but the Saml2Settings does never allow this setting to be empty... | |||
samlData.put("onelogin.saml2.sp.assertion_consumer_service.url", callbackUrl != null ? callbackUrl : ANY_URL); | |||
var saml2Settings = new SettingsBuilder().fromValues(samlData).build(); | |||
if (samlSettings.getServiceProviderPrivateKey().isPresent() && saml2Settings.getSPkey() == null) { | |||
LOGGER.error("Error in parsing service provider private key, please make sure that it is in PKCS 8 format."); | |||
} | |||
return saml2Settings; | |||
} | |||
private static void login(Auth auth, String relayState) { | |||
try { | |||
auth.login(relayState); | |||
} catch (IOException | SettingsException e) { | |||
throw new IllegalStateException("Failed to initialize SAML authentication plugin", e); | |||
} | |||
} | |||
private static void processResponse(Auth auth) { | |||
try { | |||
auth.processResponse(); | |||
} catch (Exception e) { | |||
throw new IllegalStateException("Failed to process the authentication response", e); | |||
} | |||
} | |||
private static void checkAuthentication(Auth auth) { | |||
List<String> errors = auth.getErrors(); | |||
if (auth.isAuthenticated() && errors.isEmpty()) { | |||
return; | |||
} | |||
String errorReason = auth.getLastErrorReason(); | |||
throw new UnauthorizedException(errorReason != null && !errorReason.isEmpty() ? errorReason : "Unknown error reason"); | |||
} | |||
private String getLogin(Auth auth) { | |||
return getNonNullFirstAttribute(auth, samlSettings.getUserLogin()); | |||
} | |||
private String getName(Auth auth) { | |||
return getNonNullFirstAttribute(auth, samlSettings.getUserName()); | |||
} | |||
private Optional<String> getEmail(Auth auth) { | |||
return samlSettings.getUserEmail().map(userEmailField -> getFirstAttribute(auth, userEmailField)); | |||
} | |||
private Set<String> getGroups(Auth auth) { | |||
return samlSettings.getGroupName().map(groupsField -> getGroups(auth, groupsField)).orElse(emptySet()); | |||
} | |||
private static String getNonNullFirstAttribute(Auth auth, String key) { | |||
String attribute = getFirstAttribute(auth, key); | |||
requireNonNull(attribute, String.format("%s is missing", key)); | |||
return attribute; | |||
} | |||
@CheckForNull | |||
private static String getFirstAttribute(Auth auth, String key) { | |||
Collection<String> attribute = auth.getAttribute(key); | |||
if (attribute == null || attribute.isEmpty()) { | |||
return null; | |||
} | |||
return attribute.iterator().next(); | |||
} | |||
private static Set<String> getGroups(Auth auth, String groupAttribute) { | |||
Collection<String> attribute = auth.getAttribute(groupAttribute); | |||
if (attribute == null || attribute.isEmpty()) { | |||
return emptySet(); | |||
} | |||
return new HashSet<>(attribute); | |||
} | |||
private static String getNameId(Auth auth) { | |||
return auth.getNameId(); | |||
} | |||
private static Map<String, List<String>> getAttributes(Auth auth) { | |||
return auth.getAttributes(); | |||
} | |||
private void checkMessageId(Auth auth) { | |||
samlMessageIdChecker.check(auth); | |||
} | |||
} |
@@ -19,44 +19,20 @@ | |||
*/ | |||
package org.sonar.auth.saml; | |||
import com.onelogin.saml2.Auth; | |||
import com.onelogin.saml2.exception.SettingsException; | |||
import com.onelogin.saml2.settings.Saml2Settings; | |||
import com.onelogin.saml2.settings.SettingsBuilder; | |||
import java.io.IOException; | |||
import java.util.Collection; | |||
import java.util.HashMap; | |||
import java.util.HashSet; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import java.util.regex.Pattern; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import javax.servlet.http.HttpServletRequest; | |||
import javax.servlet.http.HttpServletRequestWrapper; | |||
import javax.servlet.http.HttpServletResponse; | |||
import org.sonar.api.server.ServerSide; | |||
import org.sonar.api.server.authentication.Display; | |||
import org.sonar.api.server.authentication.OAuth2IdentityProvider; | |||
import org.sonar.api.server.authentication.UnauthorizedException; | |||
import org.sonar.api.server.authentication.UserIdentity; | |||
import org.sonar.api.utils.log.Logger; | |||
import org.sonar.api.utils.log.Loggers; | |||
import static java.util.Collections.emptySet; | |||
import static java.util.Objects.requireNonNull; | |||
@ServerSide | |||
public class SamlIdentityProvider implements OAuth2IdentityProvider { | |||
private static final Pattern HTTPS_PATTERN = Pattern.compile("https?://"); | |||
private static final String KEY = "saml"; | |||
private static final Logger LOGGER = Loggers.get(SamlIdentityProvider.class); | |||
public static final String KEY = "saml"; | |||
private static final String ANY_URL = "http://anyurl"; | |||
private static final String STATE_REQUEST_PARAMETER = "RelayState"; | |||
public static final String RSA_SHA_256_URL = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; | |||
private final SamlSettings samlSettings; | |||
@@ -97,12 +73,8 @@ public class SamlIdentityProvider implements OAuth2IdentityProvider { | |||
@Override | |||
public void init(InitContext context) { | |||
try { | |||
Auth auth = newAuth(initSettings(context.getCallbackUrl()), context.getRequest(), context.getResponse()); | |||
auth.login(context.generateCsrfState()); | |||
} catch (IOException | SettingsException e) { | |||
throw new IllegalStateException("Fail to intialize SAML authentication plugin", e); | |||
} | |||
SamlAuthenticator samlAuthenticator = new SamlAuthenticator(samlSettings, samlMessageIdChecker); | |||
samlAuthenticator.initLogin(context.getCallbackUrl(), context.generateCsrfState(), context.getRequest(), context.getResponse()); | |||
} | |||
@Override | |||
@@ -116,105 +88,11 @@ public class SamlIdentityProvider implements OAuth2IdentityProvider { | |||
// | |||
HttpServletRequest processedRequest = useProxyHeadersInRequest(context.getRequest()); | |||
Auth auth = newAuth(initSettings(null), processedRequest, context.getResponse()); | |||
processResponse(auth); | |||
context.verifyCsrfState(STATE_REQUEST_PARAMETER); | |||
LOGGER.trace("Name ID : {}", auth.getNameId()); | |||
checkAuthentication(auth); | |||
samlMessageIdChecker.check(auth); | |||
LOGGER.trace("Attributes received : {}", auth.getAttributes()); | |||
String login = getNonNullFirstAttribute(auth, samlSettings.getUserLogin()); | |||
UserIdentity.Builder userIdentityBuilder = UserIdentity.builder() | |||
.setProviderLogin(login) | |||
.setName(getNonNullFirstAttribute(auth, samlSettings.getUserName())); | |||
samlSettings.getUserEmail().ifPresent( | |||
email -> userIdentityBuilder.setEmail(getFirstAttribute(auth, email))); | |||
samlSettings.getGroupName().ifPresent( | |||
group -> userIdentityBuilder.setGroups(getGroups(auth, group))); | |||
context.authenticate(userIdentityBuilder.build()); | |||
SamlAuthenticator samlAuthenticator = new SamlAuthenticator(samlSettings, samlMessageIdChecker); | |||
UserIdentity userIdentity = samlAuthenticator.buildUserIdentity(context, processedRequest); | |||
context.authenticate(userIdentity); | |||
context.redirectToRequestedPage(); | |||
} | |||
private static Auth newAuth(Saml2Settings saml2Settings, HttpServletRequest request, HttpServletResponse response) { | |||
try { | |||
return new Auth(saml2Settings, request, response); | |||
} catch (SettingsException e) { | |||
throw new IllegalStateException("Fail to create Auth", e); | |||
} | |||
} | |||
private static void processResponse(Auth auth) { | |||
try { | |||
auth.processResponse(); | |||
} catch (Exception e) { | |||
throw new IllegalStateException("Fail to process response", e); | |||
} | |||
} | |||
private static void checkAuthentication(Auth auth) { | |||
List<String> errors = auth.getErrors(); | |||
if (auth.isAuthenticated() && errors.isEmpty()) { | |||
return; | |||
} | |||
String errorReason = auth.getLastErrorReason(); | |||
throw new UnauthorizedException(errorReason != null && !errorReason.isEmpty() ? errorReason : "Unknown error reason"); | |||
} | |||
private static String getNonNullFirstAttribute(Auth auth, String key) { | |||
String attribute = getFirstAttribute(auth, key); | |||
requireNonNull(attribute, String.format("%s is missing", key)); | |||
return attribute; | |||
} | |||
@CheckForNull | |||
private static String getFirstAttribute(Auth auth, String key) { | |||
Collection<String> attribute = auth.getAttribute(key); | |||
if (attribute == null || attribute.isEmpty()) { | |||
return null; | |||
} | |||
return attribute.iterator().next(); | |||
} | |||
private static Set<String> getGroups(Auth auth, String groupAttribute) { | |||
Collection<String> attribute = auth.getAttribute(groupAttribute); | |||
if (attribute == null || attribute.isEmpty()) { | |||
return emptySet(); | |||
} | |||
return new HashSet<>(attribute); | |||
} | |||
private Saml2Settings initSettings(@Nullable String callbackUrl) { | |||
Map<String, Object> samlData = new HashMap<>(); | |||
samlData.put("onelogin.saml2.strict", true); | |||
samlData.put("onelogin.saml2.idp.entityid", samlSettings.getProviderId()); | |||
samlData.put("onelogin.saml2.idp.single_sign_on_service.url", samlSettings.getLoginUrl()); | |||
samlData.put("onelogin.saml2.idp.x509cert", samlSettings.getCertificate()); | |||
// Service Provider configuration | |||
samlData.put("onelogin.saml2.sp.entityid", samlSettings.getApplicationId()); | |||
if (samlSettings.isSignRequestsEnabled()) { | |||
samlData.put("onelogin.saml2.security.authnrequest_signed", true); | |||
samlData.put("onelogin.saml2.security.logoutrequest_signed", true); | |||
samlData.put("onelogin.saml2.security.logoutresponse_signed", true); | |||
samlData.put("onelogin.saml2.sp.x509cert", samlSettings.getServiceProviderCertificate()); | |||
samlData.put("onelogin.saml2.sp.privatekey", | |||
samlSettings.getServiceProviderPrivateKey().orElseThrow(() -> new IllegalArgumentException("Service provider private key is missing"))); | |||
} else { | |||
samlSettings.getServiceProviderPrivateKey().ifPresent(privateKey -> samlData.put("onelogin.saml2.sp.privatekey", privateKey)); | |||
} | |||
samlData.put("onelogin.saml2.security.signature_algorithm", RSA_SHA_256_URL); | |||
// During callback, the callback URL is by definition not needed, but the Saml2Settings does never allow this setting to be empty... | |||
samlData.put("onelogin.saml2.sp.assertion_consumer_service.url", callbackUrl != null ? callbackUrl : ANY_URL); | |||
var saml2Settings = new SettingsBuilder().fromValues(samlData).build(); | |||
if (samlSettings.getServiceProviderPrivateKey().isPresent() && saml2Settings.getSPkey() == null) { | |||
LOGGER.error("Error in parsing service provider private key, please make sure that it is in PKCS 8 format."); | |||
} | |||
return saml2Settings; | |||
} | |||
private static HttpServletRequest useProxyHeadersInRequest(HttpServletRequest request) { |
@@ -30,7 +30,8 @@ public class SamlModule extends Module { | |||
add( | |||
SamlIdentityProvider.class, | |||
SamlMessageIdChecker.class, | |||
SamlSettings.class); | |||
SamlSettings.class, | |||
SamlAuthenticator.class); | |||
List<PropertyDefinition> definitions = SamlSettings.definitions(); | |||
add(definitions.toArray(new Object[definitions.size()])); | |||
} |
@@ -0,0 +1,101 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 com.onelogin.saml2.Auth; | |||
import java.util.ArrayList; | |||
import java.util.Collection; | |||
import java.util.HashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Objects; | |||
import java.util.stream.Collectors; | |||
import static org.sonar.auth.saml.SamlSettings.GROUP_NAME_ATTRIBUTE; | |||
import static org.sonar.auth.saml.SamlSettings.USER_EMAIL_ATTRIBUTE; | |||
import static org.sonar.auth.saml.SamlSettings.USER_LOGIN_ATTRIBUTE; | |||
import static org.sonar.auth.saml.SamlSettings.USER_NAME_ATTRIBUTE; | |||
public class SamlStatusChecker { | |||
private SamlStatusChecker() { | |||
throw new IllegalStateException("This Utility class cannot be instantiated"); | |||
} | |||
public static SamlAuthenticationStatus getSamlAuthenticationStatus(Auth auth, SamlSettings samlSettings) { | |||
SamlAuthenticationStatus samlAuthenticationStatus = new SamlAuthenticationStatus(); | |||
try { | |||
auth.processResponse(); | |||
} catch (Exception e) { | |||
samlAuthenticationStatus.getErrors().add(e.getMessage()); | |||
} | |||
samlAuthenticationStatus.getErrors().addAll(auth.getErrors().stream().filter(Objects::nonNull).collect(Collectors.toList())); | |||
if (auth.getLastErrorReason() != null) { | |||
samlAuthenticationStatus.getErrors().add(auth.getLastErrorReason()); | |||
} | |||
samlAuthenticationStatus.setAvailableAttributes(auth.getAttributes()); | |||
samlAuthenticationStatus.setMappedAttributes(getAttributesMapping(auth, samlSettings)); | |||
samlAuthenticationStatus.setWarnings(samlAuthenticationStatus.getErrors().isEmpty() ? generateWarnings(auth, samlSettings) : new ArrayList<>()); | |||
samlAuthenticationStatus.setStatus(samlAuthenticationStatus.getErrors().isEmpty() ? "success" : "error"); | |||
return samlAuthenticationStatus; | |||
} | |||
private static Map<String, Collection<String>> getAttributesMapping(Auth auth, SamlSettings samlSettings) { | |||
Map<String, Collection<String>> attributesMapping = new HashMap<>(); | |||
attributesMapping.put("User login value", auth.getAttribute(samlSettings.getUserLogin())); | |||
attributesMapping.put("User name value", auth.getAttribute(samlSettings.getUserName())); | |||
samlSettings.getUserEmail().ifPresent(emailFieldName -> attributesMapping.put("User email value", auth.getAttribute(emailFieldName))); | |||
samlSettings.getGroupName().ifPresent(groupFieldName -> attributesMapping.put("Groups value", auth.getAttribute(groupFieldName))); | |||
return attributesMapping; | |||
} | |||
private static List<String> generateWarnings(Auth auth, SamlSettings samlSettings) { | |||
List<String> warnings = new ArrayList<>(); | |||
warnings.addAll(generateMappingWarnings(auth, samlSettings)); | |||
return warnings; | |||
} | |||
private static List<String> generateMappingWarnings(Auth auth, SamlSettings samlSettings) { | |||
Map<String, String> mappings = Map.of( | |||
USER_NAME_ATTRIBUTE, samlSettings.getUserName(), | |||
USER_LOGIN_ATTRIBUTE, samlSettings.getUserLogin(), | |||
USER_EMAIL_ATTRIBUTE, samlSettings.getUserEmail().orElse(""), | |||
GROUP_NAME_ATTRIBUTE, samlSettings.getGroupName().orElse("")); | |||
List<String> warnings = new ArrayList<>(); | |||
mappings.forEach((key, value) -> { | |||
if (!value.isEmpty() && (auth.getAttribute(value) == null || auth.getAttribute(value).isEmpty())) { | |||
warnings.add(String.format("Mapping not found for the property %s, the field %s is not available in the SAML response.", key, value)); | |||
} | |||
}); | |||
return warnings; | |||
} | |||
} |
@@ -0,0 +1,85 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 com.google.common.io.Resources; | |||
import java.io.IOException; | |||
import java.net.URL; | |||
import java.nio.charset.StandardCharsets; | |||
import java.util.ArrayList; | |||
import java.util.HashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import org.junit.Test; | |||
import static org.junit.Assert.assertEquals; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
import static org.sonar.auth.saml.SamlAuthStatusPageGenerator.getSamlAuthStatusHtml; | |||
public class SamlAuthStatusPageGeneratorTest { | |||
private final SamlAuthenticationStatus samlAuthenticationStatus = mock(SamlAuthenticationStatus.class); | |||
private static final String HTML_TEMPLATE_NAME = "samlAuthResultComplete.html"; | |||
private static final String EMPTY_HTML_TEMPLATE_NAME = "samlAuthResultEmpty.html"; | |||
@Test | |||
public void test_full_html_generation() { | |||
when(samlAuthenticationStatus.getStatus()).thenReturn("success"); | |||
when(samlAuthenticationStatus.getErrors()).thenReturn(List.of("error1", "error2 'with message'")); | |||
when(samlAuthenticationStatus.getWarnings()).thenReturn(List.of("warning1", "warning2 'with message'")); | |||
when(samlAuthenticationStatus.getAvailableAttributes()).thenReturn(Map.of("key1", List.of("value1", "value2 with weird chars \n\t\"\\"), "key2", List.of("value3", "value4"))); | |||
when(samlAuthenticationStatus.getMappedAttributes()).thenReturn(Map.of("key1", List.of("value1", "value2"), "key2", List.of("value3", "value4"))); | |||
String completeHtmlTemplate = getSamlAuthStatusHtml(samlAuthenticationStatus); | |||
String expectedTemplate = loadTemplateFromResources(HTML_TEMPLATE_NAME); | |||
assertEquals(expectedTemplate, completeHtmlTemplate); | |||
} | |||
@Test | |||
public void test_full_html_generation_with_empty_values() { | |||
when(samlAuthenticationStatus.getStatus()).thenReturn(null); | |||
when(samlAuthenticationStatus.getErrors()).thenReturn(new ArrayList<>()); | |||
when(samlAuthenticationStatus.getWarnings()).thenReturn(new ArrayList<>()); | |||
when(samlAuthenticationStatus.getAvailableAttributes()).thenReturn(new HashMap<>()); | |||
when(samlAuthenticationStatus.getMappedAttributes()).thenReturn(new HashMap<>()); | |||
String completeHtmlTemplate = getSamlAuthStatusHtml(samlAuthenticationStatus); | |||
String expectedTemplate = loadTemplateFromResources(EMPTY_HTML_TEMPLATE_NAME); | |||
assertEquals(expectedTemplate, completeHtmlTemplate); | |||
} | |||
private String loadTemplateFromResources(String templateName) { | |||
URL url = Resources.getResource(templateName); | |||
try { | |||
return Resources.toString(url, StandardCharsets.UTF_8); | |||
} catch (IOException e) { | |||
throw new IllegalStateException(e); | |||
} | |||
} | |||
} |
@@ -127,7 +127,7 @@ public class SamlIdentityProviderTest { | |||
assertThatThrownBy(() -> underTest.init(context)) | |||
.isInstanceOf(IllegalStateException.class) | |||
.hasMessage("Fail to create Auth"); | |||
.hasMessage("Failed to create a SAML Auth"); | |||
} | |||
@Test | |||
@@ -278,7 +278,7 @@ public class SamlIdentityProviderTest { | |||
assertThatThrownBy(() -> underTest.callback(callbackContext)) | |||
.isInstanceOf(IllegalStateException.class) | |||
.hasMessage("Fail to create Auth"); | |||
.hasMessage("Failed to create a SAML Auth"); | |||
} | |||
@Test |
@@ -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(16); | |||
assertThat(container.getAddedObjects()).hasSize(17); | |||
} | |||
} |
@@ -0,0 +1,186 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 com.onelogin.saml2.Auth; | |||
import java.util.ArrayList; | |||
import java.util.Collections; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import org.junit.Test; | |||
import org.sonar.api.config.PropertyDefinitions; | |||
import org.sonar.api.config.internal.MapSettings; | |||
import org.sonar.api.utils.System2; | |||
import static org.junit.Assert.assertEquals; | |||
import static org.junit.Assert.assertFalse; | |||
import static org.junit.Assert.assertTrue; | |||
import static org.mockito.Mockito.doThrow; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
import static org.sonar.auth.saml.SamlSettings.USER_EMAIL_ATTRIBUTE; | |||
import static org.sonar.auth.saml.SamlSettings.USER_LOGIN_ATTRIBUTE; | |||
import static org.sonar.auth.saml.SamlStatusChecker.getSamlAuthenticationStatus; | |||
public class SamlStatusCheckerTest { | |||
private static final String IDP_CERTIFICATE = "-----BEGIN CERTIFICATE-----MIIF5zCCA8+gAwIBAgIUIXv9OVs/XUicgR1bsV9uccYhHfowDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNVBAYTAkFVMQ8wDQYDVQQIDAZHRU5FVkExEDAOBgNVBAcMB1ZFUk5JRVIxDjAMBgNVBAoMBVNPTkFSMQ0wCwYDVQQLDARRVUJFMQ8wDQYDVQQDDAZaaXBlbmcxIDAeBgkqhkiG9w0BCQEWEW5vcmVwbHlAZ21haWwuY29tMB4XDTIyMDYxMzEzMTQyN1oXDTMyMDYxMDEzMTQyN1owgYIxCzAJBgNVBAYTAkFVMQ8wDQYDVQQIDAZHRU5FVkExEDAOBgNVBAcMB1ZFUk5JRVIxDjAMBgNVBAoMBVNPTkFSMQ0wCwYDVQQLDARRVUJFMQ8wDQYDVQQDDAZaaXBlbmcxIDAeBgkqhkiG9w0BCQEWEW5vcmVwbHlAZ21haWwuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAu3nFXYvIYedpR84aZkdo/3yB5XHM+YCFJcDsVO10zEblLknfQsiMPa1Xd9Ustnpxw6P/SyzIJmO9jiMOdeCeY98a74jP7d4JPaO6h3l9IbWAcYeijQg956nlsVFY3FHDGr+7Pb8QcOAyV3v89jiF9DFB8wXS+5UfYr2OfoRRb4li39ezDyDdl5OLlM11nEss2z1mEv+sUUloTcyrgj37Psgewkvyym6tFGSgkV9Za4SVRhHFyThY1VFrYZSJFTnapUYaRc7kMxzwX/AAHUDJrmYcaVc5B8ODp4w2AxDJheQyCVfXjPFaUqBMG2U/rYfVXu0Za7Pn/vUo4UaSThwCBKDehCwz+65TLdA+NxyGDxnvY/SksOyLLGCmu8tKkXdu0pznnIhBXEGvjUIVS7d6a/8geg91NoTWau3i0RF+Dw/5N9DSzpld15bPtb5Ce3Bie19uvfvuH9eg+D8x/hfF6f3il4sPlIKdO/OVdM28LRfmDqmqQNPudvbqz7xy4ARuxk6ARa4d+aT9zovpwvxNGTr7h1mdgOUtUCdIXL3SHNjdwdAAz0uCWzvExbFu+NQ+V5+Xnkx71hyPFv9+DLVGIu7JhdYs806wKshO13Nga38ig6gu37lpVhfpZXhKywUiigG6LXAeyWWkMk+vlf9McZdMBD16dZP4kTsvP+rPVnUCAwEAAaNTMFEwHQYDVR0OBBYEFI5UVLtTySvbGqH7UP8xTL4wxZq3MB8GA1UdIwQYMBaAFI5UVLtTySvbGqH7UP8xTL4wxZq3MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBABAtXsKNWx0sDDFA53qZ1zRyWKWAMoh95pawFCrKgTEW4ZrA73pa790eE1Y+vT6qUXKI4li9skIDa+6psCdxhZIrHPRAnVZVeB2373Bxr5bw/XQ8elRCjWeMULbYJ9tgsLV0I9CiEP0a6Tm8t0yDVXNUfx36E5fkgLSrxoRo8XJzxHbJCnLVXHdaNBxOT7jVcom6Wo4PB2bsjVzhHm6amn5hZp4dMHm0Mv0ln1wH8jVnizHQBLsGMzvvl58+9s1pP17ceRDkpNDz+EQyA+ZArqkW1MqtwVhbzz8QgMprhflKkArrsC7v06Jv8fqUbn9LvtYK9IwHTX7J8dFcsO/gUC5PevYT3nriN3Azb20ggSQ1yOEMozvj5T96S6itfHPit7vyEQ84JPrEqfuQDZQ/LKZQqfvuXX1aAG3TU3TMWB9VMMFsTuMFS8bfrhMX77g0Ud4qJcBOYOH3hR59agSdd2QZNLP3zZsYQHLLQkq94jdTXKTqm/w7mlPFKV59HjTbHBhTtxBHMft/mvvLEuC9KKFfAOXYQ6V+s9Nk0BW4ggEfewaX58OBuy7ISqRtRFPGia18YRzzHqkhjubJYMPkIfYpFVd+C0II3F0kdy8TtpccjyKo9bcHMLxO4n8PDAl195CPthMi8gUvT008LGEotr+3kXsouTEZTT0glXKLdO2W-----END CERTIFICATE-----"; | |||
private static final String SP_CERTIFICATE = "MIICoTCCAYkCBgGBXPscaDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlzb25hcnF1YmUwHhcNMjIwNjEzMTIxMTA5WhcNMzIwNjEzMTIxMjQ5WjAUMRIwEAYDVQQDDAlzb25hcnF1YmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDSFoT371C0/klZuPgvKbGItkmTaf5CweNXL8u389d98aOXRpDQ7maTXdV/W+VcL8vUWg8yG6nn8CRwweYnGTNdn9UAdhgknvxQe3pq3EwOJyls4Fpiq6YTh+DQfiZUQizjFjDOr/GG5O2lNvTRkI4XZj/XnWjRqVZwttiA5tm1sKkvGdyOQljwn4Jja/VbITdV8GASumx66Bil/wamSsqIzm2RjsOOGSsf5VjYUPwDobpuSf+j4DLtWjem/9vIzI2wcE30uC8LBAgO3JAlIS9NQrchjS9xhMJRohOoitaSPmqsOy7D2BH0h7XX6TNgv/WYTkBY4eZPao3PsL2A6AmhAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAMBmTHUK4w+DX21tmhqdwq0WqLH5ZAkwtiocDxFXiJ4GRrUWUh3BaXsgOHB8YYnNTDfScjaU0sZMEyfC0su1zsN8B7NFckg7RcZCHuBYdgIEAmvK4YM6s6zNsiKKwt66p2MNeL+o0acrT2rYjQ1L5QDj0gpfJQAT4N7xTZfuSc2iwjotaQfvcgsO8EZlcDVrL4UuyWLbuRUlSQjxHWGYaxCW+I3enK1+8fGpF3O+k9ZQ8xt5nJsalpsZvHcPLA4IBOmjsSHqSkhg4EIAWL/sJZ1KNct4hHh5kToUTu+Q6e949VeBkWgj4O+rcGDgiN2frGiEEc0EMv8KCSENRRRrO2k="; | |||
private static final String SP_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDSFoT371C0/klZuPgvKbGItkmTaf5CweNXL8u389d98aOXRpDQ7maTXdV/W+VcL8vUWg8yG6nn8CRwweYnGTNdn9UAdhgknvxQe3pq3EwOJyls4Fpiq6YTh+DQfiZUQizjFjDOr/GG5O2lNvTRkI4XZj/XnWjRqVZwttiA5tm1sKkvGdyOQljwn4Jja/VbITdV8GASumx66Bil/wamSsqIzm2RjsOOGSsf5VjYUPwDobpuSf+j4DLtWjem/9vIzI2wcE30uC8LBAgO3JAlIS9NQrchjS9xhMJRohOoitaSPmqsOy7D2BH0h7XX6TNgv/WYTkBY4eZPao3PsL2A6AmhAgMBAAECggEBAJj11HJAR96/leBBkFGmZaBIOGGgNoOcb023evfADhGgsZ8evamhKgX5t8w2uFPaaOl/eLje82Hvslh2lH+7FW8BRDBFy2Y+ay6d+I99PdLAKKUg5C4bE5v8vm6OqpGGbPAZ5AdYit3QKEa2MKG0QgA/bhQqg3rDdDA0sIWJjtF9MLv7LI7Tm0qgiHOKsI0MEBFk+ZoibgKWYh/dnfGDRWyC3Puqe13rdSheNJYUDR/0QMkd/EJNpLWv06uk+w8w2lU4RgN6TiV76ZZUIGZAAHFgMELJysgtBTCkOQY5roPu17OmMZjKfxngeIfNyd42q3/T6DmUbbwNYfP2HRMoiMECgYEA6SVc1mZ4ykytC9M61rZwT+2zXtJKudQVa0qpTtkf0aznRmnDOuc1bL7ewKIIIp9r5HKVteO6SKgpHmrP+qmvbwZ0Pz51Zg0MetoSmT9m0599/tOU2k6OI09dvQ4Xa3ccN5Czl61Q/HkMeAIDny8MrhGVBwhallE4J4fm/OjuVK0CgYEA5q6IVgqZtfcV1azIF6uOFt6blfn142zrwq0fF39jog2f+4jXaBKw6L4aP0HvIL83UArGppYY31894bLb6YL4EjS2JNbABM2VnJpJd4oGopOE42GCZlZRpf751zOptYAN23NFSujLlfaUfMbyrqIbRFC2DCdzNTU50GT5SAXX80UCgYEAlyvQvHwJCjMZaTd3SU1WGZ1o1qzIIyHvGXh5u1Rxm0TfWPquyfys2WwRhxoI6FoyXRgnFp8oZIAU2VIstL1dsUGgEnnvKVKAqw/HS3Keu80IpziNpdeVtjN59mGysc2zkBvVNx38Cxh6Cz5TFt4s/JkN5ld2VU0oeglWrtph3qkCgYALszZ/BrKdJBcba1QKv0zJpCjIBpGOI2whx54YFwH6qi4/F8W1JZ2LcHjsVG/IfWpUyPciY+KHEdGVrPiyc04Zvkquu6WpmLPJ6ZloUrvbaxgGYF+4yRADF1ecrqYg6onJY6NUFVKeHI+TdJPCf75aTK2vGCEjxbtU8ooiOQmm8QKBgEGe9ZdrwTP9rMQ35jYtzU+dT06k1r9BE9Q8CmrXl0HwK717ZWboX4J0YoFjxZC8PDsMl3p46MJ83rKbLU728uKig1AkZo7/OedxTWvezjZ1+lDyjC2EguXbgY1ecSC2HbJh9g+v8RUuhWxuA7RYoW92xVtKj+6l4vMadVP4Myp8-----END PRIVATE KEY-----"; | |||
private final MapSettings settings = new MapSettings(new PropertyDefinitions(System2.INSTANCE, SamlSettings.definitions())); | |||
private final Auth auth = mock(Auth.class); | |||
private SamlAuthenticationStatus samlAuthenticationStatus; | |||
@Test | |||
public void authentication_status_is_success_when_no_errors() { | |||
setSettings(); | |||
when(auth.getErrors()).thenReturn(new ArrayList<String>()); | |||
when(auth.getAttributes()).thenReturn(getEmptyAttributes()); | |||
samlAuthenticationStatus = getSamlAuthenticationStatus(auth, new SamlSettings(settings.asConfig())); | |||
assertEquals("success", samlAuthenticationStatus.getStatus()); | |||
assertTrue(samlAuthenticationStatus.getErrors().isEmpty()); | |||
} | |||
@Test | |||
public void authentication_status_is_unsuccessful_when_errors_are_reported() { | |||
setSettings(); | |||
when(auth.getErrors()).thenReturn(Collections.singletonList("Error in Authentication")); | |||
when(auth.getLastErrorReason()).thenReturn("Authentication failed due to a missing parameter."); | |||
when(auth.getAttributes()).thenReturn(getEmptyAttributes()); | |||
samlAuthenticationStatus = getSamlAuthenticationStatus(auth, new SamlSettings(settings.asConfig())); | |||
assertEquals("error", samlAuthenticationStatus.getStatus()); | |||
assertFalse(samlAuthenticationStatus.getErrors().isEmpty()); | |||
assertEquals(2, samlAuthenticationStatus.getErrors().size()); | |||
assertTrue(samlAuthenticationStatus.getErrors().contains("Authentication failed due to a missing parameter.")); | |||
assertTrue(samlAuthenticationStatus.getErrors().contains("Error in Authentication")); | |||
} | |||
@Test | |||
public void authentication_status_is_unsuccessful_when_processResponse_throws_exception() { | |||
setSettings(); | |||
try { | |||
doThrow(new Exception("Exception when processing the response")).when(auth).processResponse(); | |||
} catch (Exception e) { | |||
e.printStackTrace(); | |||
} | |||
when(auth.getAttributes()).thenReturn(getEmptyAttributes()); | |||
samlAuthenticationStatus = getSamlAuthenticationStatus(auth, new SamlSettings(settings.asConfig())); | |||
assertEquals("error", samlAuthenticationStatus.getStatus()); | |||
assertFalse(samlAuthenticationStatus.getErrors().isEmpty()); | |||
assertEquals(1, samlAuthenticationStatus.getErrors().size()); | |||
assertTrue(samlAuthenticationStatus.getErrors().contains("Exception when processing the response")); | |||
} | |||
@Test | |||
public void authentication_has_warnings_when_mappings_are_not_correct() { | |||
setSettings(); | |||
settings.setProperty(USER_LOGIN_ATTRIBUTE, "wrongLoginField"); | |||
settings.setProperty(USER_EMAIL_ATTRIBUTE, "wrongEmailField"); | |||
when(auth.getErrors()).thenReturn(new ArrayList<String>()); | |||
when(auth.getAttributes()).thenReturn(getResponseAttributes()); | |||
getResponseAttributes().forEach((key, value) -> when(auth.getAttribute(key)).thenReturn(value)); | |||
samlAuthenticationStatus = getSamlAuthenticationStatus(auth, new SamlSettings(settings.asConfig())); | |||
assertEquals("success", samlAuthenticationStatus.getStatus()); | |||
assertTrue(samlAuthenticationStatus.getErrors().isEmpty()); | |||
assertEquals(2, samlAuthenticationStatus.getWarnings().size()); | |||
assertTrue(samlAuthenticationStatus.getWarnings() | |||
.contains(String.format("Mapping not found for the property %s, the field %s is not available in the SAML response.", USER_LOGIN_ATTRIBUTE, "wrongLoginField"))); | |||
assertTrue(samlAuthenticationStatus.getWarnings() | |||
.contains(String.format("Mapping not found for the property %s, the field %s is not available in the SAML response.", USER_EMAIL_ATTRIBUTE, "wrongEmailField"))); | |||
} | |||
@Test | |||
public void authentication_has_no_warnings_when_optional_mappings_are_not_provided() { | |||
setSettings(); | |||
settings.setProperty(USER_EMAIL_ATTRIBUTE, (String) null); | |||
when(auth.getErrors()).thenReturn(new ArrayList<String>()); | |||
when(auth.getAttributes()).thenReturn(getResponseAttributes()); | |||
getResponseAttributes().forEach((key, value) -> when(auth.getAttribute(key)).thenReturn(value)); | |||
samlAuthenticationStatus = getSamlAuthenticationStatus(auth, new SamlSettings(settings.asConfig())); | |||
assertEquals("success", samlAuthenticationStatus.getStatus()); | |||
assertTrue(samlAuthenticationStatus.getErrors().isEmpty()); | |||
assertTrue(samlAuthenticationStatus.getWarnings().isEmpty()); | |||
} | |||
@Test | |||
public void mapped_attributes_are_complete_when_mapping_fields_are_correct() { | |||
setSettings(); | |||
when(auth.getErrors()).thenReturn(new ArrayList<String>()); | |||
when(auth.getAttributes()).thenReturn(getResponseAttributes()); | |||
getResponseAttributes().forEach((key, value) -> when(auth.getAttribute(key)).thenReturn(value)); | |||
samlAuthenticationStatus = getSamlAuthenticationStatus(auth, new SamlSettings(settings.asConfig())); | |||
assertEquals("success", samlAuthenticationStatus.getStatus()); | |||
assertTrue(samlAuthenticationStatus.getErrors().isEmpty()); | |||
assertTrue(samlAuthenticationStatus.getWarnings().isEmpty()); | |||
assertEquals(4, samlAuthenticationStatus.getAvailableAttributes().size()); | |||
assertEquals(4, samlAuthenticationStatus.getMappedAttributes().size()); | |||
assertTrue(samlAuthenticationStatus.getAvailableAttributes().keySet().containsAll(Set.of("login", "name", "email", "groups"))); | |||
assertTrue(samlAuthenticationStatus.getMappedAttributes().keySet().containsAll(Set.of("User login value", "User name value", "User email value", "Groups value"))); | |||
} | |||
private void setSettings() { | |||
settings.setProperty("sonar.auth.saml.applicationId", "MyApp"); | |||
settings.setProperty("sonar.auth.saml.providerId", "http://localhost:8080/auth/realms/sonarqube"); | |||
settings.setProperty("sonar.auth.saml.loginUrl", "http://localhost:8080/auth/realms/sonarqube/protocol/saml"); | |||
settings.setProperty("sonar.auth.saml.certificate.secured", IDP_CERTIFICATE); | |||
settings.setProperty("sonar.auth.saml.sp.privateKey.secured", SP_PRIVATE_KEY); | |||
settings.setProperty("sonar.auth.saml.sp.certificate.secured", SP_CERTIFICATE); | |||
settings.setProperty("sonar.auth.saml.user.login", "login"); | |||
settings.setProperty("sonar.auth.saml.user.name", "name"); | |||
settings.setProperty("sonar.auth.saml.user.email", "email"); | |||
settings.setProperty("sonar.auth.saml.group.name", "groups"); | |||
} | |||
private Map<String, List<String>> getResponseAttributes() { | |||
return Map.of( | |||
"login", Collections.singletonList("loginId"), | |||
"name", Collections.singletonList("userName"), | |||
"email", Collections.singletonList("user@sonar.com"), | |||
"groups", List.of("group1", "group2")); | |||
} | |||
private Map<String, List<String>> getEmptyAttributes() { | |||
return Map.of( | |||
"login", Collections.singletonList(""), | |||
"name", Collections.singletonList(""), | |||
"email", Collections.singletonList(""), | |||
"groups", Collections.singletonList("")); | |||
} | |||
} |
@@ -0,0 +1 @@ | |||
mock-maker-inline |
@@ -0,0 +1,208 @@ | |||
<!DOCTYPE html> | |||
<html lang="en"> | |||
<head> | |||
<meta http-equiv="content-type" content="text/html; charset=UTF-8" charset="UTF-8" /> | |||
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> | |||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> | |||
<link rel="apple-touch-icon" sizes="57x57" href="/apple-touch-icon-57x57.png" /> | |||
<link rel="apple-touch-icon" sizes="60x60" href="/apple-touch-icon-60x60.png" /> | |||
<link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-72x72.png" /> | |||
<link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon-76x76.png" /> | |||
<link | |||
rel="apple-touch-icon" | |||
sizes="114x114" | |||
href="/apple-touch-icon-114x114.png" | |||
/> | |||
<link | |||
rel="apple-touch-icon" | |||
sizes="120x120" | |||
href="/apple-touch-icon-120x120.png" | |||
/> | |||
<link | |||
rel="apple-touch-icon" | |||
sizes="144x144" | |||
href="/apple-touch-icon-144x144.png" | |||
/> | |||
<link | |||
rel="apple-touch-icon" | |||
sizes="152x152" | |||
href="/apple-touch-icon-152x152.png" | |||
/> | |||
<link | |||
rel="apple-touch-icon" | |||
sizes="180x180" | |||
href="/apple-touch-icon-180x180.png" | |||
/> | |||
<link rel="icon" type="image/x-icon" href="/favicon.ico" /> | |||
<meta name="application-name" content="SonarQube" /> | |||
<meta name="msapplication-TileColor" content="#FFFFFF" /> | |||
<meta name="msapplication-TileImage" content="/mstile-512x512.png" /> | |||
<title>SAML Authentication Test</title> | |||
<style> | |||
body { | |||
background-color: #f3f3f3; | |||
} | |||
h1 { | |||
margin: 0 8px 8px; | |||
} | |||
h2 { | |||
margin: 0 0 8px; | |||
} | |||
ul { | |||
list-style: none; | |||
margin: 0 8px; | |||
padding: 0; | |||
} | |||
li + li { | |||
padding-top: 12px; | |||
margin-top: 12px; | |||
border-top: 1px solid rgba(150, 150, 150, 0.5); | |||
} | |||
table { | |||
border-collapse: collapse; | |||
} | |||
tr:nth-child(2n) { | |||
background-color: #e6e6e6; | |||
} | |||
td { | |||
border: 1px solid #a3a3a3; | |||
padding: 4px 24px 4px 8px; | |||
vertical-align: top; | |||
font-family: "Courier New", Courier, monospace; | |||
} | |||
#content { | |||
padding: 16px; | |||
} | |||
.box { | |||
padding: 8px; | |||
margin: 8px; | |||
border: 1px solid #e6e6e6; | |||
background-color: white; | |||
box-shadow: 2px 2px 3px 0px rgba(0, 0, 0, 0.5); | |||
} | |||
#status { | |||
padding: 16px 8px; | |||
font-size: large; | |||
color: white; | |||
} | |||
.error { | |||
background-color: #d02f3a; | |||
} | |||
.success { | |||
background-color: #008a25; | |||
} | |||
</style> | |||
</head> | |||
<body> | |||
<div id="content"> | |||
<h1>SAML Authentication Test</h1> | |||
<div class="box"> | |||
<div id="status"></div> | |||
</div> | |||
</div> | |||
<script> | |||
function createBox() { | |||
const box = document.createElement("div"); | |||
box.className = "box"; | |||
return box; | |||
} | |||
function createSectionTitle(title) { | |||
const node = document.createElement("h2"); | |||
node.innerText = title; | |||
return node; | |||
} | |||
function createList(arr, className = "") { | |||
const list = document.createElement("ul"); | |||
arr.forEach((item) => { | |||
const message = document.createElement("li"); | |||
message.className = className; | |||
message.innerText = item; | |||
list.appendChild(message); | |||
}); | |||
return list; | |||
} | |||
function createTable(obj) { | |||
const table = document.createElement("table"); | |||
const tbody = document.createElement("tbody"); | |||
table.appendChild(tbody); | |||
Object.keys(obj).forEach((key) => { | |||
const row = document.createElement("tr"); | |||
const keyNode = document.createElement("td"); | |||
keyNode.innerText = key; | |||
row.appendChild(keyNode); | |||
const valueNode = document.createElement("td"); | |||
// wrap in array, to handle single values as well | |||
valueNode.innerHTML = [].concat(obj[key]).join("<br />"); | |||
row.appendChild(valueNode); | |||
tbody.appendChild(row); | |||
}); | |||
return table; | |||
} | |||
function addSection(container, title, contents) { | |||
const box = createBox(); | |||
box.appendChild(createSectionTitle(title)); | |||
box.appendChild(contents); | |||
container.appendChild(box); | |||
} | |||
const status = 'success'; | |||
const attributes = {"key1":["value1","value2 with weird chars \n\t\"\\"],"key2":["value3","value4"]}; | |||
const mappings = {"key1":["value1","value2"],"key2":["value3","value4"]}; | |||
const errors = ['error1','error2 \'with message\'']; | |||
const warnings = ['warning1','warning2 \'with message\'']; | |||
// Switch status class | |||
const statusNode = document.querySelector("#status"); | |||
statusNode.classList.add(status); | |||
statusNode.innerText = status; | |||
// generate content | |||
const container = document.querySelector("#content"); | |||
if (warnings && warnings.length > 0) { | |||
addSection(container, "Warnings", createList(warnings)); | |||
} | |||
if (status === "error" && errors && errors.length > 0) { | |||
addSection(container, "Errors", createList(errors)); | |||
} | |||
if (status === "success") { | |||
if (attributes && Object.keys(attributes).length > 0) { | |||
addSection(container, "Available attributes", createTable(attributes)); | |||
} | |||
if (mappings && Object.keys(mappings).length > 0) { | |||
addSection(container, "Attribute mappings", createTable(mappings)); | |||
} | |||
} | |||
</script> | |||
</body> | |||
</html> |
@@ -0,0 +1,208 @@ | |||
<!DOCTYPE html> | |||
<html lang="en"> | |||
<head> | |||
<meta http-equiv="content-type" content="text/html; charset=UTF-8" charset="UTF-8" /> | |||
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> | |||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> | |||
<link rel="apple-touch-icon" sizes="57x57" href="/apple-touch-icon-57x57.png" /> | |||
<link rel="apple-touch-icon" sizes="60x60" href="/apple-touch-icon-60x60.png" /> | |||
<link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-72x72.png" /> | |||
<link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon-76x76.png" /> | |||
<link | |||
rel="apple-touch-icon" | |||
sizes="114x114" | |||
href="/apple-touch-icon-114x114.png" | |||
/> | |||
<link | |||
rel="apple-touch-icon" | |||
sizes="120x120" | |||
href="/apple-touch-icon-120x120.png" | |||
/> | |||
<link | |||
rel="apple-touch-icon" | |||
sizes="144x144" | |||
href="/apple-touch-icon-144x144.png" | |||
/> | |||
<link | |||
rel="apple-touch-icon" | |||
sizes="152x152" | |||
href="/apple-touch-icon-152x152.png" | |||
/> | |||
<link | |||
rel="apple-touch-icon" | |||
sizes="180x180" | |||
href="/apple-touch-icon-180x180.png" | |||
/> | |||
<link rel="icon" type="image/x-icon" href="/favicon.ico" /> | |||
<meta name="application-name" content="SonarQube" /> | |||
<meta name="msapplication-TileColor" content="#FFFFFF" /> | |||
<meta name="msapplication-TileImage" content="/mstile-512x512.png" /> | |||
<title>SAML Authentication Test</title> | |||
<style> | |||
body { | |||
background-color: #f3f3f3; | |||
} | |||
h1 { | |||
margin: 0 8px 8px; | |||
} | |||
h2 { | |||
margin: 0 0 8px; | |||
} | |||
ul { | |||
list-style: none; | |||
margin: 0 8px; | |||
padding: 0; | |||
} | |||
li + li { | |||
padding-top: 12px; | |||
margin-top: 12px; | |||
border-top: 1px solid rgba(150, 150, 150, 0.5); | |||
} | |||
table { | |||
border-collapse: collapse; | |||
} | |||
tr:nth-child(2n) { | |||
background-color: #e6e6e6; | |||
} | |||
td { | |||
border: 1px solid #a3a3a3; | |||
padding: 4px 24px 4px 8px; | |||
vertical-align: top; | |||
font-family: "Courier New", Courier, monospace; | |||
} | |||
#content { | |||
padding: 16px; | |||
} | |||
.box { | |||
padding: 8px; | |||
margin: 8px; | |||
border: 1px solid #e6e6e6; | |||
background-color: white; | |||
box-shadow: 2px 2px 3px 0px rgba(0, 0, 0, 0.5); | |||
} | |||
#status { | |||
padding: 16px 8px; | |||
font-size: large; | |||
color: white; | |||
} | |||
.error { | |||
background-color: #d02f3a; | |||
} | |||
.success { | |||
background-color: #008a25; | |||
} | |||
</style> | |||
</head> | |||
<body> | |||
<div id="content"> | |||
<h1>SAML Authentication Test</h1> | |||
<div class="box"> | |||
<div id="status"></div> | |||
</div> | |||
</div> | |||
<script> | |||
function createBox() { | |||
const box = document.createElement("div"); | |||
box.className = "box"; | |||
return box; | |||
} | |||
function createSectionTitle(title) { | |||
const node = document.createElement("h2"); | |||
node.innerText = title; | |||
return node; | |||
} | |||
function createList(arr, className = "") { | |||
const list = document.createElement("ul"); | |||
arr.forEach((item) => { | |||
const message = document.createElement("li"); | |||
message.className = className; | |||
message.innerText = item; | |||
list.appendChild(message); | |||
}); | |||
return list; | |||
} | |||
function createTable(obj) { | |||
const table = document.createElement("table"); | |||
const tbody = document.createElement("tbody"); | |||
table.appendChild(tbody); | |||
Object.keys(obj).forEach((key) => { | |||
const row = document.createElement("tr"); | |||
const keyNode = document.createElement("td"); | |||
keyNode.innerText = key; | |||
row.appendChild(keyNode); | |||
const valueNode = document.createElement("td"); | |||
// wrap in array, to handle single values as well | |||
valueNode.innerHTML = [].concat(obj[key]).join("<br />"); | |||
row.appendChild(valueNode); | |||
tbody.appendChild(row); | |||
}); | |||
return table; | |||
} | |||
function addSection(container, title, contents) { | |||
const box = createBox(); | |||
box.appendChild(createSectionTitle(title)); | |||
box.appendChild(contents); | |||
container.appendChild(box); | |||
} | |||
const status = ''; | |||
const attributes = {}; | |||
const mappings = {}; | |||
const errors = []; | |||
const warnings = []; | |||
// Switch status class | |||
const statusNode = document.querySelector("#status"); | |||
statusNode.classList.add(status); | |||
statusNode.innerText = status; | |||
// generate content | |||
const container = document.querySelector("#content"); | |||
if (warnings && warnings.length > 0) { | |||
addSection(container, "Warnings", createList(warnings)); | |||
} | |||
if (status === "error" && errors && errors.length > 0) { | |||
addSection(container, "Errors", createList(errors)); | |||
} | |||
if (status === "success") { | |||
if (attributes && Object.keys(attributes).length > 0) { | |||
addSection(container, "Available attributes", createTable(attributes)); | |||
} | |||
if (mappings && Object.keys(mappings).length > 0) { | |||
addSection(container, "Attribute mappings", createTable(mappings)); | |||
} | |||
} | |||
</script> | |||
</body> | |||
</html> |
@@ -22,6 +22,9 @@ dependencies { | |||
compile project(':server:sonar-webserver-ws') | |||
compile project(':server:sonar-webserver-pushapi') | |||
compile project(':server:sonar-alm-client') | |||
compile (project(':server:sonar-auth-saml')) { | |||
exclude group:'org.apache.santuario' | |||
} | |||
compile project(':sonar-scanner-protocol') | |||
compileOnly 'com.google.code.findbugs:jsr305' |
@@ -0,0 +1,71 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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.server.saml.ws; | |||
import java.io.IOException; | |||
import javax.servlet.FilterChain; | |||
import javax.servlet.ServletException; | |||
import javax.servlet.ServletRequest; | |||
import javax.servlet.ServletResponse; | |||
import javax.servlet.http.HttpServletRequest; | |||
import javax.servlet.http.HttpServletResponse; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.api.web.ServletFilter; | |||
import org.sonar.auth.saml.SamlAuthenticator; | |||
import org.sonar.auth.saml.SamlIdentityProvider; | |||
import org.sonar.server.authentication.OAuth2ContextFactory; | |||
import org.sonar.server.ws.ServletFilterHandler; | |||
public class SamlValidationInitAction extends ServletFilter implements SamlAction { | |||
public static final String VALIDATION_RELAY_STATE = "validation-query"; | |||
private final SamlAuthenticator samlAuthenticator; | |||
private final OAuth2ContextFactory oAuth2ContextFactory; | |||
public SamlValidationInitAction(SamlAuthenticator samlAuthenticator, OAuth2ContextFactory oAuth2ContextFactory) { | |||
this.samlAuthenticator = samlAuthenticator; | |||
this.oAuth2ContextFactory = oAuth2ContextFactory; | |||
} | |||
@Override | |||
public UrlPattern doGetPattern() { | |||
return UrlPattern.create("/api/saml/validation_init"); | |||
} | |||
@Override | |||
public void define(WebService.NewController controller) { | |||
controller | |||
.createAction("validation_init") | |||
.setInternal(true) | |||
.setPost(false) | |||
.setHandler(ServletFilterHandler.INSTANCE) | |||
.setDescription("Initiate a SAML request to the identity Provider for configuration validation purpose.") | |||
.setSince("9.7"); | |||
} | |||
@Override | |||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { | |||
HttpServletRequest request = (HttpServletRequest) servletRequest; | |||
HttpServletResponse response = (HttpServletResponse) servletResponse; | |||
samlAuthenticator.initLogin(oAuth2ContextFactory.generateCallbackUrl(SamlIdentityProvider.KEY), | |||
VALIDATION_RELAY_STATE, request, response); | |||
} | |||
} |
@@ -0,0 +1,91 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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.server.saml.ws; | |||
import java.io.IOException; | |||
import javax.servlet.FilterChain; | |||
import javax.servlet.ServletException; | |||
import javax.servlet.http.HttpServletRequest; | |||
import javax.servlet.http.HttpServletResponse; | |||
import org.junit.Before; | |||
import org.junit.Test; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.auth.saml.SamlAuthenticator; | |||
import org.sonar.server.authentication.OAuth2ContextFactory; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.ArgumentMatchers.any; | |||
import static org.mockito.ArgumentMatchers.anyString; | |||
import static org.mockito.ArgumentMatchers.matches; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.verify; | |||
import static org.mockito.Mockito.when; | |||
public class SamlValidationInitActionTest { | |||
private SamlValidationInitAction underTest; | |||
private SamlAuthenticator samlAuthenticator; | |||
private OAuth2ContextFactory oAuth2ContextFactory; | |||
@Before | |||
public void setUp() throws Exception { | |||
samlAuthenticator = mock(SamlAuthenticator.class); | |||
oAuth2ContextFactory = mock(OAuth2ContextFactory.class); | |||
underTest = new SamlValidationInitAction(samlAuthenticator, oAuth2ContextFactory); | |||
} | |||
@Test | |||
public void do_get_pattern() { | |||
assertThat(underTest.doGetPattern().matches("/api/saml/validation_init")).isTrue(); | |||
assertThat(underTest.doGetPattern().matches("/api/saml")).isFalse(); | |||
assertThat(underTest.doGetPattern().matches("/api/saml/validation_init2")).isFalse(); | |||
} | |||
@Test | |||
public void do_filter() throws IOException, ServletException { | |||
HttpServletRequest servletRequest = mock(HttpServletRequest.class); | |||
HttpServletResponse servletResponse = mock(HttpServletResponse.class); | |||
FilterChain filterChain = mock(FilterChain.class); | |||
String callbackUrl = "http://localhost:9000/api/validation_test"; | |||
when(oAuth2ContextFactory.generateCallbackUrl(anyString())) | |||
.thenReturn(callbackUrl); | |||
underTest.doFilter(servletRequest, servletResponse, filterChain); | |||
verify(samlAuthenticator).initLogin(matches(callbackUrl), | |||
matches(SamlValidationInitAction.VALIDATION_RELAY_STATE), | |||
any(), any()); | |||
} | |||
@Test | |||
public void verify_definition() { | |||
String controllerKey = "foo"; | |||
WebService.Context context = new WebService.Context(); | |||
WebService.NewController newController = context.createController(controllerKey); | |||
underTest.define(newController); | |||
newController.done(); | |||
WebService.Action validationInitAction = context.controller(controllerKey).action("validation_init"); | |||
assertThat(validationInitAction).isNotNull(); | |||
assertThat(validationInitAction.description()).isNotEmpty(); | |||
assertThat(validationInitAction.handler()).isNotNull(); | |||
} | |||
} |