]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7217 Fail to authenticate user with existing email 732/head
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Mon, 25 Jan 2016 15:23:06 +0000 (16:23 +0100)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 27 Jan 2016 13:49:51 +0000 (14:49 +0100)
17 files changed:
server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationError.java
server/sonar-server/src/main/java/org/sonar/server/authentication/EmailAlreadyExistsException.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/InitFilter.java
server/sonar-server/src/main/java/org/sonar/server/authentication/NotAllowUserToSignUpException.java
server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2CallbackFilter.java
server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java
server/sonar-server/src/test/java/org/sonar/server/authentication/InitFilterTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java
server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/email_already_exists.erb [new file with mode: 0644]
server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/not_allowed_to_sign_up.html.erb
sonar-db/src/main/java/org/sonar/db/user/UserDao.java
sonar-db/src/main/java/org/sonar/db/user/UserMapper.java
sonar-db/src/main/resources/org/sonar/db/user/UserMapper.xml
sonar-db/src/test/java/org/sonar/db/user/UserDaoTest.java
sonar-db/src/test/resources/org/sonar/db/user/UserDaoTest/exists_by_email.xml [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/BaseIdentityProvider.java
sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/OAuth2IdentityProvider.java

index 35e4bcc3d7424893fb40f2ea0c68f10ce274914f..c6628cdb055cc1f4fb7c9576d8e141fda6250fad 100644 (file)
@@ -25,13 +25,14 @@ import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
 
 import static java.lang.String.format;
+import static org.sonar.server.authentication.EmailAlreadyExistsException.EMAIL_ALREADY_EXISTS_PATH;
+import static org.sonar.server.authentication.NotAllowUserToSignUpException.NOT_ALLOWED_TO_SIGHNUP_PATH;
 
 public class AuthenticationError {
 
   private static final Logger LOGGER = Loggers.get(AuthenticationError.class);
 
   private static final String UNAUTHORIZED_PATH = "/sessions/unauthorized";
-  private static final String NOT_ALLOWED_TO_SIGHNUP_PATH = "/sessions/not_allowed_to_sign_up?providerName=%s";
 
   private AuthenticationError() {
     // Utility class
@@ -51,6 +52,10 @@ public class AuthenticationError {
     redirectTo(response, format(NOT_ALLOWED_TO_SIGHNUP_PATH, e.getProvider().getName()));
   }
 
+  public static void handleEmailAlreadyExistsError(EmailAlreadyExistsException e, HttpServletResponse response) {
+    redirectTo(response, format(EMAIL_ALREADY_EXISTS_PATH, e.getEmail()));
+  }
+
   private static void redirectToUnauthorized(HttpServletResponse response) {
     redirectTo(response, UNAUTHORIZED_PATH);
   }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/EmailAlreadyExistsException.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/EmailAlreadyExistsException.java
new file mode 100644 (file)
index 0000000..67491a4
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.authentication;
+
+public class EmailAlreadyExistsException extends RuntimeException {
+
+  public static final String EMAIL_ALREADY_EXISTS_PATH = "/sessions/email_already_exists?email=%s";
+
+  private final String email;
+
+  public EmailAlreadyExistsException(String email) {
+    this.email = email;
+  }
+
+  public String getEmail() {
+    return email;
+  }
+}
index 65b0eee5cb644f6fa019d9927b9c5f2ced378d89..5111f8d3822a9f080891a2d63fac9c4c377df883 100644 (file)
@@ -34,6 +34,7 @@ import org.sonar.api.web.ServletFilter;
 
 import static com.google.common.base.Strings.isNullOrEmpty;
 import static java.lang.String.format;
+import static org.sonar.server.authentication.AuthenticationError.handleEmailAlreadyExistsError;
 import static org.sonar.server.authentication.AuthenticationError.handleError;
 import static org.sonar.server.authentication.AuthenticationError.handleNotAllowedToSignUpError;
 
@@ -78,6 +79,8 @@ public class InitFilter extends ServletFilter {
       }
     } catch (NotAllowUserToSignUpException e) {
       handleNotAllowedToSignUpError(e, (HttpServletResponse) response);
+    } catch (EmailAlreadyExistsException e) {
+      handleEmailAlreadyExistsError(e, (HttpServletResponse) response);
     } catch (Exception e) {
       handleError(e, (HttpServletResponse) response, String.format("Fail to initialize authentication with provider '%s'", keyProvider));
     }
index 02269964a45511c7d1df9a29ba333603e1056095..2a1a2e7d9d7489c9d210276c8a3d9a79ab2b9b21 100644 (file)
@@ -23,6 +23,8 @@ import org.sonar.api.server.authentication.IdentityProvider;
 
 public class NotAllowUserToSignUpException extends RuntimeException {
 
+  public static final String NOT_ALLOWED_TO_SIGHNUP_PATH = "/sessions/not_allowed_to_sign_up?providerName=%s";
+
   private final IdentityProvider provider;
 
   public NotAllowUserToSignUpException(IdentityProvider provider) {
index 7240a936f7f82356f50a0b333c70e1ab098f8631..919a4302d763e83251a9abe3664d8ab2c76cd5fc 100644 (file)
@@ -31,6 +31,7 @@ import org.sonar.api.server.authentication.IdentityProvider;
 import org.sonar.api.server.authentication.OAuth2IdentityProvider;
 import org.sonar.api.web.ServletFilter;
 
+import static org.sonar.server.authentication.AuthenticationError.handleEmailAlreadyExistsError;
 import static org.sonar.server.authentication.AuthenticationError.handleError;
 import static org.sonar.server.authentication.AuthenticationError.handleNotAllowedToSignUpError;
 
@@ -67,6 +68,8 @@ public class OAuth2CallbackFilter extends ServletFilter {
       }
     } catch (NotAllowUserToSignUpException e) {
       handleNotAllowedToSignUpError(e, (HttpServletResponse) response);
+    } catch (EmailAlreadyExistsException e) {
+      handleEmailAlreadyExistsError(e, (HttpServletResponse) response);
     } catch (Exception e) {
       handleError(e, (HttpServletResponse) response, String.format("Fail to callback authentication with %s", keyProvider));
     }
index 803b3fc39a8058e9f47fdd62773ebc877bcddcad..7ef534e27b0401fdac0ae2fbf6585fb1e0a92cd0 100644 (file)
@@ -66,6 +66,12 @@ public class UserIdentityAuthenticator {
       if (!provider.allowsUsersToSignUp()) {
         throw new NotAllowUserToSignUpException(provider);
       }
+
+      String email = user.getEmail();
+      if (email != null && dbClient.userDao().doesEmailExist(dbSession, email)) {
+        throw new EmailAlreadyExistsException(email);
+      }
+
       userUpdater.create(dbSession, NewUser.create()
         .setLogin(uuidFactory.create())
         .setEmail(user.getEmail())
index 04e3c74edced3cb7abacd5bcd68fd3bcc316d917..adc9173d071154f10229aad989de06f20330714e 100644 (file)
@@ -142,6 +142,28 @@ public class InitFilterTest {
     assertError("Fail to initialize authentication with provider 'unsupported'");
   }
 
+  @Test
+  public void redirect_when_failing_because_of_NotAllowUserToSignUpException() throws Exception {
+    IdentityProvider identityProvider = new FailWithNotAllowUserToSignUpIdProvider("failing");
+    when(request.getRequestURI()).thenReturn("/sessions/init/" + identityProvider.getKey());
+    identityProviderRepository.addIdentityProvider(identityProvider);
+
+    underTest.doFilter(request, response, chain);
+
+    verify(response).sendRedirect("/sessions/not_allowed_to_sign_up?providerName=Failing provider");
+  }
+
+  @Test
+  public void redirect_when_failing_because_of_EmailAlreadyExistsException() throws Exception {
+    IdentityProvider identityProvider = new FailWithEmailAlreadyExistsExceptionIdProvider("failing");
+    when(request.getRequestURI()).thenReturn("/sessions/init/" + identityProvider.getKey());
+    identityProviderRepository.addIdentityProvider(identityProvider);
+
+    underTest.doFilter(request, response, chain);
+
+    verify(response).sendRedirect("/sessions/email_already_exists?email=john@email.com");
+  }
+
   private void assertOAuth2InitCalled(){
     assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty();
     assertThat(oAuth2IdentityProvider.isInitCalled()).isTrue();
@@ -157,4 +179,33 @@ public class InitFilterTest {
     verify(response).sendRedirect("/sessions/unauthorized");
     assertThat(oAuth2IdentityProvider.isInitCalled()).isFalse();
   }
+
+  private static class FailWithNotAllowUserToSignUpIdProvider extends FakeBasicIdentityProvider {
+
+    public FailWithNotAllowUserToSignUpIdProvider(String key) {
+      super(key, true);
+    }
+
+    @Override
+    public String getName() {
+      return "Failing provider";
+    }
+
+    @Override
+    public void init(Context context) {
+      throw new NotAllowUserToSignUpException(this);
+    }
+  }
+
+  private static class FailWithEmailAlreadyExistsExceptionIdProvider extends FakeBasicIdentityProvider {
+
+    public FailWithEmailAlreadyExistsExceptionIdProvider(String key) {
+      super(key, true);
+    }
+
+    @Override
+    public void init(Context context) {
+      throw new EmailAlreadyExistsException("john@email.com");
+    }
+  }
 }
index 649ba0a35c5d57ea0f010c4188c28d48dbbfc065..ab3d706ca69fa0bc3cdefc6d0de866fdf08bd379 100644 (file)
@@ -146,4 +146,14 @@ public class UserIdentityAuthenticatorTest {
     thrown.expect(NotAllowUserToSignUpException.class);
     underTest.authenticate(USER_IDENTITY, identityProvider, httpSession);
   }
+
+  @Test
+  public void fail_to_authenticate_new_user_when_email_already_exists() throws Exception {
+    when(userDao.selectByExternalIdentity(dbSession, USER_IDENTITY.getId(), IDENTITY_PROVIDER.getKey())).thenReturn(Optional.<UserDto>absent());
+    when(userDao.selectOrFailByExternalIdentity(dbSession, USER_IDENTITY.getId(), IDENTITY_PROVIDER.getKey())).thenReturn(ACTIVE_USER);
+    when(userDao.doesEmailExist(dbSession, USER_IDENTITY.getEmail())).thenReturn(true);
+
+    thrown.expect(EmailAlreadyExistsException.class);
+    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, httpSession);
+  }
 }
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/email_already_exists.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/email_already_exists.erb
new file mode 100644 (file)
index 0000000..ba75080
--- /dev/null
@@ -0,0 +1,10 @@
+<table class="spaced">
+  <tr>
+    <td align="center">
+
+      <div id="login_form">
+        <p id="unauthorized">Email <%= h params[:email] %> is already used by an existing user</p>
+      </div>
+    </td>
+  </tr>
+</table>
index dd8ddb1ce16254eea6ea29385432941e569133eb..b2ab35dbc824ce1757d8e7fe66a1f64a228b7a83 100644 (file)
@@ -3,7 +3,7 @@
     <td align="center">
 
       <div id="login_form">
-        <p id="unauthorized"><%= params[:providerName] %> users are not allowed to signup</p>
+        <p id="unauthorized"><%= params[:providerName] %> users are not allowed to signup</p>
       </div>
     </td>
   </tr>
index 8799754185a1261e3213e3ce39ec66f6fceeb3b7..8c01178d425eb66c851f03808803fa85bd85e320 100644 (file)
@@ -187,6 +187,13 @@ public class UserDao implements Dao {
     throw new RowNotFoundException(String.format("User with identity provider '%s' and id '%s' has not been found", extIdentityProvider, extIdentity));
   }
 
+  /**
+   * Please note that email is case insensitive, result for searching 'mail@email.com' or 'Mail@Email.com' will be the same
+   */
+  public boolean doesEmailExist(DbSession dbSession, String email){
+    return mapper(dbSession).countByEmail(email.toLowerCase()) > 0;
+  }
+
   protected UserMapper mapper(DbSession session) {
     return session.getMapper(UserMapper.class);
   }
index 605998588db9a5030f4dda2a2bf5634dd4ddee9f..8f461e4f673b8e44911109c553862337232155db 100644 (file)
@@ -57,6 +57,8 @@ public interface UserMapper {
   @CheckForNull
   GroupDto selectGroupByName(String name);
 
+  long countByEmail(String email);
+
   void insert(UserDto userDto);
 
   void update(UserDto userDto);
index a4d3b6364480367eb1e4e7950166544e03aa11ab..066036e250c8c07435c9dea4757228698f9357de 100644 (file)
     </where>
   </select>
 
+  <select id="countByEmail" parameterType="String" resultType="long">
+    SELECT count(u.id)
+    FROM users u
+    where lower(u.email)=#{email}
+  </select>
+
   <select id="selectGroupByName" parameterType="string" resultType="Group">
     SELECT id, name, description, created_at AS "createdAt", updated_at AS "updatedAt"
     FROM groups WHERE name=#{id}
index 42b14013f4c559805302b77f980e1d4f98fc0fe0..57294908f9bc7938a934f58000a2b65a266ccf76 100644 (file)
@@ -342,4 +342,13 @@ public class UserDaoTest {
     thrown.expectMessage("User with identity provider 'unknown' and id 'unknown' has not been found");
     underTest.selectOrFailByExternalIdentity(session, "unknown", "unknown");
   }
+
+  @Test
+  public void exists_by_email() throws Exception {
+    db.prepareDbUnit(getClass(), "exists_by_email.xml");
+
+    assertThat(underTest.doesEmailExist(session, "marius@lesbronzes.fr")).isTrue();
+    assertThat(underTest.doesEmailExist(session, "Marius@LesBronzes.fr")).isTrue();
+    assertThat(underTest.doesEmailExist(session, "unknown")).isFalse();
+  }
 }
diff --git a/sonar-db/src/test/resources/org/sonar/db/user/UserDaoTest/exists_by_email.xml b/sonar-db/src/test/resources/org/sonar/db/user/UserDaoTest/exists_by_email.xml
new file mode 100644 (file)
index 0000000..5a54ec2
--- /dev/null
@@ -0,0 +1,8 @@
+<dataset>
+
+  <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts="&#10;ma&#10;marius33&#10;" created_at="1418215735482" updated_at="1418215735485"
+         salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
+  <users id="102" login="sbrandhof" name="Simon Brandhof" email="marius@lesbronzes.fr" active="[true]" scm_accounts="[null]" created_at="1418215735482" updated_at="1418215735485"
+         salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8366" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fh"/>
+
+</dataset>
index 6f17ee506fd54e1bbfca50a4cc3c6863c4f701a5..1d5c2ce47c62b586e8c7df11181f53c9d5ca2ec7 100644 (file)
@@ -59,7 +59,11 @@ public interface BaseIdentityProvider extends IdentityProvider {
      * The first time a user is authenticated (and if {@link #allowsUsersToSignUp()} is true), a new user will be registered.
      * Then, only user's name and email are updated.
      *
-     * @throws NotAllowUserToSignUpException when {@link #allowsUsersToSignUp()} is false and a new user try to authenticate
+     * If @link #allowsUsersToSignUp()} is set to false and a new user try to authenticate,
+     * then the user is not authenticated and he's redirected to a dedicated page.
+     *
+     * If the email of the user is already used by an existing user of the platform,
+     * then the user is not authenticated and he's redirected to a dedicated page.
      */
     void authenticate(UserIdentity userIdentity);
 
index 4409803ac107ace748c562a0864c0eb33c9bc007..b453e0b846fbc88b7e3cf20cd1ddc6031687ddfd 100644 (file)
@@ -87,7 +87,8 @@ public interface OAuth2IdentityProvider extends IdentityProvider {
     void redirectToRequestedPage();
 
     /**
-     * Authenticate and register the user into the platform
+     * Authenticate and register the user into the platform.
+     * @see org.sonar.api.server.authentication.BaseIdentityProvider.Context#authenticate(UserIdentity)
      */
     void authenticate(UserIdentity userIdentity);
   }