aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Brandhof <simon.brandhof@gmail.com>2012-05-08 09:14:29 +0200
committerSimon Brandhof <simon.brandhof@gmail.com>2012-05-08 10:23:02 +0200
commit9e46b4bb203a60742f0cb58cd621a58496b7ed33 (patch)
treece4a6aac43db33c25c4bf48b6c22a9f63e88e42c
parentbd0abcb3f754f4c123f9ce3145e2cf814210b91e (diff)
downloadsonarqube-9e46b4bb203a60742f0cb58cd621a58496b7ed33.tar.gz
sonarqube-9e46b4bb203a60742f0cb58cd621a58496b7ed33.zip
SONAR-2950 Single Sign On with external authentication mechanism
-rw-r--r--sonar-plugin-api/pom.xml15
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/security/Authenticator.java70
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/security/ExternalUsersProvider.java44
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/security/LoginPasswordAuthenticator.java1
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/security/SecurityRealm.java22
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/security/UserDetails.java2
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/security/package-info.java23
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java84
-rw-r--r--sonar-plugin-api/src/test/java/org/sonar/api/security/ExternalUsersProviderTest.java67
-rw-r--r--sonar-plugin-api/src/test/java/org/sonar/api/security/SecurityRealmTest.java59
-rw-r--r--sonar-plugin-api/src/test/java/org/sonar/api/web/ServletFilterTest.java61
-rw-r--r--sonar-server/src/dev/web.xml9
-rw-r--r--sonar-server/src/main/java/org/sonar/server/platform/ServletFilters.java113
-rw-r--r--sonar-server/src/main/webapp/WEB-INF/app/controllers/account_controller.rb2
-rw-r--r--sonar-server/src/main/webapp/WEB-INF/app/controllers/sessions_controller.rb4
-rw-r--r--sonar-server/src/main/webapp/WEB-INF/lib/authenticated_system.rb4
-rw-r--r--sonar-server/src/main/webapp/WEB-INF/lib/need_authentication.rb74
-rw-r--r--sonar-server/src/main/webapp/WEB-INF/lib/slf4j_logger.rb6
-rw-r--r--sonar-server/src/main/webapp/WEB-INF/web.xml8
-rw-r--r--sonar-server/src/test/java/org/sonar/server/platform/ServletFiltersTest.java85
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);
+ }
+}