Allow to listen to Web login/logout events

When using Gerrit with external authentication systems (OAuth or other)
it would be necessary to enforce additional requirements (e.g. 2-factor)
or introduce some plug-in specific post-login screen for finalising
the user's on boarding.

Similarly when the user logs out, we may need to invalidate its token
from an external SSO system or to perform some other plugin-specific
operations or even simply request a feedback.

With the introduction of this new extension point WebLoginListener
it is possible to filter the HTTP response and override the status
code to redirect or perform additional adjustments to comply with
the company or the plugin's requirements.

It is possible to experiment this new extension with a simple
Groovy scripting plugin (see below).

```
import com.google.gerrit.extensions.annotations.*
import javax.servlet.http.*
import com.google.inject.*
import com.google.gerrit.httpd.*
import com.google.gerrit.server.*

@Singleton
@Listen
public class MyPostLogin implements WebLoginListener {

  public void onLogin(IdentifiedUser user,
                      HttpServletRequest req,
                      HttpServletResponse resp) {
    println "Post-login user=$user"
    resp.sendRedirect("https://twophase.mycompany.com/auth")
  }
  public void onLogout(IdentifiedUser user,
                       HttpServletRequest req,
                       HttpServletResponse resp) {
    println "Post-logout user=$user"
    resp.sendRedirect("https://ssologout.mycompany.com")
  }
}
```

Change-Id: I76e8ec040072e317061234665a0d865927da55b9
This commit is contained in:
Luca Milanesio 2016-05-12 11:33:30 +01:00
parent dce95ae8a9
commit 45da618565
5 changed files with 252 additions and 0 deletions

View File

@ -418,6 +418,14 @@ Garbage collection ran on a project
+
Update of the secondary index
* `com.google.gerrit.httpd.WebLoginListener`:
+
User login or logout interactively on the Web user interface.
The event listener is under the Gerrit http package to automatically
inherit the javax.servlet.http dependencies and allowing to influence
the login or logout flow with additional redirections.
[[stream-events]]
== Sending Events to the Events Stream

View File

@ -0,0 +1,80 @@
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.httpd;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
/**
* HttpServletResponse wrapper to allow response status code override.
*
* Differently from the normal HttpServletResponse, this class allows multiple
* filters to override the response http status code.
*/
public class HttpServletResponseRecorder extends HttpServletResponseWrapper {
private static final Logger log = LoggerFactory
.getLogger(HttpServletResponseWrapper.class);
private int status;
private String statusMsg = "";
/**
* Constructs a response recorder wrapping the given response.
*
* @param response the response to be wrapped
*/
public HttpServletResponseRecorder(HttpServletResponse response) {
super(response);
}
@Override
public void sendError(int sc) throws IOException {
this.status = sc;
}
@Override
public void sendError(int sc, String msg) throws IOException {
this.status = sc;
this.statusMsg = msg;
}
@Override
public void sendRedirect(String location) throws IOException {
this.status = SC_MOVED_TEMPORARILY;
super.setHeader("Location", location);
}
@Override
public int getStatus() {
return status;
}
void play() throws IOException {
if (status != 0) {
log.debug("Replaying {} {}", status, statusMsg);
if (status == SC_MOVED_TEMPORARILY) {
super.sendRedirect(getHeader("Location"));
} else {
super.sendError(status, statusMsg);
}
}
}
}

View File

@ -0,0 +1,104 @@
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.httpd;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.servlet.ServletModule;
import java.io.IOException;
import java.util.Optional;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class UniversalWebLoginFilter implements Filter {
private final DynamicItem<WebSession> session;
private final DynamicSet<WebLoginListener> webLoginListeners;
private final Provider<CurrentUser> userProvider;
public static ServletModule module() {
return new ServletModule() {
@Override
protected void configureServlets() {
filter("/login*", "/logout*").through(UniversalWebLoginFilter.class);
bind(UniversalWebLoginFilter.class).in(Singleton.class);
DynamicSet.setOf(binder(), WebLoginListener.class);
}
};
}
@Inject
public UniversalWebLoginFilter(DynamicItem<WebSession> session,
DynamicSet<WebLoginListener> webLoginListeners,
Provider<CurrentUser> userProvider) {
this.session = session;
this.webLoginListeners = webLoginListeners;
this.userProvider = userProvider;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponseRecorder wrappedResponse =
new HttpServletResponseRecorder((HttpServletResponse) response);
Optional<IdentifiedUser> loggedInUserBefore = loggedInUser();
chain.doFilter(request, wrappedResponse);
Optional<IdentifiedUser> loggedInUserAfter = loggedInUser();
if (!loggedInUserBefore.isPresent() && loggedInUserAfter.isPresent()) {
for (WebLoginListener loginListener : webLoginListeners) {
loginListener.onLogin(loggedInUserAfter.get(), httpRequest,
wrappedResponse);
}
} else if (loggedInUserBefore.isPresent() && !loggedInUserAfter.isPresent()) {
for (WebLoginListener loginListener : webLoginListeners) {
loginListener.onLogout(loggedInUserBefore.get(), httpRequest,
wrappedResponse);
}
}
wrappedResponse.play();
}
private Optional<IdentifiedUser> loggedInUser() {
return session.get().isSignedIn() ?
Optional.of(userProvider.get().asIdentifiedUser()) :
Optional.empty();
}
@Override
public void destroy() {
}
}

View File

@ -0,0 +1,58 @@
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.httpd;
import com.google.gerrit.extensions.annotations.ExtensionPoint;
import com.google.gerrit.server.IdentifiedUser;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Allows to listen and override the reponse to login/logout web actions.
*
* Allows to intercept and act when a Gerrit user logs in or logs out of
* the Web interface to perform actions or to override the output response
* status code.
*
* Typical use can be multi-factor authentication (on login) or global sign-out
* from SSO systems (on logout).
*
*/
@ExtensionPoint
public interface WebLoginListener {
/**
* Invoked after a user's web login.
*
* @param userId logged in user
* @param request request of the latest login action
* @param response response of the latest login action
*/
void onLogin(IdentifiedUser userId, HttpServletRequest request,
HttpServletResponse response) throws IOException;
/**
* Invoked after a user's web logout.
*
* @param userId logged out user
* @param request request of the latest logout action
* @param response response of the latest logout action
*/
void onLogout(IdentifiedUser userId, HttpServletRequest request,
HttpServletResponse response) throws IOException;
}

View File

@ -76,6 +76,8 @@ public class WebModule extends LifecycleModule {
bind(ProxyProperties.class).toProvider(ProxyPropertiesProvider.class);
listener().toInstance(registerInParentInjectors());
install(UniversalWebLoginFilter.module());
}
private void installAuthModule() {