View Javadoc

1   /*
2    * Copyright (c) 2004 UNINETT FAS
3    *
4    * This program is free software; you can redistribute it and/or modify it
5    * under the terms of the GNU General Public License as published by the Free
6    * Software Foundation; either version 2 of the License, or (at your option)
7    * any later version.
8    *
9    * This program is distributed in the hope that it will be useful, but WITHOUT
10   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11   * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12   * more details.
13   *
14   * You should have received a copy of the GNU General Public License along with
15   * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
16   * Place - Suite 330, Boston, MA 02111-1307, USA.
17   *
18   * $Id: MoriaCacheStore.java,v 1.40 2005/11/24 14:28:50 catoolsen Exp $
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         /* Add listener to primarily handle view events. */
114         // store.addTreeCacheListener(new MoriaTreeCacheListener(store));
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             /* Get the node id. */
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; // Remove object reference for garbage collection.
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         // Sanity checks.
338         if (ticketId == null || ticketId.equals(""))
339             throw new IllegalArgumentException("Ticket ID must be a non-empty string");
340 
341         // Accepted ticket types; login ticket or service ticket.
342         MoriaTicketType[] potentialTicketTypes = new MoriaTicketType[] {MoriaTicketType.LOGIN_TICKET, MoriaTicketType.SERVICE_TICKET};
343 
344         // Get ticket from store.
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         // Validate ticket, depending on type.
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         // Is this ticket associated with an authentication attempt?
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         /* Delete the ticket if so indicated. */
366         if (!keep) {
367             messageLogger.logDebug("Removing ticket from store", ticketId);
368             removeFromStore(ticket);
369         }
370 
371         // Return the authentication attempt.
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         // Sanity checks.
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         /* Create new SSO ticket with null-value servicePrincipal */
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         /* Validate argument. */
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         /* Validate argument. */
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         /* Primarily to check timestamp. */
494         validateTicket(loginTicket, MoriaTicketType.LOGIN_TICKET, null);
495 
496         /*
497          * Create new service ticket and associate it with the same
498          * authentication attempt as the login ticket.
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         /* Delete the now used login ticket. */
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         /* Validate arguments. */
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         /* Primarily to check timestamp. */
555         validateTicket(ssoTicket, MoriaTicketType.SSO_TICKET, null);
556 
557         /*
558          * Create new ticket granting ticket and associate it with the same user
559          * data as the SSO ticket.
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         // Set TGT in cache to the newly created TGT id
576         HashMap map = cachedUserData.getAttributes();
577         if (map.containsKey("tgt")) {
578             // try to cache the TGT
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         /* Validate arguments. */
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         /* Primarily to check timestamp. */
625         validateTicket(tgTicket, MoriaTicketType.TICKET_GRANTING_TICKET, servicePrincipal);
626 
627         /*
628          * Create new ticket granting ticket and associate it with the same user
629          * data as the TG ticket.
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         /* Validate arguments. */
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         /* Primarily to check timestamp. */
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         /* Insert into cache again to trigger distributed update. */
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         /* Validate arguments. */
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         /* Primarily to check timestamp. */
747         validateTicket(loginTicket, MoriaTicketType.LOGIN_TICKET, null);
748         validateTicket(ssoTicket, MoriaTicketType.SSO_TICKET, null);
749 
750         // Look up the authentication attempt.
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         // Get all cached attributes.
760         MoriaStoreData ssoData = ssoTicket.getData();
761         CachedUserData cachedUserData = null;
762         if (ssoData != null && ssoData instanceof CachedUserData) {
763 
764             // Create a new copy, since we'll be modifying it.
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         /* Transfer cached userdata to login attempt. */
773         authnAttempt.setTransientAttributes(cachedUserData.getAttributes());
774 
775         /* Insert into cache again to trigger distributed update. */
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         // Ignore attempts to remove an empty SSO ticket.
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         /* Validate parameter. */
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         /* Primarily to check timestamp. */
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         /* Validate parameter. */
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         /* Primarily to check timestamp. */
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         /* Validate parameter. */
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         /* Primarily to check timestamp. */
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         /* Validate arguments. */
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          * Check if it still is valid. We let the dedicated vacuuming service
962          * take care of removing it at later time, so we just throw an
963          * exception.
964          */
965         if (ticket.hasExpired()) { throw new InvalidTicketException("Ticket has expired. [" + ticket.getTicketId() + "]"); }
966 
967         /* Authorize the caller. */
968         if (servicePrincipal != null && !ticket.getServicePrincipal().equals(servicePrincipal)) { throw new InvalidTicketException("Illegal use of ticket by " + servicePrincipal + ". [" + ticket.getTicketId() + "]"); }
969 
970         /* Loop through ticket types until valid type found. */
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         /* Throw exception if all types were invalid. */
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         /* Validate parameters. */
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         /* Itterate of type array. Break if ticket is returned. */
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         // Sanity checks.
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         // The name of the TreeCache node to be retrieved.
1043         Fqn fqn = new Fqn(new Object[] {ticketType, ticketId});
1044 
1045         // Does the node exist at all?
1046         if (!store.exists(fqn))
1047             return null;
1048 
1049         // Look up the node.
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         // Sanity check.
1062         if (node == null) {
1063             messageLogger.logInfo(ticketType.toString() + " exists, but cannot be found", ticketId);
1064             return null;
1065         }
1066 
1067         // Return the node.
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         /* Validate parameters */
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         /* Validate parameters. */
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 }