CASC-166 Address code review feedback.

Use ConcurrentHashMap to avoid explicit synchronization.
Use TimeUnit to allow more user-friendly configuration of the units of the cache timeout (e.g. MINUTES, SECONDS) and rename option from cacheTimeoutUnits to cacheTimeoutUnit for consistency.
This commit is contained in:
Marvin S. Addison 2012-07-26 10:25:06 -04:00
parent ec0c7d5162
commit ee2b719098
2 changed files with 49 additions and 63 deletions

View File

@ -34,6 +34,7 @@ import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
@ -84,10 +85,8 @@ import org.jasig.cas.client.validation.TicketValidator;
* for JAAS providers that attempt to periodically reauthenticate to renew principal.
* Since CAS tickets are one-time-use, a cached assertion must be provided on reauthentication.</li>
* <li>cacheTimeout (optional) - Assertion cache timeout in minutes.</li>
* <li>cacheTimeoutUnits (optional) - Assertion cache timeout units.
* MUST be one of following:
* Calendar.HOUR(10), Calendar.MINUTE(12), Calendar.SECOND(13), Calendar.MILLISECOND(14).
* Default unit is minutes.</li>
* <li>cacheTimeoutUnit (optional) - Assertion cache timeout unit. Must be one of {@link TimeUnit} enumeration
* names, e.g. DAYS, HOURS, MINUTES, SECONDS, MILLISECONDS. Default unit is MINUTES.</li>
* </ul>
*
* <p>
@ -139,7 +138,7 @@ public class CasLoginModule implements LoginModule {
public static final int DEFAULT_CACHE_TIMEOUT = 480;
/** Default assertion cache timeout unit is minutes. */
public static final int DEFAULT_CACHE_TIMEOUT_UNITS = Calendar.MINUTE;
public static final TimeUnit DEFAULT_CACHE_TIMEOUT_UNIT = TimeUnit.MINUTES;
/**
* Stores mapping of ticket to assertion to support JAAS providers that
@ -191,16 +190,8 @@ public class CasLoginModule implements LoginModule {
/** Assertion cache timeout in minutes */
protected int cacheTimeout = DEFAULT_CACHE_TIMEOUT;
/**
* Units of cache timeout. Must be one of following {@link Calendar} time unit constants:
* <ul>
* <li>{@link Calendar#HOUR}</li>
* <li>{@link Calendar#MINUTE}</li>
* <li>{@link Calendar#SECOND}</li>
* <li>{@link Calendar#MILLISECOND}</li>
* </ul>
*/
protected int cacheTimeoutUnits = DEFAULT_CACHE_TIMEOUT_UNITS;
/** Units of cache timeout. */
protected TimeUnit cacheTimeoutUnit = DEFAULT_CACHE_TIMEOUT_UNIT;
/**
* Initializes the CAS login module.
@ -222,17 +213,20 @@ public class CasLoginModule implements LoginModule {
* which by default are single use, reauthentication fails. Assertion caching addresses this
* behavior.</li>
* <li>cacheTimeout (optional) - assertion cache timeout in minutes.</li>
* <li>cacheTimeoutUnits (optional) - Assertion cache timeout units.
* MUST be one of following:
* Calendar.HOUR(10), Calendar.MINUTE(12), Calendar.SECOND(13), Calendar.MILLISECOND(14).
* Default unit is minutes.</li>
* <li>cacheTimeoutUnit (optional) - Assertion cache timeout unit. Must be one of {@link TimeUnit} enumeration
* names, e.g. DAYS, HOURS, MINUTES, SECONDS, MILLISECONDS. Default unit is MINUTES.</li>
* </ul>
*/
public final void initialize(final Subject subject, final CallbackHandler handler, final Map<String,?> state, final Map<String, ?> options) {
public final void initialize(
final Subject subject,
final CallbackHandler handler,
final Map<String,?> state,
final Map<String, ?> options) {
this.assertion = null;
this.callbackHandler = handler;
this.subject = subject;
this.sharedState = (Map<String,Object>) state;
this.sharedState = (Map<String, Object>) state;
this.sharedState = new HashMap<String, Object>(state);
String ticketValidatorClass = null;
@ -268,15 +262,9 @@ public class CasLoginModule implements LoginModule {
} else if ("cacheTimeout".equals(key)) {
this.cacheTimeout = Integer.parseInt((String) options.get(key));
log.debug("Set cacheTimeout=" + this.cacheTimeout);
} else if ("cacheTimeoutUnits".equals(key)) {
final int units = Integer.parseInt((String) options.get(key));
if (units == Calendar.HOUR || units == Calendar.MINUTE ||
units == Calendar.SECOND || units == Calendar.MILLISECOND) {
this.cacheTimeoutUnits = units;
log.debug("Set cacheTimeoutUnits=" + this.cacheTimeoutUnits);
} else {
throw new IllegalArgumentException("Invalid time unit constant " + units);
}
} else if ("cacheTimeoutUnit".equals(key)) {
this.cacheTimeoutUnit = Enum.valueOf(TimeUnit.class, (String) options.get(key));
log.debug("Set cacheTimeoutUnit=" + this.cacheTimeoutUnit);
}
}
@ -325,20 +313,19 @@ public class CasLoginModule implements LoginModule {
throw (LoginException) new LoginException("IO exception in callback handler: " + e).initCause(e);
} catch (final UnsupportedCallbackException e) {
log.info("Login failed due to unsupported callback: " + e);
throw (LoginException) new LoginException("Callback handler does not support PasswordCallback and TextInputCallback.").initCause(e);
throw (LoginException) new LoginException(
"Callback handler does not support PasswordCallback and TextInputCallback.").initCause(e);
}
if (ticketCallback.getPassword() != null) {
this.ticket = new TicketCredential(new String(ticketCallback.getPassword()));
final String service = CommonUtils.isNotBlank(serviceCallback.getName()) ? serviceCallback.getName() : this.service;
final String service = CommonUtils.isNotBlank(
serviceCallback.getName()) ? serviceCallback.getName() : this.service;
if (this.cacheAssertions) {
// Multiple threads may be accessing the static cache concurrently
synchronized (ASSERTION_CACHE) {
if (ASSERTION_CACHE.get(ticket) != null) {
log.debug("Assertion found in cache.");
this.assertion = ASSERTION_CACHE.get(ticket);
}
this.assertion = ASSERTION_CACHE.get(ticket);
if (this.assertion != null) {
log.debug("Assertion found in cache.");
}
}
@ -346,7 +333,8 @@ public class CasLoginModule implements LoginModule {
log.debug("CAS assertion is null; ticket validation required.");
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.");
throw new LoginException(
"Neither login module nor callback handler provided required service parameter.");
}
try {
if (log.isDebugEnabled()) {
@ -413,7 +401,8 @@ public class CasLoginModule implements LoginModule {
throw new LoginException("Ticket credential not found.");
}
final AssertionPrincipal casPrincipal = new AssertionPrincipal(this.assertion.getPrincipal().getName(), this.assertion);
final AssertionPrincipal casPrincipal = new AssertionPrincipal(
this.assertion.getPrincipal().getName(), this.assertion);
this.subject.getPrincipals().add(casPrincipal);
// Add group containing principal as sole member
@ -458,10 +447,7 @@ public class CasLoginModule implements LoginModule {
if (log.isDebugEnabled()) {
log.debug("Caching assertion for principal " + this.assertion.getPrincipal());
}
// Multiple threads may be accessing the static cache concurrently
synchronized (ASSERTION_CACHE) {
ASSERTION_CACHE.put(this.ticket, this.assertion);
}
ASSERTION_CACHE.put(this.ticket, this.assertion);
}
} else {
// Login must have failed if there is no assertion defined
@ -522,10 +508,12 @@ public class CasLoginModule implements LoginModule {
* @return Ticket validator with properties set.
*/
private TicketValidator createTicketValidator(final String className, final Map<String,?> propertyMap) {
CommonUtils.assertTrue(propertyMap.containsKey("casServerUrlPrefix"), "Required property casServerUrlPrefix not found.");
CommonUtils.assertTrue(
propertyMap.containsKey("casServerUrlPrefix"), "Required property casServerUrlPrefix not found.");
final Class<TicketValidator> validatorClass = ReflectUtils.loadClass(className);
final TicketValidator validator = ReflectUtils.newInstance(validatorClass, propertyMap.get("casServerUrlPrefix"));
final TicketValidator validator = ReflectUtils.newInstance(
validatorClass, propertyMap.get("casServerUrlPrefix"));
try {
final BeanInfo info = Introspector.getBeanInfo(validatorClass);
@ -570,7 +558,8 @@ public class CasLoginModule implements LoginModule {
} 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());
throw new IllegalArgumentException(
"No conversion strategy exists for property " + pd.getName() + " of type " + pd.getPropertyType());
}
}
@ -598,21 +587,18 @@ public class CasLoginModule implements LoginModule {
if (log.isDebugEnabled()) {
log.debug("Cleaning assertion cache of size " + ASSERTION_CACHE.size());
}
// Multiple threads may be accessing the static cache concurrently
synchronized (ASSERTION_CACHE) {
final Iterator<Map.Entry<TicketCredential, Assertion>> iter = ASSERTION_CACHE.entrySet().iterator();
final Calendar cutoff = Calendar.getInstance();
cutoff.add(this.cacheTimeoutUnits, -this.cacheTimeout);
while (iter.hasNext()) {
final Assertion assertion = iter.next().getValue();
final Calendar created = Calendar.getInstance();
created.setTime(assertion.getValidFromDate());
if (created.before(cutoff)) {
if (log.isDebugEnabled()) {
log.debug("Removing expired assertion for principal " + assertion.getPrincipal());
}
iter.remove();
final Iterator<Map.Entry<TicketCredential, Assertion>> iter = ASSERTION_CACHE.entrySet().iterator();
final Calendar cutoff = Calendar.getInstance();
cutoff.setTimeInMillis(System.currentTimeMillis() - this.cacheTimeoutUnit.toMillis(this.cacheTimeout));
while (iter.hasNext()) {
final Assertion assertion = iter.next().getValue();
final Calendar created = Calendar.getInstance();
created.setTime(assertion.getValidFromDate());
if (created.before(cutoff)) {
if (log.isDebugEnabled()) {
log.debug("Removing expired assertion for principal " + assertion.getPrincipal());
}
iter.remove();
}
}
}

View File

@ -208,8 +208,8 @@ public class CasLoginModuleTests {
final String FAILURE_RESPONSE = "<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'><cas:authenticationFailure code=\"INVALID_TICKET\">Ticket ST-12345-ABCDEFGHIJKLMNOPQRSTUVWXYZ-hosta not recognized</cas:authenticationFailure></cas:serviceResponse>";
options.put("cacheAssertions", "true");
// Cache timeout is 1 second (Calendar.SECOND=13)
options.put("cacheTimeoutUnits", "13");
// Cache timeout is 1 second
options.put("cacheTimeoutUnit", "SECONDS");
options.put("cacheTimeout", "1");
server.content = SUCCESS_RESPONSE.getBytes(server.encoding);