1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package no.feide.moria.store;
22
23 import java.io.FileInputStream;
24 import java.io.FileNotFoundException;
25 import java.net.InetAddress;
26 import java.util.Date;
27 import java.util.HashMap;
28 import java.util.Iterator;
29 import java.util.Map;
30 import java.util.Properties;
31
32 import no.feide.moria.log.MessageLogger;
33
34 import org.jboss.cache.CacheException;
35 import org.jboss.cache.Fqn;
36 import org.jboss.cache.Node;
37 import org.jboss.cache.PropertyConfigurator;
38 import org.jboss.cache.TreeCache;
39 import org.jboss.cache.lock.LockingException;
40 import org.jboss.cache.lock.TimeoutException;
41 import org.jgroups.stack.IpAddress;
42
43 /***
44 * Distributed store implementation using JBoss Cache.
45 * @author Bjørn Ola Smievoll <b.o@smievoll.no>
46 * @version $Revision: 1.40 $
47 */
48 public final class MoriaCacheStore
49 implements MoriaStore {
50
51 /*** The cache instance. */
52 private TreeCache store;
53
54 /*** The configured state of the store. */
55 private Boolean isConfigured = new Boolean(false);
56
57 /*** The logger used by this class. */
58 private MessageLogger messageLogger = new MessageLogger(MoriaCacheStore.class);
59
60 /*** Map to contain the ticket ttl values. */
61 private Map ticketTTLs;
62
63 /*** Map containing the default ttl values. */
64 private final Map ticketDefaultTTLs = new HashMap();
65
66 /*** The common hashmap key for the ticket type. */
67 private static final String TICKET_TYPE_ATTRIBUTE = "TicketType";
68
69 /*** The common hashmap key for the time to live. */
70 private static final String TTL_ATTRIBUTE = "TimeToLive";
71
72 /*** The common hashmap key for the principal. */
73 private static final String PRINCIPAL_ATTRIBUTE = "Principal";
74
75 /*** The node identificator for this node ( <ip-addr>: <port>). */
76 private String nodeId;
77
78 /***
79 * The common hashmap key for the data attributes (MoriaAuthnAttempt &
80 * CachedUserData).
81 */
82 private static final String DATA_ATTRIBUTE = "MoriaData";
83
84 /***
85 * The common hashmap key for the userorg attribute.
86 */
87 private static final String USERORG_ATTRIBUTE = "Userorg";
88
89 /*** The name of the configuration file property. */
90 private static final String CACHE_CONFIG_PROPERTY_NAME = "no.feide.moria.store.cachestoreconf";
91
92 /*** The name of the ttl percentage property. */
93 private static final String REAL_TTL_PERCENTAGE_PROPERTY_NAME = "no.feide.moria.store.real_ttl_percentage";
94
95
96 /***
97 * Constructs a new instance.
98 * @throws MoriaStoreException
99 * If creation of JBoss TreeCache fails.
100 */
101 public MoriaCacheStore() throws MoriaStoreException {
102
103 messageLogger = new MessageLogger(no.feide.moria.store.MoriaCacheStore.class);
104
105 try {
106 store = new TreeCache();
107 } catch (RuntimeException re) {
108 throw re;
109 } catch (Exception e) {
110 throw new MoriaStoreException("Unable to create TreeCache instance.", e);
111 }
112
113
114
115 ticketDefaultTTLs.put(MoriaTicketType.LOGIN_TICKET, new Long(300000L));
116 ticketDefaultTTLs.put(MoriaTicketType.SERVICE_TICKET, new Long(300000L));
117 ticketDefaultTTLs.put(MoriaTicketType.SSO_TICKET, new Long(28800000L));
118 ticketDefaultTTLs.put(MoriaTicketType.TICKET_GRANTING_TICKET, new Long(7200000L));
119 ticketDefaultTTLs.put(MoriaTicketType.PROXY_TICKET, new Long(300000L));
120 }
121
122
123 /***
124 * Configures the store. This method expects the properties
125 * <code>no.feide.moria.store.cacheconf</code> and
126 * <code>no.feide.moria.store.real_ttl_percentage</code> to be set. The
127 * former must point to a JBossCache specific configuration file, the latter
128 * contain a value between 1 and 100. The method will return without
129 * actually executing and thus maintain the current state if called more
130 * than once per object instance.
131 * @param properties
132 * The properties used to configure the store.
133 * @throws MoriaStoreConfigurationException
134 * If something fails during the process of starting the store.
135 * @throws IllegalArgumentException
136 * If properties is null.
137 * @throws NullPointerException
138 * If defaultTTL is null.
139 * @see no.feide.moria.store.MoriaStore#setConfig(java.util.Properties)
140 */
141 public void setConfig(final Properties properties)
142 throws MoriaStoreConfigurationException {
143
144 synchronized (isConfigured) {
145 if (isConfigured.booleanValue()) {
146 messageLogger.logWarn("setConfig() called on already configured instance.");
147 return;
148 }
149
150 if (properties == null)
151 throw new IllegalArgumentException("properties cannot be null.");
152
153 String cacheConfigProperty = properties.getProperty(CACHE_CONFIG_PROPERTY_NAME);
154
155 if (cacheConfigProperty == null)
156 throw new MoriaStoreConfigurationException("Configuration property " + CACHE_CONFIG_PROPERTY_NAME + " must be set.");
157
158 String realTTLPercentageProperty = properties.getProperty(REAL_TTL_PERCENTAGE_PROPERTY_NAME);
159
160 if (realTTLPercentageProperty == null)
161 throw new MoriaStoreConfigurationException("Configuration property " + REAL_TTL_PERCENTAGE_PROPERTY_NAME + " must be set.");
162
163 long realTTLPercentage = Long.parseLong(realTTLPercentageProperty);
164
165 if (realTTLPercentage < 1L || realTTLPercentage > 100L)
166 throw new MoriaStoreConfigurationException(REAL_TTL_PERCENTAGE_PROPERTY_NAME + " must be between one and one hundred, inclusive.");
167
168 FileInputStream cacheConfigFile;
169
170 try {
171 cacheConfigFile = new FileInputStream(cacheConfigProperty);
172 } catch (FileNotFoundException fnnf) {
173 throw new MoriaStoreConfigurationException("Configuration file '" + cacheConfigProperty + "' not found", fnnf);
174 }
175
176 PropertyConfigurator configurator = new PropertyConfigurator();
177
178 try {
179 configurator.configure(store, cacheConfigFile);
180 } catch (Exception e) {
181 throw new MoriaStoreConfigurationException("Unable to configure the cache.", e);
182 }
183
184 messageLogger.logInfo("Using TicketTTLEvictionPolicy to get TTL configuration.");
185 TicketTTLEvictionPolicy ticketTTLEvictionPolicy = new TicketTTLEvictionPolicy();
186
187 try {
188 ticketTTLEvictionPolicy.parseConfig(store.getEvictionPolicyConfig());
189 } catch (Exception e) {
190 throw new MoriaStoreConfigurationException("Unable to get ticket TTL's from config", e);
191 }
192
193 ticketTTLs = new HashMap();
194 TicketTTLEvictionPolicy.RegionValue[] regionValues = ticketTTLEvictionPolicy.getRegionValues();
195
196 for (Iterator ticketTypeIterator = MoriaTicketType.TICKET_TYPES.iterator(); ticketTypeIterator.hasNext();) {
197 Long ttl = null;
198 MoriaTicketType ticketType = (MoriaTicketType) ticketTypeIterator.next();
199
200 for (int i = 0; i < regionValues.length; i++) {
201 if (ticketType.toString().equals(regionValues[i].getRegionName())) {
202 ttl = new Long(regionValues[i].getTimeToLive() * realTTLPercentage / 100L);
203 break;
204 }
205 }
206
207 if (ttl == null || ttl.compareTo(new Long(1000L)) < 0) {
208 Object defaultTTL = ticketDefaultTTLs.get(ticketType);
209
210 if (defaultTTL == null)
211 throw new NullPointerException("No default value defined for: " + ticketType);
212
213 ticketTTLs.put(ticketType, defaultTTL);
214 messageLogger.logCritical("TTL for " + ticketType + " not set or value to low (below ~2 seconds). Using default value.");
215 } else {
216 ticketTTLs.put(ticketType, ttl);
217 }
218 }
219
220 try {
221 messageLogger.logInfo("Attempting to start the TreeCache.");
222 store.start();
223 messageLogger.logInfo("TreeCache started.");
224 } catch (Exception e) {
225 throw new MoriaStoreConfigurationException("Unable to start the cache", e);
226 }
227
228
229 Object localAddress = store.getLocalAddress();
230
231 if (localAddress instanceof IpAddress) {
232 IpAddress ipAddress = (IpAddress) localAddress;
233 InetAddress inetAddress = ipAddress.getIpAddress();
234 nodeId = inetAddress.getHostAddress() + ":" + ipAddress.getPort();
235 } else {
236 nodeId = "0.0.0.0:0";
237 }
238
239 messageLogger.logWarn("Node id set to " + nodeId);
240
241 isConfigured = new Boolean(true);
242 }
243 }
244
245
246 /***
247 * Stops this instance of the store.
248 * @see no.feide.moria.store.MoriaStore#stop()
249 */
250 public synchronized void stop() {
251
252 synchronized (isConfigured) {
253 store.stop();
254 store = null;
255 isConfigured = new Boolean(false);
256 }
257 messageLogger.logWarn("The cache has been stopped.");
258 }
259
260
261 /***
262 * Creates an authentication attempt based on a service request.
263 * @param requestedAttributes
264 * The user attributes the requesting service asks for.
265 * @param responseURLPrefix
266 * The forward part of the url the client is to be redirected to.
267 * @param responseURLPostfix
268 * The end part of the url the client is to be redirected to.
269 * @param forceInteractiveAuthentication
270 * If the user should be forced to login interactively. I.e.
271 * disable support for single sign-on.
272 * @param servicePrincipal
273 * The id of the service doing the request.
274 * @return A login ticket identifying the authentication attempt.
275 * @throws MoriaStoreException
276 * If the operation fails.
277 * @throws IllegalArgumentException
278 * If any of the arguments are null, and if responseURLPrefix or
279 * servicePrincipal are zero length.
280 * @see no.feide.moria.store.MoriaStore#createAuthnAttempt(java.lang.String[],
281 * java.lang.String, java.lang.String, boolean, java.lang.String)
282 */
283 public String createAuthnAttempt(final String[] requestedAttributes, final String responseURLPrefix, final String responseURLPostfix, final boolean forceInteractiveAuthentication, final String servicePrincipal)
284 throws MoriaStoreException {
285
286 MoriaTicket ticket = null;
287 MoriaAuthnAttempt authnAttempt;
288
289 if (requestedAttributes == null) { throw new IllegalArgumentException("requestedAttributes cannot be null."); }
290
291 if (responseURLPrefix == null || responseURLPrefix.equals("")) { throw new IllegalArgumentException("responseURLPrefix cannot be null or empty string."); }
292
293 if (responseURLPostfix == null) { throw new IllegalArgumentException("responseURLPostfix cannot be null."); }
294
295 if (servicePrincipal == null || servicePrincipal.equals("")) { throw new IllegalArgumentException("servicePrincipal cannot be null or empty string."); }
296
297 authnAttempt = new MoriaAuthnAttempt(requestedAttributes, responseURLPrefix, responseURLPostfix, forceInteractiveAuthentication, servicePrincipal);
298
299 final Long expiryTime = new Long(((Long) ticketTTLs.get(MoriaTicketType.LOGIN_TICKET)).longValue() + new Date().getTime());
300
301 ticket = new MoriaTicket(MoriaTicketType.LOGIN_TICKET, nodeId, servicePrincipal, expiryTime, authnAttempt, null);
302
303 insertIntoStore(ticket);
304
305 return ticket.getTicketId();
306 }
307
308
309 /***
310 * Gets the authentication attempt associated with the ticket given as
311 * argument.
312 * @param ticketId
313 * The ticket ID. Must be a non-empty string.
314 * @param keep
315 * If <code>false</code>, the ticket will be removed from the
316 * store before returning. Otherwise keep the ticket.
317 * @param servicePrincipal
318 * The principal used by the service to authenticate itself to
319 * Moria. May be <code>null</code>.
320 * @return The authentication attempt.
321 * @throws IllegalArgumentException
322 * If ticket ID is <code>null</code> or an empty string.
323 * @throws NonExistentTicketException
324 * If the ticket does not exist in the store.
325 * @throws InvalidTicketException
326 * If the ticket is not associated with an authentication
327 * attempt.
328 * @throws MoriaStoreException
329 * If the operation fails.
330 * @see no.feide.moria.store.MoriaStore#getAuthnAttempt(java.lang.String,
331 * boolean, java.lang.String)
332 */
333 public MoriaAuthnAttempt getAuthnAttempt(final String ticketId, final boolean keep, final String servicePrincipal)
334 throws InvalidTicketException, NonExistentTicketException,
335 MoriaStoreException {
336
337
338 if (ticketId == null || ticketId.equals(""))
339 throw new IllegalArgumentException("Ticket ID must be a non-empty string");
340
341
342 MoriaTicketType[] potentialTicketTypes = new MoriaTicketType[] {MoriaTicketType.LOGIN_TICKET, MoriaTicketType.SERVICE_TICKET};
343
344
345 MoriaTicket ticket = getFromStore(potentialTicketTypes, ticketId);
346 if (ticket == null) {
347 messageLogger.logInfo("Ticket does not exist in the store", ticketId);
348 throw new NonExistentTicketException(ticketId);
349 }
350
351
352 if (ticket.getTicketType().equals(MoriaTicketType.LOGIN_TICKET))
353 validateTicket(ticket, MoriaTicketType.LOGIN_TICKET, null);
354 else
355 validateTicket(ticket, MoriaTicketType.SERVICE_TICKET, servicePrincipal);
356
357
358 MoriaAuthnAttempt authnAttempt = null;
359 MoriaStoreData data = ticket.getData();
360 if (data != null && data instanceof MoriaAuthnAttempt)
361 authnAttempt = (MoriaAuthnAttempt) data;
362 else
363 throw new InvalidTicketException("No authentication attempt associated with ticket. [" + ticketId + "]");
364
365
366 if (!keep) {
367 messageLogger.logDebug("Removing ticket from store", ticketId);
368 removeFromStore(ticket);
369 }
370
371
372 return authnAttempt;
373 }
374
375
376 /***
377 * Creates a new CachedUserData object in the store and associates it with
378 * an SSO ticket which is returned.
379 * @param attributes
380 * The attribute map to be cached.
381 * @param userorg
382 * The userorg that is to be associated with the ticket.
383 * @return The SSO ticket that identifies the cached user data.
384 * @throws MoriaStoreException
385 * If the operation fails.
386 * @throws IllegalArgumentException
387 * If attributes is null, or userorg is null or an empty string.
388 * @see no.feide.moria.store.MoriaStore#cacheUserData(java.util.HashMap,
389 * String)
390 */
391 public String cacheUserData(final HashMap attributes, final String userorg)
392 throws MoriaStoreException {
393
394
395 if (attributes == null)
396 throw new IllegalArgumentException("Attributes cannot be null");
397 if ((userorg == null) || (userorg.length() == 0))
398 throw new IllegalArgumentException("User organization must be a non-empty string");
399
400 CachedUserData userData = new CachedUserData(attributes);
401
402 final Long expiryTime = new Long(((Long) ticketTTLs.get(MoriaTicketType.SSO_TICKET)).longValue() + new Date().getTime());
403 MoriaTicket ssoTicket = new MoriaTicket(MoriaTicketType.SSO_TICKET, nodeId, null, expiryTime, userData, userorg);
404 insertIntoStore(ssoTicket);
405
406 return ssoTicket.getTicketId();
407 }
408
409
410 /***
411 * Returns the userdata associated with the incoming ticket, which must be
412 * either a proxy ticket, an SSO ticket or ticket granting ticket.
413 * @param ticketId
414 * A ticket to identify a userdata object (SSO, TGT or PROXY).
415 * @param servicePrincipal
416 * The name of the service requesting the data,
417 * @return A clone of the object containing the userdata.
418 * @throws InvalidTicketException
419 * If the incoming ticket is not of the correct type or has an
420 * invalid principal.
421 * @throws NonExistentTicketException
422 * If ticket does not exist.
423 * @throws MoriaStoreException
424 * If the operation fails.
425 * @throws IllegalArgumentException
426 * If ticketId is null or zero length, or SSO ticket principal
427 * is null or zero length.
428 * @see no.feide.moria.store.MoriaStore#getUserData(java.lang.String,
429 * java.lang.String)
430 */
431 public CachedUserData getUserData(final String ticketId, final String servicePrincipal)
432 throws NonExistentTicketException, InvalidTicketException,
433 MoriaStoreException {
434
435
436 if (ticketId == null || ticketId.equals("")) { throw new IllegalArgumentException("ticketId must be a non-empty string."); }
437
438 MoriaTicketType[] potentialTicketTypes = new MoriaTicketType[] {MoriaTicketType.SSO_TICKET, MoriaTicketType.TICKET_GRANTING_TICKET, MoriaTicketType.PROXY_TICKET};
439
440 MoriaTicket ticket = getFromStore(potentialTicketTypes, ticketId);
441
442 if (ticket == null) { throw new NonExistentTicketException(ticketId); }
443
444 if (!ticket.getTicketType().equals(MoriaTicketType.SSO_TICKET)) {
445 if (servicePrincipal == null || servicePrincipal.equals("")) { throw new IllegalArgumentException("servicePrincipal must be a non-empty string for this ticket type."); }
446 }
447
448 validateTicket(ticket, potentialTicketTypes, servicePrincipal);
449
450 CachedUserData cachedUserData = null;
451
452 MoriaStoreData data = ticket.getData();
453
454 if (data != null && data instanceof CachedUserData) {
455 cachedUserData = (CachedUserData) data;
456 } else {
457 throw new InvalidTicketException("No user data associated with ticket. [" + ticketId + "]");
458 }
459
460 removeFromStore(ticket);
461 return cachedUserData;
462 }
463
464
465 /***
466 * Creates a service ticket that the service will use when requesting user
467 * attributes after a successful authentication.
468 * @param loginTicketId
469 * A login ticket associated with an authentication attempt.
470 * @return A service ticket associated with the authentication attempt
471 * object.
472 * @throws InvalidTicketException
473 * If the supplied ticket is not a login ticket.
474 * @throws NonExistentTicketException
475 * If ticket does not exist.
476 * @throws MoriaStoreException
477 * If the operation fails.
478 * @throws IllegalArgumentException
479 * If loginTicketId is null or zero length.
480 * @see no.feide.moria.store.MoriaStore#createServiceTicket(java.lang.String)
481 */
482 public String createServiceTicket(final String loginTicketId)
483 throws InvalidTicketException, NonExistentTicketException,
484 MoriaStoreException {
485
486
487 if (loginTicketId == null || loginTicketId.equals("")) { throw new IllegalArgumentException("loginTicketId must be a non-empty string"); }
488
489 MoriaTicket loginTicket = getFromStore(MoriaTicketType.LOGIN_TICKET, loginTicketId);
490
491 if (loginTicket == null) { throw new NonExistentTicketException(loginTicketId); }
492
493
494 validateTicket(loginTicket, MoriaTicketType.LOGIN_TICKET, null);
495
496
497
498
499
500 MoriaAuthnAttempt authnAttempt = null;
501
502 MoriaStoreData data = loginTicket.getData();
503
504 if (data != null && data instanceof MoriaAuthnAttempt) {
505 authnAttempt = (MoriaAuthnAttempt) data;
506 } else {
507 throw new InvalidTicketException("No authentication attempt associated with login ticket. [" + loginTicketId + "]");
508 }
509
510 final Long expiryTime = new Long(((Long) ticketTTLs.get(MoriaTicketType.SERVICE_TICKET)).longValue() + new Date().getTime());
511 MoriaTicket serviceTicket = new MoriaTicket(MoriaTicketType.SERVICE_TICKET, nodeId, loginTicket.getServicePrincipal(), expiryTime, authnAttempt, loginTicket.getUserorg());
512 insertIntoStore(serviceTicket);
513
514 removeFromStore(loginTicket);
515
516 return serviceTicket.getTicketId();
517 }
518
519
520 /***
521 * Creates a new ticket granting ticket, using an sso ticket.
522 * @param ssoTicketId
523 * An sso ticket that is already associated with a cached
524 * userdata object.
525 * @param targetServicePrincipal
526 * The id of the service that will use the TGT.
527 * @return A ticket-granting ticket that the requesting service may use for
528 * later proxy authentication.
529 * @throws InvalidTicketException
530 * If the argument ticket is not an SSO ticket or has an invalid
531 * principal.
532 * @throws NonExistentTicketException
533 * If ticket does not exist.
534 * @throws MoriaStoreException
535 * If the operation fails.
536 * @throws IllegalArgumentException
537 * If any of the arguments are null or zero length.
538 * @see no.feide.moria.store.MoriaStore#createTicketGrantingTicket(java.lang.String,
539 * java.lang.String)
540 */
541 public String createTicketGrantingTicket(final String ssoTicketId, final String targetServicePrincipal)
542 throws InvalidTicketException, NonExistentTicketException,
543 MoriaStoreException {
544
545
546 if (ssoTicketId == null || ssoTicketId.equals("")) { throw new IllegalArgumentException("ssoTicketId must be a non-empty string"); }
547
548 if (targetServicePrincipal == null || targetServicePrincipal.equals("")) { throw new IllegalArgumentException("targetServicePrincipal must be a non-empty string"); }
549
550 MoriaTicket ssoTicket = getFromStore(MoriaTicketType.SSO_TICKET, ssoTicketId);
551
552 if (ssoTicket == null) { throw new NonExistentTicketException(ssoTicketId); }
553
554
555 validateTicket(ssoTicket, MoriaTicketType.SSO_TICKET, null);
556
557
558
559
560
561 CachedUserData cachedUserData = null;
562
563 MoriaStoreData data = ssoTicket.getData();
564
565 if (data != null && data instanceof CachedUserData) {
566 cachedUserData = (CachedUserData) data;
567 } else {
568 throw new InvalidTicketException("No user data associated with SSO ticket. [" + ssoTicketId + "]");
569 }
570
571 final Long expiryTime = new Long(((Long) ticketTTLs.get(MoriaTicketType.TICKET_GRANTING_TICKET)).longValue() + new Date().getTime());
572 MoriaTicket tgTicket = new MoriaTicket(MoriaTicketType.TICKET_GRANTING_TICKET, nodeId, targetServicePrincipal, expiryTime, cachedUserData, ssoTicket.getUserorg());
573 insertIntoStore(tgTicket);
574
575
576 HashMap map = cachedUserData.getAttributes();
577 if (map.containsKey("tgt")) {
578
579 removeFromStore(ssoTicket);
580 cachedUserData.addAttribute("tgt", tgTicket.getTicketId());
581 insertIntoStore(ssoTicket);
582 }
583 return tgTicket.getTicketId();
584 }
585
586
587 /***
588 * Creates a new proxy ticket from a TGT and associates the new ticket with
589 * the same user data as the TGT.
590 * @param tgTicketId
591 * A TGT issued earlier to a service.
592 * @param servicePrincipal
593 * The id of the service making the request.
594 * @param targetServicePrincipal
595 * The id of the service that will use the proxy ticket.
596 * @return Proxy ticket that may be used by the requesting service.
597 * @throws InvalidTicketException
598 * If the incoming ticket is not a TGT or has an invalid
599 * principal.
600 * @throws NonExistentTicketException
601 * If ticket does not exist.
602 * @throws MoriaStoreException
603 * If the operation fails.
604 * @throws IllegalArgumentException
605 * If any of the arguments are null or zero length.
606 * @see no.feide.moria.store.MoriaStore#createProxyTicket(java.lang.String,
607 * java.lang.String, java.lang.String)
608 */
609 public String createProxyTicket(final String tgTicketId, final String servicePrincipal, final String targetServicePrincipal)
610 throws InvalidTicketException, NonExistentTicketException,
611 MoriaStoreException {
612
613
614 if (tgTicketId == null || tgTicketId.equals("")) { throw new IllegalArgumentException("tgTicketId must be a non-empty string."); }
615
616 if (servicePrincipal == null || servicePrincipal.equals("")) { throw new IllegalArgumentException("servicePrincipal must be a non-empty string."); }
617
618 if (targetServicePrincipal == null || targetServicePrincipal.equals("")) { throw new IllegalArgumentException("targetServicePrincipal must be a non-empty string."); }
619
620 MoriaTicket tgTicket = getFromStore(MoriaTicketType.TICKET_GRANTING_TICKET, tgTicketId);
621
622 if (tgTicket == null) { throw new NonExistentTicketException(tgTicketId); }
623
624
625 validateTicket(tgTicket, MoriaTicketType.TICKET_GRANTING_TICKET, servicePrincipal);
626
627
628
629
630
631 CachedUserData cachedUserData = null;
632
633 MoriaStoreData data = tgTicket.getData();
634
635 if (data != null && data instanceof CachedUserData) {
636 cachedUserData = (CachedUserData) data;
637 } else {
638 throw new InvalidTicketException("No user data associated with ticket granting ticket. [" + tgTicketId + "]");
639 }
640
641 final Long expiryTime = new Long(((Long) ticketTTLs.get(MoriaTicketType.PROXY_TICKET)).longValue() + new Date().getTime());
642 MoriaTicket proxyTicket = new MoriaTicket(MoriaTicketType.PROXY_TICKET, nodeId, targetServicePrincipal, expiryTime, cachedUserData, tgTicket.getUserorg());
643 insertIntoStore(proxyTicket);
644
645 return proxyTicket.getTicketId();
646 }
647
648
649 /***
650 * Sets transient attributes stored with authentication attempt in an SSO
651 * context, which implies that not all cached (for potential SSO attributes)
652 * should be included.
653 * @param loginTicketId
654 * Ticket that identifies the AuthnAttempt that the attributes
655 * will be associated with.
656 * @param transientAttributes
657 * Attributes which are to be stored with the authentication
658 * attempt.
659 * @throws InvalidTicketException
660 * If ticket is found invalid.
661 * @throws NonExistentTicketException
662 * If ticket does not exist.
663 * @throws MoriaStoreException
664 * If the operation fails.
665 * @throws IllegalArgumentException
666 * If loginTicketId is null or zero length, or
667 * transientAttributes is null.
668 * @see no.feide.moria.store.MoriaStore#setTransientAttributes(java.lang.String,
669 * java.util.HashMap)
670 */
671 public void setTransientAttributes(final String loginTicketId, final HashMap transientAttributes)
672 throws InvalidTicketException, NonExistentTicketException,
673 MoriaStoreException {
674
675
676 if (loginTicketId == null || loginTicketId.equals("")) { throw new IllegalArgumentException("loginTicketId must be a non-empty string."); }
677
678 if (transientAttributes == null) { throw new IllegalArgumentException("transientAttributes cannot be null."); }
679
680 MoriaTicket loginTicket = getFromStore(MoriaTicketType.LOGIN_TICKET, loginTicketId);
681
682 if (loginTicket == null) { throw new NonExistentTicketException(loginTicketId); }
683
684
685 validateTicket(loginTicket, MoriaTicketType.LOGIN_TICKET, null);
686
687 MoriaAuthnAttempt authnAttempt = null;
688
689 MoriaStoreData data = loginTicket.getData();
690
691 if (data != null && data instanceof MoriaAuthnAttempt) {
692 authnAttempt = (MoriaAuthnAttempt) data;
693 } else {
694 throw new InvalidTicketException("No authentication attempt associated with login ticket. [" + loginTicketId + "]");
695 }
696
697 authnAttempt.setTransientAttributes(transientAttributes);
698
699
700 insertIntoStore(loginTicket);
701 }
702
703
704 /***
705 * Sets transient attributes stored with authentication attempt, copied from
706 * a cached user data object.
707 * @param loginTicketId
708 * Ticket that identifies the AuthnAttempt that the attributes
709 * will be associated with.
710 * @param ssoTicketId
711 * Ticket associated with a set of cached user data.
712 * @param ssoEnabledAttributeNames
713 * The names of those attributes which should be stored with the
714 * authentication attempt. Only those transient (cached)
715 * attributes named in this parameter will be stored.
716 * @throws InvalidTicketException
717 * If either ticket is found invalid.
718 * @throws NonExistentTicketException
719 * If either ticket does not exist.
720 * @throws MoriaStoreException
721 * If the operation fails.
722 * @throws IllegalArgumentException
723 * If either ticket id is null or zero length.
724 * @see no.feide.moria.store.MoriaStore#setTransientSSOAttributes(java.lang.String,
725 * java.lang.String, java.lang.String[])
726 */
727 public void setTransientSSOAttributes(final String loginTicketId, final String ssoTicketId, final String[] ssoEnabledAttributeNames)
728 throws InvalidTicketException, NonExistentTicketException,
729 MoriaStoreException {
730
731
732 if (loginTicketId == null || loginTicketId.equals("")) { throw new IllegalArgumentException("loginTicketId must be a non-empty string."); }
733
734 if (ssoTicketId == null || ssoTicketId.equals("")) { throw new IllegalArgumentException("ssoTicketId must be a non-empty string."); }
735 if (ssoEnabledAttributeNames == null)
736 throw new IllegalArgumentException("Allowed SSO attribute names cannot be NULL");
737
738 MoriaTicket loginTicket = getFromStore(MoriaTicketType.LOGIN_TICKET, loginTicketId);
739
740 if (loginTicket == null) { throw new NonExistentTicketException(loginTicketId); }
741
742 MoriaTicket ssoTicket = getFromStore(MoriaTicketType.SSO_TICKET, ssoTicketId);
743
744 if (ssoTicket == null) { throw new NonExistentTicketException(ssoTicketId); }
745
746
747 validateTicket(loginTicket, MoriaTicketType.LOGIN_TICKET, null);
748 validateTicket(ssoTicket, MoriaTicketType.SSO_TICKET, null);
749
750
751 MoriaStoreData loginData = loginTicket.getData();
752 MoriaAuthnAttempt authnAttempt = null;
753 if (loginData != null && loginData instanceof MoriaAuthnAttempt) {
754 authnAttempt = (MoriaAuthnAttempt) loginData;
755 } else {
756 throw new InvalidTicketException("No authentication attempt associated with login ticket. [" + loginTicketId + "]");
757 }
758
759
760 MoriaStoreData ssoData = ssoTicket.getData();
761 CachedUserData cachedUserData = null;
762 if (ssoData != null && ssoData instanceof CachedUserData) {
763
764
765 cachedUserData = new CachedUserData(((CachedUserData) ssoData).getAttributes());
766 for (int i=0; i<ssoEnabledAttributeNames.length; i++)
767 cachedUserData.removeAttribute(ssoEnabledAttributeNames[i]);
768
769 } else
770 throw new InvalidTicketException("No cached user data associated with sso ticket. [" + ssoTicketId + "]");
771
772
773 authnAttempt.setTransientAttributes(cachedUserData.getAttributes());
774
775
776 insertIntoStore(loginTicket);
777 }
778
779
780 /***
781 * Removes an SSO ticket from the store.
782 * @param ssoTicketId
783 * The ID of the ticket to remove.
784 * @throws NonExistentTicketException
785 * If ticket given by <code>ssoTicketId</code> does not exist,
786 * or is empty.
787 * @throws MoriaStoreException
788 * If the operation fails.
789 * @see no.feide.moria.store.MoriaStore#removeSSOTicket(java.lang.String)
790 */
791 public void removeSSOTicket(final String ssoTicketId)
792 throws NonExistentTicketException, MoriaStoreException {
793
794
795 if (ssoTicketId == null || ssoTicketId.equals(""))
796 throw new NonExistentTicketException("Attempt to remove an empty SSO ticket");
797
798 MoriaTicket ssoTicket = getFromStore(MoriaTicketType.SSO_TICKET, ssoTicketId);
799
800 if (ssoTicket != null)
801 removeFromStore(ssoTicket);
802 else
803 throw new NonExistentTicketException("Attempt to remove an unknown SSO ticket");
804 }
805
806
807 /***
808 * Returns the service principal for the ticket.
809 * @param ticketId
810 * The ticket id.
811 * @param ticketType
812 * The ticket type.
813 * @return Service principal.
814 * @throws InvalidTicketException
815 * If the ticket is invalid.
816 * @throws NonExistentTicketException
817 * If ticket does not exist.
818 * @throws MoriaStoreException
819 * If the operation fails.
820 * @throws IllegalArgumentException
821 * If ticketId is null or zero length.
822 * @see no.feide.moria.store.MoriaTicket#getServicePrincipal()
823 */
824 public String getTicketServicePrincipal(final String ticketId, final MoriaTicketType ticketType)
825 throws InvalidTicketException, NonExistentTicketException,
826 MoriaStoreException {
827
828
829 if (ticketId == null || ticketId.equals("")) { throw new IllegalArgumentException("ticketId must be non-empty string."); }
830
831 MoriaTicket ticket = getFromStore(ticketType, ticketId);
832
833 if (ticket == null) { throw new NonExistentTicketException(ticketId); }
834
835
836 validateTicket(ticket, ticketType, null);
837
838 return ticket.getServicePrincipal();
839 }
840
841
842 /***
843 * Sets the userorg of a ticket.
844 * @param ticketId
845 * The ticket id.
846 * @param ticketType
847 * The ticket type.
848 * @param userorg
849 * The userorg of the user creating the ticket.
850 * @throws InvalidTicketException
851 * if the ticket is invalid.
852 * @throws NonExistentTicketException
853 * If ticket does not exist.
854 * @throws MoriaStoreException
855 * If the operation fails.
856 * @throws IllegalArgumentException
857 * If ticketId is null or zero length.
858 * @see no.feide.moria.store.MoriaStore#setTicketUserorg(String,
859 * MoriaTicketType, String)
860 */
861 public void setTicketUserorg(final String ticketId, final MoriaTicketType ticketType, final String userorg)
862 throws InvalidTicketException, NonExistentTicketException,
863 MoriaStoreException {
864
865
866 if (ticketId == null || ticketId.equals("")) { throw new IllegalArgumentException("ticketId must be non-empty string."); }
867
868 MoriaTicket ticket = getFromStore(ticketType, ticketId);
869
870 if (ticket == null) { throw new NonExistentTicketException(ticketId); }
871
872
873 validateTicket(ticket, ticketType, null);
874
875 removeFromStore(ticket);
876 ticket.setUserorg(userorg);
877 insertIntoStore(ticket);
878 }
879
880
881 /***
882 * Gets the userorg of a ticket.
883 * @param ticketId
884 * the ticket id.
885 * @param ticketType
886 * the ticket type.
887 * @return the organization of the user creating the ticket, or null if not
888 * set.
889 * @throws InvalidTicketException
890 * If the ticket is invalid.
891 * @throws NonExistentTicketException
892 * If ticket does not exist.
893 * @throws MoriaStoreException
894 * If the operation fails.
895 * @throws IllegalArgumentException
896 * If ticketId is null or zero length.
897 * @see no.feide.moria.store.MoriaStore#getTicketUserorg(String,
898 * MoriaTicketType)
899 */
900 public String getTicketUserorg(final String ticketId, final MoriaTicketType ticketType)
901 throws InvalidTicketException, NonExistentTicketException,
902 MoriaStoreException {
903
904
905 if (ticketId == null || ticketId.equals("")) { throw new IllegalArgumentException("ticketId must be non-empty string."); }
906
907 MoriaTicket ticket = getFromStore(ticketType, ticketId);
908
909 if (ticket == null) { throw new NonExistentTicketException(ticketId); }
910
911
912 validateTicket(ticket, ticketType, null);
913
914 return ticket.getUserorg();
915 }
916
917
918 /***
919 * Checks validity of ticket against type and expiry time.
920 * @param ticket
921 * Ticket to be checked.
922 * @param ticketType
923 * The expected type of the ticket.
924 * @param servicePrincipal
925 * The service expected to be associated with this ticket.
926 * @throws IllegalArgumentException
927 * If ticket is null, or ticketType is null or zero length.
928 * @throws InvalidTicketException
929 * If ticket is found invalid.
930 */
931 private void validateTicket(final MoriaTicket ticket, final MoriaTicketType ticketType, final String servicePrincipal)
932 throws InvalidTicketException {
933
934 validateTicket(ticket, new MoriaTicketType[] {ticketType}, servicePrincipal);
935 }
936
937
938 /***
939 * Check validity of ticket against a set of types and expiry time.
940 * @param ticket
941 * Ticket to be checked.
942 * @param ticketTypes
943 * Array of valid types for the ticket.
944 * @param servicePrincipal
945 * The service that is using the ticket. May be null if no
946 * service is available.
947 * @throws IllegalArgumentException
948 * If ticket is null, or ticketType is null or zero length.
949 * @throws InvalidTicketException
950 * If the ticket is found to be invalid.
951 */
952 private void validateTicket(final MoriaTicket ticket, final MoriaTicketType[] ticketTypes, final String servicePrincipal)
953 throws InvalidTicketException {
954
955
956 if (ticket == null) { throw new IllegalArgumentException("ticket cannot be null."); }
957
958 if (ticketTypes == null || ticketTypes.length < 1) { throw new IllegalArgumentException("ticketTypes cannot be null or zero length."); }
959
960
961
962
963
964
965 if (ticket.hasExpired()) { throw new InvalidTicketException("Ticket has expired. [" + ticket.getTicketId() + "]"); }
966
967
968 if (servicePrincipal != null && !ticket.getServicePrincipal().equals(servicePrincipal)) { throw new InvalidTicketException("Illegal use of ticket by " + servicePrincipal + ". [" + ticket.getTicketId() + "]"); }
969
970
971 boolean valid = false;
972
973 for (int i = 0; i < ticketTypes.length; i++) {
974 if (ticket.getTicketType().equals(ticketTypes[i])) {
975 valid = true;
976 break;
977 }
978 }
979
980
981 if (!valid) { throw new InvalidTicketException("Ticket has wrong type: " + ticket.getTicketType() + ". [" + ticket.getTicketId() + "]"); }
982 }
983
984
985 /***
986 * Retrieves a ticket instance which may be one of a number of types.
987 * @param ticketTypes
988 * Array of potential ticket types for the ticket id.
989 * @param ticketId
990 * Id of the ticket to be retrieved.
991 * @return A ticket, or null if none found.
992 * @throws IllegalArgumentException
993 * If the any of arguments are null value or zero length.
994 * @throws MoriaStoreException
995 * If access to the store failed in some way.
996 */
997 MoriaTicket getFromStore(final MoriaTicketType[] ticketTypes, final String ticketId)
998 throws MoriaStoreException {
999
1000
1001 if (ticketTypes == null || ticketTypes.length < 1) { throw new IllegalArgumentException("ticketTypes cannot be null or zero length."); }
1002
1003 if (ticketId == null || ticketId.equals("")) { throw new IllegalArgumentException("ticketId must be a non-empty string."); }
1004
1005 MoriaTicket ticket = null;
1006
1007
1008 for (int i = 0; i < ticketTypes.length; i++) {
1009 ticket = getFromStore(ticketTypes[i], ticketId);
1010 if (ticket != null)
1011 break;
1012 }
1013
1014 return ticket;
1015 }
1016
1017
1018 /***
1019 * Retrieves a ticket instance from the store.
1020 * @param ticketType
1021 * The type of ticket.
1022 * @param ticketId
1023 * The ID of the ticket.
1024 * @return The ticket, or null if none found.
1025 * @throws IllegalArgumentException
1026 * If <code>ticketType</code> is <code>null</code>, or if
1027 * <code>ticketId</code> is <code>null</code> or an empty
1028 * string.
1029 * @throws MoriaStoreException
1030 * If operations on the underlying <code>TreeCache</code>
1031 * fail; acts as a wrapper.
1032 */
1033 MoriaTicket getFromStore(final MoriaTicketType ticketType, final String ticketId)
1034 throws MoriaStoreException {
1035
1036
1037 if (ticketType == null)
1038 throw new IllegalArgumentException("Ticket type cannot be null");
1039 if (ticketId == null || ticketId.equals(""))
1040 throw new IllegalArgumentException("Ticket ID must be a non-empty string");
1041
1042
1043 Fqn fqn = new Fqn(new Object[] {ticketType, ticketId});
1044
1045
1046 if (!store.exists(fqn))
1047 return null;
1048
1049
1050 Node node = null;
1051 try {
1052 node = store.get(fqn);
1053 } catch (LockingException e) {
1054 throw new MoriaStoreException("Locking of store failed for ticket. [" + ticketId + "]", e);
1055 } catch (TimeoutException e) {
1056 throw new MoriaStoreException("Access to store timed out for ticket. [" + ticketId + "]", e);
1057 } catch (CacheException e) {
1058 throw new MoriaStoreException("Cache store failure for ticket. [" + ticketId + "]", e);
1059 }
1060
1061
1062 if (node == null) {
1063 messageLogger.logInfo(ticketType.toString() + " exists, but cannot be found", ticketId);
1064 return null;
1065 }
1066
1067
1068 return new MoriaTicket(ticketId, (MoriaTicketType) node.get(TICKET_TYPE_ATTRIBUTE), (String) node.get(PRINCIPAL_ATTRIBUTE), (Long) node.get(TTL_ATTRIBUTE), (MoriaStoreData) node.get(DATA_ATTRIBUTE), (String) node.get(USERORG_ATTRIBUTE));
1069
1070 }
1071
1072
1073 /***
1074 * Inserts an authentication attempt or cached user data into the cache.
1075 * Either authnAttempt or cachedUserData must be null.
1076 * @param ticket
1077 * The ticket to connect to the inserted object.
1078 * @throws IllegalArgumentException
1079 * If ticket is null.
1080 * @throws MoriaStoreException
1081 * If operations on the TreeCache fail.
1082 */
1083 private void insertIntoStore(final MoriaTicket ticket)
1084 throws MoriaStoreException {
1085
1086
1087 if (ticket == null) { throw new IllegalArgumentException("ticket cannot be null."); }
1088
1089 Fqn fqn = new Fqn(new Object[] {ticket.getTicketType(), ticket.getTicketId()});
1090
1091 HashMap attributes = new HashMap();
1092 attributes.put(TICKET_TYPE_ATTRIBUTE, ticket.getTicketType());
1093 attributes.put(TTL_ATTRIBUTE, ticket.getExpiryTime());
1094 attributes.put(PRINCIPAL_ATTRIBUTE, ticket.getServicePrincipal());
1095 attributes.put(DATA_ATTRIBUTE, ticket.getData());
1096 attributes.put(USERORG_ATTRIBUTE, ticket.getUserorg());
1097
1098 try {
1099 store.put(fqn, attributes);
1100 } catch (RuntimeException re) {
1101 throw re;
1102 } catch (Exception e) {
1103 throw new MoriaStoreException("Insertion into store failed. [" + ticket.getTicketId() + "]", e);
1104 }
1105 }
1106
1107
1108 /***
1109 * Removes a ticket, and possibly a connected userdata or authnAttempt from
1110 * the cache.
1111 * @param ticket
1112 * The ticket to be removed.
1113 * @throws IllegalArgumentException
1114 * If ticket is null.
1115 * @throws NonExistentTicketException
1116 * If the ticket does not exist.
1117 * @throws MoriaStoreException
1118 * If an exception is thrown when operating on the store.
1119 */
1120 private void removeFromStore(final MoriaTicket ticket)
1121 throws NonExistentTicketException, MoriaStoreException {
1122
1123
1124 if (ticket == null) { throw new IllegalArgumentException("ticket cannot be null."); }
1125
1126 Fqn fqn = new Fqn(new Object[] {ticket.getTicketType(), ticket.getTicketId()});
1127
1128 if (store.exists(fqn)) {
1129 try {
1130 store.remove(fqn);
1131 } catch (RuntimeException re) {
1132 throw re;
1133 } catch (Exception e) {
1134 throw new MoriaStoreException("Removal from store failed. [" + ticket.getTicketId() + "]", e);
1135 }
1136 } else {
1137 throw new NonExistentTicketException();
1138 }
1139 }
1140 }