de.theit.hudson.crowd.CrowdSecurityRealm.java Source code

Java tutorial

Introduction

Here is the source code for de.theit.hudson.crowd.CrowdSecurityRealm.java

Source

/*
 * @(#)CrowdSecurityRealm.java
 * 
 * The MIT License
 * 
 * Copyright (C)2011 Thorsten Heit.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package de.theit.hudson.crowd;

import static de.theit.hudson.crowd.ErrorMessages.accountExpired;
import static de.theit.hudson.crowd.ErrorMessages.applicationPermission;
import static de.theit.hudson.crowd.ErrorMessages.cannotLoadCrowdProperties;
import static de.theit.hudson.crowd.ErrorMessages.expiredCredentials;
import static de.theit.hudson.crowd.ErrorMessages.groupNotFound;
import static de.theit.hudson.crowd.ErrorMessages.invalidAuthentication;
import static de.theit.hudson.crowd.ErrorMessages.operationFailed;
import static de.theit.hudson.crowd.ErrorMessages.specifyApplicationName;
import static de.theit.hudson.crowd.ErrorMessages.specifyApplicationPassword;
import static de.theit.hudson.crowd.ErrorMessages.specifyCrowdUrl;
import static de.theit.hudson.crowd.ErrorMessages.specifyGroup;
import static de.theit.hudson.crowd.ErrorMessages.specifySessionValidationInterval;
import static de.theit.hudson.crowd.ErrorMessages.userNotFound;
import static de.theit.hudson.crowd.ErrorMessages.userNotValid;
import hudson.Extension;
import hudson.model.Descriptor;
import hudson.model.Hudson;
import hudson.security.AbstractPasswordBasedSecurityRealm;
import hudson.security.GroupDetails;
import hudson.security.SecurityRealm;
import hudson.util.FormValidation;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.Filter;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;

import org.acegisecurity.AccountExpiredException;
import org.acegisecurity.AuthenticationException;
import org.acegisecurity.AuthenticationManager;
import org.acegisecurity.AuthenticationServiceException;
import org.acegisecurity.BadCredentialsException;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.InsufficientAuthenticationException;
import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.userdetails.UserDetailsService;
import org.acegisecurity.userdetails.UsernameNotFoundException;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataRetrievalFailureException;

import com.atlassian.crowd.exception.ApplicationPermissionException;
import com.atlassian.crowd.exception.ExpiredCredentialException;
import com.atlassian.crowd.exception.GroupNotFoundException;
import com.atlassian.crowd.exception.InactiveAccountException;
import com.atlassian.crowd.exception.InvalidAuthenticationException;
import com.atlassian.crowd.exception.OperationFailedException;
import com.atlassian.crowd.exception.UserNotFoundException;
import com.atlassian.crowd.integration.http.CrowdHttpAuthenticatorImpl;
import com.atlassian.crowd.integration.http.util.CrowdHttpTokenHelperImpl;
import com.atlassian.crowd.integration.http.util.CrowdHttpValidationFactorExtractorImpl;
import com.atlassian.crowd.integration.rest.service.factory.RestCrowdClientFactory;
import com.atlassian.crowd.model.group.Group;
import com.atlassian.crowd.model.user.User;
import com.atlassian.crowd.service.client.ClientPropertiesImpl;

/**
 * This class provides the security realm for authenticating users against a
 * remote Crowd server.
 * 
 * @author <a href="mailto:theit@gmx.de">Thorsten Heit (theit@gmx.de)</a>
 * @since 06.09.2011
 * @version $Id$
 */
public class CrowdSecurityRealm extends AbstractPasswordBasedSecurityRealm {
    /** Used for logging purposes. */
    private static final Logger LOG = Logger.getLogger(CrowdSecurityRealm.class.getName());

    /** Contains the Crowd server URL. */
    public final String url;

    /** Contains the application name to access Crowd. */
    public final String applicationName;

    /** Contains the application password to access Crowd. */
    public final String password;

    /** Contains the Crowd group to which a user must belong to. */
    public final String group;

    /** Specifies whether nested groups can be used. */
    public final boolean nestedGroups;

    /**
     * The number of minutes to cache authentication validation in the session.
     * If this value is set to 0, each HTTP request will be authenticated with
     * the Crowd server.
     */
    private final int sessionValidationInterval;

    /**
     * The configuration data necessary for accessing the services on the remote
     * Crowd server.
     */
    transient private CrowdConfigurationService configuration;

    /**
     * Default constructor. Fields in config.jelly must match the parameter
     * names in the "DataBoundConstructor".
     * 
     * @param url
     *            The URL for Crowd.
     * @param applicationName
     *            The application name.
     * @param password
     *            The application password.
     * @param group
     *            The group to which users must belong to. If this parameter is
     *            not specified, a users group membership will not be checked.
     * @param nestedGroups
     *            <code>true</code> when nested groups may be used.
     *            <code>false</code> else.
     * @param sessionValidationInterval
     *            The number of minutes to cache authentication validation in
     *            the session. If this value is set to <code>0</code>, each HTTP
     *            request will be authenticated with the Crowd server.
     */
    @SuppressWarnings("hiding")
    @DataBoundConstructor
    public CrowdSecurityRealm(String url, String applicationName, String password, String group,
            boolean nestedGroups, int sessionValidationInterval) {
        this.url = url.trim();
        this.applicationName = applicationName.trim();
        this.password = password.trim();
        this.group = group.trim();
        this.nestedGroups = nestedGroups;
        this.sessionValidationInterval = sessionValidationInterval;
    }

    /**
     * Initializes all objects necessary to talk to / with Crowd.
     */
    private void initializeConfiguration() {
        // configure the ClientProperties object
        Properties props = new Properties();
        try {
            props.load(getClass().getResourceAsStream("/crowd.properties"));
        } catch (IOException ex) {
            LOG.log(Level.SEVERE, cannotLoadCrowdProperties(), ex);
        }

        if (this.applicationName != null || this.password != null || this.url != null) {
            String crowdUrl = this.url;
            if (!crowdUrl.endsWith("/")) {
                crowdUrl += "/";
            }
            props.setProperty("application.name", this.applicationName);
            props.setProperty("application.password", this.password);
            props.setProperty("crowd.base.url", crowdUrl);
            props.setProperty("application.login.url", crowdUrl + "console/");
            props.setProperty("crowd.server.url", this.url + "services/");
            props.setProperty("session.validationinterval", String.valueOf(this.sessionValidationInterval));
        } else {
            LOG.warning("Client properties are incomplete");
        }

        this.configuration = new CrowdConfigurationService(this.group, this.nestedGroups);

        this.configuration.clientProperties = ClientPropertiesImpl.newInstanceFromProperties(props);
        this.configuration.crowdClient = new RestCrowdClientFactory()
                .newInstance(this.configuration.clientProperties);

        this.configuration.tokenHelper = CrowdHttpTokenHelperImpl
                .getInstance(CrowdHttpValidationFactorExtractorImpl.getInstance());
        this.configuration.crowdHttpAuthenticator = new CrowdHttpAuthenticatorImpl(this.configuration.crowdClient,
                this.configuration.clientProperties, this.configuration.tokenHelper);
    }

    /**
     * {@inheritDoc}
     * 
     * @see hudson.security.SecurityRealm#createSecurityComponents()
     */
    @Override
    public SecurityComponents createSecurityComponents() {
        if (null == this.configuration) {
            initializeConfiguration();
        }

        CrowdRememberMeServices ssoService = new CrowdRememberMeServices(this.configuration);

        AuthenticationManager crowdAuthenticationManager = new CrowdAuthenticationManager(this.configuration);
        UserDetailsService crowdUserDetails = new CrowdUserDetailsService(this.configuration);

        return new SecurityComponents(crowdAuthenticationManager, crowdUserDetails, ssoService);
    }

    /**
     * {@inheritDoc}
     * 
     * @see hudson.security.SecurityRealm#doLogout(org.kohsuke.stapler.StaplerRequest,
     *      org.kohsuke.stapler.StaplerResponse)
     */
    @Override
    public void doLogout(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
        SecurityRealm realm = Hudson.getInstance().getSecurityRealm();

        if (realm instanceof CrowdSecurityRealm
                && realm.getSecurityComponents().rememberMe instanceof CrowdRememberMeServices) {
            ((CrowdRememberMeServices) realm.getSecurityComponents().rememberMe).logout(req, rsp);
        }

        super.doLogout(req, rsp);
    }

    /**
     * {@inheritDoc}
     * 
     * @see hudson.security.SecurityRealm#createFilter(javax.servlet.FilterConfig)
     */
    @Override
    public Filter createFilter(FilterConfig filterConfig) {
        if (null == this.configuration) {
            initializeConfiguration();
        }

        Filter defaultFilter = super.createFilter(filterConfig);

        return new CrowdServletFilter(this, this.configuration, defaultFilter);
    }

    /**
     * {@inheritDoc}
     * 
     * @see hudson.security.AbstractPasswordBasedSecurityRealm#loadUserByUsername(java.lang.String)
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
        return getSecurityComponents().userDetails.loadUserByUsername(username);
    }

    /**
     * {@inheritDoc}
     * 
     * @see hudson.security.SecurityRealm#loadGroupByGroupname(java.lang.String)
     */
    @Override
    public GroupDetails loadGroupByGroupname(String groupname)
            throws UsernameNotFoundException, DataAccessException {

        try {
            // load the user object from the remote Crowd server
            if (LOG.isLoggable(Level.FINER)) {
                LOG.finer("Trying to load group: " + groupname);
            }
            final Group crowdGroup = this.configuration.crowdClient.getGroup(groupname);

            return new GroupDetails() {
                @Override
                public String getName() {
                    return crowdGroup.getName();
                }
            };
        } catch (GroupNotFoundException ex) {
            if (LOG.isLoggable(Level.INFO)) {
                LOG.info(groupNotFound(groupname));
            }
            throw new DataRetrievalFailureException(groupNotFound(groupname), ex);
        } catch (ApplicationPermissionException ex) {
            LOG.warning(applicationPermission());
            throw new DataRetrievalFailureException(applicationPermission(), ex);
        } catch (InvalidAuthenticationException ex) {
            LOG.warning(invalidAuthentication());
            throw new DataRetrievalFailureException(invalidAuthentication(), ex);
        } catch (OperationFailedException ex) {
            LOG.log(Level.SEVERE, operationFailed(), ex);
            throw new DataRetrievalFailureException(operationFailed(), ex);
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see hudson.security.AbstractPasswordBasedSecurityRealm#authenticate(java.lang.String,
     *      java.lang.String)
     */
    @Override
    protected UserDetails authenticate(String pUsername, String pPassword) throws AuthenticationException {
        // ensure that the group is available, active and that the user
        // is a member of it
        if (!this.configuration.isGroupMember(pUsername)) {
            throw new InsufficientAuthenticationException(
                    userNotValid(pUsername, this.configuration.allowedGroupNames));
        }

        User user;
        try {
            // authenticate user
            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("Authenticate user '" + pUsername + "' using password '"
                        + (null != pPassword ? "<available>'" : "<not specified>'"));
            }
            user = this.configuration.crowdClient.authenticateUser(pUsername, pPassword);
        } catch (UserNotFoundException ex) {
            if (LOG.isLoggable(Level.INFO)) {
                LOG.info(userNotFound(pUsername));
            }
            throw new BadCredentialsException(userNotFound(pUsername), ex);
        } catch (ExpiredCredentialException ex) {
            LOG.warning(expiredCredentials(pUsername));
            throw new BadCredentialsException(expiredCredentials(pUsername), ex);
        } catch (InactiveAccountException ex) {
            LOG.warning(accountExpired(pUsername));
            throw new AccountExpiredException(accountExpired(pUsername), ex);
        } catch (ApplicationPermissionException ex) {
            LOG.warning(applicationPermission());
            throw new AuthenticationServiceException(applicationPermission(), ex);
        } catch (InvalidAuthenticationException ex) {
            LOG.warning(invalidAuthentication());
            throw new AuthenticationServiceException(invalidAuthentication(), ex);
        } catch (OperationFailedException ex) {
            LOG.log(Level.SEVERE, operationFailed(), ex);
            throw new AuthenticationServiceException(operationFailed(), ex);
        }

        // create the list of granted authorities
        List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
        // add the "authenticated" authority to the list of granted
        // authorities...
        authorities.add(SecurityRealm.AUTHENTICATED_AUTHORITY);
        // ..and all authorities retrieved from the Crowd server
        authorities.addAll(this.configuration.getAuthoritiesForUser(pUsername));

        return new CrowdUser(user, authorities);
    }

    /**
     * Descriptor for {@link CrowdSecurityRealm}. Used as a singleton. The class
     * is marked as public so that it can be accessed from views.
     * 
     * @author <a href="mailto:theit@gmx.de">Thorsten Heit (theit@gmx.de)</a>
     * @since 06.09.2011 13:35:41
     * @version $Id$
     */
    @Extension
    public static final class DescriptorImpl extends Descriptor<SecurityRealm> {
        /**
         * Default constructor.
         */
        public DescriptorImpl() {
            super(CrowdSecurityRealm.class);
        }

        /**
         * Performs on-the-fly validation of the form field 'url'.
         * 
         * @param url
         *            The URL of the Crowd server.
         * 
         * @return Indicates the outcome of the validation. This is sent to the
         *         browser.
         */
        public FormValidation doCheckUrl(@QueryParameter final String url) {
            if (!Hudson.getInstance().hasPermission(Hudson.ADMINISTER)) {
                return FormValidation.ok();
            }

            if (0 == url.length()) {
                return FormValidation.error(specifyCrowdUrl());
            }

            return FormValidation.ok();
        }

        /**
         * Performs on-the-fly validation of the form field 'application name'.
         * 
         * @param applicationName
         *            The application name.
         * 
         * @return Indicates the outcome of the validation. This is sent to the
         *         browser.
         */
        public FormValidation doCheckApplicationName(@QueryParameter final String applicationName) {
            if (!Hudson.getInstance().hasPermission(Hudson.ADMINISTER)) {
                return FormValidation.ok();
            }

            if (0 == applicationName.length()) {
                return FormValidation.error(specifyApplicationName());
            }

            return FormValidation.ok();
        }

        /**
         * Performs on-the-fly validation of the form field 'password'.
         * 
         * @param password
         *            The application's password.
         * 
         * @return Indicates the outcome of the validation. This is sent to the
         *         browser.
         */
        public FormValidation doCheckPassword(@QueryParameter final String password) {
            if (!Hudson.getInstance().hasPermission(Hudson.ADMINISTER)) {
                return FormValidation.ok();
            }

            if (0 == password.length()) {
                return FormValidation.error(specifyApplicationPassword());
            }

            return FormValidation.ok();
        }

        /**
         * Performs on-the-fly validation of the form field 'group name'.
         * 
         * @param group
         *            The group name.
         * 
         * @return Indicates the outcome of the validation. This is sent to the
         *         browser.
         */
        public FormValidation doCheckGroup(@QueryParameter final String group) {
            if (!Hudson.getInstance().hasPermission(Hudson.ADMINISTER)) {
                return FormValidation.ok();
            }

            if (0 == group.length()) {
                return FormValidation.error(specifyGroup());
            }

            return FormValidation.ok();
        }

        /**
         * Performs on-the-fly validation of the form field 'session validation
         * interval'.
         * 
         * @param sessionValidationInterval
         *            The session validation interval time in minutes.
         * @return Indicates the outcome of the validation. This is sent to the
         *         browser.
         */
        public FormValidation doCheckSessionValidationInterval(
                @QueryParameter final String sessionValidationInterval) {
            if (!Hudson.getInstance().hasPermission(Hudson.ADMINISTER)) {
                return FormValidation.ok();
            }

            try {
                if (0 == sessionValidationInterval.length()
                        || Integer.valueOf(sessionValidationInterval).intValue() < 0) {
                    return FormValidation.error(specifySessionValidationInterval());
                }
            } catch (NumberFormatException ex) {
                return FormValidation.error(specifySessionValidationInterval());
            }

            return FormValidation.ok();
        }

        /**
         * Checks whether the connection to the Crowd server can be established
         * using the given credentials.
         * 
         * @param url
         *            The URL of the Crowd server.
         * @param applicationName
         *            The application name.
         * @param password
         *            The application's password.
         * @param group
         *            The Crowd groups users have to belong to if specified.
         * 
         * @return Indicates the outcome of the validation. This is sent to the
         *         browser.
         */
        public FormValidation doTestConnection(@QueryParameter String url, @QueryParameter String applicationName,
                @QueryParameter String password, @QueryParameter String group) {
            Logger log = Logger.getLogger(getClass().getName());

            Properties props = new Properties();
            props.setProperty("application.name", applicationName);
            props.setProperty("application.password", password);
            props.setProperty("crowd.server.url", url);
            props.setProperty("session.validationinterval", "5");

            CrowdConfigurationService configuration = new CrowdConfigurationService(group, false);
            configuration.clientProperties = ClientPropertiesImpl.newInstanceFromProperties(props);
            configuration.crowdClient = new RestCrowdClientFactory().newInstance(configuration.clientProperties);

            try {
                configuration.crowdClient.testConnection();

                // ensure that the given group names are available and active
                for (String groupName : configuration.allowedGroupNames) {
                    if (!configuration.isGroupActive(groupName)) {
                        return FormValidation.error(groupNotFound(groupName));
                    }
                }

                return FormValidation.ok();
            } catch (InvalidAuthenticationException ex) {
                log.log(Level.WARNING, invalidAuthentication(), ex);
                return FormValidation.error(invalidAuthentication());
            } catch (ApplicationPermissionException ex) {
                log.log(Level.WARNING, applicationPermission(), ex);
                return FormValidation.error(applicationPermission());
            } catch (OperationFailedException ex) {
                log.log(Level.SEVERE, operationFailed(), ex);
                return FormValidation.error(operationFailed());
            } finally {
                configuration.crowdClient.shutdown();
            }
        }

        /**
         * {@inheritDoc}
         * 
         * @see hudson.model.Descriptor#getDisplayName()
         */
        @Override
        public String getDisplayName() {
            return "Crowd 2";
        }
    }
}