diff --git a/cas-client-core/pom.xml b/cas-client-core/pom.xml index 27855f6..3523168 100644 --- a/cas-client-core/pom.xml +++ b/cas-client-core/pom.xml @@ -2,7 +2,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> org.jasig.cas - 3.1.10 + 3.1.11-SNAPSHOT cas-client 4.0.0 @@ -10,50 +10,6 @@ cas-client-core jar JA-SIG CAS Client for Java - Core - - src/main/java - src/test/java - - - src/test/resources - false - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.4 - 1.4 - - - - org.apache.maven.plugins - maven-surefire-plugin - - - **/*Tests* - - - - - org.apache.maven.plugins - maven-clover-plugin - - ${basedir}/src/test/clover/clover.license - - - - pre-site - - instrument - - - - - - diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/authentication/AttributePrincipalImpl.java b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/AttributePrincipalImpl.java index e30ed93..27753dd 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/authentication/AttributePrincipalImpl.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/AttributePrincipalImpl.java @@ -15,15 +15,12 @@ import java.util.Map; * @version $Revision$ $Date$ * @since 3.1 */ -public class AttributePrincipalImpl implements AttributePrincipal { +public class AttributePrincipalImpl extends SimplePrincipal implements AttributePrincipal { - private static final Log LOG = LogFactory.getLog(AttributePrincipalImpl.class); + private static final Log LOG = LogFactory.getLog(AttributePrincipalImpl.class); /** Unique Id for Serialization */ - private static final long serialVersionUID = -8810123156070148535L; - - /** The unique identifier for this principal. */ - private final String name; + private static final long serialVersionUID = -1443182634624927187L; /** Map of key/value pairs about this principal. */ private final Map attributes; @@ -73,12 +70,11 @@ public class AttributePrincipalImpl implements AttributePrincipal { * @param proxyRetriever the ProxyRetriever implementation to call back to the CAS server. */ public AttributePrincipalImpl(final String name, final Map attributes, final String proxyGrantingTicket, final ProxyRetriever proxyRetriever) { - this.name = name; + super(name); this.attributes = attributes; this.proxyGrantingTicket = proxyGrantingTicket; this.proxyRetriever = proxyRetriever; - CommonUtils.assertNotNull(this.name, "name cannot be null."); CommonUtils.assertNotNull(this.attributes, "attributes cannot be null."); } @@ -94,8 +90,4 @@ public class AttributePrincipalImpl implements AttributePrincipal { LOG.debug("No ProxyGrantingTicket was supplied, so no Proxy Ticket can be retrieved."); return null; } - - public String getName() { - return this.name; - } } diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/authentication/SimpleGroup.java b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/SimpleGroup.java new file mode 100644 index 0000000..9bdad71 --- /dev/null +++ b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/SimpleGroup.java @@ -0,0 +1,81 @@ +/* + * Copyright 2010 The JA-SIG Collaborative. All rights reserved. See license + * distributed with this file and available online at + * http://www.ja-sig.org/products/cas/overview/license/index.html + */ +package org.jasig.cas.client.authentication; + +import java.security.Principal; +import java.security.acl.Group; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * Simple security group implementation + * + * @author Marvin S. Addison + * @version $Revision$ + * @since 3.1.11 + * + */ +public final class SimpleGroup extends SimplePrincipal implements Group { + + private static final long serialVersionUID = 1541943977571896383L; + + private final Set members = new HashSet(); + + /** + * Creates a new group with the given name. + * @param name Group name. + */ + public SimpleGroup(final String name) { + super(name); + } + + public boolean addMember(final Principal user) { + return this.members.add(user); + } + + public boolean isMember(final Principal member) { + return this.members.contains(member); + } + + public Enumeration members() { + return new EnumerationAdapter(this.members.iterator()); + } + + public boolean removeMember(final Principal user) { + return this.members.remove(user); + } + + public String toString() { + return super.toString() + ": " + members.toString(); + } + + /** + * Adapts a {@link java.util.Iterator} onto an {@link java.util.Enumeration}. + */ + private static class EnumerationAdapter implements Enumeration { + + /** Iterator backing enumeration operations */ + private Iterator iterator; + + /** + * Creates a new instance backed by the given iterator. + * @param i Iterator backing enumeration operations. + */ + public EnumerationAdapter(final Iterator i) { + this.iterator = i; + } + + public boolean hasMoreElements() { + return this.iterator.hasNext(); + } + + public Object nextElement() { + return this.iterator.next(); + } + } +} diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/authentication/SimplePrincipal.java b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/SimplePrincipal.java new file mode 100644 index 0000000..832a06c --- /dev/null +++ b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/SimplePrincipal.java @@ -0,0 +1,59 @@ +/* + * Copyright 2010 The JA-SIG Collaborative. All rights reserved. See license + * distributed with this file and available online at + * http://www.ja-sig.org/products/cas/overview/license/index.html + */ +package org.jasig.cas.client.authentication; + +import java.io.Serializable; +import java.security.Principal; + +import org.jasig.cas.client.util.CommonUtils; + +/** + * Simple security principal implementation. + * + * @author Marvin S. Addison + * @version $Revision$ + * @since 3.1.11 + * + */ +public class SimplePrincipal implements Principal, Serializable { + + /** SimplePrincipal.java */ + private static final long serialVersionUID = -5645357206342793145L; + + /** The unique identifier for this principal. */ + private final String name; + + /** + * Creates a new principal with the given name. + * @param name Principal name. + */ + public SimplePrincipal(final String name) { + this.name = name; + CommonUtils.assertNotNull(this.name, "name cannot be null."); + } + + public final String getName() { + return this.name; + } + + public String toString() { + return getName(); + } + + public boolean equals(final Object o) { + if (o == null) { + return false; + } else if (!(o instanceof SimplePrincipal)) { + return false; + } else { + return getName().equals(((SimplePrincipal)o).getName()); + } + } + + public int hashCode() { + return 37 * getName().hashCode(); + } +} diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/jaas/AssertionPrincipal.java b/cas-client-core/src/main/java/org/jasig/cas/client/jaas/AssertionPrincipal.java new file mode 100644 index 0000000..3bc232a --- /dev/null +++ b/cas-client-core/src/main/java/org/jasig/cas/client/jaas/AssertionPrincipal.java @@ -0,0 +1,45 @@ +/* + * Copyright 2010 The JA-SIG Collaborative. All rights reserved. See license + * distributed with this file and available online at + * http://www.ja-sig.org/products/cas/overview/license/index.html + */ +package org.jasig.cas.client.jaas; + +import java.io.Serializable; + +import org.jasig.cas.client.authentication.SimplePrincipal; +import org.jasig.cas.client.validation.Assertion; + +/** + * Principal implementation that contains the CAS ticket validation assertion. + * + * @author Marvin S. Addison + * @version $Revision$ + * @since 3.1.11 + * + */ +public class AssertionPrincipal extends SimplePrincipal implements Serializable { + + /** AssertionPrincipal.java */ + private static final long serialVersionUID = 2288520214366461693L; + + private Assertion assertion; + + /** + * Creates a new principal containing the CAS assertion. + * + * @param name Principal name. + * @param assertion CAS assertion. + */ + public AssertionPrincipal(final String name, final Assertion assertion) { + super(name); + this.assertion = assertion; + } + + /** + * @return CAS ticket validation assertion. + */ + public Assertion getAssertion() { + return this.assertion; + } +} diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/jaas/CasLoginModule.java b/cas-client-core/src/main/java/org/jasig/cas/client/jaas/CasLoginModule.java new file mode 100644 index 0000000..a7c7063 --- /dev/null +++ b/cas-client-core/src/main/java/org/jasig/cas/client/jaas/CasLoginModule.java @@ -0,0 +1,365 @@ +/* + * Copyright 2010 The JA-SIG Collaborative. All rights reserved. See license + * distributed with this file and available online at + * http://www.ja-sig.org/products/cas/overview/license/index.html + */ +package org.jasig.cas.client.jaas; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.io.IOException; +import java.security.acl.Group; +import java.util.*; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jasig.cas.client.authentication.SimpleGroup; +import org.jasig.cas.client.authentication.SimplePrincipal; +import org.jasig.cas.client.util.CommonUtils; +import org.jasig.cas.client.util.ReflectUtils; +import org.jasig.cas.client.validation.Assertion; +import org.jasig.cas.client.validation.TicketValidator; + +/** + * JAAS login module that delegates to a CAS {@link TicketValidator} component + * for authentication, and on success populates a {@link Subject} with principal + * data including NetID and principal attributes. The module expects to be provided + * with the CAS ticket (required) and service (optional) parameters via + * {@link PasswordCallback} and {@link NameCallback}, respectively, by the + * {@link CallbackHandler} that is part of the JAAS framework in which the servlet + * resides. + * + *

+ * Module configuration options: + *

+ * + *

+ * Module options not explicitly listed above are treated as attributes of the + * given ticket validator class, e.g. tolerance in the following example. + * + *

+ * Sample jaas.config file entry for this module: + *

+ * cas {
+ *   org.jasig.cas.client.jaas.CasLoginModule required
+ *     ticketValidatorClass="org.jasig.cas.client.validation.Saml11TicketValidator"
+ *     casServerUrlPrefix="https://cas.example.com/cas"
+ *     tolerance="20000"
+ *     service="https://webapp.example.com/webapp"
+ *     defaultRoles="admin,operator"
+ *     roleAttributeNames="memberOf,eduPersonAffiliation"
+ *     principalGroupName="CallerPrincipal"
+ *     roleGroupName="Roles";
+ * }
+ * 
+ * + * @author Marvin S. Addison + * @version $Revision$ + * @since 3.1.11 + * + */ +public class CasLoginModule implements LoginModule { + /** Constant for login name stored in shared state. */ + public static final String LOGIN_NAME = "javax.security.auth.login.name"; + + /** + * Default group name for storing caller principal. + * The default value supports JBoss, but is configurable to hopefully + * support other JEE containers. + */ + public static final String DEFAULT_PRINCIPAL_GROUP_NAME = "CallerPrincipal"; + + /** + * Default group name for storing role membership data. + * The default value supports JBoss, but is configurable to hopefully + * support other JEE containers. + */ + public static final String DEFAULT_ROLE_GROUP_NAME = "Roles"; + + /** Logger instance */ + protected final Log log = LogFactory.getLog(getClass()); + + /** JAAS authentication subject */ + protected Subject subject; + + /** JAAS callback handler */ + protected CallbackHandler callbackHandler; + + /** CAS ticket validator */ + protected TicketValidator ticketValidator; + + /** CAS service parameter used if no service is provided via TextCallback on login */ + protected String service; + + /** CAS assertion */ + protected Assertion assertion; + + /** Login module shared state */ + protected Map sharedState; + + /** Roles to be added to all authenticated principals by default */ + protected String[] defaultRoles; + + /** Names of attributes in the CAS assertion that should be used for role data */ + protected Set roleAttributeNames = new HashSet(); + + /** Name of JAAS Group containing caller principal */ + protected String principalGroupName = DEFAULT_PRINCIPAL_GROUP_NAME; + + /** Name of JAAS Group containing role data */ + protected String roleGroupName = DEFAULT_ROLE_GROUP_NAME; + + + /** + * Initializes the CAS login module. + * @param subject Authentication subject. + * @param handler Callback handler. + * @param state Shared state map. + * @param options Login module options. The following are supported: + * + */ + public void initialize(final Subject subject, final CallbackHandler handler, final Map state, final Map options) { + this.callbackHandler = handler; + this.subject = subject; + this.sharedState = state; + + String ticketValidatorClass = null; + final Iterator iter = options.keySet().iterator(); + while (iter.hasNext()) { + final Object key = iter.next(); + log.trace("Processing option " + key); + if ("service".equals(key)) { + this.service = (String) options.get(key); + log.debug("Set service=" + this.service); + } else if ("ticketValidatorClass".equals(key)) { + ticketValidatorClass = (String) options.get(key); + log.debug("Set ticketValidatorClass=" + ticketValidatorClass); + } else if ("defaultRoles".equals(key)) { + final String roles = (String) options.get(key); + log.trace("Got defaultRoles value " + roles); + this.defaultRoles = roles.split(",\\s*"); + log.debug("Set defaultRoles=" + this.defaultRoles); + } else if ("roleAttributeNames".equals(key)) { + final String attrNames = (String) options.get(key); + log.trace("Got roleAttributeNames value " + attrNames); + final String[] attributes = attrNames.split(",\\s*"); + this.roleAttributeNames.addAll(Arrays.asList(attributes)); + log.debug("Set roleAttributeNames=" + this.roleAttributeNames); + } else if ("principalGroupName".equals(key)) { + this.principalGroupName = (String) options.get(key); + log.debug("Set principalGroupName=" + this.principalGroupName); + } else if ("roleGroupName".equals(key)) { + this.roleGroupName = (String) options.get(key); + log.debug("Set roleGroupName=" + this.principalGroupName); + } + } + + CommonUtils.assertNotNull(ticketValidatorClass, "ticketValidatorClass is required."); + this.ticketValidator = createTicketValidator(ticketValidatorClass, options); + } + + public boolean login() throws LoginException { + log.debug("Performing login."); + final NameCallback serviceCallback = new NameCallback("service"); + final PasswordCallback ticketCallback = new PasswordCallback("ticket", false); + try { + this.callbackHandler.handle(new Callback[] { ticketCallback, serviceCallback }); + } catch (final IOException e) { + log.info("Login failed due to IO exception in callback handler: " + e); + throw new LoginException("IO exception in callback handler: " + e); + } catch (final UnsupportedCallbackException e) { + log.info("Login failed due to unsupported callback: " + e); + throw new LoginException("Callback handler does not support PasswordCallback and TextInputCallback."); + } + if (ticketCallback.getPassword() != null) { + final String ticket = new String(ticketCallback.getPassword()); + final String service = CommonUtils.isNotBlank(serviceCallback.getName()) ? serviceCallback.getName() : this.service; + + if (CommonUtils.isBlank(service)) { + log.info("Login failed because required CAS service parameter not provided."); + throw new LoginException("Neither login module nor callback handler provided required service parameter."); + } + try { + log.debug("Attempting ticket validation with service=" + service + " and ticket=" + ticket); + this.assertion = this.ticketValidator.validate(ticket, service); + } catch (final Exception e) { + log.info("Login failed due to CAS ticket validation failure: " + e); + throw new LoginException("CAS ticket validation failed: " + e); + } + } else { + log.info("Login failed because callback handler did not provide CAS ticket."); + throw new LoginException("Callback handler did not provide CAS ticket."); + } + log.info("Login succeeded."); + return true; + } + + public boolean abort() throws LoginException { + if (this.assertion != null) { + this.assertion = null; + return true; + } + return false; + } + + public boolean commit() throws LoginException { + if (this.assertion != null) { + final AssertionPrincipal casPrincipal = new AssertionPrincipal(this.assertion.getPrincipal().getName(), this.assertion); + this.subject.getPrincipals().add(casPrincipal); + + // Add group containing principal as sole member + // Supports JBoss JAAS use case + final Group principalGroup = new SimpleGroup(this.principalGroupName); + principalGroup.addMember(casPrincipal); + this.subject.getPrincipals().add(principalGroup); + + // Add group principal containing role data + final Group roleGroup = new SimpleGroup(this.roleGroupName); + for (int i = 0; i < defaultRoles.length; i++) { + roleGroup.addMember(new SimplePrincipal(defaultRoles[i])); + } + final Map attributes = this.assertion.getPrincipal().getAttributes(); + final Iterator nameIterator = attributes.keySet().iterator(); + while (nameIterator.hasNext()) { + final Object key = nameIterator.next(); + if (this.roleAttributeNames.contains(key)) { + // Attribute value is Object if singular or Collection if plural + final Object value = attributes.get(key); + if (value instanceof Collection) { + final Iterator valueIterator = ((Collection) value).iterator(); + while (valueIterator.hasNext()) { + roleGroup.addMember(new SimplePrincipal(valueIterator.next().toString())); + } + } else { + roleGroup.addMember(new SimplePrincipal(value.toString())); + } + } + } + this.subject.getPrincipals().add(roleGroup); + + // Place principal name in shared state for downstream JAAS modules (module chaining use case) + this.sharedState.put(LOGIN_NAME, casPrincipal.getName()); + + if (log.isDebugEnabled()) { + log.debug("Created JAAS subject with principals: " + subject.getPrincipals()); + } + return true; + } + return false; + } + + public boolean logout() throws LoginException { + if (this.assertion != null) { + log.debug("Performing logout."); + this.subject.getPrincipals().remove(this.assertion.getPrincipal()); + // Remove all SimpleGroup principals + final Iterator iter = this.subject.getPrincipals().iterator(); + while (iter.hasNext()) { + if (iter.next() instanceof SimpleGroup) { + iter.remove(); + } + } + this.assertion = null; + log.info("Logout succeeded."); + return true; + } + return false; + } + + + /** + * Creates a {@link TicketValidator} instance from a class name and map of property name/value pairs. + * @param className Fully-qualified name of {@link TicketValidator} concrete class. + * @param propertyMap Map of property name/value pairs to set on validator instance. + * @return Ticket validator with properties set. + */ + private TicketValidator createTicketValidator(final String className, final Map propertyMap) { + CommonUtils.assertTrue(propertyMap.containsKey("casServerUrlPrefix"), "Required property casServerUrlPrefix not found."); + + final Class validatorClass = ReflectUtils.loadClass(className); + final TicketValidator validator = (TicketValidator) ReflectUtils.newInstance(validatorClass, new Object[] {propertyMap.get("casServerUrlPrefix")}); + + try { + final BeanInfo info = Introspector.getBeanInfo(validatorClass); + final Iterator iter = propertyMap.keySet().iterator(); + while (iter.hasNext()) { + final String property = (String) iter.next(); + if (!"casServerUrlPrefix".equals(property)) { + log.debug("Attempting to set TicketValidator property " + property); + final String value = (String) propertyMap.get(property); + final PropertyDescriptor pd = ReflectUtils.getPropertyDescriptor(info, property); + if (pd != null) { + ReflectUtils.setProperty(property, convertIfNecessary(pd, value), validator, info); + log.debug("Set " + property + "=" + value); + } else { + log.warn("Cannot find property " + property + " on " + className); + } + } + } + } catch (final IntrospectionException e) { + throw new RuntimeException("Error getting bean info for " + validatorClass); + } + + return validator; + } + + /** + * Attempts to do simple type conversion from a string value to the type expected + * by the given property. + * + * Currently only conversion to int, long, and boolean are supported. + * + * @param pd Property descriptor of target property to set. + * @param value Property value as a string. + * @return Value converted to type expected by property if a conversion strategy exists. + */ + private static Object convertIfNecessary(final PropertyDescriptor pd, final String value) { + if (String.class.equals(pd.getPropertyType())) { + return value; + } else if (boolean.class.equals(pd.getPropertyType())) { + return Boolean.valueOf(value); + } else if (int.class.equals(pd.getPropertyType())) { + return new Integer(value); + } else if (long.class.equals(pd.getPropertyType())) { + return new Long(value); + } else { + throw new IllegalArgumentException( + "No conversion strategy exists for property " + pd.getName() + + " of type " + pd.getPropertyType()); + } + } +} diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/jaas/ServiceAndTicketCallbackHandler.java b/cas-client-core/src/main/java/org/jasig/cas/client/jaas/ServiceAndTicketCallbackHandler.java new file mode 100644 index 0000000..1f906c4 --- /dev/null +++ b/cas-client-core/src/main/java/org/jasig/cas/client/jaas/ServiceAndTicketCallbackHandler.java @@ -0,0 +1,57 @@ +/* + * Copyright 2010 The JA-SIG Collaborative. All rights reserved. See license + * distributed with this file and available online at + * http://www.ja-sig.org/products/cas/overview/license/index.html + */ +package org.jasig.cas.client.jaas; + +import java.io.IOException; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; + +/** + * Callback handler that provides the CAS service and ticket to a + * {@link NameCallback} and {@link PasswordCallback} respectively, + * which meets the requirements of the {@link CasLoginModule} JAAS module. + * + * @author Marvin S. Addison + * @version $Revision$ + * @since 3.1.11 + * + */ +public class ServiceAndTicketCallbackHandler implements CallbackHandler { + + /** CAS service URL */ + private final String service; + + /** CAS service ticket */ + private final String ticket; + + /** + * Creates a new instance with the given service and ticket. + * + * @param service CAS service URL. + * @param ticket CAS service ticket. + */ + public ServiceAndTicketCallbackHandler(final String service, final String ticket) { + this.service = service; + this.ticket = ticket; + } + + public void handle(final Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (int i = 0; i < callbacks.length; i++) { + if (callbacks[i] instanceof NameCallback) { + ((NameCallback) callbacks[i]).setName(this.service); + } else if (callbacks[i] instanceof PasswordCallback) { + ((PasswordCallback) callbacks[i]).setPassword(this.ticket.toCharArray()); + } else { + throw new UnsupportedCallbackException(callbacks[i], "Callback not supported."); + } + } + } + +} diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/util/ReflectUtils.java b/cas-client-core/src/main/java/org/jasig/cas/client/util/ReflectUtils.java new file mode 100644 index 0000000..0749ac9 --- /dev/null +++ b/cas-client-core/src/main/java/org/jasig/cas/client/util/ReflectUtils.java @@ -0,0 +1,141 @@ +/* + * Copyright 2010 The JA-SIG Collaborative. All rights reserved. See license + * distributed with this file and available online at + * http://www.ja-sig.org/products/cas/overview/license/index.html + */ +package org.jasig.cas.client.util; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.InvocationTargetException; + +/** + * Helper class with reflection utility methods. + * + * @author Marvin S. Addison + * @version $Revision$ + * @since 3.1.11 + * + */ +public final class ReflectUtils { + + private ReflectUtils() { + // private constructor to prevent instanciation. + } + + /** + * Attempts to create a class from a String. + * @param className the name of the class to create. + * @return the class. CANNOT be NULL. + * @throws IllegalArgumentException if the className does not exist. + */ + public static Class loadClass(final String className) throws IllegalArgumentException { + try { + return Class.forName(className); + } catch (final ClassNotFoundException e) { + throw new IllegalArgumentException(className + " class not found."); + } + } + + + /** + * Creates a new instance of the given class by passing the given arguments + * to the constructor. + * @param className Name of class to be created. + * @param args Constructor arguments. + * @return New instance of given class. + */ + public static Object newInstance(final String className, final Object[] args) { + try { + return newInstance(Class.forName(className), args); + } catch (final ClassNotFoundException e) { + throw new IllegalArgumentException(className + " not found"); + } + } + + /** + * Creates a new instance of the given class by passing the given arguments + * to the constructor. + * @param clazz Class of instance to be created. + * @param args Constructor arguments. + * @return New instance of given class. + */ + public static Object newInstance(final Class clazz, final Object[] args) { + final Class[] argClasses = new Class[args.length]; + for (int i = 0; i < args.length; i++) { + argClasses[i] = args[i].getClass(); + } + try { + return clazz.getConstructor(argClasses).newInstance(args); + } catch (final Exception e) { + throw new IllegalArgumentException("Error creating new instance of " + clazz, e); + } + } + + /** + * Gets the property descriptor for the named property on the given class. + * @param clazz Class to which property belongs. + * @param propertyName Name of property. + * @return Property descriptor for given property or null if no property with given + * name exists in given class. + */ + public static PropertyDescriptor getPropertyDescriptor(final Class clazz, final String propertyName) { + try { + return getPropertyDescriptor(Introspector.getBeanInfo(clazz), propertyName); + } catch (final IntrospectionException e) { + throw new RuntimeException("Failed getting bean info for " + clazz, e); + } + } + + /** + * Gets the property descriptor for the named property from the bean info describing + * a particular class to which property belongs. + * @param info Bean info describing class to which property belongs. + * @param propertyName Name of property. + * @return Property descriptor for given property or null if no property with given + * name exists. + */ + public static PropertyDescriptor getPropertyDescriptor(final BeanInfo info, final String propertyName) { + for (int i = 0; i < info.getPropertyDescriptors().length; i++) { + final PropertyDescriptor pd = info.getPropertyDescriptors()[i]; + if (pd.getName().equals(propertyName)) { + return pd; + } + } + return null; + } + + /** + * Sets the given property on the target javabean using bean instrospection. + * @param propertyName Property to set. + * @param value Property value to set. + * @param target Target java bean on which to set property. + */ + public static void setProperty(final String propertyName, final Object value, final Object target) { + try { + setProperty(propertyName, value, target, Introspector.getBeanInfo(target.getClass())); + } catch (final IntrospectionException e) { + throw new RuntimeException("Failed getting bean info on target javabean " + target, e); + } + } + + /** + * Sets the given property on the target javabean using bean instrospection. + * @param propertyName Property to set. + * @param value Property value to set. + * @param target Target javabean on which to set property. + * @param info BeanInfo describing the target javabean. + */ + public static void setProperty(final String propertyName, final Object value, final Object target, final BeanInfo info) { + try { + final PropertyDescriptor pd = getPropertyDescriptor(info, propertyName); + pd.getWriteMethod().invoke(target, new Object[] { value }); + } catch (final InvocationTargetException e) { + throw new RuntimeException("Error setting property " + propertyName, e.getCause()); + } catch (final Exception e) { + throw new RuntimeException("Error setting property " + propertyName, e); + } + } +} diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/PublicTestHttpServer.java b/cas-client-core/src/test/java/org/jasig/cas/client/PublicTestHttpServer.java index 1601c28..d5f0a48 100644 --- a/cas-client-core/src/test/java/org/jasig/cas/client/PublicTestHttpServer.java +++ b/cas-client-core/src/test/java/org/jasig/cas/client/PublicTestHttpServer.java @@ -1,6 +1,5 @@ package org.jasig.cas.client; - import java.io.*; import java.net.ServerSocket; import java.net.Socket; @@ -16,35 +15,28 @@ public final class PublicTestHttpServer extends Thread { public byte[] content; - private byte[] header; + private final byte[] header; - private int port = 80; + private final int port; - public String encoding; + public final String encoding; - private PublicTestHttpServer(String data, String encoding, String MIMEType, - int port) throws UnsupportedEncodingException { + private PublicTestHttpServer(String data, String encoding, String MIMEType, int port) throws UnsupportedEncodingException { this(data.getBytes(encoding), encoding, MIMEType, port); } - private PublicTestHttpServer(byte[] data, String encoding, String MIMEType, - int port) throws UnsupportedEncodingException { - + private PublicTestHttpServer(byte[] data, String encoding, String MIMEType, int port) throws UnsupportedEncodingException { this.content = data; this.port = port; this.encoding = encoding; - String header = "HTTP/1.0 200 OK\r\n" + "Server: OneFile 1.0\r\n" - // + "Content-length: " + this.content.length + "\r\n" - + "Content-type: " + MIMEType + "\r\n\r\n"; + String header = "HTTP/1.0 200 OK\r\n" + "Server: OneFile 1.0\r\n" + "Content-type: " + MIMEType + "\r\n\r\n"; this.header = header.getBytes("ASCII"); - } public static synchronized PublicTestHttpServer instance() { if (httpServer == null) { try { - httpServer = new PublicTestHttpServer("test", "ASCII", - "text/plain", 8085); + httpServer = new PublicTestHttpServer("test", "ASCII", "text/plain", 8085); } catch (Exception e) { throw new RuntimeException(e); } @@ -59,19 +51,16 @@ public final class PublicTestHttpServer extends Thread { try { ServerSocket server = new ServerSocket(this.port); - System.out.println("Accepting connections on port " - + server.getLocalPort()); + System.out.println("Accepting connections on port " + server.getLocalPort()); while (true) { Socket connection = null; try { connection = server.accept(); - OutputStream out = new BufferedOutputStream(connection - .getOutputStream()); - InputStream in = new BufferedInputStream(connection - .getInputStream()); + final OutputStream out = new BufferedOutputStream(connection.getOutputStream()); + final InputStream in = new BufferedInputStream(connection.getInputStream()); // read the first line only; that's all we need - StringBuffer request = new StringBuffer(80); + final StringBuffer request = new StringBuffer(80); while (true) { int c = in.read(); if (c == '\r' || c == '\n' || c == -1) @@ -89,7 +78,7 @@ public final class PublicTestHttpServer extends Thread { out.write(this.content); out.flush(); } // end try - catch (IOException e) { + catch (final IOException e) { // nothing to do with this IOException } finally { if (connection != null) @@ -98,7 +87,7 @@ public final class PublicTestHttpServer extends Thread { } // end while } // end try - catch (IOException e) { + catch (final IOException e) { System.err.println("Could not start server. Port Occupied"); } diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/SerializationTests.java b/cas-client-core/src/test/java/org/jasig/cas/client/SerializationTests.java new file mode 100644 index 0000000..03d00fb --- /dev/null +++ b/cas-client-core/src/test/java/org/jasig/cas/client/SerializationTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2010 The JA-SIG Collaborative. All rights reserved. See license + * distributed with this file and available online at + * http://www.ja-sig.org/products/cas/overview/license/index.html + */ +package org.jasig.cas.client; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Collections; + +import junit.framework.Assert; +import junit.framework.TestCase; + +import org.jasig.cas.client.authentication.AttributePrincipalImpl; +import org.jasig.cas.client.authentication.SimpleGroup; +import org.jasig.cas.client.authentication.SimplePrincipal; +import org.jasig.cas.client.jaas.AssertionPrincipal; +import org.jasig.cas.client.validation.AssertionImpl; + +/** + * Confirms serialization support for classes intended for session storage or + * other potential serialization use cases. + * + * @author Marvin S. Addison + * @version $Revision$ + * @since 3.1.11 + * + */ +public class SerializationTests extends TestCase { + + public void testSerializeDeserialize() throws Exception { + final Object[] subjects = getTestSubjects(); + for (int i = 0; i < subjects.length; i++) { + final ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + final ObjectOutputStream out = new ObjectOutputStream(byteOut); + try { + out.writeObject(subjects[i]); + } catch (Exception e) { + Assert.fail("Serialization failed for " + subjects[i]); + } finally { + out.close(); + } + + final ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray()); + final ObjectInputStream in = new ObjectInputStream(byteIn); + try { + Assert.assertEquals(subjects[i], in.readObject()); + } catch (Exception e) { + Assert.fail("Deserialization failed for " + subjects[i]); + } finally { + in.close(); + } + } + } + + private Object[] getTestSubjects() { + final SimplePrincipal simplePrincipal = new SimplePrincipal("simple"); + final SimpleGroup simpleGroup = new SimpleGroup("group"); + final AttributePrincipalImpl attributePrincipal = + new AttributePrincipalImpl("attr", Collections.singletonMap("LOA", "3")); + final AssertionPrincipal assertionPrincipal = new AssertionPrincipal( + "assertion", + new AssertionImpl(attributePrincipal, Collections.singletonMap("authenticationMethod", "username"))); + + return new Object[] { + simplePrincipal, + simpleGroup, + attributePrincipal, + assertionPrincipal, + }; + } +} diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/jaas/CasLoginModuleTests.java b/cas-client-core/src/test/java/org/jasig/cas/client/jaas/CasLoginModuleTests.java new file mode 100644 index 0000000..298ac69 --- /dev/null +++ b/cas-client-core/src/test/java/org/jasig/cas/client/jaas/CasLoginModuleTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2010 The JA-SIG Collaborative. All rights reserved. See license + * distributed with this file and available online at + * http://www.ja-sig.org/products/cas/overview/license/index.html + */ +package org.jasig.cas.client.jaas; + +import java.security.Principal; +import java.security.acl.Group; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import javax.security.auth.Subject; +import javax.security.auth.login.LoginException; + +import org.jasig.cas.client.PublicTestHttpServer; + +import junit.framework.TestCase; + +/** + * Unit test for {@link CasLoginModule} class. + * + * @author Marvin S. Addison + * @version $Revision$ + * + */ +public class CasLoginModuleTests extends TestCase { + private static final String CONST_CAS_SERVER_URL = "http://localhost:8085/"; + + private CasLoginModule module; + + private Subject subject; + + /** {@inheritDoc} */ + protected void setUp() throws Exception { + super.setUp(); + + module = new CasLoginModule(); + subject = new Subject(); + final Map options = new HashMap(); + options.put("service", "https://service.example.com/webapp"); + options.put("ticketValidatorClass", "org.jasig.cas.client.validation.Cas20ServiceTicketValidator"); + options.put("casServerUrlPrefix", CONST_CAS_SERVER_URL); + options.put("proxyCallbackUrl", "https://service.example.com/webapp/proxy"); + options.put("renew", "true"); + options.put("defaultRoles", "ADMIN"); + options.put("principalGroupName", "CallerPrincipal"); + options.put("roleGroupName", "Roles"); + module.initialize( + subject, + new ServiceAndTicketCallbackHandler("myService", "myTicket"), + new HashMap(), + options); + } + + /** + * Test JAAS login success. + */ + public void testLoginSuccess() throws Exception { + final String USERNAME = "username"; + final String RESPONSE = "" + + "" + + USERNAME + + ""; + PublicTestHttpServer.instance().content = RESPONSE + .getBytes(PublicTestHttpServer.instance().encoding); + module.login(); + module.commit(); + assertEquals(this.subject.getPrincipals().size(), 3); + assertTrue(hasPrincipalName(this.subject, AssertionPrincipal.class, USERNAME)); + assertTrue(hasPrincipalName(this.subject, Group.class, "CallerPrincipal")); + assertTrue(hasPrincipalName(this.subject, Group.class, "Roles")); + } + + /** + * Test JAAS login failure. + */ + public void testLoginFailure() throws Exception { + final String RESPONSE = "Ticket ST-1856339-aA5Yuvrxzpv8Tau1cYQ7 not recognized"; + PublicTestHttpServer.instance().content = RESPONSE.getBytes(PublicTestHttpServer.instance().encoding); + try { + module.login(); + fail("Login did not throw LoginException as expected."); + } catch (Exception e) { + assertTrue(e instanceof LoginException); + } + } + + private boolean hasPrincipalName(final Subject subject, final Class principalClass, final String name) { + final Set principals = subject.getPrincipals(principalClass); + final Iterator iter = principals.iterator(); + while (iter.hasNext()) { + final Principal p = (Principal) iter.next(); + if (p.getName().equals(name)) { + return true; + } + } + return false; + } +} diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/util/ReflectUtilsTests.java b/cas-client-core/src/test/java/org/jasig/cas/client/util/ReflectUtilsTests.java new file mode 100644 index 0000000..8ed5bf8 --- /dev/null +++ b/cas-client-core/src/test/java/org/jasig/cas/client/util/ReflectUtilsTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2010 The JA-SIG Collaborative. All rights reserved. See license + * distributed with this file and available online at + * http://www.ja-sig.org/products/cas/overview/license/index.html + */ +package org.jasig.cas.client.util; + +import junit.framework.TestCase; + +/** + * Unit test for {@link ReflectUtils} class. + * + * @author Marvin S. Addison + * @version $Revision$ + * @since 3.1.11 + * + */ +public class ReflectUtilsTests extends TestCase { + /** + * Test method for {@link org.jasig.cas.client.util.ReflectUtils#newInstance(java.lang.String, java.lang.Object[])}. + */ + public void testNewInstanceStringObjectArray() { + final Object result = ReflectUtils.newInstance( + "org.jasig.cas.client.validation.Cas10TicketValidator", + new Object[] {"https://localhost/cas"} ); + assertNotNull(result); + } + + /** + * Test method for {@link org.jasig.cas.client.util.ReflectUtils#setProperty(java.lang.String, java.lang.Object, java.lang.Object)}. + */ + public void testSetPropertyStringObjectObject() { + final TestBean bean = new TestBean(); + + ReflectUtils.setProperty("count", new Integer(30000), bean); + assertEquals(30000, bean.getCount()); + + ReflectUtils.setProperty("name", "bob", bean); + assertEquals("bob", bean.getName()); + + ReflectUtils.setProperty("flag", Boolean.TRUE, bean); + assertTrue(bean.isFlag()); + } + + static class TestBean { + private int count; + private boolean flag; + private String name; + + /** + * @return the count + */ + public int getCount() { + return count; + } + + /** + * @param count the count to set + */ + public void setCount(int count) { + this.count = count; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the flag + */ + public boolean isFlag() { + return flag; + } + + /** + * @param flag the flag to set + */ + public void setFlag(boolean flag) { + this.flag = flag; + } + + } +} diff --git a/cas-client-integration-atlassian/pom.xml b/cas-client-integration-atlassian/pom.xml index 344d933..9a7161c 100644 --- a/cas-client-integration-atlassian/pom.xml +++ b/cas-client-integration-atlassian/pom.xml @@ -2,7 +2,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> org.jasig.cas - 3.1.10 + 3.1.11-SNAPSHOT cas-client 4.0.0 @@ -10,50 +10,7 @@ cas-client-integration-atlassian jar JA-SIG CAS Client for Java - Atlassian Integration - - src/main/java - src/test/java - - - src/test/resources - false - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.4 - 1.4 - - - - org.apache.maven.plugins - maven-surefire-plugin - - - **/*Tests* - - - - - org.apache.maven.plugins - maven-clover-plugin - - ${basedir}/src/test/clover/clover.license - - - - pre-site - - instrument - - - - - - + atlassian-seraph @@ -554,7 +511,8 @@ - + + diff --git a/cas-client-integration-jboss/pom.xml b/cas-client-integration-jboss/pom.xml new file mode 100644 index 0000000..2f607ee --- /dev/null +++ b/cas-client-integration-jboss/pom.xml @@ -0,0 +1,42 @@ + + + org.jasig.cas + 3.1.11-SNAPSHOT + cas-client + + 4.0.0 + org.jasig.cas + cas-client-integration-jboss + jar + JA-SIG CAS Client for Java - JBoss Integration + + + + org.jasig.cas + cas-client-core + ${project.version} + compile + + + org.jboss.jbossas + jboss-as-tomcat + ${jboss.version} + provided + + + + + + + 5.1.0.GA + + diff --git a/cas-client-integration-jboss/src/main/java/org/jasig/cas/client/jboss/authentication/WebAuthenticationFilter.java b/cas-client-integration-jboss/src/main/java/org/jasig/cas/client/jboss/authentication/WebAuthenticationFilter.java new file mode 100644 index 0000000..9d8fe30 --- /dev/null +++ b/cas-client-integration-jboss/src/main/java/org/jasig/cas/client/jboss/authentication/WebAuthenticationFilter.java @@ -0,0 +1,77 @@ +/* + * Copyright 2010 The JA-SIG Collaborative. All rights reserved. See license + * distributed with this file and available online at + * http://www.ja-sig.org/products/cas/overview/license/index.html + */ +package org.jasig.cas.client.jboss.authentication; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.jasig.cas.client.jaas.AssertionPrincipal; +import org.jasig.cas.client.util.AbstractCasFilter; +import org.jasig.cas.client.util.CommonUtils; + +import org.jboss.web.tomcat.security.login.WebAuthentication; + +/** + * This servlet filter performs a programmatic JAAS login using the JBoss + * WebAuthentication class. + * The filter executes when it receives a CAS ticket and expects the + * {@link org.jasig.cas.client.jaas.CasLoginModule} JAAS module to perform the CAS + * ticket validation in order to produce an {@link AssertionPrincipal} from which + * the CAS assertion is obtained and inserted into the session to enable SSO. + *

+ * If a service init-param is specified for this filter, it supersedes + * the service defined for the {@link org.jasig.cas.client.jaas.CasLoginModule}. + * + * @author Daniel Fisher + * @author Marvin S. Addison + * @version $Revision$ + * @since 3.1.11 + */ +public final class WebAuthenticationFilter extends AbstractCasFilter { + + public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain chain) throws IOException, ServletException { + final HttpServletRequest request = (HttpServletRequest) servletRequest; + final HttpServletResponse response = (HttpServletResponse) servletResponse; + final HttpSession session = request.getSession(); + final String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName()); + + if (session != null && session.getAttribute(CONST_CAS_ASSERTION) == null && ticket != null) { + try { + final String service = constructServiceUrl(request, response); + log.debug("Attempting CAS ticket validation with service=" + service + " and ticket=" + ticket); + if (!new WebAuthentication().login(service, ticket)) { + log.debug("JBoss Web authentication failed."); + throw new GeneralSecurityException("JBoss Web authentication failed."); + } + if (request.getUserPrincipal() instanceof AssertionPrincipal) { + final AssertionPrincipal principal = (AssertionPrincipal) request.getUserPrincipal(); + log.debug("Installing CAS assertion into session."); + session.setAttribute(CONST_CAS_ASSERTION, principal.getAssertion()); + } else { + log.debug("Aborting -- principal is not of type AssertionPrincipal"); + throw new GeneralSecurityException("JBoss Web authentication did not produce CAS AssertionPrincipal."); + } + } catch (final GeneralSecurityException e) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage()); + } + } else if (session != null && request.getUserPrincipal() == null) { + // There is evidence that in some cases the principal can disappear + // in JBoss despite a valid session. + // This block forces consistency between principal and assertion. + log.info("User principal not found. Removing CAS assertion from session to force reauthentication."); + session.removeAttribute(CONST_CAS_ASSERTION); + } + chain.doFilter(request, response); + } +} diff --git a/cas-client-support-distributed-ehcache/pom.xml b/cas-client-support-distributed-ehcache/pom.xml index 8526939..4f0aa1a 100644 --- a/cas-client-support-distributed-ehcache/pom.xml +++ b/cas-client-support-distributed-ehcache/pom.xml @@ -5,7 +5,7 @@ cas-client org.jasig.cas - 3.1.10 + 3.1.11-SNAPSHOT 4.0.0 Jasig CAS Client for Java - Distributed Proxy Storage Support: EhCache diff --git a/cas-client-support-distributed-memcached/pom.xml b/cas-client-support-distributed-memcached/pom.xml index d4dc705..6d3c4dd 100644 --- a/cas-client-support-distributed-memcached/pom.xml +++ b/cas-client-support-distributed-memcached/pom.xml @@ -5,7 +5,7 @@ cas-client org.jasig.cas - 3.1.10 + 3.1.11-SNAPSHOT 4.0.0 @@ -27,7 +27,7 @@ spy memcached - 2.4.2 + 2.5 jar provided diff --git a/pom.xml b/pom.xml index 9505754..4e7908f 100644 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,7 @@ 4.0.0 org.jasig.cas - 3.1.10 + 3.1.11-SNAPSHOT cas-client pom JA-SIG CAS Client for Java @@ -20,28 +20,7 @@ https://www.ja-sig.org/svn/cas-clients/java-client 2006 - - - CAS Community Discussion List - http://tp.its.yale.edu/mailman/listinfo/cas - http://tp.its.yale.edu/mailman/listinfo/cas - cas@tp.its.yale.edu - http://tp.its.yale.edu/pipermail/cas/ - - http://news.gmane.org/gmane.comp.java.jasig.cas.user - - - - CAS Developers Discussion List - http://tp.its.yale.edu/mailman/listinfo/cas-dev - http://tp.its.yale.edu/mailman/listinfo/cas-dev - cas-dev@tp.its.yale.edu - http://tp.its.yale.edu/pipermail/cas-dev/ - - http://news.gmane.org/gmane.comp.java.jasig.cas.devel - - - + battags @@ -66,7 +45,7 @@ JA-SIG - http://www.ja-sig.org + http://www.jasig.org @@ -88,6 +67,30 @@ 1.4 + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*Tests* + + + + + org.apache.maven.plugins + maven-clover-plugin + + ${basedir}/src/test/clover/clover.license + + + + pre-site + + instrument + + + + @@ -129,6 +132,7 @@ cas-client-core cas-client-integration-atlassian + cas-client-integration-jboss cas-client-support-distributed-ehcache cas-client-support-distributed-memcached