]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-5430 User authentication by HTTP header 1341/head
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Thu, 27 Oct 2016 08:35:16 +0000 (10:35 +0200)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Thu, 27 Oct 2016 15:47:56 +0000 (17:47 +0200)
it/it-tests/src/test/java/it/Category5Suite.java
it/it-tests/src/test/java/it/user/BaseIdentityProviderTest.java
it/it-tests/src/test/java/it/user/SsoAuthenticationTest.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java
server/sonar-server/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java
server/sonar-server/src/main/java/org/sonar/server/authentication/SsoAuthenticator.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java
server/sonar-server/src/test/java/org/sonar/server/authentication/SsoAuthenticatorTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java
sonar-application/src/main/assembly/conf/sonar.properties

index 4a775d07653ec19a807885c8f97319945534181c..94b693679dd8025bb1345991438a7c2111f8367f 100644 (file)
@@ -26,6 +26,7 @@ import it.settings.LicensesPageTest;
 import it.settings.SettingsTestRestartingOrchestrator;
 import it.updateCenter.UpdateCenterTest;
 import it.user.RealmAuthenticationTest;
+import it.user.SsoAuthenticationTest;
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
 
@@ -43,8 +44,9 @@ import org.junit.runners.Suite;
   LicensesPageTest.class,
   // update center
   UpdateCenterTest.class,
-  RealmAuthenticationTest.class
-  })
+  RealmAuthenticationTest.class,
+  SsoAuthenticationTest.class
+})
 public class Category5Suite {
 
 }
index 07471d31fdc9196eb5cae932bf647da5a10180dd..2801b4e3d48cb73a69a16c34c175498f901d0c5b 100644 (file)
@@ -32,7 +32,6 @@ import org.junit.ClassRule;
 import org.junit.Test;
 import org.sonarqube.ws.client.GetRequest;
 import org.sonarqube.ws.client.WsClient;
-import org.sonarqube.ws.client.WsResponse;
 import util.selenium.SeleneseTest;
 import util.user.UserRule;
 import util.user.Users;
@@ -277,9 +276,9 @@ public class BaseIdentityProviderTest {
   }
 
   private static void authenticateWithFakeAuthProvider() {
-    WsResponse response = adminWsClient.wsConnector().call(
-      new GetRequest(("/sessions/init/" + FAKE_PROVIDER_KEY)));
-    assertThat(response.code()).isEqualTo(200);
+    adminWsClient.wsConnector().call(
+      new GetRequest(("/sessions/init/" + FAKE_PROVIDER_KEY)))
+      .failIfNotSuccessful();
   }
 
 }
diff --git a/it/it-tests/src/test/java/it/user/SsoAuthenticationTest.java b/it/it-tests/src/test/java/it/user/SsoAuthenticationTest.java
new file mode 100644 (file)
index 0000000..7881659
--- /dev/null
@@ -0,0 +1,159 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 it.user;
+
+import com.google.common.base.Throwables;
+import com.sonar.orchestrator.Orchestrator;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.Nullable;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.apache.commons.io.Charsets;
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import util.user.UserRule;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Test SSO authentication (using HTTP headers).
+ * <p>
+ * It starts its own server as it's using a different authentication system
+ */
+public class SsoAuthenticationTest {
+
+  private static final String LOGIN_HEADER = "H-Login";
+  private static final String NAME_HEADER = "H-Name";
+  private static final String EMAIL_HEADER = "H-Email";
+  private static final String GROUPS_HEADER = "H-Groups";
+
+  static final String USER_LOGIN = "tester";
+  static final String USER_NAME = "Tester";
+  static final String USER_EMAIL = "tester@email.com";
+
+  static final String GROUP_1 = "group-1";
+  static final String GROUP_2 = "group-2";
+  static final String GROUP_3 = "group-3";
+
+  @ClassRule
+  public static final Orchestrator orchestrator = Orchestrator.builderEnv()
+    .setServerProperty("sonar.sso.enable", "true")
+    .setServerProperty("sonar.sso.loginHeader", LOGIN_HEADER)
+    .setServerProperty("sonar.sso.nameHeader", NAME_HEADER)
+    .setServerProperty("sonar.sso.emailHeader", EMAIL_HEADER)
+    .setServerProperty("sonar.sso.groupsHeader", GROUPS_HEADER)
+    .build();
+
+  @ClassRule
+  public static UserRule USER_RULE = UserRule.from(orchestrator);
+
+  @Before
+  public void resetData() throws Exception {
+    USER_RULE.resetUsers();
+  }
+
+  @Test
+  public void authenticate() {
+    call(USER_LOGIN, USER_NAME, USER_EMAIL, null);
+
+    USER_RULE.verifyUserExists(USER_LOGIN, USER_NAME, USER_EMAIL);
+  }
+
+  @Test
+  public void authenticate_with_only_login() throws Exception {
+    call(USER_LOGIN, null, null, null);
+
+    USER_RULE.verifyUserExists(USER_LOGIN, USER_LOGIN, null);
+  }
+
+  @Test
+  public void authenticate_with_groups() {
+    call(USER_LOGIN, null, null, GROUP_1);
+
+    USER_RULE.verifyUserGroupMembership(USER_LOGIN, GROUP_1);
+  }
+
+  @Test
+  public void synchronize_groups_when_authenticating_existing_user() throws Exception {
+    USER_RULE.createGroup(GROUP_1);
+    USER_RULE.createGroup(GROUP_2);
+    USER_RULE.createGroup(GROUP_3);
+    USER_RULE.createUser(USER_LOGIN, "password");
+    USER_RULE.associateGroupsToUser(USER_LOGIN, GROUP_1, GROUP_2);
+
+    call(USER_LOGIN, null, null, GROUP_2 + "," + GROUP_3);
+
+    USER_RULE.verifyUserGroupMembership(USER_LOGIN, GROUP_2, GROUP_3);
+  }
+
+  @Test
+  public void authentication_with_local_user_is_possible_when_no_header() throws Exception {
+    USER_RULE.createUser(USER_LOGIN, "password");
+
+    checkLocalAuthentication(USER_LOGIN, "password");
+  }
+
+  @Test
+  public void fail_when_login_is_invalid() throws Exception {
+    Response response = doCall("invalid login $", null, null, null);
+
+    assertThat(response.code()).isEqualTo(500);
+    List<String> logsLines = FileUtils.readLines(orchestrator.getServer().getLogs(), Charsets.UTF_8);
+    assertThat(logsLines).contains("org.sonar.server.exceptions.BadRequestException: user.bad_login");
+  }
+
+  private static Response call(String login, @Nullable String name, @Nullable String email, @Nullable String groups) {
+    return doCall(login, name, email, groups);
+  }
+
+  private static Response doCall(String login, @Nullable String name, @Nullable String email, @Nullable String groups) {
+    Request.Builder requestBuilder = new Request.Builder().get().url(orchestrator.getServer().getUrl())
+      .addHeader(LOGIN_HEADER, login);
+    if (name != null) {
+      requestBuilder.addHeader(NAME_HEADER, name);
+    }
+    if (email != null) {
+      requestBuilder.addHeader(EMAIL_HEADER, email);
+    }
+    if (groups != null) {
+      requestBuilder.addHeader(GROUPS_HEADER, groups);
+    }
+    try {
+      return new OkHttpClient.Builder()
+        .connectTimeout(30, TimeUnit.SECONDS)
+        .readTimeout(30, TimeUnit.SECONDS)
+        .build()
+        .newCall(requestBuilder.build()).execute();
+    } catch (IOException e) {
+      throw Throwables.propagate(e);
+    }
+  }
+
+  private boolean checkLocalAuthentication(String login, String password) {
+    String result = orchestrator.getServer().wsClient(login, password).get("/api/authentication/validate");
+    return result.contains("{\"valid\":true}");
+  }
+
+}
index 9ca0e666a2ab2fe62e2a49abce3b8ec8c24583f7..4d810053907486f996a13e3d474d7565edec73db 100644 (file)
@@ -44,6 +44,7 @@ public class AuthenticationModule extends Module {
       CredentialsAuthenticator.class,
       RealmAuthenticator.class,
       BasicAuthenticator.class,
-      ValidateAction.class);
+      ValidateAction.class,
+      SsoAuthenticator.class);
   }
 }
index 4ae491d5bbefc6d99b8ffccf7fb9251896fb715d..bcb9f5ba3497cc088fec7a7278e141210f0c4678 100644 (file)
@@ -22,13 +22,14 @@ package org.sonar.server.authentication;
 
 import com.google.common.collect.ImmutableMap;
 import io.jsonwebtoken.Claims;
+import java.util.Collections;
 import java.util.Date;
+import java.util.Map;
 import java.util.Optional;
 import javax.annotation.Nullable;
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import org.apache.commons.lang.time.DateUtils;
 import org.sonar.api.config.Settings;
 import org.sonar.api.server.ServerSide;
 import org.sonar.api.utils.System2;
@@ -37,6 +38,7 @@ import org.sonar.db.DbSession;
 import org.sonar.db.user.UserDto;
 
 import static java.util.Objects.requireNonNull;
+import static org.apache.commons.lang.time.DateUtils.addSeconds;
 import static org.elasticsearch.common.Strings.isNullOrEmpty;
 import static org.sonar.server.authentication.CookieUtils.findCookie;
 
@@ -74,32 +76,38 @@ public class JwtHttpHandler {
     this.jwtCsrfVerifier = jwtCsrfVerifier;
   }
 
-  public void generateToken(UserDto user, HttpServletRequest request, HttpServletResponse response) {
+  public void generateToken(UserDto user, Map<String, Object> properties, HttpServletRequest request, HttpServletResponse response) {
     String csrfState = jwtCsrfVerifier.generateState(request, response, sessionTimeoutInSeconds);
 
     String token = jwtSerializer.encode(new JwtSerializer.JwtSession(
       user.getLogin(),
       sessionTimeoutInSeconds,
-      ImmutableMap.of(
-        LAST_REFRESH_TIME_PARAM, system2.now(),
-        CSRF_JWT_PARAM, csrfState)));
+      ImmutableMap.<String, Object>builder()
+        .putAll(properties)
+        .put(LAST_REFRESH_TIME_PARAM, system2.now())
+        .put(CSRF_JWT_PARAM, csrfState)
+        .build()));
     response.addCookie(createCookie(request, JWT_COOKIE, token, sessionTimeoutInSeconds));
   }
 
+  public void generateToken(UserDto user, HttpServletRequest request, HttpServletResponse response) {
+    generateToken(user, Collections.emptyMap(), request, response);
+  }
+
   public Optional<UserDto> validateToken(HttpServletRequest request, HttpServletResponse response) {
-    Optional<UserDto> userDto = validate(request, response);
-    if (userDto.isPresent()) {
-      return userDto;
+    Optional<Token> token = getToken(request, response);
+    if (token.isPresent()) {
+      return Optional.of(token.get().getUserDto());
     }
     return Optional.empty();
   }
 
-  private Optional<UserDto> validate(HttpServletRequest request, HttpServletResponse response) {
-    Optional<String> token = getTokenFromCookie(request);
-    if (!token.isPresent()) {
+  public Optional<Token> getToken(HttpServletRequest request, HttpServletResponse response) {
+    Optional<String> encodedToken = getTokenFromCookie(request);
+    if (!encodedToken.isPresent()) {
       return Optional.empty();
     }
-    return validateToken(token.get(), request, response);
+    return validateToken(encodedToken.get(), request, response);
   }
 
   private static Optional<String> getTokenFromCookie(HttpServletRequest request) {
@@ -115,7 +123,7 @@ public class JwtHttpHandler {
     return Optional.of(token);
   }
 
-  private Optional<UserDto> validateToken(String tokenEncoded, HttpServletRequest request, HttpServletResponse response) {
+  private Optional<Token> validateToken(String tokenEncoded, HttpServletRequest request, HttpServletResponse response) {
     Optional<Claims> claims = jwtSerializer.decode(tokenEncoded);
     if (!claims.isPresent()) {
       return Optional.empty();
@@ -123,12 +131,12 @@ public class JwtHttpHandler {
 
     Date now = new Date(system2.now());
     Claims token = claims.get();
-    if (now.after(DateUtils.addSeconds(token.getIssuedAt(), SESSION_DISCONNECT_IN_SECONDS))) {
+    if (now.after(addSeconds(token.getIssuedAt(), SESSION_DISCONNECT_IN_SECONDS))) {
       return Optional.empty();
     }
     jwtCsrfVerifier.verifyState(request, (String) token.get(CSRF_JWT_PARAM));
 
-    if (now.after(DateUtils.addSeconds(getLastRefreshDate(token), SESSION_REFRESH_IN_SECONDS))) {
+    if (now.after(addSeconds(getLastRefreshDate(token), SESSION_REFRESH_IN_SECONDS))) {
       refreshToken(token, request, response);
     }
 
@@ -136,7 +144,7 @@ public class JwtHttpHandler {
     if (!user.isPresent()) {
       return Optional.empty();
     }
-    return Optional.of(user.get());
+    return Optional.of(new Token(user.get(), claims.get()));
   }
 
   private static Date getLastRefreshDate(Claims token) {
@@ -176,4 +184,22 @@ public class JwtHttpHandler {
     }
     return SESSION_TIMEOUT_DEFAULT_VALUE_IN_SECONDS;
   }
+
+  public static class Token {
+    private final UserDto userDto;
+    private final Map<String, Object> properties;
+
+    Token(UserDto userDto, Map<String, Object> properties) {
+      this.userDto = userDto;
+      this.properties = properties;
+    }
+
+    public UserDto getUserDto() {
+      return userDto;
+    }
+
+    public Map<String, Object> getProperties() {
+      return properties;
+    }
+  }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/SsoAuthenticator.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/SsoAuthenticator.java
new file mode 100644 (file)
index 0000000..2398061
--- /dev/null
@@ -0,0 +1,181 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.authentication;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableMap;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import javax.annotation.CheckForNull;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.sonar.api.config.Settings;
+import org.sonar.api.server.authentication.Display;
+import org.sonar.api.server.authentication.IdentityProvider;
+import org.sonar.api.server.authentication.UserIdentity;
+import org.sonar.api.utils.System2;
+import org.sonar.db.user.UserDto;
+
+import static org.apache.commons.lang.StringUtils.defaultIfBlank;
+import static org.apache.commons.lang.time.DateUtils.addMinutes;
+import static org.sonar.server.user.UserUpdater.SQ_AUTHORITY;
+
+public class SsoAuthenticator {
+
+  private static final Splitter COMA_SPLITTER = Splitter.on(",").trimResults().omitEmptyStrings();
+
+  private static final String ENABLE_PARAM = "sonar.sso.enable";
+
+  private static final String LOGIN_HEADER_PARAM = "sonar.sso.loginHeader";
+  private static final String LOGIN_HEADER_DEFAULT_VALUE = "X-Forwarded-Login";
+
+  private static final String NAME_HEADER_PARAM = "sonar.sso.nameHeader";
+  private static final String NAME_HEADER_DEFAULT_VALUE = "X-Forwarded-Name";
+
+  private static final String EMAIL_HEADER_PARAM = "sonar.sso.emailHeader";
+  private static final String EMAIL_HEADER_DEFAULT_VALUE = "X-Forwarded-Email";
+
+  private static final String GROUPS_HEADER_PARAM = "sonar.sso.groupsHeader";
+  private static final String GROUPS_HEADER_DEFAULT_VALUE = "X-Forwarded-Groups";
+
+  private static final String REFRESH_INTERVAL_PARAM = "sonar.sso.refreshIntervalInMinutes";
+  private static final String REFRESH_INTERVAL_DEFAULT_VALUE = "5";
+
+  private static final String LAST_REFRESH_TIME_TOKEN_PARAM = "ssoLastRefreshTime";
+
+  private static final Map<String, String> DEFAULT_VALUES_BY_PARAMETERS = ImmutableMap.of(
+    LOGIN_HEADER_PARAM, LOGIN_HEADER_DEFAULT_VALUE,
+    NAME_HEADER_PARAM, NAME_HEADER_DEFAULT_VALUE,
+    EMAIL_HEADER_PARAM, EMAIL_HEADER_DEFAULT_VALUE,
+    GROUPS_HEADER_PARAM, GROUPS_HEADER_DEFAULT_VALUE,
+    REFRESH_INTERVAL_PARAM, REFRESH_INTERVAL_DEFAULT_VALUE);
+
+  private final System2 system2;
+  private final Settings settings;
+  private final UserIdentityAuthenticator userIdentityAuthenticator;
+  private final JwtHttpHandler jwtHttpHandler;
+
+  public SsoAuthenticator(System2 system2, Settings settings, UserIdentityAuthenticator userIdentityAuthenticator, JwtHttpHandler jwtHttpHandler) {
+    this.system2 = system2;
+    this.settings = settings;
+    this.userIdentityAuthenticator = userIdentityAuthenticator;
+    this.jwtHttpHandler = jwtHttpHandler;
+  }
+
+  public Optional<UserDto> authenticate(HttpServletRequest request, HttpServletResponse response) {
+    if (!settings.getBoolean(ENABLE_PARAM)) {
+      return Optional.empty();
+    }
+    Map<String, String> headerValuesByNames = getHeaders(request);
+    String login = getHeaderValue(headerValuesByNames, LOGIN_HEADER_PARAM);
+    if (login == null) {
+      return Optional.empty();
+    }
+    Optional<UserDto> user = getUserFromToken(request, response);
+    if (user.isPresent() && login.equals(user.get().getLogin())) {
+      return user;
+    }
+
+    UserDto userDto = doAuthenticate(headerValuesByNames, login);
+    jwtHttpHandler.generateToken(userDto, ImmutableMap.of(LAST_REFRESH_TIME_TOKEN_PARAM, system2.now()), request, response);
+    return Optional.of(userDto);
+  }
+
+  private Optional<UserDto> getUserFromToken(HttpServletRequest request, HttpServletResponse response) {
+    Optional<JwtHttpHandler.Token> token = jwtHttpHandler.getToken(request, response);
+    if (!token.isPresent()) {
+      return Optional.empty();
+    }
+    Date now = new Date(system2.now());
+    int refreshIntervalInMinutes = Integer.parseInt(getSettingValue(REFRESH_INTERVAL_PARAM));
+    Long lastFreshTime = (Long) token.get().getProperties().get(LAST_REFRESH_TIME_TOKEN_PARAM);
+    if (lastFreshTime == null || now.after(addMinutes(new Date(lastFreshTime), refreshIntervalInMinutes))) {
+      return Optional.empty();
+    }
+    return Optional.of(token.get().getUserDto());
+  }
+
+  private UserDto doAuthenticate(Map<String, String> headerValuesByNames, String login) {
+    String name = getHeaderValue(headerValuesByNames, NAME_HEADER_PARAM);
+    String email = getHeaderValue(headerValuesByNames, EMAIL_HEADER_PARAM);
+    UserIdentity.Builder userIdentityBuilder = UserIdentity.builder()
+      .setLogin(login)
+      .setName(name == null ? login : name)
+      .setEmail(email)
+      .setProviderLogin(login);
+    if (hasHeader(headerValuesByNames, GROUPS_HEADER_PARAM)) {
+      String groupsValue = getHeaderValue(headerValuesByNames, GROUPS_HEADER_PARAM);
+      userIdentityBuilder.setGroups(groupsValue == null ? Collections.emptySet() : new HashSet<>(COMA_SPLITTER.splitToList(groupsValue)));
+    }
+    return userIdentityAuthenticator.authenticate(userIdentityBuilder.build(), new SsoIdentityProvider());
+  }
+
+  @CheckForNull
+  private String getHeaderValue(Map<String, String> headerValuesByNames, String settingKey) {
+    return headerValuesByNames.get(getSettingValue(settingKey).toLowerCase(Locale.ENGLISH));
+  }
+
+  private static Map<String, String> getHeaders(HttpServletRequest request) {
+    Map<String, String> headers = new HashMap<>();
+    Collections.list(request.getHeaderNames()).forEach(header -> headers.put(header.toLowerCase(Locale.ENGLISH), request.getHeader(header)));
+    return headers;
+  }
+
+  private boolean hasHeader(Map<String, String> headerValuesByNames, String settingKey) {
+    return headerValuesByNames.keySet().contains(getSettingValue(settingKey).toLowerCase(Locale.ENGLISH));
+  }
+
+  private String getSettingValue(String settingKey) {
+    return defaultIfBlank(settings.getString(settingKey), DEFAULT_VALUES_BY_PARAMETERS.get(settingKey));
+  }
+
+  private static class SsoIdentityProvider implements IdentityProvider {
+    @Override
+    public String getKey() {
+      return SQ_AUTHORITY;
+    }
+
+    @Override
+    public String getName() {
+      return getKey();
+    }
+
+    @Override
+    public Display getDisplay() {
+      return null;
+    }
+
+    @Override
+    public boolean isEnabled() {
+      return true;
+    }
+
+    @Override
+    public boolean allowsUsersToSignUp() {
+      return true;
+    }
+  }
+}
index fb7a42fec487ae154236b18edd8b42f8b88886be..4f02daea4e09e340509b055dbf03666e5cbca858 100644 (file)
@@ -72,14 +72,16 @@ public class UserSessionInitializer {
   private final Settings settings;
   private final JwtHttpHandler jwtHttpHandler;
   private final BasicAuthenticator basicAuthenticator;
+  private final SsoAuthenticator ssoAuthenticator;
   private final ThreadLocalUserSession threadLocalSession;
 
   public UserSessionInitializer(DbClient dbClient, Settings settings, JwtHttpHandler jwtHttpHandler, BasicAuthenticator basicAuthenticator,
-                                ThreadLocalUserSession threadLocalSession) {
+    SsoAuthenticator ssoAuthenticator, ThreadLocalUserSession threadLocalSession) {
     this.dbClient = dbClient;
     this.settings = settings;
     this.jwtHttpHandler = jwtHttpHandler;
     this.basicAuthenticator = basicAuthenticator;
+    this.ssoAuthenticator = ssoAuthenticator;
     this.threadLocalSession = threadLocalSession;
   }
 
@@ -121,9 +123,14 @@ public class UserSessionInitializer {
     threadLocalSession.unload();
   }
 
-  // Try first to authenticate from JWT token, then try from basic http header
+  // Try first to authenticate from SSO, then JWT token, then try from basic http header
   private Optional<UserDto> authenticate(HttpServletRequest request, HttpServletResponse response) {
-    Optional<UserDto> user = jwtHttpHandler.validateToken(request, response);
+    // SSO authentication should come first in order to update JWT if user from header is not the same is user from JWT
+    Optional<UserDto> user = ssoAuthenticator.authenticate(request, response);
+    if (user.isPresent()) {
+      return user;
+    }
+    user = jwtHttpHandler.validateToken(request, response);
     if (user.isPresent()) {
       return user;
     }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/SsoAuthenticatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/SsoAuthenticatorTest.java
new file mode 100644 (file)
index 0000000..99c3bb5
--- /dev/null
@@ -0,0 +1,404 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.authentication;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.config.MapSettings;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.internal.AlwaysIncreasingSystem2;
+import org.sonar.core.util.stream.Collectors;
+import org.sonar.db.DbTester;
+import org.sonar.db.user.GroupDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.organization.DefaultOrganizationProvider;
+import org.sonar.server.organization.TestDefaultOrganizationProvider;
+import org.sonar.server.user.NewUserNotifier;
+import org.sonar.server.user.UserUpdater;
+import org.sonar.server.user.index.UserIndexer;
+
+import static java.util.Arrays.stream;
+import static org.assertj.core.api.Java6Assertions.assertThat;
+import static org.junit.rules.ExpectedException.none;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyMap;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+import static org.sonar.db.user.UserTesting.newUserDto;
+
+public class SsoAuthenticatorTest {
+
+  @Rule
+  public ExpectedException expectedException = none();
+
+  @Rule
+  public DbTester db = DbTester.create(new AlwaysIncreasingSystem2());
+
+  private static final String DEFAULT_LOGIN = "john";
+  private static final String DEFAULT_NAME = "John";
+  private static final String DEFAULT_EMAIL = "john@doo.com";
+  private static final String GROUP1 = "dev";
+  private static final String GROUP2 = "admin";
+  private static final String GROUPS = GROUP1 + "," + GROUP2;
+
+  private static final Long NOW = 1_000_000L;
+  private static final Long CLOSE_REFRESH_TIME = NOW - 1_000L;
+
+  private static final UserDto DEFAULT_USER = newUserDto()
+    .setLogin(DEFAULT_LOGIN)
+    .setName(DEFAULT_NAME)
+    .setEmail(DEFAULT_EMAIL)
+    .setExternalIdentity(DEFAULT_LOGIN)
+    .setExternalIdentityProvider("sonarqube");
+
+  private GroupDto group1;
+  private GroupDto group2;
+
+  private System2 system2 = mock(System2.class);
+  private Settings settings = new MapSettings();
+
+  private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
+  private UserIdentityAuthenticator userIdentityAuthenticator = new UserIdentityAuthenticator(
+    db.getDbClient(),
+    new UserUpdater(mock(NewUserNotifier.class), settings, db.getDbClient(), mock(UserIndexer.class), System2.INSTANCE, defaultOrganizationProvider),
+    defaultOrganizationProvider);
+
+  private HttpServletResponse response = mock(HttpServletResponse.class);
+  private JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class);
+
+  private SsoAuthenticator underTest = new SsoAuthenticator(system2, settings, userIdentityAuthenticator, jwtHttpHandler);
+
+  @Before
+  public void setUp() throws Exception {
+    when(system2.now()).thenReturn(NOW);
+    group1 = db.users().insertGroup(db.getDefaultOrganization(), GROUP1);
+    group2 = db.users().insertGroup(db.getDefaultOrganization(), GROUP2);
+    db.commit();
+  }
+
+  @Test
+  public void create_user_when_authenticating_new_user() throws Exception {
+    enableSso();
+    setNotUserInToken();
+    HttpServletRequest request = createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, GROUPS);
+
+    underTest.authenticate(request, response);
+
+    verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1, group2);
+    verifyTokenIsUpdated(NOW);
+  }
+
+  @Test
+  public void use_login_when_name_is_not_provided() throws Exception {
+    enableSso();
+    setNotUserInToken();
+
+    underTest.authenticate(createRequest(DEFAULT_LOGIN, null, null, null), response);
+
+    verifyUserInDb(DEFAULT_LOGIN, DEFAULT_LOGIN, null);
+  }
+
+  @Test
+  public void update_user_when_authenticating_exiting_user() throws Exception {
+    enableSso();
+    setNotUserInToken();
+    insertUser(newUserDto().setLogin(DEFAULT_LOGIN).setName("old name").setEmail("old email"), group1);
+    // Name, email and groups are different
+    HttpServletRequest request = createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, GROUP2);
+
+    underTest.authenticate(request, response);
+
+    verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group2);
+    verifyTokenIsUpdated(NOW);
+  }
+
+  @Test
+  public void remove_groups_when_group_headers_is_empty() throws Exception {
+    enableSso();
+    setNotUserInToken();
+    insertUser(DEFAULT_USER, group1);
+
+    underTest.authenticate(createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, ""), response);
+
+    verityUserHasNoGroup(DEFAULT_LOGIN);
+  }
+
+  @Test
+  public void remove_groups_when_group_headers_is_null() throws Exception {
+    enableSso();
+    setNotUserInToken();
+    insertUser(DEFAULT_USER, group1);
+    Map<String, String> headerValuesByName = new HashMap<>();
+    headerValuesByName.put("X-Forwarded-Login", DEFAULT_LOGIN);
+    headerValuesByName.put("X-Forwarded-Groups", null);
+
+    underTest.authenticate(createRequest(headerValuesByName), response);
+
+    verityUserHasNoGroup(DEFAULT_LOGIN);
+  }
+
+  @Test
+  public void does_not_update_groups_when_no_group_headers() throws Exception {
+    enableSso();
+    setNotUserInToken();
+    insertUser(DEFAULT_USER, group1);
+
+    underTest.authenticate(createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, null), response);
+
+    verityUserGroups(DEFAULT_LOGIN, group1);
+  }
+
+  @Test
+  public void does_not_update_user_when_user_is_in_token_and_refresh_time_is_close() throws Exception {
+    enableSso();
+    UserDto user = insertUser(DEFAULT_USER, group1);
+    setUserInToken(user, CLOSE_REFRESH_TIME);
+    HttpServletRequest request = createRequest(DEFAULT_LOGIN, "new name", "new email", GROUP2);
+
+    underTest.authenticate(request, response);
+
+    // User is not updated
+    verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1);
+    verifyTokenIsNotUpdated();
+  }
+
+  @Test
+  public void update_user_when_user_in_token_but_refresh_time_is_old() throws Exception {
+    enableSso();
+    UserDto user = insertUser(DEFAULT_USER, group1);
+    // Refresh time was updated 6 minutes ago => more than 5 minutes
+    setUserInToken(user, NOW - 6 * 60 * 1000L);
+    HttpServletRequest request = createRequest(DEFAULT_LOGIN, "new name", "new email", GROUP2);
+
+    underTest.authenticate(request, response);
+
+    // User is updated
+    verifyUserInDb(DEFAULT_LOGIN, "new name", "new email", group2);
+    verifyTokenIsUpdated(NOW);
+  }
+
+  @Test
+  public void update_user_when_user_in_token_but_no_refresh_time() throws Exception {
+    enableSso();
+    UserDto user = insertUser(DEFAULT_USER, group1);
+    setUserInToken(user, null);
+    HttpServletRequest request = createRequest(DEFAULT_LOGIN, "new name", "new email", GROUP2);
+
+    underTest.authenticate(request, response);
+
+    // User is updated
+    verifyUserInDb(DEFAULT_LOGIN, "new name", "new email", group2);
+    verifyTokenIsUpdated(NOW);
+  }
+
+  @Test
+  public void use_refresh_time_from_settings() throws Exception {
+    enableSso();
+    settings.setProperty("sonar.sso.refreshIntervalInMinutes", "10");
+    UserDto user = insertUser(DEFAULT_USER, group1);
+    // Refresh time was updated 6 minutes ago => less than 10 minutes ago so not updated
+    setUserInToken(user, NOW - 6 * 60 * 1000L);
+    HttpServletRequest request = createRequest(DEFAULT_LOGIN, "new name", "new email", GROUP2);
+
+    underTest.authenticate(request, response);
+
+    // User is not updated
+    verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1);
+    verifyTokenIsNotUpdated();
+  }
+
+  @Test
+  public void update_user_when_login_from_token_is_different_than_login_from_request() throws Exception {
+    enableSso();
+    insertUser(DEFAULT_USER, group1);
+    setUserInToken(DEFAULT_USER, CLOSE_REFRESH_TIME);
+    HttpServletRequest request = createRequest("AnotherLogin", "Another name", "Another email", GROUP2);
+
+    underTest.authenticate(request, response);
+
+    verifyUserInDb("AnotherLogin", "Another name", "Another email", group2);
+    verifyTokenIsUpdated(NOW);
+  }
+
+  @Test
+  public void use_headers_from_settings() throws Exception {
+    enableSso();
+    setNotUserInToken();
+    settings.setProperty("sonar.sso.loginHeader", "head-login");
+    settings.setProperty("sonar.sso.nameHeader", "head-name");
+    settings.setProperty("sonar.sso.emailHeader", "head-email");
+    settings.setProperty("sonar.sso.groupsHeader", "head-groups");
+    HttpServletRequest request = createRequest(ImmutableMap.of("head-login", DEFAULT_LOGIN, "head-name", DEFAULT_NAME, "head-email", DEFAULT_EMAIL, "head-groups", GROUPS));
+
+    underTest.authenticate(request, response);
+
+    verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1, group2);
+  }
+
+  @Test
+  public void detect_group_header_even_with_wrong_case() throws Exception {
+    enableSso();
+    setNotUserInToken();
+    settings.setProperty("sonar.sso.loginHeader", "login");
+    settings.setProperty("sonar.sso.nameHeader", "name");
+    settings.setProperty("sonar.sso.emailHeader", "email");
+    settings.setProperty("sonar.sso.groupsHeader", "Groups");
+    HttpServletRequest request = createRequest(ImmutableMap.of("login", DEFAULT_LOGIN, "name", DEFAULT_NAME, "email", DEFAULT_EMAIL, "groups", GROUPS));
+
+    underTest.authenticate(request, response);
+
+    verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1, group2);
+  }
+
+  @Test
+  public void trim_groups() throws Exception {
+    enableSso();
+    setNotUserInToken();
+    HttpServletRequest request = createRequest(DEFAULT_LOGIN, null, null, "  dev ,    admin ");
+
+    underTest.authenticate(request, response);
+
+    verifyUserInDb(DEFAULT_LOGIN, DEFAULT_LOGIN, null, group1, group2);
+  }
+
+  @Test
+  public void does_not_authenticate_when_no_header() throws Exception {
+    enableSso();
+    setNotUserInToken();
+
+    underTest.authenticate(createRequest(Collections.emptyMap()), response);
+
+    verifyUserNotAuthenticated();
+    verifyTokenIsNotUpdated();
+  }
+
+  @Test
+  public void does_not_authenticate_when_not_enabled() throws Exception {
+    settings.setProperty("sonar.sso.enable", false);
+
+    underTest.authenticate(createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, GROUPS), response);
+
+    verifyUserNotAuthenticated();
+    verifyZeroInteractions(jwtHttpHandler);
+  }
+
+  private void enableSso() {
+    settings.setProperty("sonar.sso.enable", true);
+  }
+
+  private void setUserInToken(UserDto user, @Nullable Long lastRefreshTime) {
+    when(jwtHttpHandler.getToken(any(HttpServletRequest.class), any(HttpServletResponse.class)))
+      .thenReturn(Optional.of(new JwtHttpHandler.Token(
+        user,
+        lastRefreshTime == null ? Collections.emptyMap() : ImmutableMap.of("ssoLastRefreshTime", lastRefreshTime))));
+  }
+
+  private void setNotUserInToken() {
+    when(jwtHttpHandler.getToken(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(Optional.empty());
+  }
+
+  private UserDto insertUser(UserDto user, GroupDto... groups) {
+    db.users().insertUser(user);
+    stream(groups).forEach(group -> db.users().insertMember(group, user));
+    db.commit();
+    return user;
+  }
+
+  private static HttpServletRequest createRequest(Map<String, String> headerValuesByName) {
+    HttpServletRequest request = mock(HttpServletRequest.class);
+    setHeaders(request, headerValuesByName);
+    return request;
+  }
+
+  private static HttpServletRequest createRequest(String login, @Nullable String name, @Nullable String email, @Nullable String groups) {
+    Map<String, String> headerValuesByName = new HashMap<>();
+    headerValuesByName.put("X-Forwarded-Login", login);
+    if (name != null) {
+      headerValuesByName.put("X-Forwarded-Name", name);
+    }
+    if (email != null) {
+      headerValuesByName.put("X-Forwarded-Email", email);
+    }
+    if (groups != null) {
+      headerValuesByName.put("X-Forwarded-Groups", groups);
+    }
+    HttpServletRequest request = mock(HttpServletRequest.class);
+    setHeaders(request, headerValuesByName);
+    return request;
+  }
+
+  private static void setHeaders(HttpServletRequest request, Map<String, String> valuesByName) {
+    valuesByName.entrySet().forEach(entry -> when(request.getHeader(entry.getKey())).thenReturn(entry.getValue()));
+    when(request.getHeaderNames()).thenReturn(Collections.enumeration(valuesByName.keySet()));
+  }
+
+  private void verifyUserInDb(String expectedLogin, String expectedName, @Nullable String expectedEmail, GroupDto... expectedGroups) {
+    UserDto userDto = db.users().selectUserByLogin(expectedLogin).get();
+    assertThat(userDto.isActive()).isTrue();
+    assertThat(userDto.getName()).isEqualTo(expectedName);
+    assertThat(userDto.getEmail()).isEqualTo(expectedEmail);
+    assertThat(userDto.getExternalIdentity()).isEqualTo(expectedLogin);
+    assertThat(userDto.getExternalIdentityProvider()).isEqualTo("sonarqube");
+    verityUserGroups(expectedLogin, expectedGroups);
+  }
+
+  private void verityUserGroups(String login, GroupDto... expectedGroups) {
+    UserDto userDto = db.users().selectUserByLogin(login).get();
+    if (expectedGroups.length == 0) {
+      assertThat(db.users().selectGroupIdsOfUser(userDto)).isEmpty();
+    } else {
+      assertThat(db.users().selectGroupIdsOfUser(userDto)).containsOnly(stream(expectedGroups).map(GroupDto::getId).collect(Collectors.toList()).toArray(new Long[] {}));
+    }
+  }
+
+  private void verityUserHasNoGroup(String login) {
+    verityUserGroups(login);
+  }
+
+  private void verifyUserNotAuthenticated() {
+    assertThat(db.countRowsOfTable(db.getSession(), "users")).isZero();
+    verifyTokenIsNotUpdated();
+  }
+
+  private void verifyTokenIsUpdated(long refreshTime) {
+    verify(jwtHttpHandler).generateToken(any(UserDto.class), eq(ImmutableMap.of("ssoLastRefreshTime", refreshTime)), any(HttpServletRequest.class), any(HttpServletResponse.class));
+  }
+
+  private void verifyTokenIsNotUpdated() {
+    verify(jwtHttpHandler, never()).generateToken(any(UserDto.class), anyMap(), any(HttpServletRequest.class), any(HttpServletResponse.class));
+  }
+
+}
index 9f5501b9f8674c114c2b7d5918df819dd09692f6..36cc8e38e18845c874f0ebea1925bcf518784a14 100644 (file)
@@ -26,8 +26,8 @@ import javax.servlet.http.HttpServletResponse;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
-import org.sonar.api.config.Settings;
 import org.sonar.api.config.MapSettings;
+import org.sonar.api.config.Settings;
 import org.sonar.api.utils.System2;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
@@ -66,12 +66,13 @@ public class UserSessionInitializerTest {
 
   JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class);
   BasicAuthenticator basicAuthenticator = mock(BasicAuthenticator.class);
+  SsoAuthenticator ssoAuthenticator = mock(SsoAuthenticator.class);
 
   Settings settings = new MapSettings();
 
   UserDto user = newUserDto();
 
-  UserSessionInitializer underTest = new UserSessionInitializer(dbClient, settings, jwtHttpHandler, basicAuthenticator, userSession);
+  UserSessionInitializer underTest = new UserSessionInitializer(dbClient, settings, jwtHttpHandler, basicAuthenticator, ssoAuthenticator, userSession);
 
   @Before
   public void setUp() throws Exception {
@@ -112,6 +113,7 @@ public class UserSessionInitializerTest {
   @Test
   public void validate_session_from_token() throws Exception {
     when(userSession.isLoggedIn()).thenReturn(true);
+    when(ssoAuthenticator.authenticate(request, response)).thenReturn(Optional.empty());
     when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.of(user));
 
     assertThat(underTest.initUserSession(request, response)).isTrue();
@@ -124,6 +126,7 @@ public class UserSessionInitializerTest {
   public void validate_session_from_basic_authentication() throws Exception {
     when(userSession.isLoggedIn()).thenReturn(false).thenReturn(true);
     when(basicAuthenticator.authenticate(request)).thenReturn(Optional.of(user));
+    when(ssoAuthenticator.authenticate(request, response)).thenReturn(Optional.empty());
     when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.empty());
 
     assertThat(underTest.initUserSession(request, response)).isTrue();
@@ -134,8 +137,22 @@ public class UserSessionInitializerTest {
     verify(response, never()).setStatus(anyInt());
   }
 
+  @Test
+  public void validate_session_from_sso() throws Exception {
+    when(userSession.isLoggedIn()).thenReturn(true);
+    when(ssoAuthenticator.authenticate(request, response)).thenReturn(Optional.of(user));
+    when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.empty());
+
+    assertThat(underTest.initUserSession(request, response)).isTrue();
+
+    verify(ssoAuthenticator).authenticate(request, response);
+    verify(jwtHttpHandler, never()).validateToken(request, response);
+    verify(response, never()).setStatus(anyInt());
+  }
+
   @Test
   public void return_code_401_when_invalid_token_exception() throws Exception {
+    when(ssoAuthenticator.authenticate(request, response)).thenReturn(Optional.empty());
     doThrow(new UnauthorizedException("invalid token")).when(jwtHttpHandler).validateToken(request, response);
 
     assertThat(underTest.initUserSession(request, response)).isTrue();
@@ -148,6 +165,7 @@ public class UserSessionInitializerTest {
   public void return_code_401_when_not_authenticated_and_with_force_authentication() throws Exception {
     when(userSession.isLoggedIn()).thenReturn(false);
     when(basicAuthenticator.authenticate(request)).thenReturn(Optional.empty());
+    when(ssoAuthenticator.authenticate(request, response)).thenReturn(Optional.empty());
     when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.empty());
     settings.setProperty("sonar.forceAuthentication", true);
 
@@ -160,6 +178,7 @@ public class UserSessionInitializerTest {
   @Test
   public void return_401_and_stop_on_ws() throws Exception {
     when(request.getRequestURI()).thenReturn("/api/issues");
+    when(ssoAuthenticator.authenticate(request, response)).thenReturn(Optional.empty());
     doThrow(new UnauthorizedException("invalid token")).when(jwtHttpHandler).validateToken(request, response);
 
     assertThat(underTest.initUserSession(request, response)).isFalse();
@@ -171,6 +190,7 @@ public class UserSessionInitializerTest {
   @Test
   public void return_401_and_stop_on_batch_ws() throws Exception {
     when(request.getRequestURI()).thenReturn("/batch/global");
+    when(ssoAuthenticator.authenticate(request, response)).thenReturn(Optional.empty());
     doThrow(new UnauthorizedException("invalid token")).when(jwtHttpHandler).validateToken(request, response);
 
     assertThat(underTest.initUserSession(request, response)).isFalse();
@@ -190,6 +210,7 @@ public class UserSessionInitializerTest {
 
   private void assertPathIsNotIgnored(String path) {
     when(request.getRequestURI()).thenReturn(path);
+    when(ssoAuthenticator.authenticate(request, response)).thenReturn(Optional.empty());
     when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.of(user));
 
     assertThat(underTest.initUserSession(request, response)).isTrue();
index f34e1cd4c2edf883fbc98291ba75b709306ef615..a39c648033f65dc31a2f1836aabbe139f5f607ac 100644 (file)
 #sonar.web.accessLogs.pattern=combined
 
 
+#--------------------------------------------------------------------------------------------------
+# AUTHENTICATION
+
+# Enable authentication using HTTP headers
+#sonar.sso.enable=false
+
+# Name of the header to get the user login.
+# Only alphanumeric, '.' and '@' characters are allowed
+#sonar.sso.loginHeader=X-Forwarded-Login
+
+# Name of the header to get the user name
+#sonar.sso.nameHeader=X-Forwarded-Name
+
+# Name of the header to get the user email (optional)
+#sonar.sso.emailHeader=X-Forwarded-Email
+
+# Name of the header to get the list of user groups, separated by comma (optional).
+# If the sonar.sso.groupsHeader is set, the user will belong to those groups if groups exist in SonarQube.
+# If none of the provided groups exists in SonarQube, the user won't belong to any group.
+# Note that the default group will NOT be automatically added when using SSO, it should be provided in the groups list, if needed.
+#sonar.sso.groupsHeader=X-Forwarded-Groups
+
+# Interval used to know when to refresh name, email and groups.
+# During this interval, if for instance the name of the user is changed in the header, it will only be updated after X minutes.
+#sonar.sso.refreshIntervalInMinutes=5
+
+
 #--------------------------------------------------------------------------------------------------
 # OTHERS