import org.sonar.api.config.Settings;
import org.sonar.api.server.authentication.BaseIdentityProvider;
import org.sonar.api.server.authentication.Display;
+import org.sonar.api.server.authentication.UnauthorizedException;
import org.sonar.api.server.authentication.UserIdentity;
public class FakeBaseIdProvider implements BaseIdentityProvider {
private static final String ALLOWS_USERS_TO_SIGN_UP = "sonar.auth.fake-base-id-provider.allowsUsersToSignUp";
private static final String USER_INFO = "sonar.auth.fake-base-id-provider.user";
+ private static final String THROW_UNAUTHORIZED_EXCEPTION = "sonar.auth.fake-base-id-provider.throwUnauthorizedMessage";
+
private final Settings settings;
public FakeBaseIdProvider(Settings settings) {
if (userInfoProperty == null) {
throw new IllegalStateException(String.format("The property %s is required", USER_INFO));
}
+ boolean throwUnauthorizedException = settings.getBoolean(THROW_UNAUTHORIZED_EXCEPTION);
+ if (throwUnauthorizedException) {
+ throw new UnauthorizedException("A functional error has happened");
+ }
+
String[] userInfos = userInfoProperty.split(",");
context.authenticate(UserIdentity.builder()
.setLogin(userInfos[0])
import com.sonar.orchestrator.Orchestrator;
import com.sonar.orchestrator.selenium.Selenese;
import it.Category4Suite;
+import java.io.File;
+import org.apache.commons.io.FileUtils;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.ClassRule;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.sonarqube.ws.client.GetRequest;
}
@Test
- @Ignore("Do not understand why it's failing...")
public void fail_to_authenticate_when_not_allowed_to_sign_up() throws Exception {
enablePlugin();
setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_NAME, USER_EMAIL);
// TODO Add Selenium test to check login form
}
+ @Test
+ public void display_message_in_ui_but_not_in_log_when_unauthorized_exception() throws Exception {
+ enablePlugin();
+ setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_NAME, USER_EMAIL);
+ setServerProperty(ORCHESTRATOR, "sonar.auth.fake-base-id-provider.throwUnauthorizedMessage", "true");
+
+ ORCHESTRATOR.executeSelenese(Selenese.builder().setHtmlTestsInClasspath("fail_to_authenticate_when_not_allowed_to_sign_up",
+ "/user/BaseIdentityProviderTest/fail_to_authenticate_when_not_allowed_to_sign_up.html"
+ ).build());
+
+ File logFile = ORCHESTRATOR.getServer().getLogs();
+ assertThat(FileUtils.readFileToString(logFile)).doesNotContain("A functional error has happened");
+ assertThat(FileUtils.readFileToString(logFile)).doesNotContain("UnauthorizedException");
+
+ userRule.verifyUserDoesNotExist(USER_LOGIN);
+ }
+
private static void setUserCreatedByAuthPlugin(String login, String providerId, String name, String email) {
setServerProperty(ORCHESTRATOR, "sonar.auth.fake-base-id-provider.user", login + "," + providerId + "," + name + "," + email);
}
--- /dev/null
+<?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/init/fake-base-id-provider</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 : A functional error has happened*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
+import org.sonar.api.server.authentication.UnauthorizedException;
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;
+import static org.sonar.api.server.authentication.UnauthorizedException.UNAUTHORIZED_PATH;
public class AuthenticationError {
private static final Logger LOGGER = Loggers.get(AuthenticationError.class);
- private static final String UNAUTHORIZED_PATH = "/sessions/unauthorized";
-
private AuthenticationError() {
// Utility class
}
redirectToUnauthorized(response);
}
- public static void handleNotAllowedToSignUpError(NotAllowUserToSignUpException e, HttpServletResponse response) {
- 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()));
+ public static void handleUnauthorizedError(UnauthorizedException e, HttpServletResponse response) {
+ redirectTo(response, e.getPath());
}
private static void redirectToUnauthorized(HttpServletResponse response) {
import org.sonar.api.server.authentication.BaseIdentityProvider;
import org.sonar.api.server.authentication.IdentityProvider;
import org.sonar.api.server.authentication.OAuth2IdentityProvider;
+import org.sonar.api.server.authentication.UnauthorizedException;
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;
+import static org.sonar.server.authentication.AuthenticationError.handleUnauthorizedError;
public class InitFilter extends ServletFilter {
} else {
throw new UnsupportedOperationException(format("Unsupported IdentityProvider class: %s ", provider.getClass()));
}
- } catch (NotAllowUserToSignUpException e) {
- handleNotAllowedToSignUpError(e, (HttpServletResponse) response);
- } catch (EmailAlreadyExistsException e) {
- handleEmailAlreadyExistsError(e, (HttpServletResponse) response);
+ } catch (UnauthorizedException e) {
+ handleUnauthorizedError(e, (HttpServletResponse) response);
} catch (Exception e) {
- handleError(e, (HttpServletResponse) response, String.format("Fail to initialize authentication with provider '%s'", keyProvider));
+ handleError(e, (HttpServletResponse) response, format("Fail to initialize authentication with provider '%s'", keyProvider));
}
}
import javax.servlet.http.HttpServletResponse;
import org.sonar.api.server.authentication.IdentityProvider;
import org.sonar.api.server.authentication.OAuth2IdentityProvider;
+import org.sonar.api.server.authentication.UnauthorizedException;
import org.sonar.api.web.ServletFilter;
-import static org.sonar.server.authentication.AuthenticationError.handleEmailAlreadyExistsError;
+import static java.lang.String.format;
import static org.sonar.server.authentication.AuthenticationError.handleError;
-import static org.sonar.server.authentication.AuthenticationError.handleNotAllowedToSignUpError;
+import static org.sonar.server.authentication.AuthenticationError.handleUnauthorizedError;
public class OAuth2CallbackFilter extends ServletFilter {
OAuth2IdentityProvider oauthProvider = (OAuth2IdentityProvider) provider;
oauthProvider.callback(oAuth2ContextFactory.newCallback(httpRequest, (HttpServletResponse) response, oauthProvider));
} else {
- handleError((HttpServletResponse) response, String.format("Not an OAuth2IdentityProvider: %s", provider.getClass()));
+ handleError((HttpServletResponse) response, format("Not an OAuth2IdentityProvider: %s", provider.getClass()));
}
- } catch (NotAllowUserToSignUpException e) {
- handleNotAllowedToSignUpError(e, (HttpServletResponse) response);
- } catch (EmailAlreadyExistsException e) {
- handleEmailAlreadyExistsError(e, (HttpServletResponse) response);
+ } catch (UnauthorizedException e) {
+ handleUnauthorizedError(e, (HttpServletResponse) response);
} catch (Exception e) {
- handleError(e, (HttpServletResponse) response, String.format("Fail to callback authentication with %s", keyProvider));
+ handleError(e, (HttpServletResponse) response, format("Fail to callback authentication with %s", keyProvider));
}
}
import javax.servlet.http.HttpSession;
import org.sonar.api.server.authentication.IdentityProvider;
+import org.sonar.api.server.authentication.UnauthorizedException;
import org.sonar.api.server.authentication.UserIdentity;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.server.user.UpdateUser;
import org.sonar.server.user.UserUpdater;
+import static java.lang.String.format;
+
public class UserIdentityAuthenticator {
private final DbClient dbClient;
}
if (!provider.allowsUsersToSignUp()) {
- throw new NotAllowUserToSignUpException(provider);
+ throw new UnauthorizedException(format("'%s' users are not allowed to sign up", provider.getKey()));
}
String email = user.getEmail();
if (email != null && dbClient.userDao().doesEmailExist(dbSession, email)) {
- throw new EmailAlreadyExistsException(email);
+ throw new UnauthorizedException(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));
}
userUpdater.create(dbSession, NewUser.create()
import org.sonar.api.server.authentication.Display;
import org.sonar.api.server.authentication.IdentityProvider;
import org.sonar.api.server.authentication.OAuth2IdentityProvider;
+import org.sonar.api.server.authentication.UnauthorizedException;
import org.sonar.api.utils.log.LogTester;
import org.sonar.api.utils.log.LoggerLevel;
}
@Test
- public void redirect_when_failing_because_of_NotAllowUserToSignUpException() throws Exception {
- IdentityProvider identityProvider = new FailWithNotAllowUserToSignUpIdProvider("failing");
+ public void redirect_when_failing_because_of_UnauthorizedExceptionException() throws Exception {
+ IdentityProvider identityProvider = new FailWithUnauthorizedExceptionIdProvider("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");
+ verify(response).sendRedirect("/sessions/unauthorized?message=Email+john%40email.com+is+already+used");
}
- @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(){
+ private void assertOAuth2InitCalled() {
assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty();
assertThat(oAuth2IdentityProvider.isInitCalled()).isTrue();
}
- private void assertBasicInitCalled(){
+ private void assertBasicInitCalled() {
assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty();
assertThat(baseIdentityProvider.isInitCalled()).isTrue();
}
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 {
+ private static class FailWithUnauthorizedExceptionIdProvider extends FakeBasicIdentityProvider {
- public FailWithEmailAlreadyExistsExceptionIdProvider(String key) {
+ public FailWithUnauthorizedExceptionIdProvider(String key) {
super(key, true);
}
@Override
public void init(Context context) {
- throw new EmailAlreadyExistsException("john@email.com");
+ throw new UnauthorizedException("Email john@email.com is already used");
}
}
}
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.api.server.authentication.OAuth2IdentityProvider;
+import org.sonar.api.server.authentication.UnauthorizedException;
import org.sonar.api.utils.log.LogTester;
import org.sonar.api.utils.log.LoggerLevel;
assertError("Fail to callback authentication with github");
}
+ @Test
+ public void redirect_when_failing_because_of_UnauthorizedExceptionException() throws Exception {
+ TestIdentityProvider identityProvider = new FailWithUnauthorizedExceptionIdProvider()
+ .setKey("failing")
+ .setEnabled(true);
+ when(request.getRequestURI()).thenReturn("/oauth2/callback/" + identityProvider.getKey());
+ identityProviderRepository.addIdentityProvider(identityProvider);
+
+ underTest.doFilter(request, response, chain);
+
+ verify(response).sendRedirect("/sessions/unauthorized?message=Email+john%40email.com+is+already+used");
+ }
+
private void assertCallbackCalled(){
assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty();
assertThat(oAuth2IdentityProvider.isCallbackCalled()).isTrue();
assertThat(oAuth2IdentityProvider.isInitCalled()).isFalse();
}
+ private static class FailWithUnauthorizedExceptionIdProvider extends TestIdentityProvider implements OAuth2IdentityProvider {
+
+ @Override
+ public void init(InitContext context) {
+
+ }
+
+ @Override
+ public void callback(CallbackContext context) {
+ throw new UnauthorizedException("Email john@email.com is already used");
+ }
+ }
+
}
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.ArgumentCaptor;
+import org.sonar.api.server.authentication.UnauthorizedException;
import org.sonar.api.server.authentication.UserIdentity;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
.setEnabled(true)
.setAllowsUsersToSignUp(false);
- thrown.expect(NotAllowUserToSignUpException.class);
+ thrown.expect(UnauthorizedException.class);
+ thrown.expectMessage("'github' users are not allowed to sign up");
underTest.authenticate(USER_IDENTITY, identityProvider, httpSession);
}
when(userDao.selectOrFailByLogin(dbSession, USER_IDENTITY.getLogin())).thenReturn(ACTIVE_USER);
when(userDao.doesEmailExist(dbSession, USER_IDENTITY.getEmail())).thenReturn(true);
- thrown.expect(EmailAlreadyExistsException.class);
+ thrown.expect(UnauthorizedException.class);
+ thrown.expectMessage("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.");
underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, httpSession);
}
}
end
end
- def unauthorized
- flash[:error] = session['error']
- session['error'] = nil
- params[:layout]='false'
- render :action => 'unauthorized'
- end
-
end
+++ /dev/null
-<table class="spaced">
- <tr>
- <td align="center">
- <div id="login_form">
- <p id="unauthorized">You can't sign up because email '<%= h params[:email] %>' is already used by an existing user. This means that you probably already registered with another account.</p>
- </div>
- <br/>
- <a class="spacer-left" href="<%= home_path -%>"><%= message('layout.home') -%></a>
- </td>
- </tr>
-</table>
+++ /dev/null
-<table class="spaced">
- <tr>
- <td align="center">
- <div id="login_form">
- <p id="unauthorized"><%= h params[:providerName] %> users are not allowed to sign up</p>
- </div>
- <br/>
- <a class="spacer-left" href="<%= home_path -%>"><%= message('layout.home') -%></a>
- </td>
- </tr>
-</table>
<table class="spaced">
<tr>
<td align="center">
-
- <% if flash[:error] %>
- <div class="error"><%= flash[:error] %></div>
- <% end %>
-
<div id="login_form">
<p id="unauthorized">You're not authorized to access this page. Please contact the administrator.</p>
</div>
<br/>
+ <% if params[:message] %>
+ <div id="message">Reason : <%= params[:message] %></div>
+ <br/>
+ <% end %>
<a class="spacer-left" href="<%= home_path -%>"><%= message('layout.home') -%></a>
</td>
</tr>
--- /dev/null
+/*
+ * 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.api.server.authentication;
+
+import com.google.common.base.Charsets;
+import java.io.UnsupportedEncodingException;
+
+import static java.lang.String.format;
+import static java.net.URLEncoder.encode;
+
+/**
+ * This exception should be used when a functional error is generated by an Identity Provider plugin.
+ * The user will be redirected to an unauthorized page and the exception's message will be displayed in the UI.
+ *
+ * @since 5.5
+ */
+public class UnauthorizedException extends RuntimeException {
+
+ public static final String UNAUTHORIZED_PATH = "/sessions/unauthorized";
+ private static final String UNAUTHORIZED_PATH_WITH_MESSAGE = UNAUTHORIZED_PATH + "?message=%s";
+
+ public UnauthorizedException(String message) {
+ super(message);
+ }
+
+ public UnauthorizedException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public String getPath() {
+ return format(UNAUTHORIZED_PATH_WITH_MESSAGE, encodeMessage(getMessage()));
+ }
+
+ private static String encodeMessage(String message) {
+ try {
+ return encode(message, Charsets.UTF_8.name());
+ } catch (UnsupportedEncodingException unsupportedException) {
+ throw new IllegalStateException(format("Fail to encode %s", message), unsupportedException);
+ }
+ }
+}