diff options
author | Simon Brandhof <simon.brandhof@gmail.com> | 2012-05-08 09:14:29 +0200 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@gmail.com> | 2012-05-08 10:23:02 +0200 |
commit | 9e46b4bb203a60742f0cb58cd621a58496b7ed33 (patch) | |
tree | ce4a6aac43db33c25c4bf48b6c22a9f63e88e42c | |
parent | bd0abcb3f754f4c123f9ce3145e2cf814210b91e (diff) | |
download | sonarqube-9e46b4bb203a60742f0cb58cd621a58496b7ed33.tar.gz sonarqube-9e46b4bb203a60742f0cb58cd621a58496b7ed33.zip |
SONAR-2950 Single Sign On with external authentication mechanism
20 files changed, 706 insertions, 47 deletions
diff --git a/sonar-plugin-api/pom.xml b/sonar-plugin-api/pom.xml index 22a6a4fdef2..93e5a4a7522 100644 --- a/sonar-plugin-api/pom.xml +++ b/sonar-plugin-api/pom.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.codehaus.sonar</groupId> @@ -131,6 +132,12 @@ <groupId>xalan</groupId> <artifactId>xalan</artifactId> </dependency> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>servlet-api</artifactId> + <version>2.4</version> + <optional>true</optional> + </dependency> <!-- unit tests --> <dependency> @@ -169,6 +176,12 @@ <scope>test</scope> </dependency> <dependency> + <groupId>org.easytesting</groupId> + <artifactId>fest-assert</artifactId> + <scope>test</scope> + </dependency> + + <dependency> <groupId>org.mortbay.jetty</groupId> <artifactId>jetty-servlet-tester</artifactId> <scope>test</scope> diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/security/Authenticator.java b/sonar-plugin-api/src/main/java/org/sonar/api/security/Authenticator.java new file mode 100644 index 00000000000..2420eaf3d8f --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/security/Authenticator.java @@ -0,0 +1,70 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.api.security; + +import com.google.common.base.Preconditions; +import org.sonar.api.ServerExtension; + +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; + +/** + * @see SecurityRealm + * @since 3.1 + */ +public abstract class Authenticator implements ServerExtension { + + /** + * @return true if user was successfully authenticated with specified credentials, false otherwise + * @throws RuntimeException in case of unexpected error such as connection failure + */ + public abstract boolean doAuthenticate(Context context); + + public static final class Context { + private String username; + private String password; + private HttpServletRequest request; + + public Context(@Nullable String username, @Nullable String password, HttpServletRequest request) { + Preconditions.checkNotNull(request); + this.request = request; + this.username = username; + this.password = password; + } + + /** + * Username can be null, for example when using <a href="http://www.jasig.org/cas">CAS</a>. + */ + public String getUsername() { + return username; + } + + /** + * Password can be null, for example when using <a href="http://www.jasig.org/cas">CAS</a>. + */ + public String getPassword() { + return password; + } + + public HttpServletRequest getRequest() { + return request; + } + } +} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/security/ExternalUsersProvider.java b/sonar-plugin-api/src/main/java/org/sonar/api/security/ExternalUsersProvider.java index 00f7dfb4dac..b7c93c59fb2 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/security/ExternalUsersProvider.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/security/ExternalUsersProvider.java @@ -19,18 +19,58 @@ */ package org.sonar.api.security; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; + +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; + /** * Note that prefix "do" for names of methods is reserved for future enhancements, thus should not be used in subclasses. * - * @since 2.14 * @see SecurityRealm + * @since 2.14 */ public abstract class ExternalUsersProvider { /** + * This method is overridden by old versions of plugins such as LDAP 1.1. It should be overridden anymore. + * * @return details for specified user, or null if such user doesn't exist * @throws RuntimeException in case of unexpected error such as connection failure + * @deprecated replaced by {@link #doGetUserDetails(org.sonar.api.security.ExternalUsersProvider.Context)} since v. 3.1 */ - public abstract UserDetails doGetUserDetails(String username); + @Deprecated + public UserDetails doGetUserDetails(@Nullable String username) { + return null; + } + + /** + * Override this method in order load user information. + * + * @return the user, or null if user doesn't exist + * @throws RuntimeException in case of unexpected error such as connection failure + * @since 3.1 + */ + public UserDetails doGetUserDetails(Context context) { + return doGetUserDetails(context.getUsername()); + } + + public static final class Context { + private String username; + private HttpServletRequest request; + + public Context(@Nullable String username, HttpServletRequest request) { + this.username = username; + this.request = request; + } + + public String getUsername() { + return username; + } + public HttpServletRequest getRequest() { + return request; + } + } } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/security/LoginPasswordAuthenticator.java b/sonar-plugin-api/src/main/java/org/sonar/api/security/LoginPasswordAuthenticator.java index 8ca6f97864f..2883275a24e 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/security/LoginPasswordAuthenticator.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/security/LoginPasswordAuthenticator.java @@ -24,6 +24,7 @@ import org.sonar.api.ServerExtension; /** * @since 1.12 * @see SecurityRealm + * @deprecated replaced by Authenticator in version 3.1 */ public interface LoginPasswordAuthenticator extends ServerExtension { diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/security/SecurityRealm.java b/sonar-plugin-api/src/main/java/org/sonar/api/security/SecurityRealm.java index f21d4d26946..16a122933ef 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/security/SecurityRealm.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/security/SecurityRealm.java @@ -41,8 +41,27 @@ public abstract class SecurityRealm implements ServerExtension { /** * @return {@link LoginPasswordAuthenticator} associated with this realm, never null + * @deprecated replaced by doGetAuthenticator in version 3.1 */ - public abstract LoginPasswordAuthenticator getLoginPasswordAuthenticator(); + @Deprecated + public LoginPasswordAuthenticator getLoginPasswordAuthenticator() { + return null; + } + + /** + * @since 3.1 + */ + public Authenticator doGetAuthenticator() { + if (getLoginPasswordAuthenticator() == null) { + return null; + } + return new Authenticator() { + @Override + public boolean doAuthenticate(Context context) { + return getLoginPasswordAuthenticator().authenticate(context.getUsername(), context.getPassword()); + } + }; + } /** * @return {@link ExternalUsersProvider} associated with this realm, null if not supported @@ -57,5 +76,4 @@ public abstract class SecurityRealm implements ServerExtension { public ExternalGroupsProvider getGroupsProvider() { return null; } - } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/security/UserDetails.java b/sonar-plugin-api/src/main/java/org/sonar/api/security/UserDetails.java index e3519866a13..2d4afc0524c 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/security/UserDetails.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/security/UserDetails.java @@ -21,6 +21,8 @@ package org.sonar.api.security; import com.google.common.base.Objects; +import javax.annotation.Nullable; + /** * This class is not intended to be subclassed by clients. * diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/security/package-info.java b/sonar-plugin-api/src/main/java/org/sonar/api/security/package-info.java new file mode 100644 index 00000000000..8658aba99c6 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/security/package-info.java @@ -0,0 +1,23 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +@ParametersAreNonnullByDefault +package org.sonar.api.security; + +import javax.annotation.ParametersAreNonnullByDefault;
\ No newline at end of file diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java b/sonar-plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java new file mode 100644 index 00000000000..983f7fcfcfc --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java @@ -0,0 +1,84 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.api.web; + +import com.google.common.annotations.Beta; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import org.sonar.api.ServerExtension; + +import javax.servlet.Filter; + +/** + * @since 3.1 + */ +@Beta +public abstract class ServletFilter implements ServerExtension, Filter { + + /** + * Override to change URL. Default is /* + */ + public UrlPattern doGetPattern() { + return UrlPattern.create("/*"); + } + + public static final class UrlPattern { + private int code; + private String url; + private String urlToMatch; + + public static UrlPattern create(String pattern) { + return new UrlPattern(pattern); + } + + private UrlPattern(String url) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(url), "Empty url"); + this.url = url; + this.urlToMatch = url.replaceAll("/?\\*", ""); + if ("/*".equals(url)) { + code = 1; + } else if (url.startsWith("*")) { + code = 2; + } else if (url.endsWith("*")) { + code = 3; + } else { + code = 4; + } + } + + public boolean matches(String path) { + switch (code) { + case 1: + return true; + case 2: + return path.endsWith(urlToMatch); + case 3: + return path.startsWith(urlToMatch); + default: + return path.equals(urlToMatch); + } + } + + @Override + public String toString() { + return url; + } + } +} diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/security/ExternalUsersProviderTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/security/ExternalUsersProviderTest.java new file mode 100644 index 00000000000..f1ed1a023e1 --- /dev/null +++ b/sonar-plugin-api/src/test/java/org/sonar/api/security/ExternalUsersProviderTest.java @@ -0,0 +1,67 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.api.security; + +import com.google.common.base.Preconditions; +import org.junit.Test; + +import javax.servlet.http.HttpServletRequest; + +import static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class ExternalUsersProviderTest { + + @Test + public void doGetUserDetails() { + ExternalUsersProvider provider = new ExternalUsersProvider() { + @Override + public UserDetails doGetUserDetails(Context context) { + Preconditions.checkNotNull(context.getUsername()); + Preconditions.checkNotNull(context.getRequest()); + UserDetails user = new UserDetails(); + user.setName(context.getUsername()); + user.setEmail("foo@bar.com"); + return user; + } + }; + UserDetails user = provider.doGetUserDetails(new ExternalUsersProvider.Context("foo", mock(HttpServletRequest.class))); + + assertThat(user.getName()).isEqualTo("foo"); + assertThat(user.getEmail()).isEqualTo("foo@bar.com"); + } + + @Test + public void doGetUserDetails_deprecated_api() { + ExternalUsersProvider provider = new ExternalUsersProvider() { + @Override + public UserDetails doGetUserDetails(String username) { + UserDetails user = new UserDetails(); + user.setName(username); + user.setEmail("foo@bar.com"); + return user; + } + }; + UserDetails user = provider.doGetUserDetails(new ExternalUsersProvider.Context("foo", mock(HttpServletRequest.class))); + + assertThat(user.getName()).isEqualTo("foo"); + assertThat(user.getEmail()).isEqualTo("foo@bar.com"); + } +} diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/security/SecurityRealmTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/security/SecurityRealmTest.java new file mode 100644 index 00000000000..e22eabc34a2 --- /dev/null +++ b/sonar-plugin-api/src/test/java/org/sonar/api/security/SecurityRealmTest.java @@ -0,0 +1,59 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.api.security; + +import org.junit.Test; + +import javax.servlet.http.HttpServletRequest; + +import static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class SecurityRealmTest { + + @Test + public void doGetAuthenticator() { + final Authenticator authenticator = mock(Authenticator.class); + SecurityRealm realm = new SecurityRealm() { + @Override + public Authenticator doGetAuthenticator() { + return authenticator; + } + }; + assertThat(realm.doGetAuthenticator()).isSameAs(authenticator); + } + + @Test + public void getLoginPasswordAuthenticator_deprecated_method_replaced_by_getAuthenticator() { + final LoginPasswordAuthenticator deprecatedAuthenticator = mock(LoginPasswordAuthenticator.class); + SecurityRealm realm = new SecurityRealm() { + @Override + public LoginPasswordAuthenticator getLoginPasswordAuthenticator() { + return deprecatedAuthenticator; + } + }; + Authenticator proxy = realm.doGetAuthenticator(); + Authenticator.Context context = new Authenticator.Context("foo", "bar", mock(HttpServletRequest.class)); + proxy.doAuthenticate(context); + + verify(deprecatedAuthenticator).authenticate("foo", "bar"); + } +} diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/web/ServletFilterTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/web/ServletFilterTest.java new file mode 100644 index 00000000000..398cbb6d247 --- /dev/null +++ b/sonar-plugin-api/src/test/java/org/sonar/api/web/ServletFilterTest.java @@ -0,0 +1,61 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.api.web; + +import org.junit.Test; + +import static org.fest.assertions.Assertions.assertThat; + +public class ServletFilterTest { + @Test + public void matchAll() { + ServletFilter.UrlPattern pattern = ServletFilter.UrlPattern.create("/*"); + assertThat(pattern.matches("/")).isTrue(); + assertThat(pattern.matches("/foo/ooo")).isTrue(); + } + + @Test + public void matchEndOfUrl() { + ServletFilter.UrlPattern pattern = ServletFilter.UrlPattern.create("*foo"); + assertThat(pattern.matches("/")).isFalse(); + assertThat(pattern.matches("/hello/foo")).isTrue(); + assertThat(pattern.matches("/hello/bar")).isFalse(); + assertThat(pattern.matches("/foo")).isTrue(); + assertThat(pattern.matches("/foo2")).isFalse(); + } + + @Test + public void matchBeginningOfUrl() { + ServletFilter.UrlPattern pattern = ServletFilter.UrlPattern.create("/foo/*"); + assertThat(pattern.matches("/")).isFalse(); + assertThat(pattern.matches("/foo")).isTrue(); + assertThat(pattern.matches("/foo/bar")).isTrue(); + assertThat(pattern.matches("/bar")).isFalse(); + } + + @Test + public void matchExactUrl() { + ServletFilter.UrlPattern pattern = ServletFilter.UrlPattern.create("/foo"); + assertThat(pattern.matches("/")).isFalse(); + assertThat(pattern.matches("/foo")).isTrue(); + assertThat(pattern.matches("/foo/")).isFalse(); + assertThat(pattern.matches("/bar")).isFalse(); + } +} diff --git a/sonar-server/src/dev/web.xml b/sonar-server/src/dev/web.xml index a613bea2a3c..b537e1dd48a 100644 --- a/sonar-server/src/dev/web.xml +++ b/sonar-server/src/dev/web.xml @@ -35,11 +35,20 @@ <filter-name>RackFilter</filter-name> <filter-class>org.jruby.rack.RackFilter</filter-class> </filter> + <filter> + <filter-name>ServletFilters</filter-name> + <filter-class>org.sonar.server.platform.ServletFilters</filter-class> + </filter> + <filter-mapping> <filter-name>DatabaseSessionFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> + <filter-name>ServletFilters</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> + <filter-mapping> <filter-name>RackFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> diff --git a/sonar-server/src/main/java/org/sonar/server/platform/ServletFilters.java b/sonar-server/src/main/java/org/sonar/server/platform/ServletFilters.java new file mode 100644 index 00000000000..303a1e6c850 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/platform/ServletFilters.java @@ -0,0 +1,113 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.server.platform; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import org.slf4j.LoggerFactory; +import org.sonar.api.web.ServletFilter; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +/** + * Inspired by http://stackoverflow.com/a/7592883/229031 + */ +public class ServletFilters implements Filter { + + private ServletFilter[] filters; + + public void init(FilterConfig config) throws ServletException { + init(config, Platform.getInstance().getContainer().getComponentsByType(ServletFilter.class)); + } + + @VisibleForTesting + void init(FilterConfig config, List<ServletFilter> extensions) throws ServletException { + List<Filter> filterList = Lists.newArrayList(); + for (ServletFilter extension : extensions) { + try { + LoggerFactory.getLogger(ServletFilters.class).info(String.format("Initializing servlet filter %s [pattern=%s]", extension, extension.doGetPattern())); + extension.init(config); + filterList.add(extension); + } catch (RuntimeException e) { + throw new IllegalStateException("Fail to initialize servlet filter: " + extension + ". Message: " + e.getMessage(), e); + } + } + filters = filterList.toArray(new ServletFilter[filterList.size()]); + } + + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { + HttpServletRequest hsr = (HttpServletRequest) request; + if (filters.length == 0) { + chain.doFilter(request, response); + } else { + String path = hsr.getRequestURI().replaceFirst(hsr.getContextPath(), ""); + GodFilterChain godChain = new GodFilterChain(chain); + + for (ServletFilter filter : filters) { + if (filter.doGetPattern().matches(path)) { + godChain.addFilter(filter); + } + } + godChain.doFilter(request, response); + } + } + + public void destroy() { + for (ServletFilter filter : filters) { + filter.destroy(); + } + } + + @VisibleForTesting + ServletFilter[] getFilters() { + return filters; + } + + private static final class GodFilterChain implements FilterChain { + private FilterChain chain; + private List<Filter> filters = Lists.newLinkedList(); + private Iterator<Filter> iterator; + + public GodFilterChain(FilterChain chain) { + this.chain = chain; + } + + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + if (iterator == null) { + iterator = filters.iterator(); + } + if (iterator.hasNext()) { + iterator.next().doFilter(request, response, this); + } else { + chain.doFilter(request, response); + } + } + + public void addFilter(Filter filter) { + Preconditions.checkState(iterator == null); + filters.add(filter); + } + } +} diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/account_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/account_controller.rb index a643b9d1d79..fa963c06168 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/account_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/account_controller.rb @@ -35,7 +35,7 @@ class AccountController < ApplicationController def change_password return unless request.post? - if User.authenticate(current_user.login, params[:old_password]) + if User.authenticate(current_user.login, params[:old_password], servlet_request) if ((params[:password] == params[:password_confirmation])) current_user.password = params[:password] current_user.password_confirmation = params[:password] diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/sessions_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/sessions_controller.rb index 9edb145c3c0..3098ad32268 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/sessions_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/sessions_controller.rb @@ -26,8 +26,8 @@ class SessionsController < ApplicationController def login return unless request.post? - - self.current_user = User.authenticate(params[:login], params[:password]) + + self.current_user = User.authenticate(params[:login], params[:password], servlet_request) if logged_in? if params[:remember_me] == '1' self.current_user.remember_me diff --git a/sonar-server/src/main/webapp/WEB-INF/lib/authenticated_system.rb b/sonar-server/src/main/webapp/WEB-INF/lib/authenticated_system.rb index d05adeae8d1..e5f7711a472 100644 --- a/sonar-server/src/main/webapp/WEB-INF/lib/authenticated_system.rb +++ b/sonar-server/src/main/webapp/WEB-INF/lib/authenticated_system.rb @@ -13,7 +13,7 @@ module AuthenticatedSystem # Store the given user id in the session. def current_user=(new_user) - session[:user_id] = new_user ? new_user.id : nil + session[:user_id] = (new_user ? new_user.id : nil) @current_user = new_user || false end @@ -113,7 +113,7 @@ module AuthenticatedSystem # Called from #current_user. Now, attempt to login by basic authentication information. def login_from_basic_auth authenticate_with_http_basic do |login, password| - self.current_user = User.authenticate(login, password) + self.current_user = User.authenticate(login, password, servlet_request) end end diff --git a/sonar-server/src/main/webapp/WEB-INF/lib/need_authentication.rb b/sonar-server/src/main/webapp/WEB-INF/lib/need_authentication.rb index a1f8fcc9ccb..7e3ffd3a7e0 100644 --- a/sonar-server/src/main/webapp/WEB-INF/lib/need_authentication.rb +++ b/sonar-server/src/main/webapp/WEB-INF/lib/need_authentication.rb @@ -22,13 +22,13 @@ # Use Sonar database (table USERS) to authenticate users. # class DefaultRealm - def authenticate?(username, password) - user = User.find_active_by_login(username) - if user && user.authenticated?(password) - return user - else - return nil + def authenticate?(username, password, servlet_request) + result=nil + if !username.blank? && !password.blank? + user=User.find_active_by_login(username) + result=user if user && user.authenticated?(password) end + result end def editable_password? @@ -42,30 +42,33 @@ end # class PluginRealm def initialize(java_realm) - @java_authenticator = java_realm.getLoginPasswordAuthenticator() + @java_authenticator = java_realm.doGetAuthenticator() @java_users_provider = java_realm.getUsersProvider() @java_groups_provider = java_realm.getGroupsProvider() - @save_password = Java::OrgSonarServerUi::JRubyFacade.new.getSettings().getBoolean('sonar.security.savePassword') end - def authenticate?(username, password) + def authenticate?(username, password, servlet_request) + details=nil if @java_users_provider begin - details = @java_users_provider.doGetUserDetails(username) + provider_context = org.sonar.api.security.ExternalUsersProvider::Context.new(username, servlet_request) + details = @java_users_provider.doGetUserDetails(provider_context) rescue Exception => e Rails.logger.error("Error from external users provider: #{e.message}") - return false if !@save_password - return fallback(username, password) + @save_password ? fallback(username, password) : false else - # User exist in external system - return auth(username, password, details) if details - # No such user in external system - return fallback(username, password) + if details + # User exist in external system + auth(username, password, servlet_request, details) + else + # No such user in external system + fallback(username, password) + end end else # Legacy authenticator - return auth(username, password, nil) + auth(username, password, servlet_request, nil) end end @@ -73,39 +76,40 @@ class PluginRealm # Fallback to password from Sonar Database # def fallback(username, password) - user = User.find_active_by_login(username) - if user && user.authenticated?(password) - return user - else - return nil + result=nil + if !username.blank? && !password.blank? + user=User.find_active_by_login(username) + result = user if user && user.authenticated?(password) end + result end # - # Authenticate user using external system + # Authenticate user using external system. Return the user. # - def auth(username, password, details) + def auth(username, password, servlet_request, user_details) if @java_authenticator begin - status = @java_authenticator.authenticate(username, password) + authenticator_context=org.sonar.api.security.Authenticator::Context.new(username, password, servlet_request) + status = @java_authenticator.doAuthenticate(authenticator_context) rescue Exception => e Rails.logger.error("Error from external authenticator: #{e.message}") - return fallback(username, password) + fallback(username, password) else - return nil if !status - # Authenticated - return synchronize(username, password, details) + status ? synchronize(username, password, user_details) : nil end else # No authenticator - return nil + nil end end # # Authentication in external system was successful - replicate password, details and groups into Sonar + # Return the user. # def synchronize(username, password, details) + username=details.getName() if username.blank? && details user = User.find_by_login(username) if !user # No such user in Sonar database @@ -134,7 +138,7 @@ class PluginRealm user.active=true # Note that validation disabled user.save(false) - return user + user end def synchronize_groups(user) @@ -196,17 +200,15 @@ module NeedAuthentication # We really need a Dispatch Chain here or something. # This will also let us return a human error message. # - def authenticate(login, password) - return nil if login.blank? || password.blank? - + def authenticate(login, password, servlet_request) # Downcase login (typically for Active Directory) # Note that login in Sonar DB is case-sensitive, however in this case authentication and automatic user creation will always happen with downcase login downcase = Java::OrgSonarServerUi::JRubyFacade.new.getSettings().getBoolean('sonar.authenticator.downcase') - if downcase + if login && downcase login = login.downcase end - return RealmFactory.realm.authenticate?(login, password) + RealmFactory.realm.authenticate?(login, password, servlet_request) end def editable_password? diff --git a/sonar-server/src/main/webapp/WEB-INF/lib/slf4j_logger.rb b/sonar-server/src/main/webapp/WEB-INF/lib/slf4j_logger.rb index 3209901e17e..4907abe64bf 100644 --- a/sonar-server/src/main/webapp/WEB-INF/lib/slf4j_logger.rb +++ b/sonar-server/src/main/webapp/WEB-INF/lib/slf4j_logger.rb @@ -33,7 +33,7 @@ require 'java' # class Slf4jLogger def initialize(logger_name='rails') - @logger = Java::OrgSlf4j::LoggerFactory::getLogger(logger_name); + @logger = Java::OrgSlf4j::LoggerFactory::getLogger(logger_name) end attr_accessor :level @@ -101,6 +101,10 @@ class Slf4jLogger private + def to_s + @logger.getName() + end + def full_message(message, &block) if message.nil? if block_given? diff --git a/sonar-server/src/main/webapp/WEB-INF/web.xml b/sonar-server/src/main/webapp/WEB-INF/web.xml index db10b309cac..6b9231734a8 100644 --- a/sonar-server/src/main/webapp/WEB-INF/web.xml +++ b/sonar-server/src/main/webapp/WEB-INF/web.xml @@ -27,6 +27,10 @@ </context-param> <filter> + <filter-name>ServletFilters</filter-name> + <filter-class>org.sonar.server.platform.ServletFilters</filter-class> + </filter> + <filter> <filter-name>DatabaseSessionFilter</filter-name> <filter-class>org.sonar.server.ui.DatabaseSessionFilter</filter-class> </filter> @@ -75,6 +79,10 @@ <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> + <filter-name>ServletFilters</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> + <filter-mapping> <filter-name>RackFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> diff --git a/sonar-server/src/test/java/org/sonar/server/platform/ServletFiltersTest.java b/sonar-server/src/test/java/org/sonar/server/platform/ServletFiltersTest.java new file mode 100644 index 00000000000..b959d3c0665 --- /dev/null +++ b/sonar-server/src/test/java/org/sonar/server/platform/ServletFiltersTest.java @@ -0,0 +1,85 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.server.platform; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.web.ServletFilter; + +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Arrays; +import java.util.Collections; + +import static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.*; + +public class ServletFiltersTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void initAndDestroyFilters() throws Exception { + ServletFilter filter = mock(ServletFilter.class); + FilterConfig config = mock(FilterConfig.class); + ServletFilters filters = new ServletFilters(); + filters.init(config, Arrays.asList(filter)); + + assertThat(filters.getFilters()).containsOnly(filter); + verify(filter).init(config); + + filters.destroy(); + verify(filter).destroy(); + } + + @Test + public void initFilters_propagate_failure() throws Exception { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("foo"); + + ServletFilter filter = mock(ServletFilter.class); + doThrow(new IllegalStateException("foo")).when(filter).init(any(FilterConfig.class)); + + FilterConfig config = mock(FilterConfig.class); + ServletFilters filters = new ServletFilters(); + filters.init(config, Arrays.asList(filter)); + } + + @Test + public void doFilter_no_filters() throws Exception { + FilterConfig config = mock(FilterConfig.class); + ServletFilters filters = new ServletFilters(); + filters.init(config, Collections.<ServletFilter>emptyList()); + + ServletRequest request = mock(HttpServletRequest.class); + ServletResponse response = mock(HttpServletResponse.class); + FilterChain chain = mock(FilterChain.class); + filters.doFilter(request, response, chain); + + verify(chain).doFilter(request, response); + } +} |