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:
parent
dce95ae8a9
commit
45da618565
@ -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
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
@ -76,6 +76,8 @@ public class WebModule extends LifecycleModule {
|
||||
bind(ProxyProperties.class).toProvider(ProxyPropertiesProvider.class);
|
||||
|
||||
listener().toInstance(registerInParentInjectors());
|
||||
|
||||
install(UniversalWebLoginFilter.module());
|
||||
}
|
||||
|
||||
private void installAuthModule() {
|
||||
|
Loading…
Reference in New Issue
Block a user