]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-2950 Single Sign On with external authentication mechanism
authorSimon Brandhof <simon.brandhof@gmail.com>
Tue, 8 May 2012 07:14:29 +0000 (09:14 +0200)
committerSimon Brandhof <simon.brandhof@gmail.com>
Tue, 8 May 2012 08:23:02 +0000 (10:23 +0200)
20 files changed:
sonar-plugin-api/pom.xml
sonar-plugin-api/src/main/java/org/sonar/api/security/Authenticator.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/security/ExternalUsersProvider.java
sonar-plugin-api/src/main/java/org/sonar/api/security/LoginPasswordAuthenticator.java
sonar-plugin-api/src/main/java/org/sonar/api/security/SecurityRealm.java
sonar-plugin-api/src/main/java/org/sonar/api/security/UserDetails.java
sonar-plugin-api/src/main/java/org/sonar/api/security/package-info.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java [new file with mode: 0644]
sonar-plugin-api/src/test/java/org/sonar/api/security/ExternalUsersProviderTest.java [new file with mode: 0644]
sonar-plugin-api/src/test/java/org/sonar/api/security/SecurityRealmTest.java [new file with mode: 0644]
sonar-plugin-api/src/test/java/org/sonar/api/web/ServletFilterTest.java [new file with mode: 0644]
sonar-server/src/dev/web.xml
sonar-server/src/main/java/org/sonar/server/platform/ServletFilters.java [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/controllers/account_controller.rb
sonar-server/src/main/webapp/WEB-INF/app/controllers/sessions_controller.rb
sonar-server/src/main/webapp/WEB-INF/lib/authenticated_system.rb
sonar-server/src/main/webapp/WEB-INF/lib/need_authentication.rb
sonar-server/src/main/webapp/WEB-INF/lib/slf4j_logger.rb
sonar-server/src/main/webapp/WEB-INF/web.xml
sonar-server/src/test/java/org/sonar/server/platform/ServletFiltersTest.java [new file with mode: 0644]

index 22a6a4fdef2519ace78994502f1b0ca274a3759e..93e5a4a75225d0355eda6e1496d04bc726b8db50 100644 (file)
@@ -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>
       <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>
       <artifactId>dbunit</artifactId>
       <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>
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 (file)
index 0000000..2420eaf
--- /dev/null
@@ -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;
+    }
+  }
+}
index 00f7dfb4dac2958632b55304b7f1691132f15176..b7c93c59fb2abe09e6931ae4c454c4343f800933 100644 (file)
  */
 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;
+    }
+  }
 }
index 8ca6f97864f254ec24377f1e3d43b1dff4a12f52..2883275a24eb898396f7d674ac9e07e12c71f351 100644 (file)
@@ -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 {
 
index f21d4d26946e98d63360baa166926a9f21d26080..16a122933ef441a4568de7da4f26e21e1781a9b5 100644 (file)
@@ -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;
   }
-
 }
index e3519866a132b6b442626783a088724d6b4c1406..2d4afc0524cb88ec8380796831c064a8f39103b0 100644 (file)
@@ -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 (file)
index 0000000..8658aba
--- /dev/null
@@ -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 (file)
index 0000000..983f7fc
--- /dev/null
@@ -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 (file)
index 0000000..f1ed1a0
--- /dev/null
@@ -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 (file)
index 0000000..e22eabc
--- /dev/null
@@ -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 (file)
index 0000000..398cbb6
--- /dev/null
@@ -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();
+  }
+}
index a613bea2a3c13f7124d0b835a3e34721e95ba34c..b537e1dd48ae987414e700102b0053956121b70e 100644 (file)
     <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>
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 (file)
index 0000000..303a1e6
--- /dev/null
@@ -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);
+    }
+  }
+}
index a643b9d1d7904bba89fb48300e3fa2a24ade81f5..fa963c06168c6cd5461b479cb5f8ed6e9f26d630 100644 (file)
@@ -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]
index 9edb145c3c0689b4c0104fb6f2870cec4944bf3d..3098ad32268629985d130c8bd91b002c3af5eece 100644 (file)
@@ -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
index d05adeae8d1940730ea82113ace602393d74da4a..e5f7711a472fa563d96c58b48e4ada3b4e8a1b5d 100644 (file)
@@ -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
     
index a1f8fcc9ccb7d7b3ad09f506e6f73f0557b49eb8..7e3ffd3a7e021da099652f7f6c57dd9259fabcb7 100644 (file)
 # 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?
index 3209901e17e3e8c53f3e682c82468e5e94028271..4907abe64bfdd7fd7f977c24e7a7d118914d7851 100644 (file)
@@ -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?
index db10b309cac06d4720c5f5171b0041969f2f3fbb..6b9231734a8eedcd9f504ff136171399f9b6eb00 100644 (file)
     <param-value>1</param-value>
   </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-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>
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 (file)
index 0000000..b959d3c
--- /dev/null
@@ -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);
+  }
+}