@@ -166,11 +166,57 @@ qa_task: | |||
path: "**/test-results/**/*.xml" | |||
format: junit | |||
# SAML QA is executed in a dedicated task in order to not slow down the pipeline, as a Keycloak server docker image is required. | |||
qa_saml_task: | |||
depends_on: build | |||
# Comment the following line and commit with message "DO NOT MERGE" in order to run | |||
# this task on your branch | |||
only_if: $CIRRUS_BRANCH == "branch-nightly-build" | |||
gke_container: | |||
dockerfile: private/docker/Dockerfile-build | |||
builder_image_project: ci-cd-215716 | |||
builder_image_name: docker-builder-v1 | |||
cluster_name: cirrus-uscentral1a-cluster | |||
zone: us-central1-a | |||
namespace: default | |||
cpu: 2.4 | |||
memory: 10Gb | |||
additional_containers: | |||
- name: keycloak | |||
image: jboss/keycloak:7.0.0 | |||
port: 8080 | |||
cpu: 1 | |||
memory: 1Gb | |||
env: | |||
KEYCLOAK_USER: admin | |||
KEYCLOAK_PASSWORD: admin | |||
env: | |||
# No need to clone the full history. | |||
# Depth of 1 is not enough because it would fail the build in case of consecutive pushes | |||
# (example of error: "Hard resetting to c968ecaf7a1942dacecd78480b3751ac74d53c33...Failed to force reset to c968ecaf7a1942dacecd78480b3751ac74d53c33: object not found!") | |||
CIRRUS_CLONE_DEPTH: 50 | |||
QA_CATEGORY: SAML | |||
gradle_cache: | |||
folder: ~/.gradle/caches | |||
script: | |||
- ./private/cirrus/cirrus-qa.sh h2 | |||
cleanup_before_cache_script: | |||
- ./private/cirrus/cleanup-gradle-cache.sh | |||
on_failure: | |||
reports_artifacts: | |||
path: "**/build/reports/**/*" | |||
screenshots_artifacts: | |||
path: "**/build/screenshots/**/*" | |||
junit_artifacts: | |||
path: "**/test-results/**/*.xml" | |||
format: junit | |||
promote_task: | |||
depends_on: | |||
- build | |||
- validate | |||
- qa | |||
- qa_saml | |||
only_if: $CIRRUS_BRANCH !=~ "dogfood/.*" && $CIRRUS_BRANCH != "public_master" && $CIRRUS_BRANCH != "branch-nightly-build" | |||
gke_container: | |||
dockerfile: private/docker/Dockerfile-build |
@@ -14,7 +14,6 @@ dependencies { | |||
testCompile 'commons-lang:commons-lang' | |||
testCompile 'com.squareup.okhttp3:mockwebserver' | |||
testCompile 'com.squareup.okhttp3:okhttp' | |||
testCompile 'junit:junit' | |||
testCompile 'org.assertj:assertj-core' | |||
testCompile 'org.mockito:mockito-core' |
@@ -16,7 +16,6 @@ dependencies { | |||
compileOnly 'com.squareup.okhttp3:okhttp' | |||
compileOnly 'javax.servlet:javax.servlet-api' | |||
compileOnly project(':sonar-core') | |||
compileOnly project(':sonar-ws') | |||
testCompile 'com.squareup.okhttp3:mockwebserver' | |||
testCompile 'com.squareup.okhttp3:okhttp' |
@@ -16,7 +16,6 @@ dependencies { | |||
compileOnly 'com.squareup.okhttp3:okhttp' | |||
compileOnly 'javax.servlet:javax.servlet-api' | |||
compileOnly project(':sonar-core') | |||
compileOnly project(':sonar-ws') | |||
testCompile 'com.squareup.okhttp3:mockwebserver' | |||
testCompile 'com.squareup.okhttp3:okhttp' |
@@ -0,0 +1,26 @@ | |||
description = 'SonarQube :: Authentication :: SAML' | |||
configurations { | |||
testCompile.extendsFrom compileOnly | |||
} | |||
ext { | |||
oneLoginVersion = '2.5.0' | |||
} | |||
dependencies { | |||
// please keep the list ordered | |||
compile "com.onelogin:java-saml:${oneLoginVersion}" | |||
compile "com.onelogin:java-saml-core:${oneLoginVersion}" | |||
compileOnly 'com.google.code.findbugs:jsr305' | |||
compileOnly 'com.squareup.okhttp3:okhttp' | |||
compileOnly 'javax.servlet:javax.servlet-api' | |||
compileOnly project(':sonar-core') | |||
testCompile 'com.tngtech.java:junit-dataprovider' | |||
testCompile 'junit:junit' | |||
testCompile 'org.assertj:assertj-core' | |||
testCompile 'org.mockito:mockito-core' | |||
} |
@@ -0,0 +1,191 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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.Set; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import javax.servlet.http.HttpServletRequest; | |||
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 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"; | |||
private final SamlSettings samlSettings; | |||
public SamlIdentityProvider(SamlSettings samlSettings) { | |||
this.samlSettings = samlSettings; | |||
} | |||
@Override | |||
public String getKey() { | |||
return KEY; | |||
} | |||
@Override | |||
public String getName() { | |||
return samlSettings.getProviderName(); | |||
} | |||
@Override | |||
public Display getDisplay() { | |||
return Display.builder() | |||
.setIconPath("/images/saml.png") | |||
.setBackgroundColor("#444444") | |||
.build(); | |||
} | |||
@Override | |||
public boolean isEnabled() { | |||
return samlSettings.isEnabled(); | |||
} | |||
@Override | |||
public boolean allowsUsersToSignUp() { | |||
return true; | |||
} | |||
@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); | |||
} | |||
} | |||
@Override | |||
public void callback(CallbackContext context) { | |||
Auth auth = newAuth(initSettings(null), context.getRequest(), context.getResponse()); | |||
processResponse(auth); | |||
context.verifyCsrfState(STATE_REQUEST_PARAMETER); | |||
LOGGER.trace("Name ID : {}", auth.getNameId()); | |||
checkAuthentication(auth); | |||
LOGGER.trace("Attributes received : {}", auth.getAttributes()); | |||
String login = getNonNullFirstAttribute(auth, samlSettings.getUserLogin()); | |||
UserIdentity.Builder userIdentityBuilder = UserIdentity.builder() | |||
.setLogin(login) | |||
.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()); | |||
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<>(); | |||
// TODO strict mode is unfortunately not compatible with HTTPS configuration on reverse proxy => | |||
// https://jira.sonarsource.com/browse/SQAUTHSAML-8 | |||
samlData.put("onelogin.saml2.strict", false); | |||
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()); | |||
samlData.put("onelogin.saml2.sp.entityid", samlSettings.getApplicationId()); | |||
// 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); | |||
SettingsBuilder builder = new SettingsBuilder(); | |||
return builder | |||
.fromValues(samlData) | |||
.build(); | |||
} | |||
} |
@@ -0,0 +1,37 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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.List; | |||
import org.sonar.api.config.PropertyDefinition; | |||
import org.sonar.core.platform.Module; | |||
public class SamlModule extends Module { | |||
@Override | |||
protected void configureModule() { | |||
add( | |||
SamlIdentityProvider.class, | |||
SamlSettings.class); | |||
List<PropertyDefinition> definitions = SamlSettings.definitions(); | |||
add(definitions.toArray(new Object[definitions.size()])); | |||
} | |||
} |
@@ -0,0 +1,181 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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.Arrays; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import org.sonar.api.config.Configuration; | |||
import org.sonar.api.config.PropertyDefinition; | |||
import org.sonar.api.server.ServerSide; | |||
import static java.lang.String.valueOf; | |||
import static org.sonar.api.PropertyType.BOOLEAN; | |||
@ServerSide | |||
public class SamlSettings { | |||
private static final String ENABLED = "sonar.auth.saml.enabled"; | |||
private static final String PROVIDER_ID = "sonar.auth.saml.providerId"; | |||
private static final String PROVIDER_NAME = "sonar.auth.saml.providerName"; | |||
private static final String APPLICATION_ID = "sonar.auth.saml.applicationId"; | |||
private static final String LOGIN_URL = "sonar.auth.saml.loginUrl"; | |||
private static final String CERTIFICATE = "sonar.auth.saml.certificate.secured"; | |||
private static final String USER_LOGIN_ATTRIBUTE = "sonar.auth.saml.user.login"; | |||
private static final String USER_NAME_ATTRIBUTE = "sonar.auth.saml.user.name"; | |||
private static final String USER_EMAIL_ATTRIBUTE = "sonar.auth.saml.user.email"; | |||
private static final String GROUP_NAME_ATTRIBUTE = "sonar.auth.saml.group.name"; | |||
private static final String CATEGORY = "security"; | |||
private static final String SUBCATEGORY = "saml"; | |||
private final Configuration configuration; | |||
public SamlSettings(Configuration configuration) { | |||
this.configuration = configuration; | |||
} | |||
String getProviderId() { | |||
return configuration.get(PROVIDER_ID).orElseThrow(() -> new IllegalArgumentException("Provider ID is missing")); | |||
} | |||
String getProviderName() { | |||
return configuration.get(PROVIDER_NAME).orElseThrow(() -> new IllegalArgumentException("Provider Name is missing")); | |||
} | |||
String getApplicationId() { | |||
return configuration.get(APPLICATION_ID).orElseThrow(() -> new IllegalArgumentException("Application ID is missing")); | |||
} | |||
String getLoginUrl() { | |||
return configuration.get(LOGIN_URL).orElseThrow(() -> new IllegalArgumentException("Login URL is missing")); | |||
} | |||
String getCertificate() { | |||
return configuration.get(CERTIFICATE).orElseThrow(() -> new IllegalArgumentException("Certificate is missing")); | |||
} | |||
String getUserLogin() { | |||
return configuration.get(USER_LOGIN_ATTRIBUTE).orElseThrow(() -> new IllegalArgumentException("User login attribute is missing")); | |||
} | |||
String getUserName() { | |||
return configuration.get(USER_NAME_ATTRIBUTE).orElseThrow(() -> new IllegalArgumentException("User name attribute is missing")); | |||
} | |||
Optional<String> getUserEmail() { | |||
return configuration.get(USER_EMAIL_ATTRIBUTE); | |||
} | |||
Optional<String> getGroupName() { | |||
return configuration.get(GROUP_NAME_ATTRIBUTE); | |||
} | |||
boolean isEnabled() { | |||
return configuration.getBoolean(ENABLED).orElse(false) && | |||
configuration.get(PROVIDER_ID).isPresent() && | |||
configuration.get(APPLICATION_ID).isPresent() && | |||
configuration.get(LOGIN_URL).isPresent() && | |||
configuration.get(CERTIFICATE).isPresent() && | |||
configuration.get(USER_LOGIN_ATTRIBUTE).isPresent() && | |||
configuration.get(USER_NAME_ATTRIBUTE).isPresent(); | |||
} | |||
static List<PropertyDefinition> definitions() { | |||
return Arrays.asList( | |||
PropertyDefinition.builder(ENABLED) | |||
.name("Enabled") | |||
.description("Enable SAML users to login. Value is ignored if provider ID, login url, certificate, login, name attributes are not defined.") | |||
.category(CATEGORY) | |||
.subCategory(SUBCATEGORY) | |||
.type(BOOLEAN) | |||
.defaultValue(valueOf(false)) | |||
.index(1) | |||
.build(), | |||
PropertyDefinition.builder(APPLICATION_ID) | |||
.name("Application ID") | |||
.description("Identifier of the application.") | |||
.defaultValue("sonarqube") | |||
.category(CATEGORY) | |||
.subCategory(SUBCATEGORY) | |||
.index(2) | |||
.build(), | |||
PropertyDefinition.builder(PROVIDER_NAME) | |||
.name("Provider Name") | |||
.description("Name displayed for the provider in the login page.") | |||
.defaultValue("SAML") | |||
.category(CATEGORY) | |||
.subCategory(SUBCATEGORY) | |||
.index(3) | |||
.build(), | |||
PropertyDefinition.builder(PROVIDER_ID) | |||
.name("Provider ID") | |||
.description("Identifier of the identity provider, the entity that provides SAML authentication.") | |||
.category(CATEGORY) | |||
.subCategory(SUBCATEGORY) | |||
.index(4) | |||
.build(), | |||
PropertyDefinition.builder(LOGIN_URL) | |||
.name("SAML login url") | |||
.description("SAML login URL for the identity provider.") | |||
.category(CATEGORY) | |||
.subCategory(SUBCATEGORY) | |||
.index(5) | |||
.build(), | |||
PropertyDefinition.builder(CERTIFICATE) | |||
.name("Provider certificate") | |||
.description("X.509 certificate for the identity provider.") | |||
.category(CATEGORY) | |||
.subCategory(SUBCATEGORY) | |||
.index(6) | |||
.build(), | |||
PropertyDefinition.builder(USER_LOGIN_ATTRIBUTE) | |||
.name("SAML user login attribute") | |||
.description("Attribute defining the user login in SAML.") | |||
.category(CATEGORY) | |||
.subCategory(SUBCATEGORY) | |||
.index(7) | |||
.build(), | |||
PropertyDefinition.builder(USER_NAME_ATTRIBUTE) | |||
.name("SAML user name attribute") | |||
.description("Attribute defining the user name in SAML.") | |||
.category(CATEGORY) | |||
.subCategory(SUBCATEGORY) | |||
.index(8) | |||
.build(), | |||
PropertyDefinition.builder(USER_EMAIL_ATTRIBUTE) | |||
.name("SAML user email attribute") | |||
.description("Attribute defining the user email in SAML.") | |||
.category(CATEGORY) | |||
.subCategory(SUBCATEGORY) | |||
.index(9) | |||
.build(), | |||
PropertyDefinition.builder(GROUP_NAME_ATTRIBUTE) | |||
.name("SAML group attribute") | |||
.description("Attribute defining the user groups in SAML. " + | |||
"Users are associated to the default group only if no attribute is defined.") | |||
.category(CATEGORY) | |||
.subCategory(SUBCATEGORY) | |||
.index(10) | |||
.build()); | |||
} | |||
} |
@@ -0,0 +1,23 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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. | |||
*/ | |||
@ParametersAreNonnullByDefault | |||
package org.sonar.auth.saml; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -0,0 +1,337 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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.io.IOException; | |||
import java.io.InputStream; | |||
import java.nio.charset.StandardCharsets; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
import java.util.concurrent.atomic.AtomicBoolean; | |||
import javax.servlet.http.HttpServletRequest; | |||
import javax.servlet.http.HttpServletResponse; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import org.sonar.api.config.PropertyDefinitions; | |||
import org.sonar.api.config.internal.MapSettings; | |||
import org.sonar.api.internal.apachecommons.io.IOUtils; | |||
import org.sonar.api.server.authentication.OAuth2IdentityProvider; | |||
import org.sonar.api.server.authentication.UnauthorizedException; | |||
import org.sonar.api.server.authentication.UserIdentity; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.ArgumentMatchers.anyString; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.verify; | |||
import static org.mockito.Mockito.when; | |||
public class SamlIdentityProviderTest { | |||
@Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
private MapSettings settings = new MapSettings(new PropertyDefinitions(SamlSettings.definitions())); | |||
private SamlIdentityProvider underTest = new SamlIdentityProvider(new SamlSettings(settings.asConfig())); | |||
@Test | |||
public void check_fields() { | |||
setSettings(true); | |||
assertThat(underTest.getKey()).isEqualTo("saml"); | |||
assertThat(underTest.getName()).isEqualTo("SAML"); | |||
assertThat(underTest.getDisplay().getIconPath()).isEqualTo("/static/authsaml/saml.png"); | |||
assertThat(underTest.getDisplay().getBackgroundColor()).isEqualTo("#444444"); | |||
assertThat(underTest.allowsUsersToSignUp()).isTrue(); | |||
} | |||
@Test | |||
public void provider_name_is_provided_by_setting() { | |||
// Default value | |||
assertThat(underTest.getName()).isEqualTo("SAML"); | |||
settings.setProperty("sonar.auth.saml.providerName", "My Provider"); | |||
assertThat(underTest.getName()).isEqualTo("My Provider"); | |||
} | |||
@Test | |||
public void is_enabled() { | |||
setSettings(true); | |||
assertThat(underTest.isEnabled()).isTrue(); | |||
setSettings(false); | |||
assertThat(underTest.isEnabled()).isFalse(); | |||
} | |||
@Test | |||
public void init() throws IOException { | |||
setSettings(true); | |||
DumbInitContext context = new DumbInitContext(); | |||
underTest.init(context); | |||
verify(context.response).sendRedirect(anyString()); | |||
assertThat(context.generateCsrfState.get()).isTrue(); | |||
} | |||
@Test | |||
public void fail_to_init_when_login_url_is_invalid() { | |||
setSettings(true); | |||
settings.setProperty("sonar.auth.saml.loginUrl", "invalid"); | |||
DumbInitContext context = new DumbInitContext(); | |||
expectedException.expect(IllegalStateException.class); | |||
expectedException.expectMessage("Fail to create Auth"); | |||
underTest.init(context); | |||
} | |||
@Test | |||
public void callback() { | |||
setSettings(true); | |||
DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_full_response.txt"); | |||
underTest.callback(callbackContext); | |||
assertThat(callbackContext.redirectedToRequestedPage.get()).isTrue(); | |||
assertThat(callbackContext.userIdentity.getLogin()).isEqualTo("johndoe"); | |||
assertThat(callbackContext.verifyState.get()).isTrue(); | |||
} | |||
@Test | |||
public void callback_on_full_response() { | |||
setSettings(true); | |||
DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_full_response.txt"); | |||
underTest.callback(callbackContext); | |||
assertThat(callbackContext.userIdentity.getLogin()).isEqualTo("johndoe"); | |||
assertThat(callbackContext.userIdentity.getName()).isEqualTo("John Doe"); | |||
assertThat(callbackContext.userIdentity.getEmail()).isEqualTo("johndoe@email.com"); | |||
assertThat(callbackContext.userIdentity.getProviderLogin()).isEqualTo("johndoe"); | |||
assertThat(callbackContext.userIdentity.getGroups()).containsExactlyInAnyOrder("developer", "product-manager"); | |||
} | |||
@Test | |||
public void callback_on_minimal_response() { | |||
setSettings(true); | |||
DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_minimal_response.txt"); | |||
underTest.callback(callbackContext); | |||
assertThat(callbackContext.userIdentity.getLogin()).isEqualTo("johndoe"); | |||
assertThat(callbackContext.userIdentity.getName()).isEqualTo("John Doe"); | |||
assertThat(callbackContext.userIdentity.getEmail()).isNull(); | |||
assertThat(callbackContext.userIdentity.getProviderLogin()).isEqualTo("johndoe"); | |||
assertThat(callbackContext.userIdentity.getGroups()).isEmpty(); | |||
} | |||
@Test | |||
public void callback_does_not_sync_group_when_group_setting_is_not_set() { | |||
setSettings(true); | |||
settings.setProperty("sonar.auth.saml.group.name", (String) null); | |||
DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_full_response.txt"); | |||
underTest.callback(callbackContext); | |||
assertThat(callbackContext.userIdentity.getLogin()).isEqualTo("johndoe"); | |||
assertThat(callbackContext.userIdentity.getGroups()).isEmpty(); | |||
} | |||
@Test | |||
public void fail_to_callback_when_login_is_missing() { | |||
setSettings(true); | |||
DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_response_without_login.txt"); | |||
expectedException.expect(NullPointerException.class); | |||
expectedException.expectMessage("login is missing"); | |||
underTest.callback(callbackContext); | |||
} | |||
@Test | |||
public void fail_to_callback_when_name_is_missing() { | |||
setSettings(true); | |||
DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_response_without_name.txt"); | |||
expectedException.expect(NullPointerException.class); | |||
expectedException.expectMessage("name is missing"); | |||
underTest.callback(callbackContext); | |||
} | |||
@Test | |||
public void fail_to_callback_when_certificate_is_invalid() { | |||
setSettings(true); | |||
settings.setProperty("sonar.auth.saml.certificate.secured", "invalid"); | |||
DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_full_response.txt"); | |||
expectedException.expect(IllegalStateException.class); | |||
expectedException.expectMessage("Fail to create Auth"); | |||
underTest.callback(callbackContext); | |||
} | |||
@Test | |||
public void fail_to_callback_when_using_wrong_certificate() { | |||
setSettings(true); | |||
settings.setProperty("sonar.auth.saml.certificate.secured", "-----BEGIN CERTIFICATE-----\n" + | |||
"MIIEIzCCAwugAwIBAgIUHUzPjy5E2TmnsmTRT2sIUBRXFF8wDQYJKoZIhvcNAQEF\n" + | |||
"BQAwXDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC1NvbmFyU291cmNlMRUwEwYDVQQL\n" + | |||
"DAxPbmVMb2dpbiBJZFAxIDAeBgNVBAMMF09uZUxvZ2luIEFjY291bnQgMTMxMTkx\n" + | |||
"MB4XDTE4MDcxOTA4NDUwNVoXDTIzMDcxOTA4NDUwNVowXDELMAkGA1UEBhMCVVMx\n" + | |||
"FDASBgNVBAoMC1NvbmFyU291cmNlMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxIDAe\n" + | |||
"BgNVBAMMF09uZUxvZ2luIEFjY291bnQgMTMxMTkxMIIBIjANBgkqhkiG9w0BAQEF\n" + | |||
"AAOCAQ8AMIIBCgKCAQEArlpKHm4EkJiQyy+4GtZBixcy7fWnreB96T7cOoWLmWkK\n" + | |||
"05FM5M/boWHZsvaNAuHsoCAMzIY3/l+55WbORzAxsloH7rvDaDrdPYQN+sU9bzsD\n" + | |||
"ZkmDGDmA3QBSm/h/p5SiMkWU5Jg34toDdM0rmzUStIOMq6Gh/Ykx3fRRSjswy48x\n" + | |||
"wfZLy+0wU7lasHqdfk54dVbb7mCm9J3iHZizvOt2lbtzGbP6vrrjpzvZm43ZRgP8\n" + | |||
"FapYA8G3lczdIaG4IaLW6kYIRORd0UwI7IAwkao3uIo12rh1T6DLVyzjOs9PdIkb\n" + | |||
"HbICN2EehB/ut3wohuPwmwp2UmqopIMVVaBSsmSlYwIDAQABo4HcMIHZMAwGA1Ud\n" + | |||
"EwEB/wQCMAAwHQYDVR0OBBYEFAXGFMKYgtpzCpfpBUPQ1H/9AeDrMIGZBgNVHSME\n" + | |||
"gZEwgY6AFAXGFMKYgtpzCpfpBUPQ1H/9AeDroWCkXjBcMQswCQYDVQQGEwJVUzEU\n" + | |||
"MBIGA1UECgwLU29uYXJTb3VyY2UxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEgMB4G\n" + | |||
"A1UEAwwXT25lTG9naW4gQWNjb3VudCAxMzExOTGCFB1Mz48uRNk5p7Jk0U9rCFAU\n" + | |||
"VxRfMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAPHgi9IdDaTxD\n" + | |||
"R5R8KHMdt385Uq8XC5pd0Li6y5RR2k6SKjThCt+eQU7D0Y2CyYU27vfCa2DQV4hJ\n" + | |||
"4v4UfQv3NR/fYfkVSsNpxjBXBI3YWouxt2yg7uwdZBdgGYd37Yv3g9PdIZenjOhr\n" + | |||
"Ck6WjdleMAWHRgJpocmB4IOESSyTfUul3jFupWnkbnn8c0ue6zwXd7LA1/yjVT2l\n" + | |||
"Yh45+lz25aIOlyyo7OUw2TD15LIl8OOIuWRS4+UWy5+VdhXMbmpSEQH+Byod90g6\n" + | |||
"A1bKpOFhRBzcxaZ6B2hB4SqjTBzS9zdmJyyFs/WNJxHri3aorcdqG9oUakjJJqqX\n" + | |||
"E13skIMV2g==\n" + | |||
"-----END CERTIFICATE-----\n"); | |||
DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_full_response.txt"); | |||
expectedException.expect(UnauthorizedException.class); | |||
expectedException.expectMessage("Signature validation failed. SAML Response rejected"); | |||
underTest.callback(callbackContext); | |||
} | |||
private void setSettings(boolean enabled) { | |||
if (enabled) { | |||
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", | |||
"MIICoTCCAYkCBgFksusMzTANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlzb25hcnF1YmUwHhcNMTgwNzE5MTQyMDA2WhcNMjgwNzE5MTQyMTQ2WjAUMRIwEAYDVQQDDAlzb25hcnF1YmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEOth5gxpTs1f3bFGUD8hO97eMIsDZvvE3PZeKoeTRG7mOLu6rfLXphG3fE3E6/xqUhPP5p9hJl9DwgaMewhdZhfHqtOw6/SPMCQNFVNw9FQ7lprWKg8cZygYLDxhObEvCWPek8KcMb/vlKD8c8ha374O9qET51CVogDM5ropp02q0ELxoUKXqphKH4+sGXRVnDHaEsFHxse1HnciZT5mF1G45vxDItdAnWKkXYKVHC+Et52tCieqM0ygpQF1lWVJFXVOqsi03YkMu7IkWvSSfAw+uEcfmquT7FbxJ2n5gp94odAkQB0HK3fABrHr+G+n2QvWG6WwQPJTL0Ov0w+tNAgMBAAEwDQYJKoZIhvcNAQELBQADggEBACQfOrJF98nunKz6CN+YZXXMYhzQiqTD0MlzCg+Rdhir+WC/ru3Kz8omv52W/sXEMNQbEZBksVLl8W/1xeBS41Sf1nfutU560v/j3/OmOcnCw4qebqFH7nB8RL8vA4rGx430W/PeeUMikY1mSjlwhnJGiICQ3Y8I2qM6QWEr/Df2/gFCW2YnHbnS6Q/OwRQi+UFIzKklSQQa0gAnqfM4oSKU2OMhzScinWg1buMYfJSXgd4qIhPvRsZpqBsdt/OSrU2D5Y2YfSu8oIcxBRgJoERH5BV9GdOID4fS+TYw0M0QO/ORetNw1mA/8Npsy8okF8Cn7fDgbnWC8uz+/xDc14I="); | |||
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"); | |||
settings.setProperty("sonar.auth.saml.enabled", true); | |||
} else { | |||
settings.setProperty("sonar.auth.saml.enabled", false); | |||
} | |||
} | |||
private static class DumbInitContext implements OAuth2IdentityProvider.InitContext { | |||
private HttpServletResponse response = mock(HttpServletResponse.class); | |||
private final AtomicBoolean generateCsrfState = new AtomicBoolean(false); | |||
@Override | |||
public String generateCsrfState() { | |||
generateCsrfState.set(true); | |||
return null; | |||
} | |||
@Override | |||
public void redirectTo(String url) { | |||
} | |||
@Override | |||
public String getCallbackUrl() { | |||
return "http://localhost/oauth/callback/saml"; | |||
} | |||
@Override | |||
public HttpServletRequest getRequest() { | |||
return mock(HttpServletRequest.class); | |||
} | |||
@Override | |||
public HttpServletResponse getResponse() { | |||
return response; | |||
} | |||
} | |||
private static class DumbCallbackContext implements OAuth2IdentityProvider.CallbackContext { | |||
private HttpServletResponse response = mock(HttpServletResponse.class); | |||
private HttpServletRequest request = mock(HttpServletRequest.class); | |||
private final AtomicBoolean redirectedToRequestedPage = new AtomicBoolean(false); | |||
private final AtomicBoolean verifyState = new AtomicBoolean(false); | |||
private UserIdentity userIdentity = null; | |||
public DumbCallbackContext(String encodedResponseFile) { | |||
when(getRequest().getRequestURL()).thenReturn(new StringBuffer("http://localhost/oauth/callback/saml")); | |||
Map<String, String[]> parameterMap = new HashMap<>(); | |||
parameterMap.put("SAMLResponse", new String[] {loadResponse(encodedResponseFile)}); | |||
when(getRequest().getParameterMap()).thenReturn(parameterMap); | |||
} | |||
private String loadResponse(String file) { | |||
try (InputStream json = getClass().getResourceAsStream("SamlIdentityProviderTest/" + file)) { | |||
return IOUtils.toString(json, StandardCharsets.UTF_8.name()); | |||
} catch (IOException e) { | |||
throw new IllegalStateException(e); | |||
} | |||
} | |||
@Override | |||
public void verifyCsrfState() { | |||
throw new IllegalStateException("This method should not be called !"); | |||
} | |||
@Override | |||
public void verifyCsrfState(String parameterName) { | |||
assertThat(parameterName).isEqualTo("RelayState"); | |||
verifyState.set(true); | |||
} | |||
@Override | |||
public void redirectToRequestedPage() { | |||
redirectedToRequestedPage.set(true); | |||
} | |||
@Override | |||
public void authenticate(UserIdentity userIdentity) { | |||
this.userIdentity = userIdentity; | |||
} | |||
@Override | |||
public String getCallbackUrl() { | |||
return "http://localhost/oauth/callback/saml"; | |||
} | |||
@Override | |||
public HttpServletRequest getRequest() { | |||
return request; | |||
} | |||
@Override | |||
public HttpServletResponse getResponse() { | |||
return response; | |||
} | |||
} | |||
} |
@@ -0,0 +1,36 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.auth.saml; | |||
import org.junit.Test; | |||
import org.sonar.core.platform.ComponentContainer; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER; | |||
public class SamlModuleTest { | |||
@Test | |||
public void verify_count_of_added_components() { | |||
ComponentContainer container = new ComponentContainer(); | |||
new SamlModule().configure(container); | |||
assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 12); | |||
} | |||
} |
@@ -0,0 +1,227 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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.tngtech.java.junit.dataprovider.DataProvider; | |||
import com.tngtech.java.junit.dataprovider.DataProviderRunner; | |||
import com.tngtech.java.junit.dataprovider.UseDataProvider; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import org.junit.runner.RunWith; | |||
import org.sonar.api.config.PropertyDefinitions; | |||
import org.sonar.api.config.internal.MapSettings; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
@RunWith(DataProviderRunner.class) | |||
public class SamlSettingsTest { | |||
@Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
private MapSettings settings = new MapSettings(new PropertyDefinitions(SamlSettings.definitions())); | |||
private SamlSettings underTest = new SamlSettings(settings.asConfig()); | |||
@Test | |||
public void return_application_id() { | |||
settings.setProperty("sonar.auth.saml.applicationId", "MyApp"); | |||
assertThat(underTest.getApplicationId()).isEqualTo("MyApp"); | |||
} | |||
@Test | |||
public void return_default_value_of_application_id() { | |||
assertThat(underTest.getApplicationId()).isEqualTo("sonarqube"); | |||
} | |||
@Test | |||
public void return_provider_name() { | |||
settings.setProperty("sonar.auth.saml.providerName", "MyProviderName"); | |||
assertThat(underTest.getProviderName()).isEqualTo("MyProviderName"); | |||
} | |||
@Test | |||
public void return_default_value_of_application_name() { | |||
assertThat(underTest.getProviderName()).isEqualTo("SAML"); | |||
} | |||
@Test | |||
public void return_provider_id() { | |||
settings.setProperty("sonar.auth.saml.applicationId", "http://localhost:8080/auth/realms/sonarqube"); | |||
assertThat(underTest.getApplicationId()).isEqualTo("http://localhost:8080/auth/realms/sonarqube"); | |||
} | |||
@Test | |||
public void return_login_url() { | |||
settings.setProperty("sonar.auth.saml.loginUrl", "http://localhost:8080/"); | |||
assertThat(underTest.getLoginUrl()).isEqualTo("http://localhost:8080/"); | |||
settings.setProperty("sonar.auth.saml.loginUrl", "http://localhost:8080"); | |||
assertThat(underTest.getLoginUrl()).isEqualTo("http://localhost:8080"); | |||
} | |||
@Test | |||
public void return_certificate() { | |||
settings.setProperty("sonar.auth.saml.certificate.secured", "ABCDEFG"); | |||
assertThat(underTest.getCertificate()).isEqualTo("ABCDEFG"); | |||
} | |||
@Test | |||
public void return_user_login_attribute() { | |||
settings.setProperty("sonar.auth.saml.user.login", "userLogin"); | |||
assertThat(underTest.getUserLogin()).isEqualTo("userLogin"); | |||
} | |||
@Test | |||
public void return_user_name_attribute() { | |||
settings.setProperty("sonar.auth.saml.user.name", "userName"); | |||
assertThat(underTest.getUserName()).isEqualTo("userName"); | |||
} | |||
@Test | |||
public void return_user_email_attribute() { | |||
settings.setProperty("sonar.auth.saml.user.email", "userEmail"); | |||
assertThat(underTest.getUserEmail().get()).isEqualTo("userEmail"); | |||
} | |||
@Test | |||
public void return_empty_user_email_when_no_setting() { | |||
assertThat(underTest.getUserEmail()).isNotPresent(); | |||
} | |||
@Test | |||
public void return_group_name_attribute() { | |||
settings.setProperty("sonar.auth.saml.group.name", "groupName"); | |||
assertThat(underTest.getGroupName().get()).isEqualTo("groupName"); | |||
} | |||
@Test | |||
public void return_empty_group_name_when_no_setting() { | |||
assertThat(underTest.getGroupName()).isNotPresent(); | |||
} | |||
@Test | |||
public void is_enabled() { | |||
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", "ABCDEFG"); | |||
settings.setProperty("sonar.auth.saml.user.login", "login"); | |||
settings.setProperty("sonar.auth.saml.user.name", "name"); | |||
settings.setProperty("sonar.auth.saml.enabled", true); | |||
assertThat(underTest.isEnabled()).isTrue(); | |||
settings.setProperty("sonar.auth.saml.enabled", false); | |||
assertThat(underTest.isEnabled()).isFalse(); | |||
} | |||
@Test | |||
public void is_enabled_using_default_values() { | |||
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", "ABCDEFG"); | |||
settings.setProperty("sonar.auth.saml.user.login", "login"); | |||
settings.setProperty("sonar.auth.saml.user.name", "name"); | |||
settings.setProperty("sonar.auth.saml.enabled", true); | |||
assertThat(underTest.isEnabled()).isTrue(); | |||
} | |||
@DataProvider | |||
public static Object[][] settingsRequiredToEnablePlugin() { | |||
return new Object[][] { | |||
{"sonar.auth.saml.providerId"}, | |||
{"sonar.auth.saml.loginUrl"}, | |||
{"sonar.auth.saml.certificate.secured"}, | |||
{"sonar.auth.saml.user.login"}, | |||
{"sonar.auth.saml.user.name"}, | |||
{"sonar.auth.saml.enabled"}, | |||
}; | |||
} | |||
@Test | |||
@UseDataProvider("settingsRequiredToEnablePlugin") | |||
public void is_enabled_return_false_when_one_required_setting_is_missing(String setting) { | |||
initAllSettings(); | |||
settings.setProperty(setting, (String) null); | |||
assertThat(underTest.isEnabled()).isFalse(); | |||
} | |||
@Test | |||
public void fail_to_get_provider_id_when_null() { | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage("Provider ID is missing"); | |||
underTest.getProviderId(); | |||
} | |||
@Test | |||
public void fail_to_get_login_url_when_null() { | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage("Login URL is missing"); | |||
underTest.getLoginUrl(); | |||
} | |||
@Test | |||
public void fail_to_get_certificate_when_null() { | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage("Certificate is missing"); | |||
underTest.getCertificate(); | |||
} | |||
@Test | |||
public void fail_to_get_user_login_attribute_when_null() { | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage("User login attribute is missing"); | |||
underTest.getUserLogin(); | |||
} | |||
@Test | |||
public void fail_to_get_user_name_attribute_when_null() { | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage("User name attribute is missing"); | |||
underTest.getUserName(); | |||
} | |||
private void initAllSettings() { | |||
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", "ABCDEFG"); | |||
settings.setProperty("sonar.auth.saml.user.login", "login"); | |||
settings.setProperty("sonar.auth.saml.user.name", "name"); | |||
settings.setProperty("sonar.auth.saml.enabled", true); | |||
} | |||
} |
@@ -0,0 +1,6 @@ | |||
# How to generate test responses for unit tests requiring encoded user response | |||
1. Set the server log in TRACE | |||
2. Login with a user | |||
3. Search in the logs for "[c.o.saml2.Auth] processResponse success -->" | |||
4. The value after the "-->" is the encoded response that can be used in test |
@@ -131,12 +131,12 @@ The following example may be useful if you're using Keycloak as a SAML Identity | |||
[[collapse]] | |||
| ## In SonarQube, Configure SAML authentication | |||
| Go to **[Administration > Configuration > General Settings > SAML > Authentication](/#sonarqube-admin#/admin/settings?category=saml)** | |||
| Go to **[Administration > Configuration > General Settings > Security > SAML](/#sonarqube-admin#/admin/settings?category=security)** | |||
| * **Enabled** should be set to true | |||
| * **Application ID** is the value of the "Client ID" you set in Keycloak (for example "sonarqube") | |||
| * **Provider ID** is the value of the "EntityDescriptor" > "entityID" attribute in the XML configuration file (for example "http://keycloak:8080/auth/realms/sonarqube" where sonarqube is the name of the realm) | |||
| * **SAML login url** is the value of "SingleSignOnService" > "Location" attribute in the XML configuration file (for example "http://keycloak:8080/auth/realms/sonarqube/protocol/saml") | |||
| * **Provider certificate** is the value of "dsig:X509Certificate" node in the XML configuration file | |||
| * **Provider certificate** is the value you get from *Reaml Settings* -> *Keys* -> click on the *Certificate* button | |||
| * **SAML user login attribute** is the value set in the login mapper in "SAML Attribute Name" | |||
| * **SAML user name attribute** is the value set in the name mapper in "SAML Attribute Name" | |||
| * (Optional) **SAML user email attribute** is the value set in the email mapper in "SAML Attribute Name" |
@@ -47,7 +47,7 @@ import static org.sonar.core.extension.ExtensionProviderSupport.isExtensionProvi | |||
*/ | |||
public abstract class ServerExtensionInstaller { | |||
private static final Set<String> NO_MORE_COMPATIBLE_PLUGINS = ImmutableSet.of("authgithub", "authgitlab"); | |||
private static final Set<String> NO_MORE_COMPATIBLE_PLUGINS = ImmutableSet.of("authgithub", "authgitlab", "authsaml"); | |||
private final SonarRuntime sonarRuntime; | |||
private final PluginRepository pluginRepository; |
@@ -88,6 +88,18 @@ public class ServerExtensionInstallerTest { | |||
underTest.installExtensions(componentContainer); | |||
} | |||
@Test | |||
public void fail_when_detecting_saml_auth_plugin() { | |||
PluginInfo foo = newPlugin("authsaml", "SAML Auth"); | |||
pluginRepository.add(foo, mock(Plugin.class)); | |||
ComponentContainer componentContainer = new ComponentContainer(); | |||
expectedException.expect(MessageException.class); | |||
expectedException.expectMessage("Plugins 'SAML Auth' are no more compatible with SonarQube"); | |||
underTest.installExtensions(componentContainer); | |||
} | |||
private static PluginInfo newPlugin(String key, String name) { | |||
PluginInfo plugin = mock(PluginInfo.class); | |||
when(plugin.getKey()).thenReturn(key); |
@@ -14,6 +14,7 @@ dependencies { | |||
compile project(':sonar-core') | |||
compile project(':server:sonar-auth-github') | |||
compile project(':server:sonar-auth-gitlab') | |||
compile project(':server:sonar-auth-saml') | |||
compile project(':server:sonar-ce-task-projectanalysis') | |||
compile project(':server:sonar-process') | |||
compile project(':server:sonar-webserver-core') |
@@ -30,6 +30,7 @@ import org.sonar.api.rules.XMLRuleParser; | |||
import org.sonar.api.server.rule.RulesDefinitionXmlLoader; | |||
import org.sonar.auth.github.GitHubModule; | |||
import org.sonar.auth.gitlab.GitLabModule; | |||
import org.sonar.auth.saml.SamlModule; | |||
import org.sonar.ce.task.projectanalysis.notification.ReportAnalysisFailureNotificationModule; | |||
import org.sonar.ce.task.projectanalysis.taskprocessor.ReportTaskProcessor; | |||
import org.sonar.core.component.DefaultResourceTypes; | |||
@@ -354,6 +355,7 @@ public class PlatformLevel4 extends PlatformLevel { | |||
AuthenticationWsModule.class, | |||
GitHubModule.class, | |||
GitLabModule.class, | |||
SamlModule.class, | |||
// users | |||
UserSessionFactoryImpl.class, | |||
@@ -467,7 +469,7 @@ public class PlatformLevel4 extends PlatformLevel { | |||
TypeValidationModule.class, | |||
//New Code Periods | |||
// New Code Periods | |||
NewCodePeriodsWsModule.class, | |||
// Project Links |
@@ -5,6 +5,7 @@ include 'plugins:sonar-xoo-plugin' | |||
include 'server:sonar-auth-common' | |||
include 'server:sonar-auth-github' | |||
include 'server:sonar-auth-gitlab' | |||
include 'server:sonar-auth-saml' | |||
include 'server:sonar-ce' | |||
include 'server:sonar-ce-common' | |||
include 'server:sonar-ce-task' |
@@ -50,7 +50,6 @@ dependencies { | |||
jdbc_mssql 'com.microsoft.sqlserver:mssql-jdbc' | |||
jdbc_postgresql 'org.postgresql:postgresql' | |||
bundledPlugin 'org.sonarsource.auth.saml:sonar-auth-saml-plugin:1.1.0.181@jar' | |||
bundledPlugin 'org.sonarsource.css:sonar-css-plugin@jar' | |||
bundledPlugin "org.sonarsource.dotnet:sonar-csharp-plugin@jar" | |||
bundledPlugin "org.sonarsource.dotnet:sonar-vbnet-plugin@jar" |
@@ -937,6 +937,8 @@ property.category.security.encryption=Encryption | |||
property.category.security.github=GitHub | |||
property.category.security.github.description=In order to enable GitHub authentication:<ul><li>SonarQube must be publicly accessible through HTTPS only</li><li>The property 'sonar.core.serverBaseURL' must be set to this public HTTPS URL</li><li>In your GitHub profile, you need to create a Developer Application for which the 'Authorization callback URL' must be set to <code>'<value_of_sonar.core.serverBaseURL_property>/oauth2/callback'</code>.</li></ul> | |||
property.category.security.gitlab=Gitlab | |||
property.category.security.saml=SAML | |||
property.category.security.saml.description=In order to enable SAML authentication, the property 'sonar.core.serverBaseURL' must be set to the public URL | |||
property.category.java=Java | |||
property.category.differentialViews=New Code | |||
property.category.codeCoverage=Code Coverage |