@@ -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); | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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)); | |||
} |
@@ -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) { |
@@ -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)); | |||
} |
@@ -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()) |
@@ -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"); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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> |
@@ -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"><%= h params[:providerName] %> users are not allowed to signup</p> | |||
</div> | |||
</td> | |||
</tr> |
@@ -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); | |||
} |
@@ -57,6 +57,8 @@ public interface UserMapper { | |||
@CheckForNull | |||
GroupDto selectGroupByName(String name); | |||
long countByEmail(String email); | |||
void insert(UserDto userDto); | |||
void update(UserDto userDto); |
@@ -98,6 +98,12 @@ | |||
</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} |
@@ -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(); | |||
} | |||
} |
@@ -0,0 +1,8 @@ | |||
<dataset> | |||
<users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts=" ma marius33 " 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> |
@@ -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); | |||
@@ -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); | |||
} |