-
Notifications
You must be signed in to change notification settings - Fork 1
GSIP 151
Alessio Fabiani (GeoSolutions)
This proposal is for GeoServer 2.9 and 2.10.
- Under Discussion
- In Progress
- Completed
- Rejected
- Deferred
With the introduction of new GeoServer Security Plugins, especially the ones requiring user to login on an external site, it would be useful to have multiple, pluggable, login buttons allowing the user to choose the login method to access the GeoServer WEB UI.
The idea would be to allow security plugin to easily configure a login end-point which is rendered as an extension by the GeoServer Base Page, similar to the figure shown below
Allow the GeoServer Base Page to:
- Hide the default “form” login module if the “form filter chain” has been disabled
- Scan for login endpoints through GeoServer Extensions and render login buttons accordingly
- Allow Security Plugins to easily declare specific login endpoints extensions and icons to be rendered on the GeoServer Base Page
In order to do this, we propose the introduction of a new “ComponentInfo” base class on GeoServer Web Core module
/* (c) 2016 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.web;
/**
* Information about a login form that should be shown from the main page in the GeoServer UI.
* The "order" field is a sort key for the link within the category.
*
* @author Alessio Fabiani, GeoSolutions S.A.S.
*/
@SuppressWarnings("serial")
public class LoginFormInfo extends ComponentInfo<GeoServerBasePage> implements Comparable<LoginFormInfo> {
String name;
String icon = "";
private String include = "";
private String loginPath;
/**
* Name of the login extension; it will determine also the order displayed for the icons
*
* @param name
*/
public void setName(String name){
this.name = name;
}
/**
* Path to the icon; the graphic file must be places under resources on the same
* package of the "componentClass"
*
* @return
*/
public String getIcon() {
return icon;
}
/**
* Path to the icon; the graphic file must be places under resources on the same
* package of the "componentClass"
*
* @param icon
*/
public void setIcon(String icon) {
this.icon = icon;
}
/**
* Static HTML Resource to include in the form (if needed).
*
* @return the include
*/
public String getInclude() {
return include;
}
/**
* Static HTML Resource to include in the form (if needed).
*
* @param include the include to set
*/
public void setInclude(String include) {
this.include = include;
}
/**
* Name of the login extension; it will determine also the order displayed for the icons
*
* @return
*/
public String getName(){
return name;
}
/**
* Authentication Security Endpoint invoked by the pluggable form
*
* @return the loginPath
*/
public String getLoginPath() {
return loginPath;
}
/**
* Authentication Security Endpoint invoked by the pluggable form
*
* @param loginPath the loginPath to set
*/
public void setLoginPath(String loginPath) {
this.loginPath = loginPath;
}
/**
* Sorts by name the Login extensions
*/
public int compareTo(LoginFormInfo other){
return getName().compareTo(other.getName());
}
}
A security plugin exposing a specific authentication endpoint and willing to show a new “login button” on the GeoServer Base Page, can easily declare it via “applicationContext.xml” resources like as follows:
<!-- login button -->
<bean id="googleOAuth2AuthLoginButton" class="org.geoserver.web.LoginFormInfo">
<property name="id" value="googleOAuth2LoginInfo" />
<!-- property name="titleKey" value="GoogleOAuth2AuthProviderPanel.login" / -->
<property name="descriptionKey" value="GoogleOAuth2AuthProviderPanel.description" />
<property name="componentClass" value="org.geoserver.web.security.oauth2.GoogleOAuth2AuthProviderPanel" />
<property name="name" value="google" />
<property name="icon" value="google.png" />
<!-- property name="include" value="include_login_form.html" / -->
<property name="loginPath" value="j_spring_outh2_google_login" />
</bean>
In order to enable GeoServer Base Page to render those elements, we will need to slightly modify the “GeoServerBasePage” wicket core class like this
GeoServerBasePage.java
...
// login / logout stuff
List<String> securityFilters =
getGeoServerApplication().getSecurityManager().getSecurityConfig().getFilterChain().filtersFor("/web/**");
// login forms
final Authentication user = GeoServerSession.get().getAuthentication();
final boolean anonymous = user == null || user instanceof AnonymousAuthenticationToken;
List<LoginFormInfo> loginforms = filterByAuth(getGeoServerApplication().getBeansOfType(LoginFormInfo.class));
add(new ListView<LoginFormInfo>("loginforms", loginforms) {
public void populateItem(ListItem<LoginFormInfo> item) {
LoginFormInfo info = item.getModelObject();
WebMarkupContainer loginForm = new WebMarkupContainer("loginform") {
protected void onComponentTag(org.apache.wicket.markup.ComponentTag tag) {
String path = getRequest().getUrl().getPath();
StringBuilder loginPath = new StringBuilder();
if(path.isEmpty()) {
// home page
loginPath.append("../" + info.getLoginPath());
} else {
// boomarkable page of sorts
String[] pathElements = path.split("/");
for (String pathElement : pathElements) {
if(!pathElement.isEmpty()) {
loginPath.append("../");
}
}
loginPath.append(info.getLoginPath());
}
tag.put("action", loginPath);
};
};
Image image;
if(info.getIcon() != null) {
image = new Image("link.icon", new PackageResourceReference(info.getComponentClass(), info.getIcon()));
} else {
image = new Image("link.icon", new PackageResourceReference(GeoServerBasePage.class, "img/icons/silk/door-in.png"));
}
loginForm.add(image);
if (info.getTitleKey() != null && !info.getTitleKey().isEmpty()) {
loginForm.add(new Label("link.label", new StringResourceModel(info.getTitleKey(), (Component) null, null)));
image.add(AttributeModifier.replace("alt", new ParamResourceModel(info.getTitleKey(), null)));
} else {
loginForm.add(new Label("link.label", ""));
}
LoginFormHTMLInclude include;
if (info.getInclude() != null) {
include = new LoginFormHTMLInclude("login.include",
new PackageResourceReference(info.getComponentClass(), info.getInclude()));
} else {
include = new LoginFormHTMLInclude("login.include",
new PackageResourceReference(GeoServerBasePage.class, ""));
}
loginForm.add(include);
item.add(loginForm);
boolean filterInChain = false;
for (String filterName : securityFilters) {
if (filterName.toLowerCase().contains(info.getName())) {
filterInChain = true;
break;
}
}
loginForm.setVisible(anonymous && filterInChain);
}
});
// logout forms
...
GeoServerBasePage.html
...
<div id="header">
<div class="wrap">
<h2><a wicket:id="home" class="pngfix" href="#"><span wicket:id="label">GeoServer 2.0</span></a></h2>
<div class="button-group selfclear">
<span wicket:id="loginforms">
<form style="display: inline-block;" wicket:id="loginform" method="post" action="../j_spring_security_check">
<span wicket:id="login.include"></span>
<button class="positive icon" type="submit">
<div><img src="#" wicket:id="link.icon"/><span wicket:id="link.label"></span></div>
</button>
<script type="text/javascript">
$('input, textarea').placeholder();
</script>
</form>
</span>
<div wicket:id="logoutform">
<a class="button-logout icon" href="j_spring_security_logout"><span><wicket:message key="logout">Logout</wicket:message></span></a>
<span class="username"><wicket:message key="loggedInAs">Logged in as</wicket:message> <span wicket:id="username">User von Testenheimer</span></span>.
</div>
</div>
</div><!-- /.wrap -->
</div><!-- /#header -->
...
Notice that the configuration of the LoginFormInfo
allows to include static HTML into the Form.
In order to do that we propose the introduction of a new utility class LoginFormHTMLInclude
which will scan the declared class package and will render the static HTML into the login form
/* (c) 2016 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.web;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.MarkupStream;
import org.apache.wicket.markup.html.include.Include;
import org.apache.wicket.request.resource.PackageResourceReference;
import org.geotools.util.logging.Logging;
/**
* @author Alessio Fabiani, GeoSolutions S.A.S.
*
*/
public class LoginFormHTMLInclude extends Include {
protected static final Logger LOGGER = Logging.getLogger(LoginFormHTMLInclude.class);
/** serialVersionUID */
private static final long serialVersionUID = 2413413223248385722L;
private PackageResourceReference resourceReference;
public LoginFormHTMLInclude(String id, PackageResourceReference packageResourceReference) {
super(id);
this.resourceReference = packageResourceReference;
}
@Override
public void onComponentTagBody(final MarkupStream markupStream, final ComponentTag openTag) {
String content = importAsString();
replaceComponentTagBody(markupStream, openTag, content);
}
/**
* Imports the contents of the url of the model object.
*
* @return the imported contents
*/
@Override
protected String importAsString() {
try {
InputStream inputStream = this.resourceReference.getResource().getResourceStream()
.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line + "\n");
}
br.close();
return sb.toString();
} catch (Exception ex) {
LOGGER.log(Level.FINEST, "Problem reading resource contents.", ex);
}
return "";
}
}
Such approach allows us to make the standard Login Form also pluggable.
The old code will be fully replaced by the following applicaionContext.xml
configuration
...
<bean id="adminRequestCallback" class="org.geoserver.web.AdminRequestWicketCallback"/>
<!-- login button -->
<bean id="geoserverFormLoginButton" class="org.geoserver.web.LoginFormInfo">
<property name="id" value="geoserverFormLoginButton" />
<property name="titleKey" value="GeoServerBasePage.title" />
<property name="descriptionKey" value="GeoServerBasePage.description" />
<property name="componentClass" value="org.geoserver.web.GeoServerBasePage" />
<property name="name" value="form" />
<property name="icon" value="img/icons/silk/door-in.png" />
<property name="include" value="include_login_form.html" />
<property name="loginPath" value="j_spring_security_check" />
</bean>
</beans>
Although Security Filters implementing LogOutHandlers
usually do checks in order to understand if they are allowed to do redirection or not, among this proposal we would like to allow also the possibility of plug specific Logout buttons which will invoke different Logout Endpoints
.
Similarly to the LoginFormInfo
, the proposal is to render pluggable logout buttons (with customisable icons and labels) which will invoke specific logout endpoints, intercepted by the associated Seurity Filters.
It is worth notice that the proposed changes are not invasive and easily portable back to previous GeoServer versions.
Please see "Email Discussion" below.
No issues.
Project Steering Committee:
- Alessio Fabiani: +1
- Andrea Aime: +1
- Ben Caradoc-Davies: +1
- Brad Hards
- Christian Mueller: +1
- Ian Turton: +1
- Jody Garnett: +1
- Jukka Rahkonen
- Kevin Smith
- Simone Giannecchini: +1
Committers:
- @afabiani
©2020 Open Source Geospatial Foundation