]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7444 Provide an UnauthorizedException to display functional error
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Mon, 14 Mar 2016 10:54:57 +0000 (11:54 +0100)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 16 Mar 2016 10:06:14 +0000 (11:06 +0100)
15 files changed:
it/it-plugins/base-auth-plugin/src/main/java/FakeBaseIdProvider.java
it/it-tests/src/test/java/it/user/BaseIdentityProviderTest.java
it/it-tests/src/test/resources/user/BaseIdentityProviderTest/diplay_message_in_ui_but_not_in_log_when_unauthorized_exception.html [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationError.java
server/sonar-server/src/main/java/org/sonar/server/authentication/InitFilter.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/OAuth2CallbackFilterTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/sessions_controller.rb
server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/email_already_exists.erb [deleted file]
server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/not_allowed_to_sign_up.html.erb [deleted file]
server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/unauthorized.html.erb
sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/UnauthorizedException.java [new file with mode: 0644]

index 730bef971e142c831e970792e05ae24228e31c5c..a7d897b137fbe67f91eeaeeb4bba922dec63c452 100644 (file)
@@ -21,6 +21,7 @@ import java.io.IOException;
 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 {
@@ -29,6 +30,8 @@ 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) {
@@ -41,6 +44,11 @@ public class FakeBaseIdProvider implements BaseIdentityProvider {
     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])
index 242d78caf7de80d0a62c73a3afa8151f006e366c..eda3d581f1219d9011dabff3b34c9507663d3d90 100644 (file)
@@ -23,10 +23,11 @@ import com.google.common.base.Optional;
 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;
@@ -119,7 +120,6 @@ public class BaseIdentityProviderTest {
   }
 
   @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);
@@ -182,6 +182,23 @@ public class BaseIdentityProviderTest {
     // 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);
   }
diff --git a/it/it-tests/src/test/resources/user/BaseIdentityProviderTest/diplay_message_in_ui_but_not_in_log_when_unauthorized_exception.html b/it/it-tests/src/test/resources/user/BaseIdentityProviderTest/diplay_message_in_ui_but_not_in_log_when_unauthorized_exception.html
new file mode 100644 (file)
index 0000000..4d06368
--- /dev/null
@@ -0,0 +1,29 @@
+<?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>
index c6628cdb055cc1f4fb7c9576d8e141fda6250fad..5376261766cded19d00942be4405e0639ca13e07 100644 (file)
@@ -21,19 +21,17 @@ package org.sonar.server.authentication;
 
 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
   }
@@ -48,12 +46,8 @@ public class AuthenticationError {
     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) {
index 5111f8d3822a9f080891a2d63fac9c4c377df883..dd09ebe9d7bc92b0ac8ea2690fe7f0c9ba78b414 100644 (file)
@@ -30,13 +30,13 @@ import javax.servlet.http.HttpServletResponse;
 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 {
 
@@ -77,12 +77,10 @@ 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));
     }
   }
 
index 919a4302d763e83251a9abe3664d8ab2c76cd5fc..4701be2bc5a303ef20ac2775e74f767ea61d6491 100644 (file)
@@ -29,11 +29,12 @@ import javax.servlet.http.HttpServletRequest;
 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 {
 
@@ -64,14 +65,12 @@ 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));
     }
   }
 
index 9c3f11427e4b442d453b9a4a509b9cfdd36461fb..643daf690910063fe90c82ee1b92b6e053f20d4c 100644 (file)
@@ -21,6 +21,7 @@ package org.sonar.server.authentication;
 
 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;
@@ -30,6 +31,8 @@ import org.sonar.server.user.NewUser;
 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;
@@ -62,12 +65,13 @@ public class UserIdentityAuthenticator {
       }
 
       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()
index 65cb212bbeb9ec6ffe913cf8b2f99cb6b8b98c90..532d45430c09ff0865bb9c592f4ec2bd77ba4cee 100644 (file)
@@ -30,6 +30,7 @@ import org.sonar.api.server.authentication.BaseIdentityProvider;
 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;
 
@@ -144,33 +145,22 @@ public class InitFilterTest {
   }
 
   @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();
   }
@@ -181,32 +171,15 @@ public class InitFilterTest {
     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");
     }
   }
 }
index e8e6c76591668634950386d22a0dde5dd2131509..04d58e6928e21d309efebff9d49a73c9ba1b0246 100644 (file)
@@ -27,6 +27,7 @@ import org.junit.Rule;
 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;
 
@@ -100,6 +101,19 @@ public class OAuth2CallbackFilterTest {
     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();
@@ -111,4 +125,17 @@ public class OAuth2CallbackFilterTest {
     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");
+    }
+  }
+
 }
index 6b32b960b162e7c298661de20ff7f8b3f0fd3d7f..d0cfc2f72c1e39d2210b7b48144e03e0b223dfe2 100644 (file)
@@ -25,6 +25,7 @@ import org.junit.Rule;
 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;
@@ -144,7 +145,8 @@ public class UserIdentityAuthenticatorTest {
       .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);
   }
 
@@ -154,7 +156,8 @@ public class UserIdentityAuthenticatorTest {
     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);
   }
 }
index 535c53b7a4d6ea525af038f840fd66017d742f5c..5385d577fb1e20924ba7d4ce116bcb370d6f0536 100644 (file)
@@ -70,11 +70,4 @@ class SessionsController < ApplicationController
     end
   end
 
-  def unauthorized
-    flash[:error] = session['error']
-    session['error'] = nil
-    params[:layout]='false'
-    render :action => 'unauthorized'
-  end
-
 end
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
deleted file mode 100644 (file)
index 559e0ec..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<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>
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/not_allowed_to_sign_up.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/not_allowed_to_sign_up.html.erb
deleted file mode 100644 (file)
index 2208ad0..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<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>
index a7fe781a8297182b8548b1c64cd0068d20082f10..dee79e3add7b3b793edb17639869051817a8633c 100644 (file)
@@ -1,15 +1,14 @@
 <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>
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/UnauthorizedException.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/UnauthorizedException.java
new file mode 100644 (file)
index 0000000..a2713e5
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * 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);
+    }
+  }
+}