]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10652 Correctly fail when authentication and several emails exists
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Thu, 24 May 2018 13:56:11 +0000 (15:56 +0200)
committerSonarTech <sonartech@sonarsource.com>
Tue, 29 May 2018 18:20:46 +0000 (20:20 +0200)
29 files changed:
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserMapper.java
server/sonar-db-dao/src/test/java/org/sonar/db/user/UserDaoTest.java
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/GroupTester.java
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/LoginPage.java
server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java
server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticatorsImpl.java
server/sonar-server/src/main/java/org/sonar/server/authentication/HttpHeadersAuthenticator.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/SsoAuthenticator.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticatorImpl.java
server/sonar-server/src/test/java/org/sonar/server/authentication/AuthenticatorsImplTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/HttpHeadersAuthenticatorTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/authentication/SsoAuthenticatorTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorImplTest.java
tests/src/test/java/org/sonarqube/tests/Category5Suite.java
tests/src/test/java/org/sonarqube/tests/user/BaseIdentityProviderTest.java
tests/src/test/java/org/sonarqube/tests/user/HttpHeadersAuthenticationTest.java [new file with mode: 0644]
tests/src/test/java/org/sonarqube/tests/user/OAuth2IdentityProviderTest.java
tests/src/test/java/org/sonarqube/tests/user/RealmAuthenticationTest.java
tests/src/test/java/org/sonarqube/tests/user/SsoAuthenticationTest.java [deleted file]
tests/src/test/resources/user/BaseIdentityProviderTest/display_message_in_ui_but_not_in_log_when_unauthorized_exception.html [deleted file]
tests/src/test/resources/user/BaseIdentityProviderTest/display_unauthorized_page_when_authentication_failed.html [deleted file]
tests/src/test/resources/user/BaseIdentityProviderTest/fail_to_authenticate_when_not_allowed_to_sign_up.html [deleted file]
tests/src/test/resources/user/BaseIdentityProviderTest/fail_when_email_already_exists.html [deleted file]
tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details.html [deleted file]
tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details2.html [deleted file]
tests/src/test/resources/user/OAuth2IdentityProviderTest/display_message_in_ui_but_not_in_log_when_unauthorized_exception.html [deleted file]
tests/src/test/resources/user/OAuth2IdentityProviderTest/display_unauthorized_page_when_authentication_failed.html [deleted file]
tests/src/test/resources/user/OAuth2IdentityProviderTest/fail_to_authenticate_when_not_allowed_to_sign_up.html [deleted file]

index 79318d4d02a6811977803babc3fa9610a054da95..f78aebc4f71ef8cfb3bd39b48cda1f47d348da6e 100644 (file)
@@ -22,7 +22,6 @@ package org.sonar.db.user;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.function.Consumer;
@@ -38,15 +37,16 @@ import org.sonar.db.DbSession;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.organization.OrganizationDto;
 
+import static java.util.Locale.ENGLISH;
 import static org.sonar.db.DatabaseUtils.executeLargeInputs;
 import static org.sonar.db.DatabaseUtils.executeLargeInputsWithoutOutput;
+import static org.sonar.db.user.UserDto.SCM_ACCOUNTS_SEPARATOR;
 
 public class UserDao implements Dao {
 
   private final System2 system2;
   private final UuidFactory uuidFactory;
 
-
   public UserDao(System2 system2, UuidFactory uuidFactory) {
     this.system2 = system2;
     this.uuidFactory = uuidFactory;
@@ -154,19 +154,18 @@ public class UserDao implements Dao {
 
   public List<UserDto> selectByScmAccountOrLoginOrEmail(DbSession session, String scmAccountOrLoginOrEmail) {
     String like = new StringBuilder().append("%")
-      .append(UserDto.SCM_ACCOUNTS_SEPARATOR).append(scmAccountOrLoginOrEmail)
-      .append(UserDto.SCM_ACCOUNTS_SEPARATOR).append("%").toString();
+      .append(SCM_ACCOUNTS_SEPARATOR).append(scmAccountOrLoginOrEmail)
+      .append(SCM_ACCOUNTS_SEPARATOR).append("%").toString();
     return mapper(session).selectNullableByScmAccountOrLoginOrEmail(scmAccountOrLoginOrEmail, like);
   }
 
   /**
-   * Search for an active user with the given email exits in database
+   * Search for an active user with the given emailCaseInsensitive exits in database
    *
-   * Please note that email is case insensitive, result for searching 'mail@email.com' or 'Mail@Email.com' will be the same
+   * Select is case insensitive. Result for searching 'mail@emailCaseInsensitive.com' or 'Mail@Email.com' is the same
    */
-  @CheckForNull
-  public UserDto selectByEmail(DbSession dbSession, String email) {
-    return mapper(dbSession).selectByEmail(email.toLowerCase(Locale.ENGLISH));
+  public List<UserDto> selectByEmail(DbSession dbSession, String emailCaseInsensitive) {
+    return mapper(dbSession).selectByEmail(emailCaseInsensitive.toLowerCase(ENGLISH));
   }
 
   @CheckForNull
index c83c98a739a8e4d430b7ec88f58fd088013439ce..2ab7fe93dabb8f16bc475d3a9c4d022722a26dd3 100644 (file)
@@ -57,8 +57,7 @@ public interface UserMapper {
 
   List<UserDto> selectByIds(@Param("ids") List<Integer> ids);
 
-  @CheckForNull
-  UserDto selectByEmail(String email);
+  List<UserDto> selectByEmail(String email);
 
   @CheckForNull
   UserDto selectByExternalIdAndIdentityProvider(@Param("externalId") String externalId, @Param("externalIdentityProvider") String externalExternalIdentityProvider);
index 1ca0e6a9c01420cab03affa7bea96ae3325a60df..f54f8923af80eb2430c5daf39ae2bd291de7171c 100644 (file)
@@ -585,12 +585,13 @@ public class UserDaoTest {
 
   @Test
   public void select_by_email() {
-    UserDto activeUser = db.users().insertUser();
+    UserDto activeUser1 = db.users().insertUser(u -> u.setEmail("user1@email.com"));
+    UserDto activeUser2 = db.users().insertUser(u -> u.setEmail("user1@email.com"));
     UserDto disableUser = db.users().insertUser(u -> u.setActive(false));
 
-    assertThat(underTest.selectByEmail(session, activeUser.getEmail())).isNotNull();
-    assertThat(underTest.selectByEmail(session, disableUser.getEmail())).isNull();
-    assertThat(underTest.selectByEmail(session, "unknown")).isNull();
+    assertThat(underTest.selectByEmail(session, "user1@email.com")).hasSize(2);
+    assertThat(underTest.selectByEmail(session, disableUser.getEmail())).isEmpty();
+    assertThat(underTest.selectByEmail(session, "unknown")).isEmpty();
   }
 
   @Test
index 4b369210c88c3004b0f674d5dd1cfe17f95a756b..3e1c0cbee85d1136ccb52017fe52ba79720a781c 100644 (file)
@@ -70,11 +70,11 @@ public class GroupTester {
     return response.getGroupsList();
   }
 
-  public GroupTester addMemberToGroups(Organizations.Organization organization, String userLogin, String... groups) {
+  public GroupTester addMemberToGroups(@Nullable Organizations.Organization organization, String userLogin, String... groups) {
     for (String group : groups) {
       AddUserRequest request = new AddUserRequest()
         .setLogin(userLogin)
-        .setOrganization(organization.getKey())
+        .setOrganization(organization != null ? organization.getKey() : null)
         .setName(group);
       session.wsClient().userGroups().addUser(request);
     }
index 3f765429d47ac5d7cc783a493b3a4e47350b9914..154386aacb039e1e34c84e2b890a2e9ced38f726 100644 (file)
@@ -45,6 +45,11 @@ public class LoginPage {
     return Selenide.page(Navigation.class);
   }
 
+  public Navigation useBaseAuth() {
+    Selenide.$(".oauth-providers a").click();
+    return Selenide.page(Navigation.class);
+  }
+
   public LoginPage submitWrongCredentials(String login, String password) {
     Selenide.$("#login").val(login);
     Selenide.$("#password").val(password);
index 7cc3041bdd34b4afa02fee4324959307e58af62d..dfc64b840e090eb76da38bce921b662652f82a1e 100644 (file)
@@ -51,7 +51,7 @@ public class AuthenticationModule extends Module {
       RealmAuthenticator.class,
       BasicAuthenticator.class,
       ValidateAction.class,
-      SsoAuthenticator.class,
+      HttpHeadersAuthenticator.class,
       AuthenticatorsImpl.class);
   }
 }
index 358b149f78707706573a056cab62def377160fb7..a1bd581afa587e65767d467513c1e213bba06f28 100644 (file)
@@ -28,19 +28,19 @@ public class AuthenticatorsImpl implements Authenticators {
 
   private final JwtHttpHandler jwtHttpHandler;
   private final BasicAuthenticator basicAuthenticator;
-  private final SsoAuthenticator ssoAuthenticator;
+  private final HttpHeadersAuthenticator httpHeadersAuthenticator;
 
-  public AuthenticatorsImpl(JwtHttpHandler jwtHttpHandler, BasicAuthenticator basicAuthenticator, SsoAuthenticator ssoAuthenticator) {
+  public AuthenticatorsImpl(JwtHttpHandler jwtHttpHandler, BasicAuthenticator basicAuthenticator, HttpHeadersAuthenticator httpHeadersAuthenticator) {
     this.jwtHttpHandler = jwtHttpHandler;
     this.basicAuthenticator = basicAuthenticator;
-    this.ssoAuthenticator = ssoAuthenticator;
+    this.httpHeadersAuthenticator = httpHeadersAuthenticator;
   }
 
   // Try first to authenticate from SSO, then JWT token, then try from basic http header
   @Override
   public Optional<UserDto> authenticate(HttpServletRequest request, HttpServletResponse 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);
+    Optional<UserDto> user = httpHeadersAuthenticator.authenticate(request, response);
     if (user.isPresent()) {
       return user;
     }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/HttpHeadersAuthenticator.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/HttpHeadersAuthenticator.java
new file mode 100644 (file)
index 0000000..5282b62
--- /dev/null
@@ -0,0 +1,216 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.authentication;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableMap;
+import java.util.Collections;
+import java.util.Date;
+import java.util.EnumSet;
+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.Startable;
+import org.sonar.api.config.Configuration;
+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.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.db.user.UserDto;
+import org.sonar.process.ProcessProperties;
+import org.sonar.server.authentication.UserIdentityAuthenticatorParameters.ExistingEmailStrategy;
+import org.sonar.server.authentication.UserIdentityAuthenticatorParameters.UpdateLoginStrategy;
+import org.sonar.server.authentication.event.AuthenticationEvent;
+import org.sonar.server.authentication.event.AuthenticationEvent.Source;
+import org.sonar.server.authentication.event.AuthenticationException;
+import org.sonar.server.exceptions.BadRequestException;
+
+import static org.apache.commons.lang.time.DateUtils.addMinutes;
+import static org.sonar.process.ProcessProperties.Property.SONAR_WEB_SSO_EMAIL_HEADER;
+import static org.sonar.process.ProcessProperties.Property.SONAR_WEB_SSO_ENABLE;
+import static org.sonar.process.ProcessProperties.Property.SONAR_WEB_SSO_GROUPS_HEADER;
+import static org.sonar.process.ProcessProperties.Property.SONAR_WEB_SSO_LOGIN_HEADER;
+import static org.sonar.process.ProcessProperties.Property.SONAR_WEB_SSO_NAME_HEADER;
+import static org.sonar.process.ProcessProperties.Property.SONAR_WEB_SSO_REFRESH_INTERVAL_IN_MINUTES;
+import static org.sonar.server.user.ExternalIdentity.SQ_AUTHORITY;
+
+public class HttpHeadersAuthenticator implements Startable {
+
+  private static final Logger LOG = Loggers.get(HttpHeadersAuthenticator.class);
+
+  private static final Splitter COMA_SPLITTER = Splitter.on(",").trimResults().omitEmptyStrings();
+
+  private static final String LAST_REFRESH_TIME_TOKEN_PARAM = "ssoLastRefreshTime";
+
+  private static final EnumSet<ProcessProperties.Property> SETTINGS = EnumSet.of(
+    SONAR_WEB_SSO_LOGIN_HEADER,
+    SONAR_WEB_SSO_NAME_HEADER,
+    SONAR_WEB_SSO_EMAIL_HEADER,
+    SONAR_WEB_SSO_GROUPS_HEADER,
+    SONAR_WEB_SSO_REFRESH_INTERVAL_IN_MINUTES);
+
+  private final System2 system2;
+  private final Configuration config;
+  private final UserIdentityAuthenticator userIdentityAuthenticator;
+  private final JwtHttpHandler jwtHttpHandler;
+  private final AuthenticationEvent authenticationEvent;
+
+  private boolean enabled = false;
+  private Map<String, String> settingsByKey = new HashMap<>();
+
+  public HttpHeadersAuthenticator(System2 system2, Configuration config, UserIdentityAuthenticator userIdentityAuthenticator,
+                                  JwtHttpHandler jwtHttpHandler, AuthenticationEvent authenticationEvent) {
+    this.system2 = system2;
+    this.config = config;
+    this.userIdentityAuthenticator = userIdentityAuthenticator;
+    this.jwtHttpHandler = jwtHttpHandler;
+    this.authenticationEvent = authenticationEvent;
+  }
+
+  @Override
+  public void start() {
+    if (config.getBoolean(SONAR_WEB_SSO_ENABLE.getKey()).orElse(false)) {
+      LOG.info("HTTP headers authentication enabled");
+      enabled = true;
+      SETTINGS.forEach(entry -> settingsByKey.put(entry.getKey(), config.get(entry.getKey()).orElse(entry.getDefaultValue())));
+    }
+  }
+
+  @Override
+  public void stop() {
+    // Nothing to do
+  }
+
+  public Optional<UserDto> authenticate(HttpServletRequest request, HttpServletResponse response) {
+    try {
+      return doAuthenticate(request, response);
+    } catch (BadRequestException e) {
+      throw AuthenticationException.newBuilder()
+        .setSource(Source.sso())
+        .setMessage(e.getMessage())
+        .build();
+    }
+  }
+
+  private Optional<UserDto> doAuthenticate(HttpServletRequest request, HttpServletResponse response) {
+    if (!enabled) {
+      return Optional.empty();
+    }
+    Map<String, String> headerValuesByNames = getHeaders(request);
+    String login = getHeaderValue(headerValuesByNames, SONAR_WEB_SSO_LOGIN_HEADER.getKey());
+    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);
+    authenticationEvent.loginSuccess(request, userDto.getLogin(), Source.sso());
+    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(settingsByKey.get(SONAR_WEB_SSO_REFRESH_INTERVAL_IN_MINUTES.getKey()));
+    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, SONAR_WEB_SSO_NAME_HEADER.getKey());
+    String email = getHeaderValue(headerValuesByNames, SONAR_WEB_SSO_EMAIL_HEADER.getKey());
+    UserIdentity.Builder userIdentityBuilder = UserIdentity.builder()
+      .setLogin(login)
+      .setName(name == null ? login : name)
+      .setEmail(email)
+      .setProviderLogin(login);
+    if (hasHeader(headerValuesByNames, SONAR_WEB_SSO_GROUPS_HEADER.getKey())) {
+      String groupsValue = getHeaderValue(headerValuesByNames, SONAR_WEB_SSO_GROUPS_HEADER.getKey());
+      userIdentityBuilder.setGroups(groupsValue == null ? Collections.emptySet() : new HashSet<>(COMA_SPLITTER.splitToList(groupsValue)));
+    }
+    return userIdentityAuthenticator.authenticate(
+      UserIdentityAuthenticatorParameters.builder()
+        .setUserIdentity(userIdentityBuilder.build())
+        .setProvider(new SsoIdentityProvider())
+        .setSource(Source.sso())
+        .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+        .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+        .build());
+  }
+
+  @CheckForNull
+  private String getHeaderValue(Map<String, String> headerValuesByNames, String settingKey) {
+    return headerValuesByNames.get(settingsByKey.get(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(settingsByKey.get(settingKey).toLowerCase(Locale.ENGLISH));
+  }
+
+  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;
+    }
+  }
+}
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
deleted file mode 100644 (file)
index 8d59e94..0000000
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.server.authentication;
-
-import com.google.common.base.Splitter;
-import com.google.common.collect.ImmutableMap;
-import java.util.Collections;
-import java.util.Date;
-import java.util.EnumSet;
-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.Startable;
-import org.sonar.api.config.Configuration;
-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.api.utils.log.Logger;
-import org.sonar.api.utils.log.Loggers;
-import org.sonar.db.user.UserDto;
-import org.sonar.process.ProcessProperties;
-import org.sonar.server.authentication.UserIdentityAuthenticatorParameters.ExistingEmailStrategy;
-import org.sonar.server.authentication.UserIdentityAuthenticatorParameters.UpdateLoginStrategy;
-import org.sonar.server.authentication.event.AuthenticationEvent;
-import org.sonar.server.authentication.event.AuthenticationEvent.Source;
-import org.sonar.server.authentication.event.AuthenticationException;
-import org.sonar.server.exceptions.BadRequestException;
-
-import static org.apache.commons.lang.time.DateUtils.addMinutes;
-import static org.sonar.process.ProcessProperties.Property.SONAR_WEB_SSO_EMAIL_HEADER;
-import static org.sonar.process.ProcessProperties.Property.SONAR_WEB_SSO_ENABLE;
-import static org.sonar.process.ProcessProperties.Property.SONAR_WEB_SSO_GROUPS_HEADER;
-import static org.sonar.process.ProcessProperties.Property.SONAR_WEB_SSO_LOGIN_HEADER;
-import static org.sonar.process.ProcessProperties.Property.SONAR_WEB_SSO_NAME_HEADER;
-import static org.sonar.process.ProcessProperties.Property.SONAR_WEB_SSO_REFRESH_INTERVAL_IN_MINUTES;
-import static org.sonar.server.user.ExternalIdentity.SQ_AUTHORITY;
-
-public class SsoAuthenticator implements Startable {
-
-  private static final Logger LOG = Loggers.get(SsoAuthenticator.class);
-
-  private static final Splitter COMA_SPLITTER = Splitter.on(",").trimResults().omitEmptyStrings();
-
-  private static final String LAST_REFRESH_TIME_TOKEN_PARAM = "ssoLastRefreshTime";
-
-  private static final EnumSet<ProcessProperties.Property> SETTINGS = EnumSet.of(
-    SONAR_WEB_SSO_LOGIN_HEADER,
-    SONAR_WEB_SSO_NAME_HEADER,
-    SONAR_WEB_SSO_EMAIL_HEADER,
-    SONAR_WEB_SSO_GROUPS_HEADER,
-    SONAR_WEB_SSO_REFRESH_INTERVAL_IN_MINUTES);
-
-  private final System2 system2;
-  private final Configuration config;
-  private final UserIdentityAuthenticator userIdentityAuthenticator;
-  private final JwtHttpHandler jwtHttpHandler;
-  private final AuthenticationEvent authenticationEvent;
-
-  private boolean enabled = false;
-  private Map<String, String> settingsByKey = new HashMap<>();
-
-  public SsoAuthenticator(System2 system2, Configuration config, UserIdentityAuthenticator userIdentityAuthenticator,
-    JwtHttpHandler jwtHttpHandler, AuthenticationEvent authenticationEvent) {
-    this.system2 = system2;
-    this.config = config;
-    this.userIdentityAuthenticator = userIdentityAuthenticator;
-    this.jwtHttpHandler = jwtHttpHandler;
-    this.authenticationEvent = authenticationEvent;
-  }
-
-  @Override
-  public void start() {
-    if (config.getBoolean(SONAR_WEB_SSO_ENABLE.getKey()).orElse(false)) {
-      LOG.info("SSO Authentication enabled");
-      enabled = true;
-      SETTINGS.forEach(entry -> settingsByKey.put(entry.getKey(), config.get(entry.getKey()).orElse(entry.getDefaultValue())));
-    }
-  }
-
-  @Override
-  public void stop() {
-    // Nothing to do
-  }
-
-  public Optional<UserDto> authenticate(HttpServletRequest request, HttpServletResponse response) {
-    try {
-      return doAuthenticate(request, response);
-    } catch (BadRequestException e) {
-      throw AuthenticationException.newBuilder()
-        .setSource(Source.sso())
-        .setMessage(e.getMessage())
-        .build();
-    }
-  }
-
-  private Optional<UserDto> doAuthenticate(HttpServletRequest request, HttpServletResponse response) {
-    if (!enabled) {
-      return Optional.empty();
-    }
-    Map<String, String> headerValuesByNames = getHeaders(request);
-    String login = getHeaderValue(headerValuesByNames, SONAR_WEB_SSO_LOGIN_HEADER.getKey());
-    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);
-    authenticationEvent.loginSuccess(request, userDto.getLogin(), Source.sso());
-    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(settingsByKey.get(SONAR_WEB_SSO_REFRESH_INTERVAL_IN_MINUTES.getKey()));
-    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, SONAR_WEB_SSO_NAME_HEADER.getKey());
-    String email = getHeaderValue(headerValuesByNames, SONAR_WEB_SSO_EMAIL_HEADER.getKey());
-    UserIdentity.Builder userIdentityBuilder = UserIdentity.builder()
-      .setLogin(login)
-      .setName(name == null ? login : name)
-      .setEmail(email)
-      .setProviderLogin(login);
-    if (hasHeader(headerValuesByNames, SONAR_WEB_SSO_GROUPS_HEADER.getKey())) {
-      String groupsValue = getHeaderValue(headerValuesByNames, SONAR_WEB_SSO_GROUPS_HEADER.getKey());
-      userIdentityBuilder.setGroups(groupsValue == null ? Collections.emptySet() : new HashSet<>(COMA_SPLITTER.splitToList(groupsValue)));
-    }
-    return userIdentityAuthenticator.authenticate(
-      UserIdentityAuthenticatorParameters.builder()
-        .setUserIdentity(userIdentityBuilder.build())
-        .setProvider(new SsoIdentityProvider())
-        .setSource(Source.sso())
-        .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
-        .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
-        .build());
-  }
-
-  @CheckForNull
-  private String getHeaderValue(Map<String, String> headerValuesByNames, String settingKey) {
-    return headerValuesByNames.get(settingsByKey.get(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(settingsByKey.get(settingKey).toLowerCase(Locale.ENGLISH));
-  }
-
-  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 966f0d97e63f8a9c03f42b4d9b68c54a233912f7..a5247b533b9e30b006c1aead80f3f2ae407838de 100644 (file)
@@ -23,6 +23,7 @@ import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
@@ -135,7 +136,15 @@ public class UserIdentityAuthenticatorImpl implements UserIdentityAuthenticator
     if (email == null) {
       return Optional.empty();
     }
-    UserDto existingUser = dbClient.userDao().selectByEmail(dbSession, email);
+    List<UserDto> existingUsers = dbClient.userDao().selectByEmail(dbSession, email);
+    if (existingUsers.isEmpty()) {
+      return Optional.empty();
+    }
+    if (existingUsers.size() > 1) {
+      throw generateExistingEmailError(authenticatorParameters, email);
+    }
+
+    UserDto existingUser = existingUsers.get(0);
     if (existingUser == null
       || Objects.equals(existingUser.getLogin(), authenticatorParameters.getUserIdentity().getLogin())
       || (Objects.equals(existingUser.getExternalId(), authenticatorParameters.getUserIdentity().getProviderId())
@@ -151,14 +160,7 @@ public class UserIdentityAuthenticatorImpl implements UserIdentityAuthenticator
       case WARN:
         throw new EmailAlreadyExistsRedirectionException(email, existingUser, authenticatorParameters.getUserIdentity(), authenticatorParameters.getProvider());
       case FORBID:
-        throw AuthenticationException.newBuilder()
-          .setSource(authenticatorParameters.getSource())
-          .setLogin(authenticatorParameters.getUserIdentity().getLogin())
-          .setMessage(format("Email '%s' is already used", email))
-          .setPublicMessage(format(
-            "You can't sign up because email '%s' is already used by an existing user. This means that you probably already registered with another account.",
-            email))
-          .build();
+        throw generateExistingEmailError(authenticatorParameters, email);
       default:
         throw new IllegalStateException(format("Unknown strategy %s", existingEmailStrategy));
     }
@@ -264,4 +266,15 @@ public class UserIdentityAuthenticatorImpl implements UserIdentityAuthenticator
     return userDto.map(u -> new UserDto[] {u}).orElse(new UserDto[] {});
   }
 
+  private static AuthenticationException generateExistingEmailError(UserIdentityAuthenticatorParameters authenticatorParameters, String email) {
+    return AuthenticationException.newBuilder()
+      .setSource(authenticatorParameters.getSource())
+      .setLogin(authenticatorParameters.getUserIdentity().getLogin())
+      .setMessage(format("Email '%s' is already used", email))
+      .setPublicMessage(format(
+        "You can't sign up because email '%s' is already used by an existing user. This means that you probably already registered with another account.",
+        email))
+      .build();
+  }
+
 }
index 0924ad34d15a3bb8bc05e52f4838ebc903883033..492fb8bfa0b236b59d6572e4bfa7868824874b32 100644 (file)
@@ -40,12 +40,12 @@ public class AuthenticatorsImplTest {
   private HttpServletResponse response = mock(HttpServletResponse.class);
   private JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class);
   private BasicAuthenticator basicAuthenticator = mock(BasicAuthenticator.class);
-  private SsoAuthenticator ssoAuthenticator = mock(SsoAuthenticator.class);
-  private Authenticators underTest = new AuthenticatorsImpl(jwtHttpHandler, basicAuthenticator, ssoAuthenticator);
+  private HttpHeadersAuthenticator httpHeadersAuthenticator = mock(HttpHeadersAuthenticator.class);
+  private Authenticators underTest = new AuthenticatorsImpl(jwtHttpHandler, basicAuthenticator, httpHeadersAuthenticator);
 
   @Test
   public void authenticate_from_jwt_token() {
-    when(ssoAuthenticator.authenticate(request, response)).thenReturn(Optional.empty());
+    when(httpHeadersAuthenticator.authenticate(request, response)).thenReturn(Optional.empty());
     when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.of(user));
 
     assertThat(underTest.authenticate(request, response)).hasValue(user);
@@ -55,7 +55,7 @@ public class AuthenticatorsImplTest {
   @Test
   public void authenticate_from_basic_header() {
     when(basicAuthenticator.authenticate(request)).thenReturn(Optional.of(user));
-    when(ssoAuthenticator.authenticate(request, response)).thenReturn(Optional.empty());
+    when(httpHeadersAuthenticator.authenticate(request, response)).thenReturn(Optional.empty());
     when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.empty());
 
     assertThat(underTest.authenticate(request, response)).hasValue(user);
@@ -67,12 +67,12 @@ public class AuthenticatorsImplTest {
 
   @Test
   public void authenticate_from_sso() {
-    when(ssoAuthenticator.authenticate(request, response)).thenReturn(Optional.of(user));
+    when(httpHeadersAuthenticator.authenticate(request, response)).thenReturn(Optional.of(user));
     when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.empty());
 
     assertThat(underTest.authenticate(request, response)).hasValue(user);
 
-    verify(ssoAuthenticator).authenticate(request, response);
+    verify(httpHeadersAuthenticator).authenticate(request, response);
     verify(jwtHttpHandler, never()).validateToken(request, response);
     verify(response, never()).setStatus(anyInt());
   }
@@ -80,7 +80,7 @@ public class AuthenticatorsImplTest {
   @Test
   public void return_empty_if_not_authenticated() {
     when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.empty());
-    when(ssoAuthenticator.authenticate(request, response)).thenReturn(Optional.empty());
+    when(httpHeadersAuthenticator.authenticate(request, response)).thenReturn(Optional.empty());
     when(basicAuthenticator.authenticate(request)).thenReturn(Optional.empty());
 
     assertThat(underTest.authenticate(request, response)).isEmpty();
diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/HttpHeadersAuthenticatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/HttpHeadersAuthenticatorTest.java
new file mode 100644 (file)
index 0000000..cc60488
--- /dev/null
@@ -0,0 +1,457 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.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.internal.MapSettings;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.internal.AlwaysIncreasingSystem2;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.DbTester;
+import org.sonar.db.user.GroupDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.authentication.event.AuthenticationEvent;
+import org.sonar.server.authentication.event.AuthenticationEvent.Source;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.organization.DefaultOrganizationProvider;
+import org.sonar.server.organization.OrganizationUpdater;
+import org.sonar.server.organization.TestDefaultOrganizationProvider;
+import org.sonar.server.organization.TestOrganizationFlags;
+import org.sonar.server.user.NewUserNotifier;
+import org.sonar.server.user.UserUpdater;
+import org.sonar.server.user.index.UserIndexer;
+import org.sonar.server.usergroups.DefaultGroupFinder;
+
+import static java.util.Arrays.stream;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.rules.ExpectedException.none;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.ArgumentMatchers.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;
+import static org.sonar.server.authentication.event.AuthenticationExceptionMatcher.authenticationException;
+
+public class HttpHeadersAuthenticatorTest {
+
+  private MapSettings settings = new MapSettings();
+
+  @Rule
+  public ExpectedException expectedException = none();
+  @Rule
+  public DbTester db = DbTester.create(new AlwaysIncreasingSystem2());
+  @Rule
+  public EsTester es = EsTester.create();
+
+  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)
+    .setExternalLogin(DEFAULT_LOGIN)
+    .setExternalIdentityProvider("sonarqube");
+
+  private GroupDto group1;
+  private GroupDto group2;
+  private GroupDto sonarUsers;
+
+  private System2 system2 = mock(System2.class);
+  private OrganizationUpdater organizationUpdater = mock(OrganizationUpdater.class);
+  private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
+  private TestOrganizationFlags organizationFlags = TestOrganizationFlags.standalone();
+  private LocalAuthentication localAuthentication = new LocalAuthentication(db.getDbClient());
+
+  private UserIndexer userIndexer = new UserIndexer(db.getDbClient(), es.client());
+  private UserIdentityAuthenticatorImpl userIdentityAuthenticator = new UserIdentityAuthenticatorImpl(
+    db.getDbClient(),
+    new UserUpdater(mock(NewUserNotifier.class), db.getDbClient(), userIndexer, organizationFlags, defaultOrganizationProvider, organizationUpdater,
+      new DefaultGroupFinder(db.getDbClient()), settings.asConfig(), localAuthentication),
+    defaultOrganizationProvider, organizationFlags, mock(OrganizationUpdater.class), new DefaultGroupFinder(db.getDbClient()));
+
+  private HttpServletResponse response = mock(HttpServletResponse.class);
+  private JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class);
+  private AuthenticationEvent authenticationEvent = mock(AuthenticationEvent.class);
+
+  private HttpHeadersAuthenticator underTest = new HttpHeadersAuthenticator(system2, settings.asConfig(), userIdentityAuthenticator, jwtHttpHandler, authenticationEvent);
+
+  @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);
+    sonarUsers = db.users().insertDefaultGroup(db.getDefaultOrganization(), "sonar-users");
+  }
+
+  @Test
+  public void create_user_when_authenticating_new_user() {
+    startWithSso();
+    setNotUserInToken();
+    HttpServletRequest request = createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, GROUPS);
+
+    underTest.authenticate(request, response);
+
+    verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1, group2, sonarUsers);
+    verifyTokenIsUpdated(NOW);
+    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
+  }
+
+  @Test
+  public void use_login_when_name_is_not_provided() {
+    startWithSso();
+    setNotUserInToken();
+
+    HttpServletRequest request = createRequest(DEFAULT_LOGIN, null, null, null);
+    underTest.authenticate(request, response);
+
+    verifyUserInDb(DEFAULT_LOGIN, DEFAULT_LOGIN, null, sonarUsers);
+    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
+  }
+
+  @Test
+  public void update_user_when_authenticating_exiting_user() {
+    startWithSso();
+    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);
+    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
+  }
+
+  @Test
+  public void remove_groups_when_group_headers_is_empty() {
+    startWithSso();
+    setNotUserInToken();
+    insertUser(DEFAULT_USER, group1);
+    HttpServletRequest request = createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, "");
+
+    underTest.authenticate(request, response);
+
+    verityUserHasNoGroup(DEFAULT_LOGIN);
+    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
+  }
+
+  @Test
+  public void remove_groups_when_group_headers_is_null() {
+    startWithSso();
+    setNotUserInToken();
+    insertUser(DEFAULT_USER, group1);
+    Map<String, String> headerValuesByName = new HashMap<>();
+    headerValuesByName.put("X-Forwarded-Login", DEFAULT_LOGIN);
+    headerValuesByName.put("X-Forwarded-Groups", null);
+    HttpServletRequest request = createRequest(headerValuesByName);
+
+    underTest.authenticate(request, response);
+
+    verityUserHasNoGroup(DEFAULT_LOGIN);
+    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
+  }
+
+  @Test
+  public void does_not_update_groups_when_no_group_headers() {
+    startWithSso();
+    setNotUserInToken();
+    insertUser(DEFAULT_USER, group1, sonarUsers);
+    HttpServletRequest request = createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, null);
+
+    underTest.authenticate(request, response);
+
+    verityUserGroups(DEFAULT_LOGIN, group1, sonarUsers);
+    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
+  }
+
+  @Test
+  public void does_not_update_user_when_user_is_in_token_and_refresh_time_is_close() {
+    startWithSso();
+    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();
+    verifyZeroInteractions(authenticationEvent);
+  }
+
+  @Test
+  public void update_user_when_user_in_token_but_refresh_time_is_old() {
+    startWithSso();
+    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);
+    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
+  }
+
+  @Test
+  public void update_user_when_user_in_token_but_no_refresh_time() {
+    startWithSso();
+    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);
+    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
+  }
+
+  @Test
+  public void use_refresh_time_from_settings() {
+    settings.setProperty("sonar.web.sso.refreshIntervalInMinutes", "10");
+    startWithSso();
+    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();
+    verifyZeroInteractions(authenticationEvent);
+  }
+
+  @Test
+  public void update_user_when_login_from_token_is_different_than_login_from_request() {
+    startWithSso();
+    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, sonarUsers);
+    verifyTokenIsUpdated(NOW);
+    verify(authenticationEvent).loginSuccess(request, "AnotherLogin", Source.sso());
+  }
+
+  @Test
+  public void use_headers_from_settings() {
+    settings.setProperty("sonar.web.sso.loginHeader", "head-login");
+    settings.setProperty("sonar.web.sso.nameHeader", "head-name");
+    settings.setProperty("sonar.web.sso.emailHeader", "head-email");
+    settings.setProperty("sonar.web.sso.groupsHeader", "head-groups");
+    startWithSso();
+    setNotUserInToken();
+    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, sonarUsers);
+    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
+  }
+
+  @Test
+  public void detect_group_header_even_with_wrong_case() {
+    settings.setProperty("sonar.web.sso.loginHeader", "login");
+    settings.setProperty("sonar.web.sso.nameHeader", "name");
+    settings.setProperty("sonar.web.sso.emailHeader", "email");
+    settings.setProperty("sonar.web.sso.groupsHeader", "Groups");
+    startWithSso();
+    setNotUserInToken();
+    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, sonarUsers);
+    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
+  }
+
+  @Test
+  public void trim_groups() {
+    startWithSso();
+    setNotUserInToken();
+    HttpServletRequest request = createRequest(DEFAULT_LOGIN, null, null, "  dev ,    admin ");
+
+    underTest.authenticate(request, response);
+
+    verifyUserInDb(DEFAULT_LOGIN, DEFAULT_LOGIN, null, group1, group2, sonarUsers);
+    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
+  }
+
+  @Test
+  public void does_not_authenticate_when_no_header() {
+    startWithSso();
+    setNotUserInToken();
+
+    underTest.authenticate(createRequest(Collections.emptyMap()), response);
+
+    verifyUserNotAuthenticated();
+    verifyTokenIsNotUpdated();
+    verifyZeroInteractions(authenticationEvent);
+  }
+
+  @Test
+  public void does_not_authenticate_when_not_enabled() {
+    startWithoutSso();
+
+    underTest.authenticate(createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, GROUPS), response);
+
+    verifyUserNotAuthenticated();
+    verifyZeroInteractions(jwtHttpHandler, authenticationEvent);
+  }
+
+  @Test
+  public void throw_AuthenticationException_when_BadRequestException_is_generated() {
+    startWithSso();
+    setNotUserInToken();
+
+    expectedException.expect(authenticationException().from(Source.sso()).withoutLogin().andNoPublicMessage());
+    expectedException.expectMessage("Use only letters, numbers, and .-_@ please.");
+    try {
+      underTest.authenticate(createRequest("invalid login", DEFAULT_NAME, DEFAULT_EMAIL, GROUPS), response);
+    } finally {
+      verifyZeroInteractions(authenticationEvent);
+    }
+  }
+
+  private void startWithSso() {
+    settings.setProperty("sonar.web.sso.enable", true);
+    underTest.start();
+  }
+
+  private void startWithoutSso() {
+    settings.setProperty("sonar.web.sso.enable", false);
+    underTest.start();
+  }
+
+  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.getExternalLogin()).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(MoreCollectors.toList()).toArray(new Integer[] {}));
+    }
+  }
+
+  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));
+  }
+
+}
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
deleted file mode 100644 (file)
index 98d8b00..0000000
+++ /dev/null
@@ -1,457 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.server.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.internal.MapSettings;
-import org.sonar.api.utils.System2;
-import org.sonar.api.utils.internal.AlwaysIncreasingSystem2;
-import org.sonar.core.util.stream.MoreCollectors;
-import org.sonar.db.DbTester;
-import org.sonar.db.user.GroupDto;
-import org.sonar.db.user.UserDto;
-import org.sonar.server.authentication.event.AuthenticationEvent;
-import org.sonar.server.authentication.event.AuthenticationEvent.Source;
-import org.sonar.server.es.EsTester;
-import org.sonar.server.organization.DefaultOrganizationProvider;
-import org.sonar.server.organization.OrganizationUpdater;
-import org.sonar.server.organization.TestDefaultOrganizationProvider;
-import org.sonar.server.organization.TestOrganizationFlags;
-import org.sonar.server.user.NewUserNotifier;
-import org.sonar.server.user.UserUpdater;
-import org.sonar.server.user.index.UserIndexer;
-import org.sonar.server.usergroups.DefaultGroupFinder;
-
-import static java.util.Arrays.stream;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.rules.ExpectedException.none;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyMap;
-import static org.mockito.ArgumentMatchers.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;
-import static org.sonar.server.authentication.event.AuthenticationExceptionMatcher.authenticationException;
-
-public class SsoAuthenticatorTest {
-
-  private MapSettings settings = new MapSettings();
-
-  @Rule
-  public ExpectedException expectedException = none();
-  @Rule
-  public DbTester db = DbTester.create(new AlwaysIncreasingSystem2());
-  @Rule
-  public EsTester es = EsTester.create();
-
-  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)
-    .setExternalLogin(DEFAULT_LOGIN)
-    .setExternalIdentityProvider("sonarqube");
-
-  private GroupDto group1;
-  private GroupDto group2;
-  private GroupDto sonarUsers;
-
-  private System2 system2 = mock(System2.class);
-  private OrganizationUpdater organizationUpdater = mock(OrganizationUpdater.class);
-  private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
-  private TestOrganizationFlags organizationFlags = TestOrganizationFlags.standalone();
-  private LocalAuthentication localAuthentication = new LocalAuthentication(db.getDbClient());
-
-  private UserIndexer userIndexer = new UserIndexer(db.getDbClient(), es.client());
-  private UserIdentityAuthenticatorImpl userIdentityAuthenticator = new UserIdentityAuthenticatorImpl(
-    db.getDbClient(),
-    new UserUpdater(mock(NewUserNotifier.class), db.getDbClient(), userIndexer, organizationFlags, defaultOrganizationProvider, organizationUpdater,
-      new DefaultGroupFinder(db.getDbClient()), settings.asConfig(), localAuthentication),
-    defaultOrganizationProvider, organizationFlags, mock(OrganizationUpdater.class), new DefaultGroupFinder(db.getDbClient()));
-
-  private HttpServletResponse response = mock(HttpServletResponse.class);
-  private JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class);
-  private AuthenticationEvent authenticationEvent = mock(AuthenticationEvent.class);
-
-  private SsoAuthenticator underTest = new SsoAuthenticator(system2, settings.asConfig(), userIdentityAuthenticator, jwtHttpHandler, authenticationEvent);
-
-  @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);
-    sonarUsers = db.users().insertDefaultGroup(db.getDefaultOrganization(), "sonar-users");
-  }
-
-  @Test
-  public void create_user_when_authenticating_new_user() {
-    startWithSso();
-    setNotUserInToken();
-    HttpServletRequest request = createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, GROUPS);
-
-    underTest.authenticate(request, response);
-
-    verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1, group2, sonarUsers);
-    verifyTokenIsUpdated(NOW);
-    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
-  }
-
-  @Test
-  public void use_login_when_name_is_not_provided() {
-    startWithSso();
-    setNotUserInToken();
-
-    HttpServletRequest request = createRequest(DEFAULT_LOGIN, null, null, null);
-    underTest.authenticate(request, response);
-
-    verifyUserInDb(DEFAULT_LOGIN, DEFAULT_LOGIN, null, sonarUsers);
-    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
-  }
-
-  @Test
-  public void update_user_when_authenticating_exiting_user() {
-    startWithSso();
-    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);
-    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
-  }
-
-  @Test
-  public void remove_groups_when_group_headers_is_empty() {
-    startWithSso();
-    setNotUserInToken();
-    insertUser(DEFAULT_USER, group1);
-    HttpServletRequest request = createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, "");
-
-    underTest.authenticate(request, response);
-
-    verityUserHasNoGroup(DEFAULT_LOGIN);
-    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
-  }
-
-  @Test
-  public void remove_groups_when_group_headers_is_null() {
-    startWithSso();
-    setNotUserInToken();
-    insertUser(DEFAULT_USER, group1);
-    Map<String, String> headerValuesByName = new HashMap<>();
-    headerValuesByName.put("X-Forwarded-Login", DEFAULT_LOGIN);
-    headerValuesByName.put("X-Forwarded-Groups", null);
-    HttpServletRequest request = createRequest(headerValuesByName);
-
-    underTest.authenticate(request, response);
-
-    verityUserHasNoGroup(DEFAULT_LOGIN);
-    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
-  }
-
-  @Test
-  public void does_not_update_groups_when_no_group_headers() {
-    startWithSso();
-    setNotUserInToken();
-    insertUser(DEFAULT_USER, group1, sonarUsers);
-    HttpServletRequest request = createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, null);
-
-    underTest.authenticate(request, response);
-
-    verityUserGroups(DEFAULT_LOGIN, group1, sonarUsers);
-    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
-  }
-
-  @Test
-  public void does_not_update_user_when_user_is_in_token_and_refresh_time_is_close() {
-    startWithSso();
-    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();
-    verifyZeroInteractions(authenticationEvent);
-  }
-
-  @Test
-  public void update_user_when_user_in_token_but_refresh_time_is_old() {
-    startWithSso();
-    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);
-    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
-  }
-
-  @Test
-  public void update_user_when_user_in_token_but_no_refresh_time() {
-    startWithSso();
-    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);
-    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
-  }
-
-  @Test
-  public void use_refresh_time_from_settings() {
-    settings.setProperty("sonar.web.sso.refreshIntervalInMinutes", "10");
-    startWithSso();
-    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();
-    verifyZeroInteractions(authenticationEvent);
-  }
-
-  @Test
-  public void update_user_when_login_from_token_is_different_than_login_from_request() {
-    startWithSso();
-    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, sonarUsers);
-    verifyTokenIsUpdated(NOW);
-    verify(authenticationEvent).loginSuccess(request, "AnotherLogin", Source.sso());
-  }
-
-  @Test
-  public void use_headers_from_settings() {
-    settings.setProperty("sonar.web.sso.loginHeader", "head-login");
-    settings.setProperty("sonar.web.sso.nameHeader", "head-name");
-    settings.setProperty("sonar.web.sso.emailHeader", "head-email");
-    settings.setProperty("sonar.web.sso.groupsHeader", "head-groups");
-    startWithSso();
-    setNotUserInToken();
-    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, sonarUsers);
-    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
-  }
-
-  @Test
-  public void detect_group_header_even_with_wrong_case() {
-    settings.setProperty("sonar.web.sso.loginHeader", "login");
-    settings.setProperty("sonar.web.sso.nameHeader", "name");
-    settings.setProperty("sonar.web.sso.emailHeader", "email");
-    settings.setProperty("sonar.web.sso.groupsHeader", "Groups");
-    startWithSso();
-    setNotUserInToken();
-    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, sonarUsers);
-    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
-  }
-
-  @Test
-  public void trim_groups() {
-    startWithSso();
-    setNotUserInToken();
-    HttpServletRequest request = createRequest(DEFAULT_LOGIN, null, null, "  dev ,    admin ");
-
-    underTest.authenticate(request, response);
-
-    verifyUserInDb(DEFAULT_LOGIN, DEFAULT_LOGIN, null, group1, group2, sonarUsers);
-    verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
-  }
-
-  @Test
-  public void does_not_authenticate_when_no_header() {
-    startWithSso();
-    setNotUserInToken();
-
-    underTest.authenticate(createRequest(Collections.emptyMap()), response);
-
-    verifyUserNotAuthenticated();
-    verifyTokenIsNotUpdated();
-    verifyZeroInteractions(authenticationEvent);
-  }
-
-  @Test
-  public void does_not_authenticate_when_not_enabled() {
-    startWithoutSso();
-
-    underTest.authenticate(createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, GROUPS), response);
-
-    verifyUserNotAuthenticated();
-    verifyZeroInteractions(jwtHttpHandler, authenticationEvent);
-  }
-
-  @Test
-  public void throw_AuthenticationException_when_BadRequestException_is_generated() {
-    startWithSso();
-    setNotUserInToken();
-
-    expectedException.expect(authenticationException().from(Source.sso()).withoutLogin().andNoPublicMessage());
-    expectedException.expectMessage("Use only letters, numbers, and .-_@ please.");
-    try {
-      underTest.authenticate(createRequest("invalid login", DEFAULT_NAME, DEFAULT_EMAIL, GROUPS), response);
-    } finally {
-      verifyZeroInteractions(authenticationEvent);
-    }
-  }
-
-  private void startWithSso() {
-    settings.setProperty("sonar.web.sso.enable", true);
-    underTest.start();
-  }
-
-  private void startWithoutSso() {
-    settings.setProperty("sonar.web.sso.enable", false);
-    underTest.start();
-  }
-
-  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.getExternalLogin()).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(MoreCollectors.toList()).toArray(new Integer[] {}));
-    }
-  }
-
-  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 fe778cb894d6767f17383f91dfa7e994ab53398d..c58afab14bc5a9e3a9b88e89a29117b9bd9d07b6 100644 (file)
@@ -277,10 +277,29 @@ public class UserIdentityAuthenticatorImplTest {
 
   @Test
   public void throw_AuthenticationException_when_authenticating_new_user_when_email_already_exists_and_strategy_is_FORBID() {
-    db.users().insertUser(newUserDto()
-      .setLogin("Existing user with same email")
-      .setActive(true)
-      .setEmail("john@email.com"));
+    db.users().insertUser(u -> u.setEmail("john@email.com"));
+    Source source = Source.realm(AuthenticationEvent.Method.FORM, IDENTITY_PROVIDER.getName());
+
+    expectedException.expect(authenticationException().from(source)
+      .withLogin(USER_IDENTITY.getLogin())
+      .andPublicMessage("You can't sign up because email 'john@email.com' is already used by an existing user. " +
+        "This means that you probably already registered with another account."));
+    expectedException.expectMessage("Email 'john@email.com' is already used");
+
+    underTest.authenticate(UserIdentityAuthenticatorParameters.builder()
+      .setUserIdentity(USER_IDENTITY)
+      .setProvider(IDENTITY_PROVIDER)
+      .setSource(source)
+      .setExistingEmailStrategy(FORBID)
+      .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
+      .setUpdateLoginStrategy(UpdateLoginStrategy.ALLOW)
+      .build());
+  }
+
+  @Test
+  public void throw_AuthenticationException_when_authenticating_new_user_and_email_already_exists_multiple_times() {
+    db.users().insertUser(u -> u.setEmail("john@email.com"));
+    db.users().insertUser(u -> u.setEmail("john@email.com"));
     Source source = Source.realm(AuthenticationEvent.Method.FORM, IDENTITY_PROVIDER.getName());
 
     expectedException.expect(authenticationException().from(source)
index d45b79800ccee45b3429ea1027347d9eb810ef19..b2a635e07039e9f3a60a387ad8c89fcfa7238eed 100644 (file)
@@ -37,9 +37,9 @@ import org.sonarqube.tests.settings.SettingsTestRestartingOrchestrator;
 import org.sonarqube.tests.startup.StartupIndexationTest;
 import org.sonarqube.tests.telemetry.TelemetryOptOutTest;
 import org.sonarqube.tests.telemetry.TelemetryUploadTest;
+import org.sonarqube.tests.user.HttpHeadersAuthenticationTest;
 import org.sonarqube.tests.user.OnboardingTest;
 import org.sonarqube.tests.user.RealmAuthenticationTest;
-import org.sonarqube.tests.user.SsoAuthenticationTest;
 import org.sonarqube.tests.user.UserEsResilienceTest;
 
 /**
@@ -61,7 +61,7 @@ import org.sonarqube.tests.user.UserEsResilienceTest;
   // update center
   UpdateCenterTest.class,
   RealmAuthenticationTest.class,
-  SsoAuthenticationTest.class,
+  HttpHeadersAuthenticationTest.class,
   OnboardingTest.class,
   BuiltInQualityProfilesNotificationTest.class,
   ActiveRuleEsResilienceTest.class,
index 4545bc08ecd99890daff4529d238cb3400a272d3..f9bfa51ce659af855d75055da35e66f80fd7e054 100644 (file)
  */
 package org.sonarqube.tests.user;
 
+import com.codeborne.selenide.Condition;
 import com.google.common.base.Joiner;
 import com.sonar.orchestrator.Orchestrator;
-import java.io.File;
 import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
-import org.apache.commons.io.FileUtils;
 import org.junit.After;
 import org.junit.ClassRule;
 import org.junit.Rule;
@@ -41,10 +40,13 @@ import org.sonarqube.ws.client.users.DeactivateRequest;
 import org.sonarqube.ws.client.users.GroupsRequest;
 import org.sonarqube.ws.client.users.SearchRequest;
 
+import static com.codeborne.selenide.Selenide.$;
+import static java.lang.String.format;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.commons.io.FileUtils.readFileToString;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.tuple;
 import static org.sonarqube.ws.UserGroups.Group;
-import static util.selenium.Selenese.runSelenese;
 
 public class BaseIdentityProviderTest {
 
@@ -105,8 +107,11 @@ public class BaseIdentityProviderTest {
     // As this property is null, the plugin will throw an exception
     tester.settings().setGlobalSettings("sonar.auth.fake-base-id-provider.user", null);
 
-    runSelenese(ORCHESTRATOR, "/user/BaseIdentityProviderTest/display_unauthorized_page_when_authentication_failed.html");
+    tester.openBrowser()
+      .logIn()
+      .useBaseAuth();
 
+    $("#bd").shouldHave(Condition.text("You're not authorized to access this page. Please contact the administrator"));
     assertThat(tester.users().getByLogin(USER_LOGIN)).isNotPresent();
   }
 
@@ -116,21 +121,45 @@ public class BaseIdentityProviderTest {
     setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_PROVIDER_LOGIN, USER_NAME, USER_EMAIL);
     tester.users().generate(u -> u.setLogin("another").setName("Another").setEmail(USER_EMAIL).setPassword("another"));
 
-    runSelenese(ORCHESTRATOR, "/user/BaseIdentityProviderTest/fail_when_email_already_exists.html");
+    tester.openBrowser()
+      .logIn()
+      .useBaseAuth();
 
-    File logFile = ORCHESTRATOR.getServer().getWebLogs();
-    assertThat(FileUtils.readFileToString(logFile))
+    $("#bd").shouldHave(Condition.text(
+      format("You can't sign up because email '%s' is already used by an existing user. This means that you probably already registered with another account.", USER_EMAIL)));
+    assertThat(readFileToString(ORCHESTRATOR.getServer().getWebLogs(), UTF_8))
       .doesNotContain("You can't sign up because email 'john@email.com' is already used by an existing user. This means that you probably already registered with another account");
   }
 
+  @Test
+  public void fail_to_authenticate_user_when_email_already_exists_on_several_users() {
+    enablePlugin();
+    setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_PROVIDER_LOGIN, USER_NAME, USER_EMAIL);
+    tester.users().generate(u -> u.setEmail(USER_EMAIL));
+    tester.users().generate(u -> u.setEmail(USER_EMAIL));
+
+    tester.openBrowser()
+      .logIn()
+      .useBaseAuth();
+
+    $("#bd").shouldHave(Condition.text(
+      format("You can't sign up because email '%s' is already used by an existing user. This means that you probably already registered with another account.", USER_EMAIL)));
+    assertThat(tester.users().getByLogin(USER_LOGIN)).isNotPresent();
+  }
+
   @Test
   public void fail_to_authenticate_when_not_allowed_to_sign_up() {
     enablePlugin();
     setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_PROVIDER_LOGIN, USER_NAME, USER_EMAIL);
     tester.settings().setGlobalSettings("sonar.auth.fake-base-id-provider.allowsUsersToSignUp", "false");
 
-    runSelenese(ORCHESTRATOR, "/user/BaseIdentityProviderTest/fail_to_authenticate_when_not_allowed_to_sign_up.html");
+    tester.openBrowser()
+      .logIn()
+      .useBaseAuth();
 
+    $("#bd")
+      .shouldHave(Condition.text("You're not authorized to access this page. Please contact the administrator."))
+      .shouldHave(Condition.text(format("Reason: '%s' users are not allowed to sign up", FAKE_PROVIDER_KEY)));
     assertThat(tester.users().getByLogin(USER_LOGIN)).isNotPresent();
   }
 
@@ -190,13 +219,16 @@ public class BaseIdentityProviderTest {
     setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_PROVIDER_LOGIN, USER_NAME, USER_EMAIL);
     tester.settings().setGlobalSettings("sonar.auth.fake-base-id-provider.throwUnauthorizedMessage", "true");
 
-    runSelenese(ORCHESTRATOR,
-      "/user/BaseIdentityProviderTest/display_message_in_ui_but_not_in_log_when_unauthorized_exception.html");
-
-    File logFile = ORCHESTRATOR.getServer().getWebLogs();
-    assertThat(FileUtils.readFileToString(logFile)).doesNotContain("A functional error has happened");
-    assertThat(FileUtils.readFileToString(logFile)).doesNotContain("UnauthorizedException");
+    tester.openBrowser()
+      .logIn()
+      .useBaseAuth();
 
+    $("#bd")
+      .shouldHave(Condition.text("You're not authorized to access this page. Please contact the administrator"))
+      .shouldHave(Condition.text("Reason: A functional error has happened"));
+    assertThat(readFileToString(ORCHESTRATOR.getServer().getWebLogs(), UTF_8))
+      .doesNotContain("A functional error has happened")
+      .doesNotContain("UnauthorizedException");
     assertThat(tester.users().getByLogin(USER_LOGIN)).isNotPresent();
   }
 
diff --git a/tests/src/test/java/org/sonarqube/tests/user/HttpHeadersAuthenticationTest.java b/tests/src/test/java/org/sonarqube/tests/user/HttpHeadersAuthenticationTest.java
new file mode 100644 (file)
index 0000000..300c60c
--- /dev/null
@@ -0,0 +1,191 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.sonarqube.tests.user;
+
+import com.sonar.orchestrator.Orchestrator;
+import java.net.URLEncoder;
+import java.util.List;
+import javax.annotation.Nullable;
+import okhttp3.Response;
+import org.apache.commons.io.FileUtils;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.qa.util.Tester;
+import org.sonarqube.ws.UserGroups.Group;
+import org.sonarqube.ws.Users.CreateWsResponse.User;
+import org.sonarqube.ws.Users.SearchWsResponse;
+import org.sonarqube.ws.client.users.SearchRequest;
+
+import static java.lang.String.format;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.groups.Tuple.tuple;
+import static util.ItUtils.call;
+import static util.ItUtils.newOrchestratorBuilder;
+
+/**
+ * Test authentication using HTTP headers.
+ * <p>
+ * It starts its own server as it's using a different authentication system
+ */
+public class HttpHeadersAuthenticationTest {
+
+  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";
+
+  @ClassRule
+  public static final Orchestrator orchestrator = newOrchestratorBuilder()
+    .setServerProperty("sonar.web.sso.enable", "true")
+    .setServerProperty("sonar.web.sso.loginHeader", LOGIN_HEADER)
+    .setServerProperty("sonar.web.sso.nameHeader", NAME_HEADER)
+    .setServerProperty("sonar.web.sso.emailHeader", EMAIL_HEADER)
+    .setServerProperty("sonar.web.sso.groupsHeader", GROUPS_HEADER)
+    .build();
+
+  @Rule
+  public Tester tester = new Tester(orchestrator).disableOrganizations();
+
+  @Test
+  public void authenticate() {
+    String login = tester.users().generateLogin();
+
+    doCall(login, "Tester", "tester@email.com", null);
+
+    verifyUser(login, "Tester", "tester@email.com");
+  }
+
+  @Test
+  public void authenticate_with_only_login() {
+    String login = tester.users().generateLogin();
+
+    doCall(login, null, null, null);
+
+    assertThat(tester.wsClient().users().search(new SearchRequest().setQ(login)).getUsersList())
+      .extracting(SearchWsResponse.User::getLogin, SearchWsResponse.User::getName, SearchWsResponse.User::hasEmail)
+      .containsExactlyInAnyOrder(tuple(login, login, false));
+  }
+
+  @Test
+  public void update_user_when_headers_are_updated() {
+    String login = tester.users().generateLogin();
+    doCall(login, "Tester", "tester@email.com", null);
+    verifyUser(login, "Tester", "tester@email.com");
+
+    // As we don't keep the JWT token is the test, the user is updated
+    doCall(login, "new name", "new email", null);
+    verifyUser(login, "new name", "new email");
+  }
+
+  @Test
+  public void authenticate_with_groups() {
+    String login = tester.users().generateLogin();
+    Group group = tester.groups().generate();
+
+    doCall(login, null, null, group.getName());
+
+    assertThat(tester.wsClient().users().search(new SearchRequest().setQ(login)).getUsersList())
+      .extracting(SearchWsResponse.User::getLogin, u -> u.getGroups().getGroupsList())
+      .containsExactlyInAnyOrder(tuple(login, asList(group.getName(), "sonar-users")));
+  }
+
+  @Test
+  public void synchronize_groups_when_authenticating_existing_user() {
+    User user = tester.users().generate();
+    Group group1 = tester.groups().generate();
+    Group group2 = tester.groups().generate();
+    Group group3 = tester.groups().generate();
+    tester.groups().addMemberToGroups(null, user.getLogin(), group1.getName(), group2.getName());
+
+    doCall(user.getLogin(), null, null, group2.getName() + "," + group3.getName());
+
+    assertThat(tester.wsClient().users().search(new SearchRequest().setQ(user.getLogin())).getUsersList())
+      .extracting(SearchWsResponse.User::getLogin, u -> u.getGroups().getGroupsList())
+      .containsExactlyInAnyOrder(tuple(user.getLogin(), asList(group2.getName(), group3.getName(), "sonar-users")));
+  }
+
+  @Test
+  public void authentication_with_local_user_is_possible_when_no_header() {
+    User user = tester.users().generate();
+
+    // Check any ws, no error should be thrown
+    tester.as(user.getLogin(), user.getLogin()).wsClient().system().ping();
+  }
+
+  @Test
+  public void display_message_in_ui_but_not_in_log_when_unauthorized_exception() throws Exception {
+    String login = "invalid login $";
+    Response response = doCall(login, null, null, null);
+
+    assertThat(response.code()).isEqualTo(200);
+    assertThat(response.request().url().toString()).contains("sessions/unauthorized");
+
+    List<String> logsLines = FileUtils.readLines(orchestrator.getServer().getWebLogs(), UTF_8);
+    assertThat(logsLines).doesNotContain("org.sonar.server.exceptions.BadRequestException: Use only letters, numbers, and .-_@ please.");
+    assertThat(tester.wsClient().users().search(new SearchRequest().setQ(login)).getUsersList()).isEmpty();
+  }
+
+  @Test
+  public void fail_when_email_already_exists() throws Exception {
+    String login = tester.users().generateLogin();
+    User existingUser = tester.users().generate();
+
+    Response response = doCall(login, "Tester", existingUser.getEmail(), null);
+
+    String expectedError = format("You can't sign up because email '%s' is already used by an existing user. This means that you probably already registered with another account",
+      existingUser.getEmail());
+    assertThat(response.code()).isEqualTo(200);
+    assertThat(response.request().url().toString()).contains(URLEncoder.encode(expectedError, UTF_8.name()));
+    assertThat(FileUtils.readLines(orchestrator.getServer().getWebLogs(), UTF_8)).doesNotContain(expectedError);
+  }
+
+  @Test
+  public void fail_to_authenticate_user_when_email_already_exists_on_several_users() throws Exception {
+    String login = tester.users().generateLogin();
+    tester.users().generate(u -> u.setEmail("john@email.com"));
+    tester.users().generate(u -> u.setEmail("john@email.com"));
+
+    Response response = doCall(login, "Tester", "john@email.com", null);
+
+    String expectedError = "You can't sign up because email 'john@email.com' is already used by an existing user. This means that you probably already registered with another account";
+    assertThat(response.code()).isEqualTo(200);
+    assertThat(response.request().url().toString()).contains(URLEncoder.encode(expectedError, UTF_8.name()));
+    assertThat(FileUtils.readLines(orchestrator.getServer().getWebLogs(), UTF_8)).doesNotContain(expectedError);
+  }
+
+  private static Response doCall(String login, @Nullable String name, @Nullable String email, @Nullable String groups) {
+    return call(orchestrator.getServer().getUrl(),
+      LOGIN_HEADER, login,
+      NAME_HEADER, name,
+      EMAIL_HEADER, email,
+      GROUPS_HEADER, groups);
+  }
+
+  private void verifyUser(String login, String name, String email) {
+    assertThat(tester.wsClient().users().search(new SearchRequest().setQ(login)).getUsersList())
+      .extracting(SearchWsResponse.User::getLogin, SearchWsResponse.User::getName, SearchWsResponse.User::getEmail,
+        SearchWsResponse.User::getExternalIdentity, SearchWsResponse.User::getExternalProvider, SearchWsResponse.User::getLocal)
+      .containsExactlyInAnyOrder(tuple(login, name, email, login, "sonarqube", false));
+  }
+
+}
index b7e194b37859694ee5db6854b12f67f24fbfeb9b..b6a74623aa13190f60c6ca2d0fdf11f4420ebc50 100644 (file)
@@ -21,11 +21,9 @@ package org.sonarqube.tests.user;
 
 import com.codeborne.selenide.Condition;
 import com.sonar.orchestrator.Orchestrator;
-import java.io.File;
 import java.net.HttpURLConnection;
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
-import org.apache.commons.io.FileUtils;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.ClassRule;
@@ -37,10 +35,12 @@ import org.sonarqube.ws.Users.SearchWsResponse.User;
 import org.sonarqube.ws.client.GetRequest;
 import org.sonarqube.ws.client.permissions.AddUserRequest;
 import org.sonarqube.ws.client.users.CreateRequest;
-import util.selenium.Selenese;
 
 import static com.codeborne.selenide.Condition.visible;
 import static com.codeborne.selenide.Selenide.$;
+import static java.lang.String.format;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.commons.io.FileUtils.readFileToString;
 import static org.assertj.core.api.Assertions.assertThat;
 
 /**
@@ -137,8 +137,11 @@ public class OAuth2IdentityProviderTest {
     // As this property is null, the plugin will throw an exception
     tester.settings().setGlobalSettings("sonar.auth.fake-oauth2-id-provider.user", null);
 
-    Selenese.runSelenese(orchestrator, "/user/OAuth2IdentityProviderTest/display_unauthorized_page_when_authentication_failed.html");
+    tester.openBrowser()
+      .logIn()
+      .useOAuth2();
 
+    $("#bd").shouldHave(Condition.text("You're not authorized to access this page. Please contact the administrator"));
     assertThatUserDoesNotExist(USER_LOGIN);
   }
 
@@ -148,8 +151,13 @@ public class OAuth2IdentityProviderTest {
     enablePlugin();
     tester.settings().setGlobalSettings("sonar.auth.fake-oauth2-id-provider.allowsUsersToSignUp", "false");
 
-    Selenese.runSelenese(orchestrator, "/user/OAuth2IdentityProviderTest/fail_to_authenticate_when_not_allowed_to_sign_up.html");
+    tester.openBrowser()
+      .logIn()
+      .useOAuth2();
 
+    $("#bd")
+      .shouldHave(Condition.text("You're not authorized to access this page. Please contact the administrator."))
+      .shouldHave(Condition.text(format("Reason: '%s' users are not allowed to sign up", FAKE_PROVIDER_KEY)));
     assertThatUserDoesNotExist(USER_LOGIN);
   }
 
@@ -159,12 +167,16 @@ public class OAuth2IdentityProviderTest {
     enablePlugin();
     tester.settings().setGlobalSettings("sonar.auth.fake-oauth2-id-provider.throwUnauthorizedMessage", "true");
 
-    Selenese.runSelenese(orchestrator, "/user/OAuth2IdentityProviderTest/display_message_in_ui_but_not_in_log_when_unauthorized_exception.html");
-
-    File logFile = orchestrator.getServer().getWebLogs();
-    assertThat(FileUtils.readFileToString(logFile)).doesNotContain("A functional error has happened");
-    assertThat(FileUtils.readFileToString(logFile)).doesNotContain("UnauthorizedException");
-
+    tester.openBrowser()
+      .logIn()
+      .useOAuth2();
+
+    $("#bd")
+      .shouldHave(Condition.text("You're not authorized to access this page. Please contact the administrator"))
+      .shouldHave(Condition.text("Reason: A functional error has happened"));
+    assertThat(readFileToString(orchestrator.getServer().getWebLogs(), UTF_8))
+      .doesNotContain("A functional error has happened")
+      .doesNotContain("UnauthorizedException");
     assertThatUserDoesNotExist(USER_LOGIN);
   }
 
@@ -205,6 +217,22 @@ public class OAuth2IdentityProviderTest {
     assertThat(tester.users().getByLogin("another").get().getEmail()).isNullOrEmpty();
   }
 
+  @Test
+  public void fail_to_authenticate_user_when_email_already_exists_on_several_users() {
+    simulateRedirectionToCallback();
+    enablePlugin();
+    tester.users().generate(u -> u.setEmail(USER_EMAIL));
+    tester.users().generate(u -> u.setEmail(USER_EMAIL));
+
+    tester.openBrowser()
+      .logIn()
+      .useOAuth2();
+
+    $("#bd").shouldHave(Condition.text(
+      format("You can't sign up because email '%s' is already used by an existing user. This means that you probably already registered with another account.", USER_EMAIL)));
+    assertThatUserDoesNotExist(USER_LOGIN);
+  }
+
   @Test
   public void provision_user_before_authentication() {
     simulateRedirectionToCallback();
index c733ffab7e64a1a69cd4979d3134724d36e6cf9a..e34e637116838b35bac04c9701275e348e959fd9 100644 (file)
@@ -21,43 +21,30 @@ package org.sonarqube.tests.user;
 
 import com.codeborne.selenide.Condition;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Maps;
 import com.sonar.orchestrator.Orchestrator;
+import java.util.Collections;
 import java.util.Map;
 import javax.annotation.CheckForNull;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
-import org.sonar.wsclient.Host;
-import org.sonar.wsclient.Sonar;
-import org.sonar.wsclient.base.HttpException;
-import org.sonar.wsclient.connectors.HttpClient4Connector;
-import org.sonar.wsclient.services.AuthenticationQuery;
-import org.sonar.wsclient.user.UserParameters;
 import org.sonarqube.qa.util.Tester;
 import org.sonarqube.qa.util.pageobjects.Navigation;
 import org.sonarqube.qa.util.pageobjects.SystemInfoPage;
 import org.sonarqube.qa.util.pageobjects.UsersManagementPage;
+import org.sonarqube.ws.UserGroups.Group;
+import org.sonarqube.ws.Users;
 import org.sonarqube.ws.Users.SearchWsResponse.User;
-import org.sonarqube.ws.client.GetRequest;
-import org.sonarqube.ws.client.WsResponse;
-import org.sonarqube.ws.client.users.CreateRequest;
+import org.sonarqube.ws.client.users.ChangePasswordRequest;
 import org.sonarqube.ws.client.users.SearchRequest;
-import util.user.UserRule;
 
-import static java.net.HttpURLConnection.HTTP_OK;
-import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
+import static java.util.Arrays.asList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.groups.Tuple.tuple;
-import static org.junit.Assert.fail;
+import static util.ItUtils.expectHttpError;
 import static util.ItUtils.newOrchestratorBuilder;
-import static util.ItUtils.newUserWsClient;
 import static util.ItUtils.pluginArtifact;
-import static util.ItUtils.resetSettings;
-import static util.selenium.Selenese.runSelenese;
 
 /**
  * Test REALM authentication.
@@ -66,72 +53,46 @@ import static util.selenium.Selenese.runSelenese;
  */
 public class RealmAuthenticationTest {
 
-  private static final String TECH_USER = "techUser";
-  private static final String USER_LOGIN = "tester";
-  private static final String ADMIN_USER_LOGIN = "admin-user";
-
   @Rule
   public ExpectedException thrown = ExpectedException.none();
 
-  /**
-   * Property from security-plugin for user management.
-   */
-  private static final String USERS_PROPERTY = "sonar.fakeauthenticator.users";
-
   @ClassRule
   public static final Orchestrator orchestrator = newOrchestratorBuilder()
     .addPlugin(pluginArtifact("security-plugin"))
     .setServerProperty("sonar.security.realm", "FakeRealm")
     .build();
 
-  @Rule
-  public UserRule userRule = UserRule.from(orchestrator);
-
   @Rule
   public Tester tester = new Tester(orchestrator).disableOrganizations();
 
-  @Before
-  @After
-  public void resetData() {
-    resetSettings(orchestrator, null, USERS_PROPERTY, "sonar.security.updateUserAttributes");
-  }
-
-  @Before
-  public void initAdminUser() {
-    userRule.createAdminUser(ADMIN_USER_LOGIN, ADMIN_USER_LOGIN);
-  }
-
-  @After
-  public void deleteAdminUser() {
-    userRule.resetUsers();
-  }
-
   /**
    * SONAR-3137, SONAR-2292
    * Restriction on password length (minimum 4 characters) should be disabled, when external system enabled.
    */
   @Test
-  public void shouldSynchronizeDetailsAndGroups() {
+  public void synchronize_details_and_groups() {
     // Given clean Sonar installation and no users in external system
-    String username = USER_LOGIN;
+    String username = tester.users().generateLogin();
     String password = "123";
-    Map<String, String> users = Maps.newHashMap();
+    Group group = tester.groups().generate();
 
     // When user created in external system
-    users.put(username + ".password", password);
-    users.put(username + ".name", "Tester Testerovich");
-    users.put(username + ".email", "tester@example.org");
-    users.put(username + ".groups", "sonar-user");
-    updateUsersInExtAuth(users);
+    updateUsersInExtAuth(ImmutableMap.of(
+      username + ".password", password,
+      username + ".name", "Tester Testerovich",
+      username + ".email", "tester@example.org",
+      username + ".groups", group.getName()));
+
     // Then
     verifyAuthenticationIsOk(username, password);
-    verifyUser();
-
-    // with external details and groups
-    runSelenese(orchestrator, "/user/ExternalAuthenticationTest/external-user-details.html");
+    assertThat(tester.wsClient().users().search(new SearchRequest().setQ(username)).getUsersList())
+      .extracting(User::getLogin, User::getName, User::getEmail,
+        User::getExternalIdentity, User::getExternalProvider, User::getLocal, u -> u.getGroups().getGroupsList())
+      .containsExactlyInAnyOrder(tuple(username, "Tester Testerovich", "tester@example.org", username, "sonarqube", false, asList(group.getName(), "sonar-users")));
 
     // SONAR-4462
-    SystemInfoPage page = tester.openBrowser().logIn().submitCredentials(ADMIN_USER_LOGIN).openSystemInfo();
+    Users.CreateWsResponse.User adminUser = tester.users().generateAdministrator();
+    SystemInfoPage page = tester.openBrowser().logIn().submitCredentials(adminUser.getLogin()).openSystemInfo();
     page.getCardItem("System").shouldHaveFieldWithValue("External User Authentication", "FakeRealm");
   }
 
@@ -139,56 +100,54 @@ public class RealmAuthenticationTest {
    * SONAR-4034
    */
   @Test
-  public void shouldUpdateDetailsByDefault() {
+  public void update_details_by_default() {
     // Given clean Sonar installation and no users in external system
-    String username = USER_LOGIN;
+    String username = tester.users().generateLogin();
     String password = "123";
-    Map<String, String> users = Maps.newHashMap();
 
     // When user created in external system
-    users.put(username + ".password", password);
-    users.put(username + ".name", "Tester Testerovich");
-    users.put(username + ".email", "tester@example.org");
-    users.put(username + ".groups", "sonar-user");
-    updateUsersInExtAuth(users);
+    updateUsersInExtAuth(ImmutableMap.of(
+      username + ".password", password,
+      username + ".name", "Tester Testerovich",
+      username + ".email", "tester@example.org"));
+
     // Then
     verifyAuthenticationIsOk(username, password);
-    verifyUser();
-
-    // with external details and groups
-    // TODO replace by WS ? Or with new Selenese utils
-    runSelenese(orchestrator, "/user/ExternalAuthenticationTest/external-user-details.html");
+    assertThat(tester.wsClient().users().search(new SearchRequest().setQ(username)).getUsersList())
+      .extracting(User::getLogin, User::getName, User::getEmail)
+      .containsExactlyInAnyOrder(tuple(username, "Tester Testerovich", "tester@example.org"));
 
     // Now update user details
-    users.put(username + ".name", "Tester2 Testerovich");
-    users.put(username + ".email", "tester2@example.org");
-    updateUsersInExtAuth(users);
+    updateUsersInExtAuth(ImmutableMap.of(
+      username + ".password", password,
+      username + ".name", "Tester2 Testerovich",
+      username + ".email", "tester2@example.org"));
+
     // Then
     verifyAuthenticationIsOk(username, password);
 
     // with external details and groups updated
-    runSelenese(orchestrator, "/user/ExternalAuthenticationTest/external-user-details2.html");
+    assertThat(tester.wsClient().users().search(new SearchRequest().setQ(username)).getUsersList())
+      .extracting(User::getLogin, User::getName, User::getEmail)
+      .containsExactlyInAnyOrder(tuple(username, "Tester2 Testerovich", "tester2@example.org"));
   }
 
   /**
    * SONAR-3138
    */
   @Test
-  public void shouldNotFallback() {
+  public void does_not_fallback() {
     // Given clean Sonar installation and no users in external system
-    String login = USER_LOGIN;
+    String login = tester.users().generateLogin();
     String password = "1234567";
-    Map<String, String> users = Maps.newHashMap();
 
     // When user created in external system
-    users.put(login + ".password", password);
-    updateUsersInExtAuth(users);
+    updateUsersInExtAuth(ImmutableMap.of(login + ".password", password));
     // Then
     verifyAuthenticationIsOk(login, password);
 
     // When external system does not work
-    users.remove(login + ".password");
-    updateUsersInExtAuth(users);
+    updateUsersInExtAuth(Collections.emptyMap());
     // Then
     verifyAuthenticationIsNotOk(login, password);
   }
@@ -197,16 +156,14 @@ public class RealmAuthenticationTest {
    * SONAR-4543
    */
   @Test
-  public void adminIsLocalAccountByDefault() {
+  public void admin_is_local_account_by_default() {
     // Given clean Sonar installation and no users in external system
     String login = "admin";
     String localPassword = "admin";
     String remotePassword = "nimda";
-    Map<String, String> users = Maps.newHashMap();
 
     // When admin created in external system with a different password
-    users.put(login + ".password", remotePassword);
-    updateUsersInExtAuth(users);
+    updateUsersInExtAuth(ImmutableMap.of(login + ".password", remotePassword));
 
     // Then this is local DB that should be used
     verifyAuthenticationIsNotOk(login, remotePassword);
@@ -217,52 +174,49 @@ public class RealmAuthenticationTest {
    * SONAR-1334, SONAR-3185 (createUsers=true is default)
    */
   @Test
-  public void shouldCreateNewUsers() {
+  public void create_new_users() {
     // Given clean Sonar installation and no users in external system
-    String username = USER_LOGIN;
+    String username = tester.users().generateLogin();
     String password = "1234567";
-    Map<String, String> users = Maps.newHashMap();
 
     // When user not exists in external system
     // Then
     verifyAuthenticationIsNotOk(username, password);
 
     // When user created in external system
-    users.put(username + ".password", password);
-    updateUsersInExtAuth(users);
+    updateUsersInExtAuth(ImmutableMap.of(username + ".password", password));
     // Then
     verifyAuthenticationIsOk(username, password);
-    verifyUser();
+    verifyUser(username);
     verifyAuthenticationIsNotOk(username, "wrong");
   }
 
   // SONAR-3258
   @Test
-  public void shouldAutomaticallyReactivateDeletedUser() {
+  public void reactivate_deleted_user() {
     // Given clean Sonar installation and no users in external system
+    Users.CreateWsResponse.User adminUser = tester.users().generateAdministrator();
 
     // Let's create and delete the user "tester" in Sonar DB
+    String login = tester.users().generateLogin();
     Navigation nav = tester.openBrowser();
-    UsersManagementPage page = nav.logIn().submitCredentials(ADMIN_USER_LOGIN).openUsersManagement();
+    UsersManagementPage page = nav.logIn().submitCredentials(adminUser.getLogin()).openUsersManagement();
     page
-      .createUser(USER_LOGIN)
+      .createUser(login)
       .hasUsersCount(3)
-      .getUser(USER_LOGIN)
+      .getUser(login)
       .deactivateUser();
     page.hasUsersCount(2);
     nav.logOut()
-      .logIn().submitWrongCredentials(USER_LOGIN, USER_LOGIN)
+      .logIn().submitWrongCredentials(login, login)
       .getErrorMessage().shouldHave(Condition.text("Authentication failed"));
 
     // And now update the security with the user that was deleted
-    String login = USER_LOGIN;
     String password = "1234567";
-    Map<String, String> users = Maps.newHashMap();
-    users.put(login + ".password", password);
-    updateUsersInExtAuth(users);
+    updateUsersInExtAuth(ImmutableMap.of(login + ".password", password));
     // check that the deleted/deactivated user "tester" has been reactivated and can now log in
     verifyAuthenticationIsOk(login, password);
-    verifyUser();
+    verifyUser(login);
   }
 
   /**
@@ -271,24 +225,20 @@ public class RealmAuthenticationTest {
   @Test
   public void update_password_of_technical_user() {
     // Create user in external authentication
-    updateUsersInExtAuth(ImmutableMap.of(USER_LOGIN + ".password", USER_LOGIN));
-    verifyAuthenticationIsOk(USER_LOGIN, USER_LOGIN);
+    String login = tester.users().generateLogin();
+    updateUsersInExtAuth(ImmutableMap.of(login + ".password", login));
+    verifyAuthenticationIsOk(login, login);
 
     // Create technical user in db
-    createUserInDb(TECH_USER, "old_password");
-    assertThat(checkAuthenticationThroughWebService(TECH_USER, "old_password")).isTrue();
+    Users.CreateWsResponse.User techUser = tester.users().generate(u -> u.setPassword("old_password"));
+    verifyAuthenticationIsOk(techUser.getLogin(), "old_password");
 
     // Updating password of technical user is allowed
-    updateUserPasswordInDb(TECH_USER, "new_password");
-    assertThat(checkAuthenticationThroughWebService(TECH_USER, "new_password")).isTrue();
+    tester.users().service().changePassword(new ChangePasswordRequest().setLogin(techUser.getLogin()).setPassword("new_password"));
+    verifyAuthenticationIsOk(techUser.getLogin(), "new_password");
 
     // But updating password of none local user is not allowed
-    try {
-      updateUserPasswordInDb(USER_LOGIN, "new_password");
-      fail();
-    } catch (HttpException e) {
-      verifyHttpException(e, 400);
-    }
+    expectHttpError(400, () -> tester.users().service().changePassword(new ChangePasswordRequest().setLogin(login).setPassword("new_password")));
   }
 
   /**
@@ -297,16 +247,14 @@ public class RealmAuthenticationTest {
   @Test
   public void authentication_with_ws() {
     // Given clean Sonar installation and no users in external system
-    String login = USER_LOGIN;
+    String login = tester.users().generateLogin();
     String password = "1234567";
-    Map<String, String> users = Maps.newHashMap();
 
     // When user created in external system
-    users.put(login + ".password", password);
-    updateUsersInExtAuth(users);
+    updateUsersInExtAuth(ImmutableMap.of(login + ".password", password));
 
     verifyAuthenticationIsOk(login, password);
-    verifyUser();
+    verifyUser(login);
     verifyAuthenticationIsNotOk("wrong", password);
     verifyAuthenticationIsNotOk(login, "wrong");
     verifyAuthenticationIsNotOk(login, null);
@@ -332,71 +280,54 @@ public class RealmAuthenticationTest {
 
   @Test
   public void provision_user_before_authentication() {
-    tester.wsClient().users().create(new CreateRequest()
-      .setLogin(USER_LOGIN)
-      .setName("Tester Testerovich")
+    Users.CreateWsResponse.User user = tester.users().generate(u -> u.setName("Tester Testerovich")
       .setEmail("tester@example.org")
+      .setPassword(null)
       .setLocal("false"));
     // The user is created in SonarQube but doesn't exist yet in external authentication system
-    verifyAuthenticationIsNotOk(USER_LOGIN, "123");
+    verifyAuthenticationIsNotOk(user.getLogin(), "123");
 
     updateUsersInExtAuth(ImmutableMap.of(
-      USER_LOGIN + ".password", "123",
-      USER_LOGIN + ".name", "Tester Testerovich",
-      USER_LOGIN + ".email", "tester@example.org"));
+      user.getLogin() + ".password", "123",
+      user.getLogin() + ".name", "Tester Testerovich",
+      user.getLogin() + ".email", "tester@example.org"));
 
-    verifyAuthenticationIsOk(USER_LOGIN, "123");
-    verifyUser();
+    verifyAuthenticationIsOk(user.getLogin(), "123");
+    verifyUser(user.getLogin());
   }
 
   @Test
   public void fail_to_authenticate_user_when_email_already_exists() {
-    userRule.createUser("another", "Another", "tester@example.org", "another");
-
-    String username = USER_LOGIN;
+    Users.CreateWsResponse.User user = tester.users().generate();
+    String username = tester.users().generateLogin();
     String password = "123";
-    Map<String, String> users = Maps.newHashMap();
-    users.put(username + ".password", password);
-    users.put(username + ".name", "Tester Testerovich");
-    users.put(username + ".email", "tester@example.org");
-    users.put(username + ".groups", "sonar-user");
-    updateUsersInExtAuth(users);
+
+    updateUsersInExtAuth(ImmutableMap.of(
+      username + ".password", password,
+      username + ".email", user.getEmail()));
 
     verifyAuthenticationIsNotOk(username, password);
   }
 
-  private void verifyHttpException(Exception e, int expectedCode) {
-    assertThat(e).isInstanceOf(HttpException.class);
-    HttpException exception = (HttpException) e;
-    assertThat(exception.status()).isEqualTo(expectedCode);
-  }
+  @Test
+  public void fail_to_authenticate_user_when_email_already_exists_on_several_users() {
+    Users.CreateWsResponse.User user1 = tester.users().generate(u -> u.setEmail("user@email.com"));
+    Users.CreateWsResponse.User user2 = tester.users().generate(u -> u.setEmail("user@email.com"));
+    String username = tester.users().generateLogin();
+    String password = "123";
 
-  private boolean checkAuthenticationThroughWebService(String login, String password) {
-    return createWsClient(login, password).find(new AuthenticationQuery()).isValid();
+    updateUsersInExtAuth(ImmutableMap.of(
+      username + ".password", password,
+      username + ".email", "user@email.com"));
+
+    verifyAuthenticationIsNotOk(username, password);
   }
 
   /**
    * Updates information about users in security-plugin.
    */
   private void updateUsersInExtAuth(Map<String, String> users) {
-    tester.settings().setGlobalSettings(USERS_PROPERTY, format(users));
-  }
-
-  private void createUserInDb(String login, String password) {
-    orchestrator.getServer().adminWsClient().userClient().create(UserParameters.create().login(login).name(login)
-      .password(password).passwordConfirmation(password));
-  }
-
-  private void updateUserPasswordInDb(String login, String newPassword) {
-    orchestrator.getServer().adminWsClient().post("/api/users/change_password", "login", login, "password", newPassword);
-  }
-
-  /**
-   * Utility method to create {@link Sonar} with specified {@code username} and {@code password}.
-   * Orchestrator does not provide such method.
-   */
-  private Sonar createWsClient(String username, String password) {
-    return new Sonar(new HttpClient4Connector(new Host(orchestrator.getServer().getUrl(), username, password)));
+    tester.settings().setGlobalSettings("sonar.fakeauthenticator.users", format(users));
   }
 
   @CheckForNull
@@ -412,22 +343,17 @@ public class RealmAuthenticationTest {
   }
 
   private void verifyAuthenticationIsOk(String login, String password) {
-    assertThat(checkAuthenticationWithWebService(login, password).code()).isEqualTo(HTTP_OK);
+    tester.as(login, password).wsClient().system().ping();
   }
 
   private void verifyAuthenticationIsNotOk(String login, String password) {
-    assertThat(checkAuthenticationWithWebService(login, password).code()).isEqualTo(HTTP_UNAUTHORIZED);
-  }
-
-  private void verifyUser(){
-    assertThat(tester.wsClient().users().search(new SearchRequest().setQ(USER_LOGIN)).getUsersList())
-    .extracting(User::getLogin, User::getExternalIdentity, User::getExternalProvider, User::getLocal)
-    .containsExactlyInAnyOrder(tuple(USER_LOGIN, USER_LOGIN, "sonarqube", false));
+    expectHttpError(401, () -> tester.as(login, password).wsClient().system().ping());
   }
 
-  private WsResponse checkAuthenticationWithWebService(String login, String password) {
-    // Call any WS
-    return newUserWsClient(orchestrator, login, password).wsConnector().call(new GetRequest("api/rules/search"));
+  private void verifyUser(String login) {
+    assertThat(tester.wsClient().users().search(new SearchRequest().setQ(login)).getUsersList())
+      .extracting(User::getLogin, User::getExternalIdentity, User::getExternalProvider, User::getLocal)
+      .containsExactlyInAnyOrder(tuple(login, login, "sonarqube", false));
   }
 
 }
diff --git a/tests/src/test/java/org/sonarqube/tests/user/SsoAuthenticationTest.java b/tests/src/test/java/org/sonarqube/tests/user/SsoAuthenticationTest.java
deleted file mode 100644 (file)
index 146f37e..0000000
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.sonarqube.tests.user;
-
-import com.sonar.orchestrator.Orchestrator;
-import java.net.URLEncoder;
-import java.util.List;
-import javax.annotation.Nullable;
-import okhttp3.Response;
-import org.apache.commons.io.FileUtils;
-import org.junit.Before;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.rules.RuleChain;
-import util.user.UserRule;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.assertj.core.api.Assertions.assertThat;
-import static util.ItUtils.call;
-import static util.ItUtils.newOrchestratorBuilder;
-
-/**
- * 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 = newOrchestratorBuilder()
-    .setServerProperty("sonar.web.sso.enable", "true")
-    .setServerProperty("sonar.web.sso.loginHeader", LOGIN_HEADER)
-    .setServerProperty("sonar.web.sso.nameHeader", NAME_HEADER)
-    .setServerProperty("sonar.web.sso.emailHeader", EMAIL_HEADER)
-    .setServerProperty("sonar.web.sso.groupsHeader", GROUPS_HEADER)
-    .build();
-
-  private static UserRule userRule = UserRule.from(orchestrator);
-
-  @ClassRule
-  public static RuleChain ruleChain = RuleChain.outerRule(orchestrator).around(userRule);
-
-  @Before
-  public void resetData() {
-    userRule.resetUsers();
-  }
-
-  @Test
-  public void authenticate() {
-    doCall(USER_LOGIN, USER_NAME, USER_EMAIL, null);
-
-    userRule.verifyUserExists(USER_LOGIN, USER_NAME, USER_EMAIL);
-  }
-
-  @Test
-  public void authenticate_with_only_login() {
-    doCall(USER_LOGIN, null, null, null);
-
-    userRule.verifyUserExists(USER_LOGIN, USER_LOGIN, null);
-  }
-
-  @Test
-  public void update_user_when_headers_are_updated() {
-    doCall(USER_LOGIN, USER_NAME, USER_EMAIL, null);
-    userRule.verifyUserExists(USER_LOGIN, USER_NAME, USER_EMAIL);
-
-    // As we don't keep the JWT token is the test, the user is updated
-    doCall(USER_LOGIN, "new name", "new email", null);
-    userRule.verifyUserExists(USER_LOGIN, "new name", "new email");
-  }
-
-  @Test
-  public void authenticate_with_groups() {
-    doCall(USER_LOGIN, null, null, GROUP_1);
-
-    userRule.verifyUserGroupMembership(USER_LOGIN, GROUP_1, "sonar-users");
-  }
-
-  @Test
-  public void synchronize_groups_when_authenticating_existing_user() {
-    userRule.createGroup(GROUP_1);
-    userRule.createGroup(GROUP_2);
-    userRule.createGroup(GROUP_3);
-    userRule.createUser(USER_LOGIN, "password");
-    userRule.associateGroupsToUser(USER_LOGIN, GROUP_1, GROUP_2);
-
-    doCall(USER_LOGIN, null, null, GROUP_2 + "," + GROUP_3);
-
-    userRule.verifyUserGroupMembership(USER_LOGIN, GROUP_2, GROUP_3, "sonar-users");
-  }
-
-  @Test
-  public void authentication_with_local_user_is_possible_when_no_header() {
-    userRule.createUser(USER_LOGIN, "password");
-
-    checkLocalAuthentication(USER_LOGIN, "password");
-  }
-
-  @Test
-  public void display_message_in_ui_but_not_in_log_when_unauthorized_exception() throws Exception {
-    Response response = doCall("invalid login $", null, null, null);
-
-    assertThat(response.code()).isEqualTo(200);
-    assertThat(response.request().url().toString()).contains("sessions/unauthorized");
-
-    List<String> logsLines = FileUtils.readLines(orchestrator.getServer().getWebLogs(), UTF_8);
-    assertThat(logsLines).doesNotContain("org.sonar.server.exceptions.BadRequestException: Use only letters, numbers, and .-_@ please.");
-    userRule.verifyUserDoesNotExist(USER_LOGIN);
-  }
-
-  @Test
-  public void fail_when_email_already_exists() throws Exception {
-    userRule.createUser("another", "Another", USER_EMAIL, "another");
-
-    Response response = doCall(USER_LOGIN, USER_NAME, USER_EMAIL, null);
-
-    String expectedError = "You can't sign up because email 'tester@email.com' is already used by an existing user. This means that you probably already registered with another account";
-    assertThat(response.code()).isEqualTo(200);
-    assertThat(response.request().url().toString()).contains(URLEncoder.encode(expectedError, UTF_8.name()));
-    assertThat(FileUtils.readLines(orchestrator.getServer().getWebLogs(), UTF_8)).doesNotContain(expectedError);
-  }
-
-  private static Response doCall(String login, @Nullable String name, @Nullable String email, @Nullable String groups) {
-    return call(orchestrator.getServer().getUrl(),
-      LOGIN_HEADER, login,
-      NAME_HEADER, name,
-      EMAIL_HEADER, email,
-      GROUPS_HEADER, groups);
-  }
-
-  private boolean checkLocalAuthentication(String login, String password) {
-    String result = orchestrator.getServer().wsClient(login, password).get("/api/authentication/validate");
-    return result.contains("{\"valid\":true}");
-  }
-
-}
diff --git a/tests/src/test/resources/user/BaseIdentityProviderTest/display_message_in_ui_but_not_in_log_when_unauthorized_exception.html b/tests/src/test/resources/user/BaseIdentityProviderTest/display_message_in_ui_but_not_in_log_when_unauthorized_exception.html
deleted file mode 100644 (file)
index d202404..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
-  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-  <title>fail_to_authenticate_when_not_allowed_to_sign_up</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
-  <thead>
-  <tr>
-    <td rowspan="1" colspan="3">french</td>
-  </tr>
-  </thead>
-  <tbody>
-  <tr>
-    <td>open</td>
-    <td>/sessions/new</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>content</td>
-    <td>*Log in with Fake base identity provider*</td>
-  </tr>
-  <tr>
-    <td>click</td>
-    <td>css=.oauth-providers a</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>bd</td>
-    <td>*You're not authorized to access this page. Please contact the administrator.*</td>
-  </tr>
-  <tr>
-    <td>assertText</td>
-    <td>bd</td>
-    <td>*Reason: A functional error has happened*</td>
-  </tr>
-  </tbody>
-</table>
-</body>
-</html>
diff --git a/tests/src/test/resources/user/BaseIdentityProviderTest/display_unauthorized_page_when_authentication_failed.html b/tests/src/test/resources/user/BaseIdentityProviderTest/display_unauthorized_page_when_authentication_failed.html
deleted file mode 100644 (file)
index 47a19a2..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
-  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-  <title>display_unauthorized_page_when_authentication_failed</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
-  <thead>
-  <tr>
-    <td rowspan="1" colspan="3">french</td>
-  </tr>
-  </thead>
-  <tbody>
-  <tr>
-    <td>open</td>
-    <td>/sessions/new</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>content</td>
-    <td>*Log in with Fake base identity provider*</td>
-  </tr>
-  <tr>
-    <td>click</td>
-    <td>css=.oauth-providers a</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>bd</td>
-    <td>*You're not authorized to access this page. Please contact the administrator.*</td>
-  </tr>
-  </tbody>
-</table>
-</body>
-</html>
diff --git a/tests/src/test/resources/user/BaseIdentityProviderTest/fail_to_authenticate_when_not_allowed_to_sign_up.html b/tests/src/test/resources/user/BaseIdentityProviderTest/fail_to_authenticate_when_not_allowed_to_sign_up.html
deleted file mode 100644 (file)
index ddf1b83..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
-  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-  <title>fail_to_authenticate_when_not_allowed_to_sign_up</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
-  <thead>
-  <tr>
-    <td rowspan="1" colspan="3">french</td>
-  </tr>
-  </thead>
-  <tbody>
-  <tr>
-    <td>open</td>
-    <td>/sessions/new</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>content</td>
-    <td>*Log in with Fake base identity provider*</td>
-  </tr>
-  <tr>
-    <td>click</td>
-    <td>css=.oauth-providers a</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>bd</td>
-    <td>*You're not authorized to access this page. Please contact the administrator.*Reason: 'fake-base-id-provider' users are not allowed to sign up*</td>
-  </tr>
-  </tbody>
-</table>
-</body>
-</html>
diff --git a/tests/src/test/resources/user/BaseIdentityProviderTest/fail_when_email_already_exists.html b/tests/src/test/resources/user/BaseIdentityProviderTest/fail_when_email_already_exists.html
deleted file mode 100644 (file)
index b6f7e60..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
-  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-  <title>fail_when_email_already_exists</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
-  <thead>
-  <tr>
-    <td rowspan="1" colspan="3">french</td>
-  </tr>
-  </thead>
-  <tbody>
-  <tr>
-    <td>open</td>
-    <td>/sessions/new</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>content</td>
-    <td>*Log in with Fake base identity provider*</td>
-  </tr>
-  <tr>
-    <td>click</td>
-    <td>css=.oauth-providers a</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>bd</td>
-    <td>*You're not authorized to access this page. Please contact the administrator.*</td>
-  </tr>
-  <tr>
-    <td>assertText</td>
-    <td>bd</td>
-    <td>*You can't sign up because email 'john@email.com' is already used by an existing user. This means that you probably already registered with another account*</td>
-  </tr>
-  </tbody>
-</table>
-</body>
-</html>
diff --git a/tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details.html b/tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details.html
deleted file mode 100644 (file)
index 4b540f7..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-/W3C/DTD XHTML 1.0 Strict/EN" "http:/www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http:/www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http:/selenium-ide.openqa.org/profiles/test-case">
-  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-  <title>external_user_details</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
-  <thead>
-  <tr>
-    <td rowspan="1" colspan="3">external_user_details</td>
-  </tr>
-  </thead>
-  <tbody>
-  <tr>
-    <td>open</td>
-    <td>/sessions/new</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>type</td>
-    <td>login</td>
-    <td>tester</td>
-  </tr>
-  <tr>
-    <td>type</td>
-    <td>password</td>
-    <td>123</td>
-  </tr>
-  <tr>
-    <td>clickAndWait</td>
-    <td>[type=submit]</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForElementPresent</td>
-    <td>css=.js-user-authenticated</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>open</td>
-    <td>/account/</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>login</td>
-    <td>tester</td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>id=name</td>
-    <td>Tester Testerovich</td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>id=email</td>
-    <td>tester@example.org</td>
-  </tr>
-  </tbody>
-</table>
-</body>
-</html>
diff --git a/tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details2.html b/tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details2.html
deleted file mode 100644 (file)
index b83aa73..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-/W3C/DTD XHTML 1.0 Strict/EN" "http:/www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http:/www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http:/selenium-ide.openqa.org/profiles/test-case">
-  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-  <title>external_user_details</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
-  <thead>
-  <tr>
-    <td rowspan="1" colspan="3">external_user_details</td>
-  </tr>
-  </thead>
-  <tbody>
-  <tr>
-    <td>open</td>
-    <td>/sessions/new</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>type</td>
-    <td>login</td>
-    <td>tester</td>
-  </tr>
-  <tr>
-    <td>type</td>
-    <td>password</td>
-    <td>123</td>
-  </tr>
-  <tr>
-    <td>clickAndWait</td>
-    <td>[type=submit]</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForElementPresent</td>
-    <td>css=.js-user-authenticated</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>open</td>
-    <td>/account</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>login</td>
-    <td>tester</td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>id=name</td>
-    <td>Tester2 Testerovich</td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>id=email</td>
-    <td>tester2@example.org</td>
-  </tr>
-  </tbody>
-</table>
-</body>
-</html>
diff --git a/tests/src/test/resources/user/OAuth2IdentityProviderTest/display_message_in_ui_but_not_in_log_when_unauthorized_exception.html b/tests/src/test/resources/user/OAuth2IdentityProviderTest/display_message_in_ui_but_not_in_log_when_unauthorized_exception.html
deleted file mode 100644 (file)
index a78424c..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
-  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-  <title>fail_to_authenticate_when_not_allowed_to_sign_up</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
-  <thead>
-  <tr>
-    <td rowspan="1" colspan="3">french</td>
-  </tr>
-  </thead>
-  <tbody>
-  <tr>
-    <td>open</td>
-    <td>/sessions/new</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>content</td>
-    <td>*Log in with Fake oauth2 identity provider*</td>
-  </tr>
-  <tr>
-    <td>click</td>
-    <td>css=.oauth-providers a</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>bd</td>
-    <td>*You're not authorized to access this page. Please contact the administrator.*</td>
-  </tr>
-  <tr>
-    <td>assertText</td>
-    <td>bd</td>
-    <td>*Reason: A functional error has happened*</td>
-  </tr>
-  </tbody>
-</table>
-</body>
-</html>
diff --git a/tests/src/test/resources/user/OAuth2IdentityProviderTest/display_unauthorized_page_when_authentication_failed.html b/tests/src/test/resources/user/OAuth2IdentityProviderTest/display_unauthorized_page_when_authentication_failed.html
deleted file mode 100644 (file)
index b01d24a..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
-  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-  <title>display_unauthorized_page_when_authentication_failed</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
-  <thead>
-  <tr>
-    <td rowspan="1" colspan="3">french</td>
-  </tr>
-  </thead>
-  <tbody>
-  <tr>
-    <td>open</td>
-    <td>/sessions/new</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>content</td>
-    <td>*Log in with Fake oauth2 identity provider*</td>
-  </tr>
-  <tr>
-    <td>click</td>
-    <td>css=.oauth-providers a</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>bd</td>
-    <td>*You're not authorized to access this page. Please contact the administrator.*</td>
-  </tr>
-  </tbody>
-</table>
-</body>
-</html>
diff --git a/tests/src/test/resources/user/OAuth2IdentityProviderTest/fail_to_authenticate_when_not_allowed_to_sign_up.html b/tests/src/test/resources/user/OAuth2IdentityProviderTest/fail_to_authenticate_when_not_allowed_to_sign_up.html
deleted file mode 100644 (file)
index addc1e7..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
-  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-  <title>fail_to_authenticate_when_not_allowed_to_sign_up</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
-  <thead>
-  <tr>
-    <td rowspan="1" colspan="3">french</td>
-  </tr>
-  </thead>
-  <tbody>
-  <tr>
-    <td>open</td>
-    <td>/sessions/new</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>content</td>
-    <td>*Log in with Fake oauth2 identity provider*</td>
-  </tr>
-  <tr>
-    <td>click</td>
-    <td>css=.oauth-providers a</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td>waitForText</td>
-    <td>bd</td>
-    <td>*You're not authorized to access this page. Please contact the administrator.*Reason: 'fake-oauth2-id-provider' users are not allowed to sign up*</td>
-  </tr>
-  </tbody>
-</table>
-</body>
-</html>