Skip to content

GSIP 151

Alessio Fabiani edited this page Oct 26, 2016 · 8 revisions

GSIP 151 - Pluggable Login Web Component

Overview

Proposed By

Alessio Fabiani (GeoSolutions)

Assigned to Release

This proposal is for GeoServer 2.9, 2.10 and 2.11.

State

  • Under Discussion
  • In Progress
  • Completed
  • Rejected
  • Deferred

Motivation

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

GeoServer Base Page Login Form

GeoServer Login Page

Proposal

Allow the GeoServer Base Page to:

  1. Hide the default “form” login module if the “form filter chain” has been disabled
  2. Scan for login endpoints through GeoServer Extensions and render login buttons accordingly
  3. 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 -->
  ... 

Include static HTML on the Login Form

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>

Logout Handlers

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.

Conclusions

It is worth notice that the proposed changes are not invasive and easily portable back to previous GeoServer versions.

Feedbacks

Please see "Email Discussion" below.

Backwards Compatibility

No issues.

Voting

Project Steering Committee:

  • Alessio Fabiani: +1
  • Andrea Aime: +1
  • Ben Caradoc-Davies: +1
  • Brad Hards: +1
  • Christian Mueller: +1
  • Ian Turton: +1
  • Jody Garnett: +1
  • Jukka Rahkonen: +1
  • Kevin Smith: +1
  • Simone Giannecchini: +1

Committers:

  • @afabiani

Links

Clone this wiki locally