Browse Source

SONAR-12471 Embed SAML authentication

tags/8.0
Julien Lancelot 4 years ago
parent
commit
69dd7210a6
26 changed files with 1136 additions and 8 deletions
  1. 46
    0
      .cirrus.yml
  2. 0
    1
      server/sonar-auth-common/build.gradle
  3. 0
    1
      server/sonar-auth-github/build.gradle
  4. 0
    1
      server/sonar-auth-gitlab/build.gradle
  5. 26
    0
      server/sonar-auth-saml/build.gradle
  6. 191
    0
      server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlIdentityProvider.java
  7. 37
    0
      server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlModule.java
  8. 181
    0
      server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlSettings.java
  9. 23
    0
      server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/package-info.java
  10. 337
    0
      server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlIdentityProviderTest.java
  11. 36
    0
      server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlModuleTest.java
  12. 227
    0
      server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlSettingsTest.java
  13. 1
    0
      server/sonar-auth-saml/src/test/resources/org/sonar/auth/saml/SamlIdentityProviderTest/encoded_full_response.txt
  14. 1
    0
      server/sonar-auth-saml/src/test/resources/org/sonar/auth/saml/SamlIdentityProviderTest/encoded_minimal_response.txt
  15. 1
    0
      server/sonar-auth-saml/src/test/resources/org/sonar/auth/saml/SamlIdentityProviderTest/encoded_response_without_login.txt
  16. 1
    0
      server/sonar-auth-saml/src/test/resources/org/sonar/auth/saml/SamlIdentityProviderTest/encoded_response_without_name.txt
  17. 6
    0
      server/sonar-auth-saml/src/test/resources/org/sonar/auth/saml/SamlIdentityProviderTest/how_to_generate_test_response.txt
  18. 2
    2
      server/sonar-docs/src/pages/instance-administration/delegated-auth.md
  19. 1
    1
      server/sonar-server-common/src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java
  20. 12
    0
      server/sonar-server-common/src/test/java/org/sonar/server/plugins/ServerExtensionInstallerTest.java
  21. BIN
      server/sonar-web/public/images/saml.png
  22. 1
    0
      server/sonar-webserver/build.gradle
  23. 3
    1
      server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
  24. 1
    0
      settings.gradle
  25. 0
    1
      sonar-application/build.gradle
  26. 2
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 46
- 0
.cirrus.yml View File

@@ -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

+ 0
- 1
server/sonar-auth-common/build.gradle View File

@@ -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'

+ 0
- 1
server/sonar-auth-github/build.gradle View File

@@ -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
- 1
server/sonar-auth-gitlab/build.gradle View File

@@ -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'

+ 26
- 0
server/sonar-auth-saml/build.gradle View File

@@ -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'
}

+ 191
- 0
server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlIdentityProvider.java View File

@@ -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();
}
}

+ 37
- 0
server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlModule.java View File

@@ -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()]));
}

}

+ 181
- 0
server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlSettings.java View File

@@ -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());
}
}

+ 23
- 0
server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/package-info.java View File

@@ -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;

+ 337
- 0
server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlIdentityProviderTest.java View File

@@ -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;
}
}
}

+ 36
- 0
server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlModuleTest.java View File

@@ -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);
}
}

+ 227
- 0
server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlSettingsTest.java View File

@@ -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);
}

}

+ 1
- 0
server/sonar-auth-saml/src/test/resources/org/sonar/auth/saml/SamlIdentityProviderTest/encoded_full_response.txt
File diff suppressed because it is too large
View File


+ 1
- 0
server/sonar-auth-saml/src/test/resources/org/sonar/auth/saml/SamlIdentityProviderTest/encoded_minimal_response.txt
File diff suppressed because it is too large
View File


+ 1
- 0
server/sonar-auth-saml/src/test/resources/org/sonar/auth/saml/SamlIdentityProviderTest/encoded_response_without_login.txt
File diff suppressed because it is too large
View File


+ 1
- 0
server/sonar-auth-saml/src/test/resources/org/sonar/auth/saml/SamlIdentityProviderTest/encoded_response_without_name.txt
File diff suppressed because it is too large
View File


+ 6
- 0
server/sonar-auth-saml/src/test/resources/org/sonar/auth/saml/SamlIdentityProviderTest/how_to_generate_test_response.txt View File

@@ -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

+ 2
- 2
server/sonar-docs/src/pages/instance-administration/delegated-auth.md View File

@@ -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"

+ 1
- 1
server/sonar-server-common/src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java View File

@@ -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;

+ 12
- 0
server/sonar-server-common/src/test/java/org/sonar/server/plugins/ServerExtensionInstallerTest.java View File

@@ -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);

BIN
server/sonar-web/public/images/saml.png View File


+ 1
- 0
server/sonar-webserver/build.gradle View File

@@ -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')

+ 3
- 1
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java View File

@@ -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

+ 1
- 0
settings.gradle View File

@@ -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'

+ 0
- 1
sonar-application/build.gradle View File

@@ -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"

+ 2
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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>'&lt;value_of_sonar.core.serverBaseURL_property&gt;/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

Loading…
Cancel
Save