]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7796 Rewrite /api/authentication/validate in Java
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 22 Jun 2016 15:04:00 +0000 (17:04 +0200)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 29 Jun 2016 06:41:53 +0000 (08:41 +0200)
server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java
server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java
server/sonar-server/src/main/java/org/sonar/server/authentication/ws/AuthenticationWs.java
server/sonar-server/src/main/java/org/sonar/server/authentication/ws/ValidateAction.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/ws/AuthenticationWsTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/ws/ValidateActionTest.java [new file with mode: 0644]
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/authentication_controller.rb [deleted file]

index db451df6810cdd85bb19fc70a86b57067139263c..9ca0e666a2ab2fe62e2a49abce3b8ec8c24583f7 100644 (file)
@@ -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);
   }
 }
index 5aa8b8e2d3b7b362e5ec6c47d843ca9dbc5e6cad..d25063ba7fde1d87927474f228f43d2fa035c595 100644 (file)
@@ -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("/*")
index f35a25e1b0dfa5762fd73261bd59b5ee2056062e..e55a96f5dc75b070bfac55405b59a1706f6cdbe7 100644 (file)
@@ -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 (file)
index 0000000..825b245
--- /dev/null
@@ -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<UserDto> 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
+  }
+}
index d78b73df527aea08f00cf33792f72e542f406bc3..6ea3fcd20071f3e6f59bf264027232d9686f395f 100644 (file)
@@ -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");
index 728a1e61975667f55bd7373d7feccebf3927452b..6fabde5e6702bb35ea96ec11222f720be3ee7dd6 100644 (file)
@@ -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 (file)
index 0000000..cefd822
--- /dev/null
@@ -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 (file)
index a55e536..0000000
+++ /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