mirror of
https://github.com/SonarSource/sonarqube.git
synced 2024-08-19 02:10:24 +02:00
SONAR-17296 build the callback response for displaying the SAML Authentication status.
This commit is contained in:
parent
934ad1652a
commit
492c498c74
@ -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";
|
||||
public static final String KEY = "saml";
|
||||
|
||||
private static final Logger LOGGER = Loggers.get(SamlIdentityProvider.class);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user