From a2c1cc8cd4a407aba13d733330fb5308a1f5168e Mon Sep 17 00:00:00 2001 From: Julien Lancelot Date: Wed, 22 Jun 2016 17:04:00 +0200 Subject: [PATCH] SONAR-7796 Rewrite /api/authentication/validate in Java --- .../authentication/AuthenticationModule.java | 4 +- .../UserSessionInitializer.java | 3 +- .../authentication/ws/AuthenticationWs.java | 2 +- .../authentication/ws/ValidateAction.java | 101 ++++++++++++++ .../UserSessionInitializerTest.java | 1 + .../ws/AuthenticationWsTest.java | 3 +- .../authentication/ws/ValidateActionTest.java | 132 ++++++++++++++++++ .../api/authentication_controller.rb | 70 ---------- 8 files changed, 241 insertions(+), 75 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/authentication/ws/ValidateAction.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/authentication/ws/ValidateActionTest.java delete mode 100644 server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/authentication_controller.rb diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java index db451df6810..9ca0e666a2a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java @@ -22,6 +22,7 @@ package org.sonar.server.authentication; import org.sonar.core.platform.Module; import org.sonar.server.authentication.ws.AuthenticationWs; import org.sonar.server.authentication.ws.LoginAction; +import org.sonar.server.authentication.ws.ValidateAction; public class AuthenticationModule extends Module { @Override @@ -42,6 +43,7 @@ public class AuthenticationModule extends Module { LoginAction.class, CredentialsAuthenticator.class, RealmAuthenticator.class, - BasicAuthenticator.class); + BasicAuthenticator.class, + ValidateAction.class); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java index 5aa8b8e2d3b..d25063ba7fd 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java @@ -25,6 +25,7 @@ import static org.sonar.api.CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY; import static org.sonar.api.web.ServletFilter.UrlPattern; import static org.sonar.api.web.ServletFilter.UrlPattern.Builder.staticResourcePatterns; import static org.sonar.server.authentication.ws.LoginAction.AUTH_LOGIN_URL; +import static org.sonar.server.authentication.ws.ValidateAction.AUTH_VALIDATE_URL; import static org.sonar.server.user.ServerUserSession.createForAnonymous; import static org.sonar.server.user.ServerUserSession.createForUser; @@ -51,7 +52,7 @@ public class UserSessionInitializer { "/sessions/*", "/api/system/db_migration_status", "/api/system/status", "/api/system/migrate_db", "/api/server/*", - AUTH_LOGIN_URL); + AUTH_LOGIN_URL, AUTH_VALIDATE_URL); private static final UrlPattern URL_PATTERN = UrlPattern.builder() .includes("/*") diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/AuthenticationWs.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/AuthenticationWs.java index f35a25e1b0d..e55a96f5dc7 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/AuthenticationWs.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/AuthenticationWs.java @@ -41,7 +41,7 @@ public class AuthenticationWs implements WebService { NewAction action = controller.createAction("validate") .setDescription("Check credentials.") .setSince("3.3") - .setHandler(RailsHandler.INSTANCE) + .setHandler(ServletFilterHandler.INSTANCE) .setResponseExample(Resources.getResource(this.getClass(), "example-validate.json")); RailsHandler.addFormatParam(action); diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/ValidateAction.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/ValidateAction.java new file mode 100644 index 00000000000..825b245ce91 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/ValidateAction.java @@ -0,0 +1,101 @@ +/* + * 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.ws; + +import static org.sonar.api.CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY; + +import java.io.IOException; +import java.util.Optional; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.sonar.api.config.Settings; +import org.sonar.api.utils.text.JsonWriter; +import org.sonar.api.web.ServletFilter; +import org.sonar.db.user.UserDto; +import org.sonar.server.authentication.BasicAuthenticator; +import org.sonar.server.authentication.JwtHttpHandler; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonarqube.ws.MediaTypes; + +public class ValidateAction extends ServletFilter { + + public static final String AUTH_VALIDATE_URL = "/api/authentication/validate"; + + private final Settings settings; + private final JwtHttpHandler jwtHttpHandler; + private final BasicAuthenticator basicAuthenticator; + + public ValidateAction(Settings settings, BasicAuthenticator basicAuthenticator, JwtHttpHandler jwtHttpHandler) { + this.settings = settings; + this.basicAuthenticator = basicAuthenticator; + this.jwtHttpHandler = jwtHttpHandler; + } + + @Override + public UrlPattern doGetPattern() { + return UrlPattern.create(AUTH_VALIDATE_URL); + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + boolean isAuthenticated = authenticate(request, response); + response.setContentType(MediaTypes.JSON); + + JsonWriter jsonWriter = JsonWriter.of(response.getWriter()); + jsonWriter.beginObject(); + jsonWriter.prop("valid", isAuthenticated); + jsonWriter.endObject(); + } + + private boolean authenticate(HttpServletRequest request, HttpServletResponse response) { + try { + Optional user = jwtHttpHandler.validateToken(request, response); + if (user.isPresent()) { + return true; + } + user = basicAuthenticator.authenticate(request); + if (user.isPresent()) { + return true; + } + return !settings.getBoolean(CORE_FORCE_AUTHENTICATION_PROPERTY); + } catch (UnauthorizedException e) { + return false; + } + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // Nothing to do + } + + @Override + public void destroy() { + // Nothing to do + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java index d78b73df527..6ea3fcd2007 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java @@ -86,6 +86,7 @@ public class UserSessionInitializerTest { assertPathIsNotIgnored("/foo"); assertPathIsIgnored("/api/authentication/login"); + assertPathIsIgnored("/api/authentication/validate"); assertPathIsIgnored("/batch/index"); assertPathIsIgnored("/batch/file"); assertPathIsIgnored("/maintenance/index"); diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/AuthenticationWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/AuthenticationWsTest.java index 728a1e61975..6fabde5e670 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/AuthenticationWsTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/AuthenticationWsTest.java @@ -22,7 +22,6 @@ package org.sonar.server.authentication.ws; import static org.assertj.core.api.Assertions.assertThat; import org.junit.Test; -import org.sonar.api.server.ws.RailsHandler; import org.sonar.api.server.ws.WebService; import org.sonar.server.ws.ServletFilterHandler; import org.sonar.server.ws.WsTester; @@ -40,7 +39,7 @@ public class AuthenticationWsTest { WebService.Action validate = controller.action("validate"); assertThat(validate).isNotNull(); - assertThat(validate.handler()).isInstanceOf(RailsHandler.class); + assertThat(validate.handler()).isInstanceOf(ServletFilterHandler.class); assertThat(validate.responseExampleAsString()).isNotEmpty(); assertThat(validate.params()).hasSize(1); diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/ValidateActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/ValidateActionTest.java new file mode 100644 index 00000000000..cefd822ab26 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/ValidateActionTest.java @@ -0,0 +1,132 @@ +/* + * 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.ws; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.sonar.db.user.UserTesting.newUserDto; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Optional; +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.config.Settings; +import org.sonar.server.authentication.BasicAuthenticator; +import org.sonar.server.authentication.JwtHttpHandler; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.test.JsonAssert; +import org.sonarqube.ws.MediaTypes; + +public class ValidateActionTest { + + StringWriter stringWriter = new StringWriter(); + + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain chain = mock(FilterChain.class); + + BasicAuthenticator basicAuthenticator = mock(BasicAuthenticator.class); + JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class); + + Settings settings = new Settings(); + + ValidateAction underTest = new ValidateAction(settings, basicAuthenticator, jwtHttpHandler); + + @Before + public void setUp() throws Exception { + PrintWriter writer = new PrintWriter(stringWriter); + when(response.getWriter()).thenReturn(writer); + } + + @Test + public void return_true_when_jwt_token_is_set() throws Exception { + when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.of(newUserDto())); + when(basicAuthenticator.authenticate(request)).thenReturn(Optional.empty()); + + underTest.doFilter(request, response, chain); + + verify(response).setContentType(MediaTypes.JSON); + JsonAssert.assertJson(stringWriter.toString()).isSimilarTo("{\"valid\":true}"); + } + + @Test + public void return_true_when_basic_auth() throws Exception { + when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.empty()); + when(basicAuthenticator.authenticate(request)).thenReturn(Optional.of(newUserDto())); + + underTest.doFilter(request, response, chain); + + verify(response).setContentType(MediaTypes.JSON); + JsonAssert.assertJson(stringWriter.toString()).isSimilarTo("{\"valid\":true}"); + } + + @Test + public void return_true_when_no_jwt_nor_basic_auth_and_no_force_authentication() throws Exception { + settings.setProperty("sonar.forceAuthentication", "false"); + when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.empty()); + when(basicAuthenticator.authenticate(request)).thenReturn(Optional.empty()); + + underTest.doFilter(request, response, chain); + + verify(response).setContentType(MediaTypes.JSON); + JsonAssert.assertJson(stringWriter.toString()).isSimilarTo("{\"valid\":true}"); + } + + @Test + public void return_false_when_no_jwt_nor_basic_auth_and_force_authentication_is_true() throws Exception { + settings.setProperty("sonar.forceAuthentication", "true"); + when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.empty()); + when(basicAuthenticator.authenticate(request)).thenReturn(Optional.empty()); + + underTest.doFilter(request, response, chain); + + verify(response).setContentType(MediaTypes.JSON); + JsonAssert.assertJson(stringWriter.toString()).isSimilarTo("{\"valid\":false}"); + } + + @Test + public void return_false_when_jwt_throws_unauthorized_exception() throws Exception { + doThrow(UnauthorizedException.class).when(jwtHttpHandler).validateToken(request, response); + when(basicAuthenticator.authenticate(request)).thenReturn(Optional.empty()); + + underTest.doFilter(request, response, chain); + + verify(response).setContentType(MediaTypes.JSON); + JsonAssert.assertJson(stringWriter.toString()).isSimilarTo("{\"valid\":false}"); + } + + @Test + public void return_false_when_basic_authenticator_throws_unauthorized_exception() throws Exception { + when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.empty()); + doThrow(UnauthorizedException.class).when(basicAuthenticator).authenticate(request); + + underTest.doFilter(request, response, chain); + + verify(response).setContentType(MediaTypes.JSON); + JsonAssert.assertJson(stringWriter.toString()).isSimilarTo("{\"valid\":false}"); + } +} diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/authentication_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/authentication_controller.rb deleted file mode 100644 index a55e5363ac9..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/authentication_controller.rb +++ /dev/null @@ -1,70 +0,0 @@ -# -# SonarQube, open source software quality management tool. -# Copyright (C) 2008-2014 SonarSource -# mailto:contact AT sonarsource DOT com -# -# SonarQube 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. -# -# SonarQube 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. -# -class Api::AuthenticationController < Api::ApiController - skip_before_filter :check_authentication, :set_user_session - - # prevent HTTP proxies from caching authentication status - before_filter :set_cache_buster - - # - # GET /api/authentication/validate - # curl http://localhost:9000/api/authentication/validate -v -u admin:admin - # - # Since v.3.3 - def validate - hash={:valid => valid?} - - # make sure no authentication information is left by - # this validation - reset_session - - respond_to do |format| - format.json { render :json => jsonp(hash) } - format.xml { render :xml => hash.to_xml(:skip_types => true, :root => 'authentication') } - format.text { render :text => text_not_supported } - end - end - - private - - def valid? - begin - logged_in? || (!force_authentication? && anonymous?) - rescue Errors::AccessDenied - false - end - end - - def force_authentication? - property = Property.by_key(org.sonar.api.CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY) - property ? property.value == 'true' : false - end - - def anonymous? - current_user.nil? - end - - def set_cache_buster - response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate" - response.headers["Pragma"] = "no-cache" - response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" - end - -end -- 2.39.5