]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7763 Allow authentication using basic HTTP authentication in Java
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Tue, 21 Jun 2016 12:03:15 +0000 (14:03 +0200)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 29 Jun 2016 06:39:32 +0000 (08:39 +0200)
22 files changed:
server/sonar-server/src/main/java/org/sonar/server/authentication/AuthLoginAction.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java
server/sonar-server/src/main/java/org/sonar/server/authentication/BaseContextFactory.java
server/sonar-server/src/main/java/org/sonar/server/authentication/BasicAuthenticator.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java
server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java
server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/ValidateJwtTokenFilter.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/authentication/ws/LoginAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/user/UserSessionFilter.java
server/sonar-server/src/main/java/org/sonar/server/usertoken/UserTokenAuthenticator.java
server/sonar-server/src/test/java/org/sonar/server/authentication/AuthLoginActionTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/authentication/BaseContextFactoryTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/BasicAuthenticatorTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2ContextFactoryTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/authentication/ValidateJwtTokenFilterTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/authentication/ws/LoginActionTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/user/UserSessionFilterTest.java
server/sonar-server/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticatorTest.java
server/sonar-web/src/main/webapp/WEB-INF/lib/authenticated_system.rb

diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthLoginAction.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthLoginAction.java
deleted file mode 100644 (file)
index 770cd63..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * 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;
-
-import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
-import static org.elasticsearch.common.Strings.isNullOrEmpty;
-
-import java.io.IOException;
-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.web.ServletFilter;
-import org.sonar.db.user.UserDto;
-import org.sonar.server.exceptions.UnauthorizedException;
-
-public class AuthLoginAction extends ServletFilter {
-
-  static final String AUTH_LOGIN_URL = "/api/authentication/login";
-
-  private static final String POST = "POST";
-
-  private final CredentialsAuthenticator credentialsAuthenticator;
-  private final JwtHttpHandler jwtHttpHandler;
-
-  public AuthLoginAction(CredentialsAuthenticator credentialsAuthenticator, JwtHttpHandler jwtHttpHandler) {
-    this.credentialsAuthenticator = credentialsAuthenticator;
-    this.jwtHttpHandler = jwtHttpHandler;
-  }
-
-  @Override
-  public UrlPattern doGetPattern() {
-    return UrlPattern.create(AUTH_LOGIN_URL);
-  }
-
-  @Override
-  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
-    HttpServletRequest request = (HttpServletRequest) servletRequest;
-    HttpServletResponse response = (HttpServletResponse) servletResponse;
-
-    if (!request.getMethod().equals(POST)) {
-      response.setStatus(HTTP_BAD_REQUEST);
-      return;
-    }
-    try {
-      UserDto userDto = authenticate(request);
-      jwtHttpHandler.generateToken(userDto, response);
-      // TODO add chain.doFilter when Rack filter will not be executed after this filter (or use a Servlet)
-    } catch (UnauthorizedException e) {
-      response.setStatus(e.httpCode());
-    }
-  }
-
-  private UserDto authenticate(HttpServletRequest request) {
-    String login = request.getParameter("login");
-    String password = request.getParameter("password");
-    if (isNullOrEmpty(login) || isNullOrEmpty(password)) {
-      throw new UnauthorizedException();
-    }
-    return credentialsAuthenticator.authenticate(login, password, request);
-  }
-
-  @Override
-  public void init(FilterConfig filterConfig) throws ServletException {
-    // Nothing to do
-  }
-
-  @Override
-  public void destroy() {
-    // Nothing to do
-  }
-}
index a55033f0e7eeacec2ec70b1703e9ffed4e1982bc..db451df6810cdd85bb19fc70a86b57067139263c 100644 (file)
@@ -21,6 +21,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;
 
 public class AuthenticationModule extends Module {
   @Override
@@ -34,12 +35,13 @@ public class AuthenticationModule extends Module {
       OAuth2ContextFactory.class,
       UserIdentityAuthenticator.class,
       OAuthCsrfVerifier.class,
-      ValidateJwtTokenFilter.class,
+      UserSessionInitializer.class,
       JwtSerializer.class,
       JwtHttpHandler.class,
       JwtCsrfVerifier.class,
-      AuthLoginAction.class,
+      LoginAction.class,
       CredentialsAuthenticator.class,
-      RealmAuthenticator.class);
+      RealmAuthenticator.class,
+      BasicAuthenticator.class);
   }
 }
index ea7df897a90b89ae05d6b132d2b50e7fe8873ada..e62b3cb28ae24d1954b61e89ec4f778dfa63e475 100644 (file)
@@ -24,18 +24,26 @@ import javax.servlet.http.HttpServletResponse;
 import org.sonar.api.platform.Server;
 import org.sonar.api.server.authentication.BaseIdentityProvider;
 import org.sonar.api.server.authentication.UserIdentity;
+import org.sonar.db.DbClient;
 import org.sonar.db.user.UserDto;
+import org.sonar.server.user.ServerUserSession;
+import org.sonar.server.user.ThreadLocalUserSession;
 
 public class BaseContextFactory {
 
+  private final DbClient dbClient;
+  private final ThreadLocalUserSession threadLocalUserSession;
   private final UserIdentityAuthenticator userIdentityAuthenticator;
   private final Server server;
   private final JwtHttpHandler jwtHttpHandler;
 
-  public BaseContextFactory(UserIdentityAuthenticator userIdentityAuthenticator, Server server, JwtHttpHandler jwtHttpHandler) {
+  public BaseContextFactory(DbClient dbClient, UserIdentityAuthenticator userIdentityAuthenticator, Server server, JwtHttpHandler jwtHttpHandler,
+    ThreadLocalUserSession threadLocalUserSession) {
+    this.dbClient = dbClient;
     this.userIdentityAuthenticator = userIdentityAuthenticator;
     this.server = server;
     this.jwtHttpHandler = jwtHttpHandler;
+    this.threadLocalUserSession = threadLocalUserSession;
   }
 
   public BaseIdentityProvider.Context newContext(HttpServletRequest request, HttpServletResponse response, BaseIdentityProvider identityProvider) {
@@ -72,6 +80,7 @@ public class BaseContextFactory {
     public void authenticate(UserIdentity userIdentity) {
       UserDto userDto = userIdentityAuthenticator.authenticate(userIdentity, identityProvider);
       jwtHttpHandler.generateToken(userDto, response);
+      threadLocalUserSession.set(ServerUserSession.createForUser(dbClient, userDto));
     }
   }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/BasicAuthenticator.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/BasicAuthenticator.java
new file mode 100644 (file)
index 0000000..3c000d9
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * 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;
+
+import static java.util.Locale.ENGLISH;
+import static org.elasticsearch.common.Strings.isEmpty;
+
+import com.google.common.base.Charsets;
+import java.util.Base64;
+import java.util.Optional;
+import javax.servlet.http.HttpServletRequest;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.usertoken.UserTokenAuthenticator;
+
+public class BasicAuthenticator {
+
+  private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder();
+
+  private static final String AUTHORIZATION_HEADER = "Authorization";
+  private static final String BASIC_AUTHORIZATION = "BASIC";
+
+  private final DbClient dbClient;
+  private final CredentialsAuthenticator credentialsAuthenticator;
+  private final UserTokenAuthenticator userTokenAuthenticator;
+
+  public BasicAuthenticator(DbClient dbClient, CredentialsAuthenticator credentialsAuthenticator,
+    UserTokenAuthenticator userTokenAuthenticator) {
+    this.dbClient = dbClient;
+    this.credentialsAuthenticator = credentialsAuthenticator;
+    this.userTokenAuthenticator = userTokenAuthenticator;
+  }
+
+  public Optional<UserDto> authenticate(HttpServletRequest request) {
+    String authorizationHeader = request.getHeader(AUTHORIZATION_HEADER);
+    if (authorizationHeader == null || !authorizationHeader.toUpperCase(ENGLISH).startsWith(BASIC_AUTHORIZATION)) {
+      return Optional.empty();
+    }
+
+    String[] credentials = getCredentials(authorizationHeader);
+    String login = credentials[0];
+    String password = credentials[1];
+    return Optional.of(authenticate(login, password, request));
+  }
+
+  private static String[] getCredentials(String authorizationHeader) {
+    String basicAuthEncoded = authorizationHeader.substring(6);
+    String basicAuthDecoded = new String(BASE64_DECODER.decode(basicAuthEncoded.getBytes(Charsets.UTF_8)), Charsets.UTF_8);
+
+    int semiColonPos = basicAuthDecoded.indexOf(':');
+    if (semiColonPos <= 0) {
+      throw new UnauthorizedException("Invalid credentials : " + basicAuthDecoded);
+    }
+    String login = basicAuthDecoded.substring(0, semiColonPos);
+    String password = basicAuthDecoded.substring(semiColonPos + 1);
+    return new String[] {login, password};
+  }
+
+  private UserDto authenticate(String login, String password, HttpServletRequest request) {
+    if (isEmpty(password)) {
+      return authenticateFromUserToken(login);
+    } else {
+      return credentialsAuthenticator.authenticate(login, password, request);
+    }
+  }
+
+  private UserDto authenticateFromUserToken(String token) {
+    Optional<String> authenticatedLogin = userTokenAuthenticator.authenticate(token);
+    if (!authenticatedLogin.isPresent()) {
+      throw new UnauthorizedException("Token doesn't exist");
+    }
+    DbSession dbSession = dbClient.openSession(false);
+    try {
+      UserDto userDto = dbClient.userDao().selectActiveUserByLogin(dbSession, authenticatedLogin.get());
+      if (userDto == null) {
+        throw new UnauthorizedException("User doesn't exist");
+      }
+      return userDto;
+    } finally {
+      dbClient.closeSession(dbSession);
+    }
+  }
+
+}
index d42349b5f6ed74efe1ace8e11149aeedd89de827..2e8e88e258f896263cbd9b93040512e3a5e82c1a 100644 (file)
@@ -23,7 +23,6 @@ package org.sonar.server.authentication;
 import static java.util.Objects.requireNonNull;
 import static org.elasticsearch.common.Strings.isNullOrEmpty;
 import static org.sonar.server.authentication.CookieUtils.findCookie;
-import static org.sonar.server.user.ServerUserSession.createForUser;
 
 import com.google.common.collect.ImmutableMap;
 import io.jsonwebtoken.Claims;
@@ -41,9 +40,6 @@ import org.sonar.api.utils.System2;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.user.UserDto;
-import org.sonar.server.exceptions.UnauthorizedException;
-import org.sonar.server.user.ServerUserSession;
-import org.sonar.server.user.ThreadLocalUserSession;
 
 @ServerSide
 public class JwtHttpHandler {
@@ -71,20 +67,17 @@ public class JwtHttpHandler {
   // This timeout is used to disconnect the user we he has not browse any page for a while
   private final int sessionTimeoutInSeconds;
   private final JwtCsrfVerifier jwtCsrfVerifier;
-  private final ThreadLocalUserSession threadLocalUserSession;
 
-  public JwtHttpHandler(System2 system2, DbClient dbClient, Server server, Settings settings, JwtSerializer jwtSerializer, JwtCsrfVerifier jwtCsrfVerifier,
-    ThreadLocalUserSession threadLocalUserSession) {
+  public JwtHttpHandler(System2 system2, DbClient dbClient, Server server, Settings settings, JwtSerializer jwtSerializer, JwtCsrfVerifier jwtCsrfVerifier) {
     this.jwtSerializer = jwtSerializer;
     this.server = server;
     this.dbClient = dbClient;
     this.system2 = system2;
     this.sessionTimeoutInSeconds = getSessionTimeoutInSeconds(settings);
     this.jwtCsrfVerifier = jwtCsrfVerifier;
-    this.threadLocalUserSession = threadLocalUserSession;
   }
 
-  void generateToken(UserDto user, HttpServletResponse response) {
+  public void generateToken(UserDto user, HttpServletResponse response) {
     String csrfState = jwtCsrfVerifier.generateState(response, sessionTimeoutInSeconds);
 
     String token = jwtSerializer.encode(new JwtSerializer.JwtSession(
@@ -94,22 +87,23 @@ public class JwtHttpHandler {
         LAST_REFRESH_TIME_PARAM, system2.now(),
         CSRF_JWT_PARAM, csrfState)));
     response.addCookie(createCookie(JWT_COOKIE, token, sessionTimeoutInSeconds));
-    threadLocalUserSession.set(createForUser(dbClient, user));
   }
 
-  void validateToken(HttpServletRequest request, HttpServletResponse response) {
-    validate(request, response);
-    if (!threadLocalUserSession.isLoggedIn()) {
-      threadLocalUserSession.set(ServerUserSession.createForAnonymous(dbClient));
+  public Optional<UserDto> validateToken(HttpServletRequest request, HttpServletResponse response) {
+    Optional<UserDto> userDto = validate(request, response);
+    if (userDto.isPresent()) {
+      return userDto;
     }
+    removeToken(response);
+    return Optional.empty();
   }
 
-  private void validate(HttpServletRequest request, HttpServletResponse response) {
+  private Optional<UserDto> validate(HttpServletRequest request, HttpServletResponse response) {
     Optional<String> token = getTokenFromCookie(request);
     if (!token.isPresent()) {
-      return;
+      return Optional.empty();
     }
-    validateToken(token.get(), request, response);
+    return validateToken(token.get(), request, response);
   }
 
   private static Optional<String> getTokenFromCookie(HttpServletRequest request) {
@@ -125,18 +119,16 @@ public class JwtHttpHandler {
     return Optional.of(token);
   }
 
-  private void validateToken(String tokenEncoded, HttpServletRequest request, HttpServletResponse response) {
+  private Optional<UserDto> validateToken(String tokenEncoded, HttpServletRequest request, HttpServletResponse response) {
     Optional<Claims> claims = jwtSerializer.decode(tokenEncoded);
     if (!claims.isPresent()) {
-      removeToken(response);
-      return;
+      return Optional.empty();
     }
 
     Date now = new Date(system2.now());
     Claims token = claims.get();
     if (now.after(DateUtils.addSeconds(token.getIssuedAt(), SESSION_DISCONNECT_IN_SECONDS))) {
-      removeToken(response);
-      return;
+      return Optional.empty();
     }
     jwtCsrfVerifier.verifyState(request, (String) token.get(CSRF_JWT_PARAM));
 
@@ -146,10 +138,9 @@ public class JwtHttpHandler {
 
     Optional<UserDto> user = selectUserFromDb(token.getSubject());
     if (!user.isPresent()) {
-      removeToken(response);
-      throw new UnauthorizedException("User does not exist");
+      return Optional.empty();
     }
-    threadLocalUserSession.set(createForUser(dbClient, user.get()));
+    return Optional.of(user.get());
   }
 
   private static Date getLastRefreshDate(Claims token) {
@@ -167,7 +158,6 @@ public class JwtHttpHandler {
   void removeToken(HttpServletResponse response) {
     response.addCookie(createCookie(JWT_COOKIE, null, 0));
     jwtCsrfVerifier.removeState(response);
-    threadLocalUserSession.remove();
   }
 
   private Cookie createCookie(String name, @Nullable String value, int expirationInSeconds) {
index 6f5948cc6161f799603e5690f24d19477871dca5..18cbc0d8808f924dc4bab67a01a42cde139669a1 100644 (file)
@@ -30,16 +30,24 @@ import org.sonar.api.platform.Server;
 import org.sonar.api.server.authentication.OAuth2IdentityProvider;
 import org.sonar.api.server.authentication.UserIdentity;
 import org.sonar.api.utils.MessageException;
+import org.sonar.db.DbClient;
 import org.sonar.db.user.UserDto;
+import org.sonar.server.user.ServerUserSession;
+import org.sonar.server.user.ThreadLocalUserSession;
 
 public class OAuth2ContextFactory {
 
+  private final DbClient dbClient;
+  private final ThreadLocalUserSession threadLocalUserSession;
   private final UserIdentityAuthenticator userIdentityAuthenticator;
   private final Server server;
   private final OAuthCsrfVerifier csrfVerifier;
   private final JwtHttpHandler jwtHttpHandler;
 
-  public OAuth2ContextFactory(UserIdentityAuthenticator userIdentityAuthenticator, Server server, OAuthCsrfVerifier csrfVerifier, JwtHttpHandler jwtHttpHandler) {
+  public OAuth2ContextFactory(DbClient dbClient, ThreadLocalUserSession threadLocalUserSession, UserIdentityAuthenticator userIdentityAuthenticator, Server server,
+    OAuthCsrfVerifier csrfVerifier, JwtHttpHandler jwtHttpHandler) {
+    this.dbClient = dbClient;
+    this.threadLocalUserSession = threadLocalUserSession;
     this.userIdentityAuthenticator = userIdentityAuthenticator;
     this.server = server;
     this.csrfVerifier = csrfVerifier;
@@ -117,6 +125,7 @@ public class OAuth2ContextFactory {
     public void authenticate(UserIdentity userIdentity) {
       UserDto userDto = userIdentityAuthenticator.authenticate(userIdentity, identityProvider);
       jwtHttpHandler.generateToken(userDto, response);
+      threadLocalUserSession.set(ServerUserSession.createForUser(dbClient, userDto));
     }
   }
 }
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
new file mode 100644 (file)
index 0000000..5aa8b8e
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * 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;
+
+import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
+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.user.ServerUserSession.createForAnonymous;
+import static org.sonar.server.user.ServerUserSession.createForUser;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Optional;
+import java.util.Set;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.sonar.api.config.Settings;
+import org.sonar.api.server.ServerSide;
+import org.sonar.db.DbClient;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.user.ThreadLocalUserSession;
+
+@ServerSide
+public class UserSessionInitializer {
+
+  // SONAR-6546 these urls should be get from WebService
+  private static final Set<String> SKIPPED_URLS = ImmutableSet.of(
+    "/batch/index", "/batch/file",
+    "/maintenance/*",
+    "/setup/*",
+    "/sessions/*",
+    "/api/system/db_migration_status", "/api/system/status", "/api/system/migrate_db",
+    "/api/server/*",
+    AUTH_LOGIN_URL);
+
+  private static final UrlPattern URL_PATTERN = UrlPattern.builder()
+    .includes("/*")
+    .excludes(staticResourcePatterns())
+    .excludes(SKIPPED_URLS)
+    .build();
+
+  private final DbClient dbClient;
+  private final Settings settings;
+  private final JwtHttpHandler jwtHttpHandler;
+  private final BasicAuthenticator basicAuthenticator;
+  private final ThreadLocalUserSession userSession;
+
+  public UserSessionInitializer(DbClient dbClient, Settings settings, JwtHttpHandler jwtHttpHandler, BasicAuthenticator basicAuthenticator,
+                                ThreadLocalUserSession userSession) {
+    this.dbClient = dbClient;
+    this.settings = settings;
+    this.jwtHttpHandler = jwtHttpHandler;
+    this.basicAuthenticator = basicAuthenticator;
+    this.userSession = userSession;
+  }
+
+  public boolean initUserSession(HttpServletRequest request, HttpServletResponse response) {
+    String path = request.getRequestURI().replaceFirst(request.getContextPath(), "");
+    try {
+      // Do not set user session when url is excluded
+      if (!URL_PATTERN.matches(path)) {
+        return true;
+      }
+      setUserSession(request, response);
+      return true;
+    } catch (UnauthorizedException e) {
+      jwtHttpHandler.removeToken(response);
+      response.setStatus(HTTP_UNAUTHORIZED);
+      if (isWsUrl(path)) {
+        return false;
+      }
+      // WS should stop here. Rails page should continue in order to deal with redirection
+      return true;
+    }
+  }
+
+  private void setUserSession(HttpServletRequest request, HttpServletResponse response) {
+    Optional<UserDto> user = authenticate(request, response);
+    if (user.isPresent()) {
+      userSession.set(createForUser(dbClient, user.get()));
+    } else {
+      if (settings.getBoolean(CORE_FORCE_AUTHENTICATION_PROPERTY)) {
+        throw new UnauthorizedException("User must be authenticated");
+      }
+      userSession.set(createForAnonymous(dbClient));
+    }
+  }
+
+  public void removeUserSession() {
+    userSession.remove();
+  }
+
+  // Try first to authenticate from JWT token, then try from basic http header
+  private Optional<UserDto> authenticate(HttpServletRequest request, HttpServletResponse response) {
+    Optional<UserDto> user = jwtHttpHandler.validateToken(request, response);
+    if (user.isPresent()) {
+      return user;
+    }
+    return basicAuthenticator.authenticate(request);
+  }
+
+  private static boolean isWsUrl(String path) {
+    return path.startsWith("/batch/") || path.startsWith("/api/");
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/ValidateJwtTokenFilter.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/ValidateJwtTokenFilter.java
deleted file mode 100644 (file)
index fd3878b..0000000
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * 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;
-
-import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
-import static org.sonar.api.CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY;
-import static org.sonar.api.web.ServletFilter.UrlPattern.Builder.staticResourcePatterns;
-import static org.sonar.server.authentication.AuthLoginAction.AUTH_LOGIN_URL;
-
-import com.google.common.collect.ImmutableSet;
-import java.io.IOException;
-import java.util.Set;
-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.server.ServerSide;
-import org.sonar.api.web.ServletFilter;
-import org.sonar.server.exceptions.UnauthorizedException;
-import org.sonar.server.user.UserSession;
-
-@ServerSide
-public class ValidateJwtTokenFilter extends ServletFilter {
-
-  // SONAR-6546 these urls should be get from WebService
-  private static final Set<String> SKIPPED_URLS = ImmutableSet.of(
-    "/batch/index", "/batch/file", "/batch_bootstrap/index",
-    "/maintenance/*",
-    "/setup/*",
-    "/sessions/*",
-    "/api/system/db_migration_status", "/api/system/status", "/api/system/migrate_db",
-    "/api/server/*",
-    AUTH_LOGIN_URL
-  );
-
-  private final Settings settings;
-  private final JwtHttpHandler jwtHttpHandler;
-  private final UserSession userSession;
-
-  public ValidateJwtTokenFilter(Settings settings, JwtHttpHandler jwtHttpHandler, UserSession userSession) {
-    this.settings = settings;
-    this.jwtHttpHandler = jwtHttpHandler;
-    this.userSession = userSession;
-  }
-
-  @Override
-  public UrlPattern doGetPattern() {
-    return UrlPattern.builder()
-      .includes("/*")
-      .excludes(staticResourcePatterns())
-      .excludes(SKIPPED_URLS)
-      .build();
-  }
-
-  @Override
-  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
-    HttpServletRequest request = (HttpServletRequest) servletRequest;
-    HttpServletResponse response = (HttpServletResponse) servletResponse;
-    String path = request.getRequestURI().replaceFirst(request.getContextPath(), "");
-
-    try {
-      if (isDeprecatedBatchWs(path)) {
-        chain.doFilter(request, response);
-        return;
-      }
-
-      jwtHttpHandler.validateToken(request, response);
-      // TODO handle basic authentication
-      if (!userSession.isLoggedIn() && settings.getBoolean(CORE_FORCE_AUTHENTICATION_PROPERTY)) {
-        throw new UnauthorizedException("User must be authenticated");
-      }
-      chain.doFilter(request, response);
-    } catch (UnauthorizedException e) {
-      jwtHttpHandler.removeToken(response);
-      response.setStatus(HTTP_UNAUTHORIZED);
-
-      if (isWsUrl(path)) {
-        return;
-      }
-      // WS should stop here. Rails page should continue in order to deal with redirection
-      chain.doFilter(request, response);
-    }
-  }
-
-  // Scanner is still using deprecated /batch/<File name>.jar WS
-  private static boolean isDeprecatedBatchWs(String path){
-    return path.startsWith("/batch/") && path.endsWith(".jar");
-  }
-
-  private static boolean isWsUrl(String path){
-    return path.startsWith("/batch/") || path.startsWith("/api/");
-  }
-
-  @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/main/java/org/sonar/server/authentication/ws/LoginAction.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/LoginAction.java
new file mode 100644 (file)
index 0000000..4407c90
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * 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 java.net.HttpURLConnection.HTTP_BAD_REQUEST;
+import static org.elasticsearch.common.Strings.isNullOrEmpty;
+
+import java.io.IOException;
+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.web.ServletFilter;
+import org.sonar.db.DbClient;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.authentication.CredentialsAuthenticator;
+import org.sonar.server.authentication.JwtHttpHandler;
+import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.user.ServerUserSession;
+import org.sonar.server.user.ThreadLocalUserSession;
+
+public class LoginAction extends ServletFilter {
+
+  public static final String AUTH_LOGIN_URL = "/api/authentication/login";
+
+  private static final String POST = "POST";
+
+  private final DbClient dbClient;
+  private final CredentialsAuthenticator credentialsAuthenticator;
+  private final JwtHttpHandler jwtHttpHandler;
+  private final ThreadLocalUserSession threadLocalUserSession;
+
+  public LoginAction(DbClient dbClient, CredentialsAuthenticator credentialsAuthenticator, JwtHttpHandler jwtHttpHandler, ThreadLocalUserSession threadLocalUserSession) {
+    this.dbClient = dbClient;
+    this.credentialsAuthenticator = credentialsAuthenticator;
+    this.jwtHttpHandler = jwtHttpHandler;
+    this.threadLocalUserSession = threadLocalUserSession;
+  }
+
+  @Override
+  public UrlPattern doGetPattern() {
+    return UrlPattern.create(AUTH_LOGIN_URL);
+  }
+
+  @Override
+  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
+    HttpServletRequest request = (HttpServletRequest) servletRequest;
+    HttpServletResponse response = (HttpServletResponse) servletResponse;
+
+    if (!request.getMethod().equals(POST)) {
+      response.setStatus(HTTP_BAD_REQUEST);
+      return;
+    }
+    try {
+      UserDto userDto = authenticate(request);
+      jwtHttpHandler.generateToken(userDto, response);
+      threadLocalUserSession.set(ServerUserSession.createForUser(dbClient, userDto));
+      // TODO add chain.doFilter when Rack filter will not be executed after this filter (or use a Servlet)
+    } catch (UnauthorizedException e) {
+      response.setStatus(e.httpCode());
+    }
+  }
+
+  private UserDto authenticate(HttpServletRequest request) {
+    String login = request.getParameter("login");
+    String password = request.getParameter("password");
+    if (isNullOrEmpty(login) || isNullOrEmpty(password)) {
+      throw new UnauthorizedException();
+    }
+    return credentialsAuthenticator.authenticate(login, password, request);
+  }
+
+  @Override
+  public void init(FilterConfig filterConfig) throws ServletException {
+    // Nothing to do
+  }
+
+  @Override
+  public void destroy() {
+    // Nothing to do
+  }
+}
index f5b7ccc4afca10f8fc5eaae0a2eecfde1f36e7dc..58c351e6ea2da5a08373e8665fbed17405e64252 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.server.user;
 
+import com.google.common.annotations.VisibleForTesting;
 import java.io.IOException;
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
@@ -26,44 +27,58 @@ import javax.servlet.FilterConfig;
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
-import org.sonar.api.utils.log.Loggers;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.sonar.server.authentication.UserSessionInitializer;
 import org.sonar.server.platform.Platform;
 
-/**
- * @since 3.6
- */
 public class UserSessionFilter implements Filter {
+
   private final Platform platform;
+  private UserSessionInitializer userSessionInitializer;
 
   public UserSessionFilter() {
     this.platform = Platform.getInstance();
   }
 
-  public UserSessionFilter(Platform platform) {
+  @VisibleForTesting
+  UserSessionFilter(Platform platform) {
     this.platform = platform;
   }
 
   @Override
-  public void init(FilterConfig filterConfig) throws ServletException {
-    // nothing to do
+  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
+    try {
+      HttpServletRequest request = (HttpServletRequest) servletRequest;
+      HttpServletResponse response = (HttpServletResponse) servletResponse;
+      init();
+      if (!isInitialized() || userSessionInitializer.initUserSession(request, response)) {
+        chain.doFilter(servletRequest, servletResponse);
+      }
+    } finally {
+      if (isInitialized()) {
+        userSessionInitializer.removeUserSession();
+      }
+    }
+  }
+
+  private boolean isInitialized() {
+    return userSessionInitializer != null;
+  }
+
+  private void init() {
+    if (userSessionInitializer == null) {
+      userSessionInitializer = platform.getContainer().getComponentByType(UserSessionInitializer.class);
+    }
   }
 
   @Override
-  public void destroy() {
+  public void init(FilterConfig filterConfig) throws ServletException {
     // nothing to do
   }
 
   @Override
-  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
-    try {
-      chain.doFilter(servletRequest, servletResponse);
-    } finally {
-      ThreadLocalUserSession userSession = platform.getContainer().getComponentByType(ThreadLocalUserSession.class);
-      if (userSession == null) {
-        Loggers.get(UserSessionFilter.class).error("Can not retrieve ThreadLocalUserSession from Platform");
-      } else {
-        userSession.remove();
-      }
-    }
+  public void destroy() {
+    // nothing to do
   }
 }
index fb26c6f5c784da3ad37b826832a546b465cec901..bb751b20ae9ff48856cc6f098aa8a0fb7d3a0622 100644 (file)
@@ -38,15 +38,15 @@ public class UserTokenAuthenticator {
    * The returned login is not validated. If database is corrupted (table USER_TOKENS badly purged
    * for instance), then the login may not relate to a valid user.
    */
-  public Optional<String> authenticate(String token) {
+  public java.util.Optional<String> authenticate(String token) {
     String tokenHash = tokenGenerator.hash(token);
     DbSession dbSession = dbClient.openSession(false);
     try {
       Optional<UserTokenDto> userToken = dbClient.userTokenDao().selectByTokenHash(dbSession, tokenHash);
       if (userToken.isPresent()) {
-        return Optional.of(userToken.get().getLogin());
+        return java.util.Optional.of(userToken.get().getLogin());
       }
-      return Optional.absent();
+      return java.util.Optional.empty();
     } finally {
       dbClient.closeSession(dbSession);
     }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/AuthLoginActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/AuthLoginActionTest.java
deleted file mode 100644 (file)
index cbd7f12..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * 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;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-
-import java.io.IOException;
-import javax.servlet.FilterChain;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import org.junit.Test;
-import org.sonar.db.user.UserDto;
-import org.sonar.db.user.UserTesting;
-import org.sonar.server.exceptions.UnauthorizedException;
-
-public class AuthLoginActionTest {
-
-  static final String LOGIN = "LOGIN";
-  static final String PASSWORD = "PASSWORD";
-
-  static final UserDto USER = UserTesting.newUserDto().setLogin(LOGIN);
-
-  HttpServletRequest request = mock(HttpServletRequest.class);
-  HttpServletResponse response = mock(HttpServletResponse.class);
-  FilterChain chain = mock(FilterChain.class);
-
-  CredentialsAuthenticator credentialsAuthenticator = mock(CredentialsAuthenticator.class);
-  JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class);
-
-  AuthLoginAction underTest  = new AuthLoginAction(credentialsAuthenticator, jwtHttpHandler);
-
-  @Test
-  public void do_get_pattern() throws Exception {
-    assertThat(underTest.doGetPattern().matches("/api/authentication/login")).isTrue();
-    assertThat(underTest.doGetPattern().matches("/api/authentication/logout")).isFalse();
-    assertThat(underTest.doGetPattern().matches("/foo")).isFalse();
-  }
-
-  @Test
-  public void do_authenticate() throws Exception {
-    when(credentialsAuthenticator.authenticate(LOGIN, PASSWORD, request)).thenReturn(USER);
-
-    executeRequest(LOGIN, PASSWORD);
-
-    verify(credentialsAuthenticator).authenticate(LOGIN, PASSWORD, request);
-    verify(jwtHttpHandler).generateToken(USER, response);
-    verifyZeroInteractions(chain);
-  }
-
-  @Test
-  public void ignore_get_request() throws Exception {
-    when(request.getMethod()).thenReturn("GET");
-
-    underTest.doFilter(request, response, chain);
-
-    verifyZeroInteractions(credentialsAuthenticator, jwtHttpHandler, chain);
-  }
-
-  @Test
-  public void return_authorized_code_when_unauthorized_exception_is_thrown() throws Exception {
-    doThrow(new UnauthorizedException()).when(credentialsAuthenticator).authenticate(LOGIN, PASSWORD, request);
-
-    executeRequest(LOGIN, PASSWORD);
-
-    verify(response).setStatus(401);
-  }
-
-  @Test
-  public void return_unauthorized_code_when_no_login() throws Exception {
-    executeRequest(null, PASSWORD);
-    verify(response).setStatus(401);
-  }
-
-  @Test
-  public void return_unauthorized_code_when_empty_login() throws Exception {
-    executeRequest("", PASSWORD);
-    verify(response).setStatus(401);
-  }
-
-  @Test
-  public void return_unauthorized_code_when_no_password() throws Exception {
-    executeRequest(LOGIN, null);
-    verify(response).setStatus(401);
-  }
-
-  @Test
-  public void return_unauthorized_code_when_empty_password() throws Exception {
-    executeRequest(LOGIN, "");
-    verify(response).setStatus(401);
-  }
-
-  private void executeRequest(String login, String password) throws IOException, ServletException {
-    when(request.getMethod()).thenReturn("POST");
-    when(request.getParameter("login")).thenReturn(login);
-    when(request.getParameter("password")).thenReturn(password);
-    underTest.doFilter(request, response, chain);
-  }
-}
index 8128b3ac55ead9b375e88810a630b1ebb3947310..cbe62a314723197d9cebdf8ed07006ae55beab33 100644 (file)
@@ -25,16 +25,24 @@ import static org.mockito.Matchers.eq;
 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 javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.sonar.api.platform.Server;
 import org.sonar.api.server.authentication.BaseIdentityProvider;
 import org.sonar.api.server.authentication.UserIdentity;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
 import org.sonar.db.user.UserDto;
+import org.sonar.server.user.ThreadLocalUserSession;
+import org.sonar.server.user.UserSession;
 
 public class BaseContextFactoryTest {
 
@@ -47,6 +55,15 @@ public class BaseContextFactoryTest {
     .setEmail("john@email.com")
     .build();
 
+  @Rule
+  public DbTester dbTester = DbTester.create(System2.INSTANCE);
+
+  DbClient dbClient = dbTester.getDbClient();
+
+  DbSession dbSession = dbTester.getSession();
+
+  ThreadLocalUserSession threadLocalUserSession = mock(ThreadLocalUserSession.class);
+
   UserIdentityAuthenticator userIdentityAuthenticator = mock(UserIdentityAuthenticator.class);
   Server server = mock(Server.class);
 
@@ -55,11 +72,15 @@ public class BaseContextFactoryTest {
   BaseIdentityProvider identityProvider = mock(BaseIdentityProvider.class);
   JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class);
 
-  BaseContextFactory underTest = new BaseContextFactory(userIdentityAuthenticator, server, jwtHttpHandler);
+  BaseContextFactory underTest = new BaseContextFactory(dbClient, userIdentityAuthenticator, server, jwtHttpHandler, threadLocalUserSession);
 
   @Before
   public void setUp() throws Exception {
     when(server.getPublicRootUrl()).thenReturn(PUBLIC_ROOT_URL);
+
+    UserDto userDto = dbClient.userDao().insert(dbSession, newUserDto());
+    dbSession.commit();
+    when(userIdentityAuthenticator.authenticate(USER_IDENTITY, identityProvider)).thenReturn(userDto);
   }
 
   @Test
@@ -80,5 +101,6 @@ public class BaseContextFactoryTest {
     context.authenticate(USER_IDENTITY);
     verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider);
     verify(jwtHttpHandler).generateToken(any(UserDto.class), eq(response));
+    verify(threadLocalUserSession).set(any(UserSession.class));
   }
 }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/BasicAuthenticatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/BasicAuthenticatorTest.java
new file mode 100644 (file)
index 0000000..692db7e
--- /dev/null
@@ -0,0 +1,162 @@
+/*
+ * 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;
+
+import static com.google.common.base.Charsets.UTF_8;
+import static org.assertj.core.api.Java6Assertions.assertThat;
+import static org.junit.rules.ExpectedException.none;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import java.util.Base64;
+import java.util.Optional;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserTesting;
+import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.usertoken.UserTokenAuthenticator;
+
+public class BasicAuthenticatorTest {
+
+  private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder();
+
+  static final String LOGIN = "login";
+  static final String PASSWORD = "password";
+  static final String CREDENTIALS_IN_BASE64 = toBase64(LOGIN + ":" + PASSWORD);
+
+  static final UserDto USER = UserTesting.newUserDto().setLogin(LOGIN);
+
+  @Rule
+  public ExpectedException expectedException = none();
+
+  @Rule
+  public DbTester dbTester = DbTester.create(System2.INSTANCE);
+
+  DbClient dbClient = dbTester.getDbClient();
+
+  DbSession dbSession = dbTester.getSession();
+
+  CredentialsAuthenticator credentialsAuthenticator = mock(CredentialsAuthenticator.class);
+  UserTokenAuthenticator userTokenAuthenticator = mock(UserTokenAuthenticator.class);
+
+  HttpServletRequest request = mock(HttpServletRequest.class);
+  HttpServletResponse response = mock(HttpServletResponse.class);
+
+  BasicAuthenticator underTest = new BasicAuthenticator(dbClient, credentialsAuthenticator, userTokenAuthenticator);
+
+  @Test
+  public void authenticate_from_basic_http_header() throws Exception {
+    when(request.getHeader("Authorization")).thenReturn("Basic " + CREDENTIALS_IN_BASE64);
+    when(credentialsAuthenticator.authenticate(LOGIN, PASSWORD, request)).thenReturn(USER);
+
+    underTest.authenticate(request);
+
+    verify(credentialsAuthenticator).authenticate(LOGIN, PASSWORD, request);
+  }
+
+  @Test
+  public void authenticate_from_basic_http_header_with_password_containing_semi_colon() throws Exception {
+    String password = "!ascii-only:-)@";
+    when(request.getHeader("Authorization")).thenReturn("Basic " + toBase64(LOGIN + ":" + password));
+    when(credentialsAuthenticator.authenticate(LOGIN, password, request)).thenReturn(USER);
+
+    underTest.authenticate(request);
+
+    verify(credentialsAuthenticator).authenticate(LOGIN, password, request);
+  }
+
+  @Test
+  public void does_not_authenticate_when_no_authorization_header() throws Exception {
+    underTest.authenticate(request);
+
+    verifyZeroInteractions(credentialsAuthenticator);
+  }
+
+  @Test
+  public void does_not_authenticate_when_authorization_header_is_not_BASIC() throws Exception {
+    when(request.getHeader("Authorization")).thenReturn("OTHER " + CREDENTIALS_IN_BASE64);
+
+    underTest.authenticate(request);
+
+    verifyZeroInteractions(credentialsAuthenticator);
+  }
+
+  @Test
+  public void fail_to_authenticate_when_no_login() throws Exception {
+    when(request.getHeader("Authorization")).thenReturn("Basic " + toBase64(":" + PASSWORD));
+
+    expectedException.expect(UnauthorizedException.class);
+    underTest.authenticate(request);
+  }
+
+  @Test
+  public void authenticate_from_user_token() throws Exception {
+    insertUser(UserTesting.newUserDto().setLogin(LOGIN));
+    when(userTokenAuthenticator.authenticate("token")).thenReturn(Optional.of(LOGIN));
+    when(request.getHeader("Authorization")).thenReturn("Basic " + toBase64("token:"));
+
+    Optional<UserDto> userDto = underTest.authenticate(request);
+
+    assertThat(userDto.isPresent()).isTrue();
+    assertThat(userDto.get().getLogin()).isEqualTo(LOGIN);
+  }
+
+  @Test
+  public void does_not_authenticate_from_user_token_when_token_is_invalid() throws Exception {
+    insertUser(UserTesting.newUserDto().setLogin(LOGIN));
+    when(userTokenAuthenticator.authenticate("token")).thenReturn(Optional.empty());
+    when(request.getHeader("Authorization")).thenReturn("Basic " + toBase64("token:"));
+
+    expectedException.expect(UnauthorizedException.class);
+    underTest.authenticate(request);
+  }
+
+  @Test
+  public void does_not_authenticate_from_user_token_when_token_does_not_match_active_user() throws Exception {
+    insertUser(UserTesting.newUserDto().setLogin(LOGIN));
+    when(userTokenAuthenticator.authenticate("token")).thenReturn(Optional.of("Unknown user"));
+    when(request.getHeader("Authorization")).thenReturn("Basic " + toBase64("token:"));
+
+    expectedException.expect(UnauthorizedException.class);
+    underTest.authenticate(request);
+  }
+
+  private UserDto insertUser(UserDto userDto){
+    dbClient.userDao().insert(dbSession, userDto);
+    dbSession.commit();
+    return userDto;
+  }
+
+  private static String toBase64(String text){
+    return new String(BASE64_ENCODER.encode(text.getBytes(UTF_8)));
+  }
+
+}
index 2f67f52021a53838568a7203e937cb28e1fb3cd5..e276fde60075b3d89a3698dc729b5d9dca5febb5 100644 (file)
@@ -54,9 +54,6 @@ import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
 import org.sonar.db.user.UserDto;
-import org.sonar.server.exceptions.UnauthorizedException;
-import org.sonar.server.user.ServerUserSession;
-import org.sonar.server.user.ThreadLocalUserSession;
 
 public class JwtHttpHandlerTest {
 
@@ -75,8 +72,6 @@ public class JwtHttpHandlerTest {
   @Rule
   public DbTester dbTester = DbTester.create(INSTANCE);
 
-  ThreadLocalUserSession threadLocalUserSession = new ThreadLocalUserSession();
-
   DbClient dbClient = dbTester.getDbClient();
 
   DbSession dbSession = dbTester.getSession();
@@ -96,11 +91,10 @@ public class JwtHttpHandlerTest {
 
   UserDto userDto = newUserDto().setLogin(USER_LOGIN);
 
-  JwtHttpHandler underTest = new JwtHttpHandler(system2, dbClient, server, settings, jwtSerializer, jwtCsrfVerifier, threadLocalUserSession);
+  JwtHttpHandler underTest = new JwtHttpHandler(system2, dbClient, server, settings, jwtSerializer, jwtCsrfVerifier);
 
   @Before
   public void setUp() throws Exception {
-    threadLocalUserSession.remove();
     when(system2.now()).thenReturn(NOW);
     when(server.isSecured()).thenReturn(true);
     when(request.getSession()).thenReturn(httpSession);
@@ -120,7 +114,6 @@ public class JwtHttpHandlerTest {
 
     verify(jwtSerializer).encode(jwtArgumentCaptor.capture());
     verifyToken(jwtArgumentCaptor.getValue(), 3 * 24 * 60 * 60, NOW);
-    assertThat(threadLocalUserSession.get().isLoggedIn()).isTrue();
   }
 
   @Test
@@ -139,7 +132,7 @@ public class JwtHttpHandlerTest {
     int sessionTimeoutInHours = 10;
     settings.setProperty("sonar.auth.sessionTimeoutInHours", sessionTimeoutInHours);
 
-    underTest = new JwtHttpHandler(system2, dbClient, server, settings, jwtSerializer, jwtCsrfVerifier, threadLocalUserSession);
+    underTest = new JwtHttpHandler(system2, dbClient, server, settings, jwtSerializer, jwtCsrfVerifier);
     underTest.generateToken(userDto, response);
 
     verify(jwtSerializer).encode(jwtArgumentCaptor.capture());
@@ -151,7 +144,7 @@ public class JwtHttpHandlerTest {
     int firstSessionTimeoutInHours = 10;
     settings.setProperty("sonar.auth.sessionTimeoutInHours", firstSessionTimeoutInHours);
 
-    underTest = new JwtHttpHandler(system2, dbClient, server, settings, jwtSerializer, jwtCsrfVerifier, threadLocalUserSession);
+    underTest = new JwtHttpHandler(system2, dbClient, server, settings, jwtSerializer, jwtCsrfVerifier);
     underTest.generateToken(userDto, response);
 
     // The property is updated, but it won't be taking into account
@@ -169,10 +162,9 @@ public class JwtHttpHandlerTest {
     Claims claims = createToken(USER_LOGIN, NOW);
     when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims));
 
-    underTest.validateToken(request, response);
+    assertThat(underTest.validateToken(request, response).isPresent()).isTrue();
 
     verify(jwtSerializer, never()).encode(any(JwtSerializer.JwtSession.class));
-    assertThat(threadLocalUserSession.get().isLoggedIn()).isTrue();
   }
 
   @Test
@@ -184,10 +176,9 @@ public class JwtHttpHandlerTest {
     claims.put("lastRefreshTime", SIX_MINUTES_AGO);
     when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims));
 
-    underTest.validateToken(request, response);
+    assertThat(underTest.validateToken(request, response).isPresent()).isTrue();
 
     verify(jwtSerializer).refresh(any(Claims.class), eq(3 * 24 * 60 * 60));
-    assertThat(threadLocalUserSession.get().isLoggedIn()).isTrue();
   }
 
   @Test
@@ -199,10 +190,9 @@ public class JwtHttpHandlerTest {
     claims.put("lastRefreshTime", FOUR_MINUTES_AGO);
     when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims));
 
-    underTest.validateToken(request, response);
+    assertThat(underTest.validateToken(request, response).isPresent()).isTrue();
 
     verify(jwtSerializer, never()).refresh(any(Claims.class), anyInt());
-    assertThat(threadLocalUserSession.get().isLoggedIn()).isTrue();
   }
 
   @Test
@@ -215,22 +205,22 @@ public class JwtHttpHandlerTest {
     claims.put("lastRefreshTime", FOUR_MINUTES_AGO);
     when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims));
 
-    underTest.validateToken(request, response);
+    assertThat(underTest.validateToken(request, response).isPresent()).isFalse();
 
     verifyCookie(findCookie("JWT-SESSION").get(), null, 0);
-    assertThat(threadLocalUserSession.get().isLoggedIn()).isFalse();
   }
 
   @Test
-  public void validate_token_fails_with_unauthorized_when_user_is_disabled() throws Exception {
+  public void validate_token_removes_session_when_user_is_disabled() throws Exception {
     addJwtCookie();
     UserDto user = addUser(false);
 
     Claims claims = createToken(user.getLogin(), NOW);
     when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims));
 
-    thrown.expect(UnauthorizedException.class);
-    underTest.validateToken(request, response);
+    assertThat(underTest.validateToken(request, response).isPresent()).isFalse();
+
+    verifyCookie(findCookie("JWT-SESSION").get(), null, 0);
   }
 
   @Test
@@ -239,10 +229,9 @@ public class JwtHttpHandlerTest {
 
     when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.empty());
 
-    underTest.validateToken(request, response);
+    assertThat(underTest.validateToken(request, response).isPresent()).isFalse();
 
     verifyCookie(findCookie("JWT-SESSION").get(), null, 0);
-    assertThat(threadLocalUserSession.get().isLoggedIn()).isFalse();
   }
 
   @Test
@@ -250,7 +239,7 @@ public class JwtHttpHandlerTest {
     underTest.validateToken(request, response);
 
     verifyZeroInteractions(httpSession, jwtSerializer);
-    assertThat(threadLocalUserSession.get().isLoggedIn()).isFalse();
+    assertThat(underTest.validateToken(request, response).isPresent()).isFalse();
   }
 
   @Test
@@ -260,7 +249,7 @@ public class JwtHttpHandlerTest {
     underTest.validateToken(request, response);
 
     verifyZeroInteractions(httpSession, jwtSerializer);
-    assertThat(threadLocalUserSession.get().isLoggedIn()).isFalse();
+    assertThat(underTest.validateToken(request, response).isPresent()).isFalse();
   }
 
   @Test
@@ -287,7 +276,7 @@ public class JwtHttpHandlerTest {
     underTest.validateToken(request, response);
 
     verify(jwtSerializer).refresh(any(Claims.class), anyInt());
-    verify(jwtCsrfVerifier).refreshState(response,  "CSRF_STATE", 3 * 24 * 60 * 60);
+    verify(jwtCsrfVerifier).refreshState(response, "CSRF_STATE", 3 * 24 * 60 * 60);
   }
 
   @Test
@@ -308,16 +297,6 @@ public class JwtHttpHandlerTest {
 
     verifyCookie(findCookie("JWT-SESSION").get(), null, 0);
     verify(jwtCsrfVerifier).removeState(response);
-    assertThat(threadLocalUserSession.get().isLoggedIn()).isFalse();
-  }
-
-  @Test
-  public void remove_token_is_removing_user_session() throws Exception {
-    threadLocalUserSession.set(ServerUserSession.createForUser(dbClient, userDto));
-
-    underTest.removeToken(response);
-
-    assertThat(threadLocalUserSession.get().isLoggedIn()).isFalse();
   }
 
   private void verifyToken(JwtSerializer.JwtSession token, int expectedExpirationTime, long expectedRefreshTime) {
index 9afb606bf41f579f7d4c188fae2ad4cfe47147fa..46dc9582542d830248b8459fde34e30b4b59de44 100644 (file)
@@ -25,6 +25,7 @@ import static org.mockito.Matchers.eq;
 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 javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -37,7 +38,13 @@ import org.sonar.api.platform.Server;
 import org.sonar.api.server.authentication.OAuth2IdentityProvider;
 import org.sonar.api.server.authentication.UserIdentity;
 import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
 import org.sonar.db.user.UserDto;
+import org.sonar.server.user.ThreadLocalUserSession;
+import org.sonar.server.user.UserSession;
 
 public class OAuth2ContextFactoryTest {
 
@@ -56,6 +63,14 @@ public class OAuth2ContextFactoryTest {
     .setEmail("john@email.com")
     .build();
 
+  @Rule
+  public DbTester dbTester = DbTester.create(System2.INSTANCE);
+
+  DbClient dbClient = dbTester.getDbClient();
+
+  DbSession dbSession = dbTester.getSession();
+
+  ThreadLocalUserSession threadLocalUserSession = mock(ThreadLocalUserSession.class);
   UserIdentityAuthenticator userIdentityAuthenticator = mock(UserIdentityAuthenticator.class);
   Server server = mock(Server.class);
   OAuthCsrfVerifier csrfVerifier = mock(OAuthCsrfVerifier.class);
@@ -66,12 +81,16 @@ public class OAuth2ContextFactoryTest {
   HttpSession session = mock(HttpSession.class);
   OAuth2IdentityProvider identityProvider = mock(OAuth2IdentityProvider.class);
 
-  OAuth2ContextFactory underTest = new OAuth2ContextFactory(userIdentityAuthenticator, server, csrfVerifier, jwtHttpHandler);
+  OAuth2ContextFactory underTest = new OAuth2ContextFactory(dbClient, threadLocalUserSession, userIdentityAuthenticator, server, csrfVerifier, jwtHttpHandler);
 
   @Before
   public void setUp() throws Exception {
+    UserDto userDto = dbClient.userDao().insert(dbSession, newUserDto());
+    dbSession.commit();
+
     when(request.getSession()).thenReturn(session);
     when(identityProvider.getKey()).thenReturn(PROVIDER_KEY);
+    when(userIdentityAuthenticator.authenticate(USER_IDENTITY, identityProvider)).thenReturn(userDto);
   }
 
   @Test
@@ -133,6 +152,7 @@ public class OAuth2ContextFactoryTest {
 
     verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider);
     verify(jwtHttpHandler).generateToken(any(UserDto.class), eq(response));
+    verify(threadLocalUserSession).set(any(UserSession.class));
   }
 
   @Test
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
new file mode 100644 (file)
index 0000000..d78b73d
--- /dev/null
@@ -0,0 +1,198 @@
+/*
+ * 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;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+import static org.sonar.db.user.UserTesting.newUserDto;
+
+import java.util.Optional;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.user.ServerUserSession;
+import org.sonar.server.user.ThreadLocalUserSession;
+import org.sonar.server.user.UserSession;
+
+public class UserSessionInitializerTest {
+
+  @Rule
+  public DbTester dbTester = DbTester.create(System2.INSTANCE);
+
+  DbClient dbClient = dbTester.getDbClient();
+
+  DbSession dbSession = dbTester.getSession();
+
+  ThreadLocalUserSession userSession = mock(ThreadLocalUserSession.class);
+
+  HttpServletRequest request = mock(HttpServletRequest.class);
+  HttpServletResponse response = mock(HttpServletResponse.class);
+
+  JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class);
+  BasicAuthenticator basicAuthenticator = mock(BasicAuthenticator.class);
+
+  Settings settings = new Settings();
+
+  UserDto user = newUserDto();
+
+  UserSessionInitializer underTest = new UserSessionInitializer(dbClient, settings, jwtHttpHandler, basicAuthenticator, userSession);
+
+  @Before
+  public void setUp() throws Exception {
+    dbClient.userDao().insert(dbSession, user);
+    dbSession.commit();
+    when(request.getContextPath()).thenReturn("");
+    when(request.getRequestURI()).thenReturn("/measures");
+  }
+
+  @Test
+  public void check_urls() throws Exception {
+    assertPathIsNotIgnored("/");
+    assertPathIsNotIgnored("/foo");
+
+    assertPathIsIgnored("/api/authentication/login");
+    assertPathIsIgnored("/batch/index");
+    assertPathIsIgnored("/batch/file");
+    assertPathIsIgnored("/maintenance/index");
+    assertPathIsIgnored("/setup/index");
+    assertPathIsIgnored("/sessions/new");
+    assertPathIsIgnored("/sessions/logout");
+    assertPathIsIgnored("/api/system/db_migration_status");
+    assertPathIsIgnored("/api/system/status");
+    assertPathIsIgnored("/api/system/migrate_db");
+    assertPathIsIgnored("/api/server/index");
+
+    // exclude static resources
+    assertPathIsIgnored("/css/style.css");
+    assertPathIsIgnored("/fonts/font.ttf");
+    assertPathIsIgnored("/images/logo.png");
+    assertPathIsIgnored("/js/jquery.js");
+  }
+
+  @Test
+  public void validate_session_from_token() throws Exception {
+    when(userSession.isLoggedIn()).thenReturn(true);
+    when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.of(user));
+
+    assertThat(underTest.initUserSession(request, response)).isTrue();
+
+    verify(jwtHttpHandler).validateToken(request, response);
+    verify(response, never()).setStatus(anyInt());
+  }
+
+  @Test
+  public void validate_session_from_basic_authentication() throws Exception {
+    when(userSession.isLoggedIn()).thenReturn(false).thenReturn(true);
+    when(basicAuthenticator.authenticate(request)).thenReturn(Optional.of(user));
+    when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.empty());
+
+    assertThat(underTest.initUserSession(request, response)).isTrue();
+
+    verify(jwtHttpHandler).validateToken(request, response);
+    verify(basicAuthenticator).authenticate(request);
+    verify(userSession).set(any(ServerUserSession.class));
+    verify(response, never()).setStatus(anyInt());
+  }
+
+  @Test
+  public void return_code_401_when_invalid_token_exception() throws Exception {
+    doThrow(new UnauthorizedException("invalid token")).when(jwtHttpHandler).validateToken(request, response);
+
+    assertThat(underTest.initUserSession(request, response)).isTrue();
+
+    verify(response).setStatus(401);
+  }
+
+  @Test
+  public void return_code_401_when_not_authenticated_and_with_force_authentication() throws Exception {
+    when(userSession.isLoggedIn()).thenReturn(false);
+    when(basicAuthenticator.authenticate(request)).thenReturn(Optional.empty());
+    when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.empty());
+    settings.setProperty("sonar.forceAuthentication", true);
+
+    assertThat(underTest.initUserSession(request, response)).isTrue();
+
+    verify(response).setStatus(401);
+  }
+
+  @Test
+  public void return_401_and_stop_on_ws() throws Exception {
+    when(request.getRequestURI()).thenReturn("/api/issues");
+    doThrow(new UnauthorizedException("invalid token")).when(jwtHttpHandler).validateToken(request, response);
+
+    assertThat(underTest.initUserSession(request, response)).isFalse();
+
+    verify(response).setStatus(401);
+  }
+
+  @Test
+  public void return_401_and_stop_on_batch_ws() throws Exception {
+    when(request.getRequestURI()).thenReturn("/batch/global");
+    doThrow(new UnauthorizedException("invalid token")).when(jwtHttpHandler).validateToken(request, response);
+
+    assertThat(underTest.initUserSession(request, response)).isFalse();
+
+    verify(response).setStatus(401);
+  }
+
+  @Test
+  public void remove_user_session() throws Exception {
+    underTest.removeUserSession();
+
+    verify(userSession).remove();
+  }
+
+  private void assertPathIsIgnored(String path) {
+    when(request.getRequestURI()).thenReturn(path);
+
+    assertThat(underTest.initUserSession(request, response)).isTrue();
+
+    verifyZeroInteractions(userSession, jwtHttpHandler, basicAuthenticator);
+    reset(userSession, jwtHttpHandler, basicAuthenticator);
+  }
+
+  private void assertPathIsNotIgnored(String path) {
+    when(request.getRequestURI()).thenReturn(path);
+    when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.of(user));
+
+    assertThat(underTest.initUserSession(request, response)).isTrue();
+
+    verify(userSession).set(any(UserSession.class));
+    reset(userSession, jwtHttpHandler, basicAuthenticator);
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/ValidateJwtTokenFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/ValidateJwtTokenFilterTest.java
deleted file mode 100644 (file)
index 210d940..0000000
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * 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;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-
-import javax.servlet.FilterChain;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.sonar.api.config.Settings;
-import org.sonar.server.exceptions.UnauthorizedException;
-import org.sonar.server.tester.UserSessionRule;
-
-public class ValidateJwtTokenFilterTest {
-
-  @Rule
-  public UserSessionRule userSession = UserSessionRule.standalone();
-
-  HttpServletRequest request = mock(HttpServletRequest.class);
-  HttpServletResponse response = mock(HttpServletResponse.class);
-  FilterChain chain = mock(FilterChain.class);
-
-  JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class);
-
-  Settings settings = new Settings();
-
-  ValidateJwtTokenFilter underTest = new ValidateJwtTokenFilter(settings, jwtHttpHandler, userSession);
-
-  @Before
-  public void setUp() throws Exception {
-    when(request.getContextPath()).thenReturn("");
-    when(request.getRequestURI()).thenReturn("/measures");
-  }
-
-  @Test
-  public void do_get_pattern() throws Exception {
-    assertThat(underTest.doGetPattern().matches("/")).isTrue();
-    assertThat(underTest.doGetPattern().matches("/foo")).isTrue();
-
-    assertThat(underTest.doGetPattern().matches("/api/authentication/login")).isFalse();
-    assertThat(underTest.doGetPattern().matches("/batch/index")).isFalse();
-    assertThat(underTest.doGetPattern().matches("/batch/file")).isFalse();
-    assertThat(underTest.doGetPattern().matches("/batch_bootstrap/index")).isFalse();
-    assertThat(underTest.doGetPattern().matches("/maintenance/index")).isFalse();
-    assertThat(underTest.doGetPattern().matches("/setup/index")).isFalse();
-    assertThat(underTest.doGetPattern().matches("/sessions/new")).isFalse();
-    assertThat(underTest.doGetPattern().matches("/sessions/logout")).isFalse();
-    assertThat(underTest.doGetPattern().matches("/api/system/db_migration_status")).isFalse();
-    assertThat(underTest.doGetPattern().matches("/api/system/status")).isFalse();
-    assertThat(underTest.doGetPattern().matches("/api/system/status")).isFalse();
-    assertThat(underTest.doGetPattern().matches("/api/system/migrate_db")).isFalse();
-    assertThat(underTest.doGetPattern().matches("/api/server/index")).isFalse();
-
-    // exclude static resources
-    assertThat(underTest.doGetPattern().matches("/css/style.css")).isFalse();
-    assertThat(underTest.doGetPattern().matches("/fonts/font.ttf")).isFalse();
-    assertThat(underTest.doGetPattern().matches("/images/logo.png")).isFalse();
-    assertThat(underTest.doGetPattern().matches("/js/jquery.js")).isFalse();
-  }
-
-  @Test
-  public void validate_session() throws Exception {
-    userSession.login("john");
-    underTest.doFilter(request, response, chain);
-
-    verify(jwtHttpHandler).validateToken(request, response);
-    verify(chain).doFilter(request, response);
-  }
-
-  @Test
-  public void return_code_401_when_invalid_token_exception() throws Exception {
-    userSession.login("john");
-    doThrow(new UnauthorizedException("invalid token")).when(jwtHttpHandler).validateToken(request, response);
-
-    underTest.doFilter(request, response, chain);
-
-    verify(response).setStatus(401);
-    verify(chain).doFilter(request, response);
-  }
-
-  @Test
-  public void return_code_401_when_not_authenticated_and_with_force_authentication() throws Exception {
-    settings.setProperty("sonar.forceAuthentication", true);
-    userSession.anonymous();
-
-    underTest.doFilter(request, response, chain);
-
-    verify(response).setStatus(401);
-    verify(chain).doFilter(request, response);
-  }
-
-  @Test
-  public void return_401_and_stop_on_ws() throws Exception {
-    when(request.getRequestURI()).thenReturn("/api/issues");
-    doThrow(new UnauthorizedException("invalid token")).when(jwtHttpHandler).validateToken(request, response);
-
-    underTest.doFilter(request, response, chain);
-
-    verify(response).setStatus(401);
-    verifyZeroInteractions(chain);
-  }
-
-  @Test
-  public void return_401_and_stop_on_batch_ws() throws Exception {
-    when(request.getRequestURI()).thenReturn("/batch/index");
-    doThrow(new UnauthorizedException("invalid token")).when(jwtHttpHandler).validateToken(request, response);
-
-    underTest.doFilter(request, response, chain);
-
-    verify(response).setStatus(401);
-    verifyZeroInteractions(chain);
-  }
-
-  @Test
-  public void ignore_old_batch_ws() throws Exception {
-    when(request.getRequestURI()).thenReturn("/batch/name.jar");
-
-    underTest.doFilter(request, response, chain);
-
-    verify(chain).doFilter(request, response);
-    verifyZeroInteractions(jwtHttpHandler, response);
-  }
-}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/LoginActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/LoginActionTest.java
new file mode 100644 (file)
index 0000000..3f6fd7c
--- /dev/null
@@ -0,0 +1,149 @@
+/*
+ * 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.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserTesting;
+import org.sonar.server.authentication.CredentialsAuthenticator;
+import org.sonar.server.authentication.JwtHttpHandler;
+import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.user.ThreadLocalUserSession;
+
+public class LoginActionTest {
+
+  static final String LOGIN = "LOGIN";
+  static final String PASSWORD = "PASSWORD";
+
+  @Rule
+  public DbTester dbTester = DbTester.create(System2.INSTANCE);
+
+  DbClient dbClient = dbTester.getDbClient();
+
+  DbSession dbSession = dbTester.getSession();
+
+  ThreadLocalUserSession threadLocalUserSession = new ThreadLocalUserSession();
+
+  HttpServletRequest request = mock(HttpServletRequest.class);
+  HttpServletResponse response = mock(HttpServletResponse.class);
+  FilterChain chain = mock(FilterChain.class);
+
+  CredentialsAuthenticator credentialsAuthenticator = mock(CredentialsAuthenticator.class);
+  JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class);
+
+  UserDto user = UserTesting.newUserDto().setLogin(LOGIN);
+
+  LoginAction underTest  = new LoginAction(dbClient, credentialsAuthenticator, jwtHttpHandler, threadLocalUserSession);
+
+  @Before
+  public void setUp() throws Exception {
+    threadLocalUserSession.remove();
+    dbClient.userDao().insert(dbSession, user);
+    dbSession.commit();
+  }
+
+  @Test
+  public void do_get_pattern() throws Exception {
+    assertThat(underTest.doGetPattern().matches("/api/authentication/login")).isTrue();
+    assertThat(underTest.doGetPattern().matches("/api/authentication/logout")).isFalse();
+    assertThat(underTest.doGetPattern().matches("/foo")).isFalse();
+  }
+
+  @Test
+  public void do_authenticate() throws Exception {
+    when(credentialsAuthenticator.authenticate(LOGIN, PASSWORD, request)).thenReturn(user);
+
+    executeRequest(LOGIN, PASSWORD);
+
+    assertThat(threadLocalUserSession.isLoggedIn()).isTrue();
+    verify(credentialsAuthenticator).authenticate(LOGIN, PASSWORD, request);
+    verify(jwtHttpHandler).generateToken(user, response);
+    verifyZeroInteractions(chain);
+  }
+
+  @Test
+  public void ignore_get_request() throws Exception {
+    when(request.getMethod()).thenReturn("GET");
+
+    underTest.doFilter(request, response, chain);
+
+    verifyZeroInteractions(credentialsAuthenticator, jwtHttpHandler, chain);
+  }
+
+  @Test
+  public void return_authorized_code_when_unauthorized_exception_is_thrown() throws Exception {
+    doThrow(new UnauthorizedException()).when(credentialsAuthenticator).authenticate(LOGIN, PASSWORD, request);
+
+    executeRequest(LOGIN, PASSWORD);
+
+    verify(response).setStatus(401);
+    assertThat(threadLocalUserSession.isLoggedIn()).isFalse();
+  }
+
+  @Test
+  public void return_unauthorized_code_when_no_login() throws Exception {
+    executeRequest(null, PASSWORD);
+    verify(response).setStatus(401);
+  }
+
+  @Test
+  public void return_unauthorized_code_when_empty_login() throws Exception {
+    executeRequest("", PASSWORD);
+    verify(response).setStatus(401);
+  }
+
+  @Test
+  public void return_unauthorized_code_when_no_password() throws Exception {
+    executeRequest(LOGIN, null);
+    verify(response).setStatus(401);
+  }
+
+  @Test
+  public void return_unauthorized_code_when_empty_password() throws Exception {
+    executeRequest(LOGIN, "");
+    verify(response).setStatus(401);
+  }
+
+  private void executeRequest(String login, String password) throws IOException, ServletException {
+    when(request.getMethod()).thenReturn("POST");
+    when(request.getParameter("login")).thenReturn(login);
+    when(request.getParameter("password")).thenReturn(password);
+    underTest.doFilter(request, response, chain);
+  }
+}
index 06a8be8d1fcf2c09589d6a78ed45d0a67e10b619..1e75fe2279a9e707b32dbea4bf625ebd06c2350d 100644 (file)
  */
 package org.sonar.server.user;
 
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
 import java.io.IOException;
 import javax.servlet.FilterChain;
 import javax.servlet.FilterConfig;
 import javax.servlet.ServletException;
-import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
-import org.junit.After;
+import javax.servlet.http.HttpServletResponse;
 import org.junit.Before;
 import org.junit.Test;
 import org.sonar.core.platform.ComponentContainer;
+import org.sonar.server.authentication.UserSessionInitializer;
 import org.sonar.server.platform.Platform;
-import org.sonar.server.tester.MockUserSession;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
 public class UserSessionFilterTest {
-  private ThreadLocalUserSession threadLocalUserSession = new ThreadLocalUserSession();
-  private Platform platform = mock(Platform.class);
-  private ComponentContainer componentContainer = mock(ComponentContainer.class);
-  private HttpServletRequest httpRequest = mock(HttpServletRequest.class);
-  private ServletResponse httpResponse = mock(ServletResponse.class);
-  private FilterChain chain = mock(FilterChain.class);
+
+  UserSessionInitializer userSessionInitializer = mock(UserSessionInitializer.class);
+  Platform platform = mock(Platform.class);
+  ComponentContainer componentContainer = mock(ComponentContainer.class);
+
+  HttpServletRequest request = mock(HttpServletRequest.class);
+  HttpServletResponse response = mock(HttpServletResponse.class);
+  FilterChain chain = mock(FilterChain.class);
+
+  UserSessionFilter underTest = new UserSessionFilter(platform);
 
   @Before
   public void setUp() {
     when(platform.getContainer()).thenReturn(componentContainer);
-    // for test isolation
-    threadLocalUserSession.remove();
   }
 
-  @After
-  public void tearDown() {
-    threadLocalUserSession.remove();
+  @Test
+  public void cleanup_user_session_after_request_handling() throws IOException, ServletException {
+    when(componentContainer.getComponentByType(UserSessionInitializer.class)).thenReturn(userSessionInitializer);
+    when(userSessionInitializer.initUserSession(request, response)).thenReturn(true);
+
+    underTest.doFilter(request, response, chain);
+
+    verify(chain).doFilter(request, response);
+    verify(userSessionInitializer).initUserSession(request, response);
+    verify(userSessionInitializer).removeUserSession();
   }
 
   @Test
-  public void should_cleanup_user_session_after_request_handling() throws IOException, ServletException {
-    when(componentContainer.getComponentByType(ThreadLocalUserSession.class)).thenReturn(threadLocalUserSession);
+  public void stop_when_user_session_return_false() throws Exception {
+    when(componentContainer.getComponentByType(UserSessionInitializer.class)).thenReturn(userSessionInitializer);
+    when(userSessionInitializer.initUserSession(request, response)).thenReturn(false);
 
-    threadLocalUserSession.set(new MockUserSession("karadoc").setUserId(123));
-    assertThat(threadLocalUserSession.hasSession()).isTrue();
-    UserSessionFilter filter = new UserSessionFilter(platform);
-    filter.doFilter(httpRequest, httpResponse, chain);
+    underTest.doFilter(request, response, chain);
 
-    verify(chain).doFilter(httpRequest, httpResponse);
-    assertThat(threadLocalUserSession.hasSession()).isFalse();
+    verify(chain, never()).doFilter(request, response);
+    verify(userSessionInitializer).initUserSession(request, response);
+    verify(userSessionInitializer).removeUserSession();
   }
 
   @Test
-  public void does_not_fail_if_container_has_no_ThreadLocalUserSession() throws Exception {
-    UserSessionFilter filter = new UserSessionFilter(platform);
-    filter.doFilter(httpRequest, httpResponse, chain);
+  public void does_nothing_when_not_initialized() throws Exception {
+    underTest.doFilter(request, response, chain);
+
+    verify(chain).doFilter(request, response);
+    verifyZeroInteractions(userSessionInitializer);
   }
 
   @Test
   public void just_for_fun_and_coverage() throws ServletException {
-    UserSessionFilter filter = new UserSessionFilter(platform);
+    UserSessionFilter filter = new UserSessionFilter();
     filter.init(mock(FilterConfig.class));
     filter.destroy();
     // do not fail
index 74de38b0037935d65f8aacde807a7a73f312dc8d..e0b4516aba4ab9a18276bde9819717883ac7dfa7 100644 (file)
  */
 package org.sonar.server.usertoken;
 
-import com.google.common.base.Optional;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.db.user.UserTokenTesting.newUserToken;
+
+import java.util.Optional;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
@@ -28,12 +33,6 @@ import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.sonar.db.user.UserTokenTesting.newUserToken;
-
-
 public class UserTokenAuthenticatorTest {
   static final String GRACE_HOPPER = "grace.hopper";
   static final String ADA_LOVELACE = "ada.lovelace";
index 02f70471036c2f5ad1f99fc41714237aa6f7483f..c9be7f12c8ad1e0aae0e2b039b3655610ff08e81 100644 (file)
@@ -8,7 +8,7 @@ module AuthenticatedSystem
   # Accesses the current user from the session.
   # Future calls avoid the database because nil is not equal to false.
   def current_user
-    @current_user ||= (login_from_java_user_session || login_from_basic_auth) unless @current_user == false
+    @current_user ||= login_from_java_user_session unless @current_user == false
   end
 
   # Store the given user
@@ -124,31 +124,6 @@ module AuthenticatedSystem
     self.current_user = User.find_by_id(user_id) if user_id
   end
 
-  # Called from #current_user.  Now, attempt to login by basic authentication information.
-  def login_from_basic_auth
-    authenticate_with_http_basic do |login, password|
-      # The access token is sent as the login of Basic authentication. To distinguish with regular logins,
-      # the convention is that the password is empty
-      if password.empty? && login.present?
-        # authentication by access token
-        token_authenticator = Java::OrgSonarServerPlatform::Platform.component(Java::OrgSonarServerUsertoken::UserTokenAuthenticator.java_class)
-        authenticated_login = token_authenticator.authenticate(login)
-        if authenticated_login.isPresent()
-          user = User.find_active_by_login(authenticated_login.get())
-          if user
-            user.token_authenticated=true
-            result = user
-          end
-        end
-      else
-        # regular Basic authentication with login and password
-        result = User.authenticate(login, password, servlet_request)
-      end
-      raise Errors::AccessDenied unless login.blank? || result
-      self.current_user = result
-    end
-  end
-
   #
   # Logout
   #