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.controller;
22
23 import java.net.URI;
24 import java.net.URISyntaxException;
25 import java.util.Arrays;
26 import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.Iterator;
29 import java.util.Map;
30 import java.util.Properties;
31
32 import javax.servlet.ServletContext;
33
34 import no.feide.moria.authorization.AuthorizationManager;
35 import no.feide.moria.authorization.UnknownAttributeException;
36 import no.feide.moria.authorization.UnknownServicePrincipalException;
37 import no.feide.moria.configuration.ConfigurationManager;
38 import no.feide.moria.configuration.ConfigurationManagerException;
39 import no.feide.moria.directory.Credentials;
40 import no.feide.moria.directory.DirectoryManager;
41 import no.feide.moria.directory.backend.AuthenticationFailedException;
42 import no.feide.moria.directory.backend.BackendException;
43
44 import no.feide.moria.log.AccessLogger;
45 import no.feide.moria.log.AccessStatusType;
46 import no.feide.moria.log.MessageLogger;
47 import no.feide.moria.store.InvalidTicketException;
48 import no.feide.moria.store.MoriaAuthnAttempt;
49 import no.feide.moria.store.MoriaStore;
50 import no.feide.moria.store.MoriaTicketType;
51 import no.feide.moria.store.MoriaStoreConfigurationException;
52 import no.feide.moria.store.MoriaStoreException;
53 import no.feide.moria.store.MoriaStoreFactory;
54 import no.feide.moria.store.NonExistentTicketException;
55
56 import org.apache.log4j.Level;
57
58 /***
59 * Intermediator for the sub modules of Moria. The controller is the only entry
60 * point for accessing Moria. Basically, all work is done by the authorization
61 * module, the distributed store, the directory manager and the logger. The
62 * controller must be initialized from the servlets that are using it. This can
63 * be done by calling the <code>initController</code> method.
64 * @author Lars Preben S. Arnesen <lars.preben.arnesen@conduct.no>
65 * @version $Revision: 1.82 $
66 * @see MoriaController#initController(javax.servlet.ServletContext)
67 */
68 public final class MoriaController {
69
70 /***
71 * Ticket type constant, indicating an SSO ticket, for use when returning a
72 * HashMap of two tickets.
73 * @see MoriaController#attemptLogin(java.lang.String, java.lang.String,
74 * java.lang.String, java.lang.String, boolean)
75 * @see MoriaController#attemptSingleSignOn(java.lang.String,
76 * java.lang.String)
77 */
78 public static final String SSO_TICKET = "sso";
79
80 /***
81 * Ticket type constant, indicating a login ticket, for use when returning a
82 * HashMap with multiple tickets.
83 * @see MoriaController#attemptLogin(java.lang.String, java.lang.String,
84 * java.lang.String, java.lang.String, boolean)
85 * @see MoriaController#attemptSingleSignOn(java.lang.String,
86 * java.lang.String)
87 */
88 public static final String SERVICE_TICKET = "service";
89
90 /***
91 * Operation type for local authentication.
92 */
93 private static final String DIRECT_AUTH_OPER = "DirectAuth";
94
95 /***
96 * Operation type for interactive authentication.
97 */
98 private static final String INTERACTIVE_AUTH_OPER = "InteractiveAuth";
99
100 /***
101 * Operation type for interactive authentication.
102 */
103 private static final String PROXY_AUTH_OPER = "ProxyAuth";
104
105 /***
106 * Operation type for verify user existence.
107 */
108 private static final String VERIFY_USER_EXISTENCE_OPER = "VerifyUserExistence";
109
110 /***
111 * Identifier for the TGT used in attribute requests.
112 */
113 static final String TGT_IDENTIFIER = "tgt";
114
115 /***
116 * Standard exception message for indication that the store is unavailable.
117 */
118 private static final String STORE_DOWN = "Moria is unavailable, the store is down.";
119
120 /***
121 * Standard exception message for indication that the controller is not
122 * ready.
123 */
124 private static final String NOT_READY = "Moria is unavailable, the controller is not ready.";
125
126 /***
127 * Standard exception message for indication that ticket does not exist.
128 */
129 private static final String NONEXISTENT_TICKET = "Ticket does not exist.";
130
131 /***
132 * Standard log message for NonExistentTicketException.
133 */
134 private static final String CAUGHT_NONEXISTENT_TICKET = "NonExistentTicketException caught";
135
136 /***
137 * Standard log message for InvalidTicketException.
138 */
139 private static final String CAUGHT_INVALID_TICKET = "InvalidTicketException caught";
140
141 /***
142 * Standard log message for InvalidTicketException.
143 */
144 private static final String CAUGHT_STORE = "MoriaStoreException caught";
145
146 /***
147 * Log message for AuthorizationException.
148 */
149 private static final String CAUGHT_DENIED_USERORG = "AuthorizationException caught";
150
151 /***
152 * The single instance of the data store.
153 */
154 private static MoriaStore store;
155
156 /***
157 * The single instance of the configuration manager.
158 */
159 private static ConfigurationManager configManager;
160
161 /***
162 * The single instance of the authorization manager.
163 */
164 private static AuthorizationManager authzManager;
165
166 /***
167 * The single instance of the directory manager.
168 */
169 private static DirectoryManager directoryManager;
170
171 /***
172 * Flag set to true if the controller has been initialized.
173 */
174 private static Boolean isInitialized = new Boolean(false);
175
176 /***
177 * Flag set to true if the controller and all modules are ready.
178 */
179 private static boolean ready = false;
180
181 /***
182 * Flag set to true if the authorization manager is ready.
183 */
184 private static boolean amReady = false;
185
186 /***
187 * Flag set to true if the directory manager is ready.
188 */
189 private static boolean dmReady = false;
190
191 /***
192 * Flag set to true if the store manager is ready.
193 */
194 private static boolean smReady = false;
195
196 /***
197 * The servlet context for the servlets using the controller.
198 */
199 private static ServletContext servletContext;
200
201 /***
202 * Used for access logging.
203 */
204 private static AccessLogger accessLogger;
205
206 /***
207 * Used for message/error logging.
208 */
209 private static MessageLogger messageLogger;
210
211
212 /***
213 * Private constructor. Never to be used.
214 */
215 private MoriaController() {
216
217
218
219 }
220
221
222 /***
223 * Initiates the controller. The initialization includes the initialization
224 * of all sub modules.
225 * @throws InoperableStateException
226 * If Moria is not ready for use.
227 */
228 static synchronized void init() throws InoperableStateException {
229
230 synchronized (isInitialized) {
231
232
233 if (isInitialized.booleanValue()) { return; }
234 isInitialized = new Boolean(true);
235
236
237 messageLogger = new MessageLogger(MoriaController.class);
238 accessLogger = new AccessLogger();
239
240
241 try {
242 store = MoriaStoreFactory.createMoriaStore();
243 } catch (MoriaStoreException e) {
244 messageLogger.logCritical("Store failed to start", e);
245 throw new InoperableStateException("Moria cannot start, the store is unavailable.");
246 }
247
248
249 authzManager = new AuthorizationManager();
250
251
252 directoryManager = new DirectoryManager();
253
254
255 try {
256 configManager = new ConfigurationManager();
257 } catch (ConfigurationManagerException e) {
258 messageLogger.logCritical("Moria cannot start, configuration failed.", e);
259 throw new InoperableStateException("Moria cannot start, configuration failed. " + e.getMessage());
260 }
261
262
263
264
265 }
266 }
267
268
269 /***
270 * Shuts down the controller. All ready status fields are set to false.
271 */
272 static synchronized void stop() {
273
274 synchronized (isInitialized) {
275 if (ready) {
276
277
278
279
280 authzManager = null;
281 amReady = false;
282 configManager.stop();
283 configManager = null;
284 directoryManager.stop();
285 directoryManager = null;
286 dmReady = false;
287 store.stop();
288 store = null;
289 smReady = false;
290 servletContext = null;
291 ready = false;
292 isInitialized = new Boolean(false);
293 }
294 }
295 }
296
297
298 /***
299 * Gets the total status of the controller. The method returns a HashMap
300 * with Boolean values. The following elements are in the map:
301 * <ul>
302 * <li>init: <code>true</code> if the <code>initController</code>
303 * method has been called, else <code>false</code>.
304 * <li>dm: <code>true</code> if the
305 * <code>DirectoryManager.setConfig</code> method has been called, else
306 * <code>false</code>.
307 * <li>sm: <code>true</code> if the <code>MoriaStore.setConfig</code>
308 * method has been called, else <code>false</code>.
309 * <li>am: <code>true</code> if the
310 * <code>AuthorizationManager.setConfig</code> method has been called,
311 * else <code>false</code>.
312 * <li>moria: <code>true</code> all the above are true (the controller is
313 * ready to use).
314 * </ul>
315 * @return A <code>HashMap</code> with all status fields for the
316 * controller (<code>init</code>,<code>dm</code>,
317 * <code>sm</code>,<code>am</code> and <code>moria</code>).
318 * @see MoriaController#initController(javax.servlet.ServletContext)
319 * @see DirectoryManager#setConfig(java.util.Properties)
320 * @see MoriaStore#setConfig(java.util.Properties)
321 * @see AuthorizationManager#setConfig(java.util.Properties)
322 */
323 public static HashMap getStatus() {
324
325 final HashMap totalStatus = new HashMap();
326 totalStatus.put("init", isInitialized);
327 totalStatus.put("dm", new Boolean(dmReady));
328 totalStatus.put("sm", new Boolean(smReady));
329 totalStatus.put("am", new Boolean(amReady));
330 totalStatus.put("moria", new Boolean(ready));
331
332 return totalStatus;
333 }
334
335
336 /***
337 * Attempts single sign on (non-interactive) with an SSO ticket together
338 * with the login ticket. If both tickets are valid and the requested
339 * attributes are cached, a service ticket is returned and there is no need
340 * to perform the regular interactive authentication.
341 * @param loginTicketId
342 * The reference to the authentication attempt.
343 * @param ssoTicketId
344 * The SSO ticket received from the users browser.
345 * @return A service ticket.
346 * @throws UnknownTicketException
347 * If either the login ticket or the SSO ticket is invalid or
348 * non-existing, the authetication attempt requires interactive
349 * authentication, or the SSO ticket does not point to a cached
350 * user data object with enough attributes.
351 * @throws InoperableStateException
352 * If the controller is not ready.
353 * @throws IllegalInputException
354 * If the <code>loginTicketId</code> and/or
355 * <code>ssoTicketId</code> is null or empty.
356 * @throws UnknownServicePrincipalException
357 * If the service principal cannot be resolved, in which case
358 * there is probably an issue with the Authentication Module
359 * configuration.
360 */
361 public static String attemptSingleSignOn(final String loginTicketId,
362 final String ssoTicketId)
363 throws UnknownTicketException, InoperableStateException,
364 IllegalInputException, UnknownServicePrincipalException {
365
366
367 if (!ready) { throw new InoperableStateException(NOT_READY); }
368
369
370 if (loginTicketId == null || loginTicketId.equals("")) { throw new IllegalInputException("loginTicketId must be a non-empty string."); }
371 if (ssoTicketId == null || ssoTicketId.equals("")) { throw new IllegalInputException("ssoTicketId must be a non-empty string."); }
372
373
374 final MoriaAuthnAttempt authnAttempt;
375 try {
376 authnAttempt = store.getAuthnAttempt(loginTicketId, true, null);
377
378
379
380 final String servicePrincipal = authnAttempt.getServicePrincipal();
381 final String[] nonSSOAttributes = authzManager.getNonSSOAttributeNames(servicePrincipal);
382 final String[] requestedAttributes = authnAttempt.getRequestedAttributes();
383 String unavailableAttributes = "";
384 for (int i = 0; i < requestedAttributes.length; i++)
385 for (int j = 0; j < nonSSOAttributes.length; j++)
386 if (requestedAttributes[i].equalsIgnoreCase(nonSSOAttributes[j]))
387 unavailableAttributes = unavailableAttributes + requestedAttributes[i] + ", ";
388 if (unavailableAttributes.length() > 0) {
389 unavailableAttributes = unavailableAttributes.substring(0, unavailableAttributes.length() - 2);
390 messageLogger.logWarn("Service '" + servicePrincipal + "' denied attributes in SSO context: [" + unavailableAttributes + "]", loginTicketId);
391 }
392
393 } catch (InvalidTicketException e) {
394 accessLogger.logUser(AccessStatusType.INVALID_LOGIN_TICKET, null, null, loginTicketId, null);
395 messageLogger.logWarn(CAUGHT_INVALID_TICKET, loginTicketId, e);
396 throw new UnknownTicketException(NONEXISTENT_TICKET);
397 } catch (NonExistentTicketException e) {
398 accessLogger.logUser(AccessStatusType.NONEXISTENT_LOGIN_TICKET, null, null, loginTicketId, null);
399 messageLogger.logInfo(CAUGHT_NONEXISTENT_TICKET, loginTicketId, e);
400 throw new UnknownTicketException(NONEXISTENT_TICKET);
401 } catch (MoriaStoreException e) {
402 messageLogger.logCritical(CAUGHT_STORE, loginTicketId, e);
403 throw new InoperableStateException(STORE_DOWN);
404 }
405
406
407 if (authnAttempt.isForceInterativeAuthentication()) {
408 messageLogger.logInfo("SSO authentication attempt denied by web service provider.");
409 throw new UnknownTicketException("Authentication attempt requires interactive authentication.");
410 }
411
412
413 final String[] requestedAttributes = authnAttempt.getRequestedAttributes();
414 final HashSet cachedAttributes = authzManager.getCachableAttributes();
415 for (int i = 0; i < requestedAttributes.length; i++) {
416 if (!cachedAttributes.contains(requestedAttributes[i])) {
417 messageLogger.logDebug("SSO authentication failed, request for non-cached attributes.");
418 throw new UnknownTicketException("SSO ticket not sufficient, service requests uncached attributes.");
419 }
420 }
421
422
423 final String serviceTicket;
424 try {
425
426
427 store.setTransientSSOAttributes(loginTicketId, ssoTicketId, authzManager.getNonSSOAttributeNames(authnAttempt.getServicePrincipal()));
428
429
430 String userorg = store.getTicketUserorg(ssoTicketId, MoriaTicketType.SSO_TICKET);
431 store.setTicketUserorg(loginTicketId, MoriaTicketType.LOGIN_TICKET, userorg);
432
433
434 serviceTicket = store.createServiceTicket(loginTicketId);
435 } catch (InvalidTicketException e) {
436 accessLogger.logUser(AccessStatusType.INVALID_SSO_TICKET, null, null, ssoTicketId, null);
437 messageLogger.logWarn(CAUGHT_INVALID_TICKET, ssoTicketId, e);
438 throw new UnknownTicketException(NONEXISTENT_TICKET);
439 } catch (NonExistentTicketException e) {
440 accessLogger.logUser(AccessStatusType.NONEXISTENT_SSO_TICKET, null, null, ssoTicketId, null);
441 messageLogger.logInfo(CAUGHT_NONEXISTENT_TICKET, ssoTicketId, e);
442 throw new UnknownTicketException(NONEXISTENT_TICKET);
443 } catch (MoriaStoreException e) {
444 messageLogger.logCritical(CAUGHT_STORE, ssoTicketId, e);
445 throw new InoperableStateException(STORE_DOWN);
446 }
447
448 accessLogger.logUser(AccessStatusType.SUCCESSFUL_SSO_AUTHENTICATION, authnAttempt.getServicePrincipal(), null, loginTicketId, serviceTicket);
449 return serviceTicket;
450 }
451
452
453 /***
454 * Performs interactive login attempt using tickets and credentials. The
455 * authentication is performed by the directory service, using the supplied
456 * username and password. All retrieved user data is cached in the
457 * authentication attempt, identified by the <code>loginTicketId</code>.
458 * A new cached userdata object is created and all cachable attributes are
459 * stored in it. The existing SSO ticket is removed. After a successful
460 * authentication a new service ticket, pointing to the same authentication
461 * attempt, is created. A new SSO ticket is created, pointing to the cached
462 * userdata object.
463 * @param loginTicketId
464 * The ticket identifying the authentication attempt.
465 * @param ssoTicketId
466 * The ticket identifying the existing cached user data object.
467 * @param userId
468 * The user's userId.
469 * @param password
470 * The user's password.
471 * @param denySSO
472 * The user's SSO choice.
473 * @return A HashMap with two tickets: login and SSO, indexed with
474 * <code>MoriaController.SSO_TICKET</code> and
475 * <code>MoiraController.LOGIN_TICKET</code>.
476 * @throws UnknownTicketException
477 * If the login ticket is invalid or does not exist.
478 * @throws InoperableStateException
479 * If the controller is not ready to be used, or the store
480 * cannot be accessed.
481 * @throws IllegalInputException
482 * If any of <code>loginTicketId</code>,<code>userId</code>,
483 * or <code>password</code> are <code>null</code> or an
484 * empty string.
485 * @throws AuthenticationException
486 * If the authentication failed due to wrong credentials.
487 * @throws AuthorizationException
488 * If the user's organization is not allowed to use this service
489 * @throws DirectoryUnavailableException
490 * If the directory of the user's home organization is
491 * unavailable.
492 */
493 public static Map attemptLogin(final String loginTicketId,
494 final String ssoTicketId,
495 final String userId,
496 final String password,
497 final boolean denySSO)
498 throws UnknownTicketException, InoperableStateException,
499 IllegalInputException, AuthenticationException,
500 DirectoryUnavailableException, AuthorizationException {
501
502
503 if (!ready)
504 throw new InoperableStateException(NOT_READY);
505 if (loginTicketId == null || loginTicketId.equals(""))
506 throw new IllegalInputException("Login ticket ID must be a non-empty string");
507 if (userId == null || userId.equals(""))
508 throw new IllegalInputException("User ID must be a non-empty string");
509 if (password == null || password.equals(""))
510 throw new IllegalInputException("Password must be a non-empty string");
511
512
513 final MoriaAuthnAttempt authnAttempt;
514 String userorg = null;
515 try {
516 String servicePrincipal = store.getTicketServicePrincipal(loginTicketId, MoriaTicketType.LOGIN_TICKET);
517 userorg = getUserOrg(userId);
518
519 store.setTicketUserorg(loginTicketId, MoriaTicketType.LOGIN_TICKET, userorg);
520
521 if (!authzManager.allowUserorg(servicePrincipal, userorg)) { throw new AuthorizationException("Access to the requested service is denied for " + userorg + "."); }
522 authnAttempt = store.getAuthnAttempt(loginTicketId, true, null);
523 } catch (NonExistentTicketException e) {
524 accessLogger.logUser(AccessStatusType.NONEXISTENT_LOGIN_TICKET, null, userId, loginTicketId, null);
525 messageLogger.logDebug(CAUGHT_NONEXISTENT_TICKET, loginTicketId, e);
526 throw new UnknownTicketException(NONEXISTENT_TICKET);
527 } catch (InvalidTicketException e) {
528 accessLogger.logUser(AccessStatusType.INVALID_LOGIN_TICKET, null, userId, loginTicketId, null);
529 messageLogger.logWarn(CAUGHT_INVALID_TICKET, loginTicketId, e);
530 throw new UnknownTicketException(NONEXISTENT_TICKET);
531 } catch (MoriaStoreException e) {
532 messageLogger.logCritical(CAUGHT_STORE, loginTicketId, e);
533 throw new InoperableStateException(STORE_DOWN);
534 } catch (UnknownServicePrincipalException e) {
535
536 throw new AuthorizationException("Access to the requested service is denied for " + userorg + ".");
537 }
538
539
540
541
542
543 final String[] requestedAttributes = authnAttempt.getRequestedAttributes();
544 final HashSet parsedRequestAttributes = new HashSet();
545 boolean appendTGT = false;
546
547
548 for (int i = 0; i < requestedAttributes.length; i++) {
549 if (requestedAttributes[i].equals(TGT_IDENTIFIER) && !denySSO) {
550 appendTGT = true;
551 } else {
552 parsedRequestAttributes.add(requestedAttributes[i]);
553 }
554 }
555
556
557 final HashSet cachableAttributes = authzManager.getCachableAttributes();
558 final HashSet retrieveAttributes = new HashSet(cachableAttributes);
559 retrieveAttributes.addAll(parsedRequestAttributes);
560
561
562 final HashMap fetchedAttributes;
563 try {
564 fetchedAttributes = authenticate(loginTicketId, new Credentials(userId, password), (String[]) retrieveAttributes.toArray(new String[retrieveAttributes.size()]));
565 } catch (AuthenticationFailedException e) {
566 accessLogger.logUser(AccessStatusType.BAD_USER_CREDENTIALS, null, userId, loginTicketId, null);
567 messageLogger.logDebug("AuthenticationFailedException caught", loginTicketId, e);
568 throw new AuthenticationException();
569 } catch (BackendException e) {
570 messageLogger.logWarn("BackendException caught", loginTicketId, e);
571 throw new DirectoryUnavailableException();
572 }
573
574
575 if (ssoTicketId != null && !ssoTicketId.equals("")) {
576 try {
577 store.removeSSOTicket(ssoTicketId);
578 } catch (NonExistentTicketException e) {
579
580 messageLogger.logDebug("SSO ticket does not exist - may have timed out", ssoTicketId, e);
581 } catch (MoriaStoreException e) {
582
583 messageLogger.logCritical(CAUGHT_STORE, ssoTicketId, e);
584 throw new InoperableStateException(STORE_DOWN);
585 }
586 }
587
588
589 final String serviceTicketId;
590 final String newSSOTicketId;
591 final HashMap cacheAttributes = new HashMap();
592 final HashMap authnAttemptAttrs = new HashMap();
593
594 final Iterator it = cachableAttributes.iterator();
595 while (it.hasNext()) {
596 final String attrName = (String) it.next();
597 cacheAttributes.put(attrName, fetchedAttributes.get(attrName));
598 }
599
600 for (int i = 0; i < requestedAttributes.length; i++) {
601 authnAttemptAttrs.put(requestedAttributes[i], fetchedAttributes.get(requestedAttributes[i]));
602 }
603
604 try {
605 newSSOTicketId = store.cacheUserData(cacheAttributes, userorg);
606 store.setTicketUserorg(newSSOTicketId, MoriaTicketType.SSO_TICKET, userorg);
607 if (appendTGT) {
608 authnAttemptAttrs.put(TGT_IDENTIFIER, store.createTicketGrantingTicket(newSSOTicketId, authnAttempt.getServicePrincipal()));
609 }
610 store.setTransientAttributes(loginTicketId, authnAttemptAttrs);
611 serviceTicketId = store.createServiceTicket(loginTicketId);
612 store.setTicketUserorg(serviceTicketId, MoriaTicketType.SERVICE_TICKET, userorg);
613
614 } catch (NonExistentTicketException e) {
615
616 messageLogger.logWarn(CAUGHT_NONEXISTENT_TICKET + ", should not happen (already validated)", loginTicketId, e);
617 throw new UnknownTicketException(NONEXISTENT_TICKET);
618 } catch (InvalidTicketException e) {
619 messageLogger.logWarn(CAUGHT_INVALID_TICKET + ", should not happen (already validated)", loginTicketId, e);
620 throw new UnknownTicketException(NONEXISTENT_TICKET);
621 } catch (MoriaStoreException e) {
622 messageLogger.logCritical(CAUGHT_STORE, ssoTicketId, e);
623 throw new InoperableStateException(STORE_DOWN);
624 }
625
626
627 final HashMap tickets = new HashMap();
628 tickets.put(SERVICE_TICKET, serviceTicketId);
629 tickets.put(SSO_TICKET, newSSOTicketId);
630
631 accessLogger.logUser(AccessStatusType.SUCCESSFUL_INTERACTIVE_AUTHENTICATION, authnAttempt.getServicePrincipal(), userId, loginTicketId, "Service: " + serviceTicketId + " SSO: " + ssoTicketId);
632
633 return tickets;
634 }
635
636
637 /***
638 * Initiates authentication through Moria. An authentication attempt is
639 * created and the supplied argument is stored in it for later use. After a
640 * successful authentication, the user is redirected back to a URL
641 * consisting of the URL prefix and postfix, with the service ticket added
642 * in the middle.
643 * @param attributes
644 * The requested attributes. Cannot be <code>null</code>.
645 * @param returnURLPrefix
646 * Prefix of the redirect URL, used to direct the user back to
647 * the web service. Cannot be <code>null</code> or an empty
648 * string.
649 * @param returnURLPostfix
650 * Postfix of the redirect URL, used to direct the user back to
651 * the web service. Cannot be <code>null</code>.
652 * @param forceInteractiveAuthentication
653 * If <code>true</code>, do not use SSO.
654 * @param servicePrincipal
655 * The principal of the requesting service. Cannot be
656 * <code>null</code> or an empty string.
657 * @return A login ticket ID.
658 * @throws AuthorizationException
659 * If the service requests attributes it is not authorized to
660 * receive.
661 * @throws IllegalInputException
662 * If <code>attributes</code> or <code>returnURLPostfix</code>
663 * is <code>null</code>, or <code>returnURLPrefix</code> or
664 * <code>servicePrincipal</code> is <code>null</code> or an
665 * empty string.
666 * @throws InoperableStateException
667 * If the controller is not yet ready for use, or if the store
668 * cannot be accessed at this time.
669 */
670 public static String initiateAuthentication(final String[] attributes,
671 final String returnURLPrefix,
672 final String returnURLPostfix,
673 final boolean forceInteractiveAuthentication,
674 final String servicePrincipal)
675 throws AuthorizationException, IllegalInputException,
676 InoperableStateException {
677
678
679 if (!ready)
680 throw new InoperableStateException("Controller is not ready");
681
682
683 if (servicePrincipal == null || servicePrincipal.equals("")) {
684 messageLogger.logCritical("Missing service principal - check service container configuration");
685 throw new IllegalInputException("Service principal cannot be null or an empty string");
686 }
687 if (attributes == null)
688 throw new IllegalInputException("Attributes cannot be null");
689 if (returnURLPrefix == null || returnURLPrefix.equals(""))
690 throw new IllegalInputException("URL prefix cannot be null or an empty string");
691 if (returnURLPostfix == null)
692 throw new IllegalInputException("URL postfix cannot be null");
693
694
695 authorizationCheck(servicePrincipal, attributes, INTERACTIVE_AUTH_OPER);
696
697
698 final String validationURL = returnURLPrefix + "FakeMoriaID" + "urlPostfix";
699 if (!(isLegalURL(validationURL))) {
700 accessLogger.logService(AccessStatusType.INITIATE_DENIED_INVALID_URL, servicePrincipal, null, null);
701 messageLogger.logWarn("Service '" + servicePrincipal + "' tried to submit invalid URL '" + validationURL + "'");
702 throw new IllegalInputException("Service supplied an invalid URL");
703 }
704
705
706 final String loginTicketId;
707 try {
708 loginTicketId = store.createAuthnAttempt(attributes, returnURLPrefix, returnURLPostfix, forceInteractiveAuthentication, servicePrincipal);
709 } catch (MoriaStoreException e) {
710 messageLogger.logCritical(CAUGHT_STORE, e);
711 throw new InoperableStateException(STORE_DOWN);
712 }
713
714
715
716 accessLogger.logService(AccessStatusType.SUCCESSFUL_AUTH_INIT, servicePrincipal, null, loginTicketId);
717
718 return loginTicketId;
719 }
720
721
722 /***
723 * Performs an authorization validation of a service request; can the
724 * service perform the requested operation? If no exception is thrown, the
725 * authorization was successful.
726 * @param servicePrincipal
727 * The principal for the service performing the request. Must be
728 * a non-empty string.
729 * @param attributes
730 * The requested attributes, if any.
731 * @param operation
732 * The requested operation. Must be a non-empty string.
733 * @throws AuthorizationException
734 * If the authorization failed, for some reason.
735 * @throws IllegalArgumentException
736 * If <code>servicePrincipal</code> is an empty string, or
737 * <code>operation</code> is unknown or <code>null</code>.
738 */
739 private static void authorizationCheck(final String servicePrincipal,
740 final String[] attributes,
741 final String operation)
742 throws AuthorizationException {
743
744
745 if (servicePrincipal == null || servicePrincipal.equals(""))
746 throw new IllegalArgumentException("Service principal must be a non-empty string");
747 if (operation == null || operation.length() == 0)
748 throw new IllegalArgumentException("Operation must be a non-empty string");
749
750
751 final AccessStatusType statusType;
752 if (operation == DIRECT_AUTH_OPER)
753 statusType = AccessStatusType.ACCESS_DENIED_DIRECT_AUTH;
754 else if (operation.equals(INTERACTIVE_AUTH_OPER))
755 statusType = AccessStatusType.ACCESS_DENIED_INITIATE_AUTH;
756 else if (operation.equals(PROXY_AUTH_OPER))
757 statusType = AccessStatusType.ACCESS_DENIED_PROXY_AUTH;
758 else if (operation.equals(VERIFY_USER_EXISTENCE_OPER))
759 statusType = AccessStatusType.ACCESS_DENIED_VERIFY_USER_EXISTENCE;
760 else
761 throw new IllegalArgumentException("Unknown operation '" + operation + "' attempted");
762
763 try {
764
765
766 if (!authzManager.allowOperations(servicePrincipal, new String[] {operation})) {
767 accessLogger.logService(statusType, servicePrincipal, null, null);
768 messageLogger.logInfo("Service '" + servicePrincipal + "' tried to perform '" + operation + "', but can only do '" + authzManager.getOperations(servicePrincipal));
769 throw new AuthorizationException("Access to the requested operation is denied");
770 }
771
772
773 if (!authzManager.allowAccessTo(servicePrincipal, attributes)) {
774
775
776 HashSet deniedAttributes = new HashSet(Arrays.asList(attributes));
777 final HashSet allowedAttributes = authzManager.getAttributes(servicePrincipal);
778 Iterator i = allowedAttributes.iterator();
779 while (i.hasNext())
780 deniedAttributes.remove(i.next());
781
782 accessLogger.logService(statusType, servicePrincipal, null, null);
783 messageLogger.logInfo("Service \"" + servicePrincipal + "\" does not have access to attribute(s) " + deniedAttributes + "");
784 throw new AuthorizationException("Access to the requested attribute(s) is denied");
785 }
786
787 } catch (UnknownServicePrincipalException e) {
788
789
790 messageLogger.logWarn("UnknownServicePrincipalException caught during authorizationCheck, " + "service probably not configured in authorization database.", e);
791 throw new AuthorizationException("Authorization failed for service '" + servicePrincipal + "'");
792
793 }
794 }
795
796
797 /***
798 * Checks whether the user's organization is allowed to use the service in
799 * question. If no exception is thrown, this is allowed.
800 * @param servicePrincipal
801 * The principal for the service performing the request.
802 * @param userOrganization
803 * The organization the user comes from. Must be a non-empty
804 * string.
805 * @throws AuthorizationException
806 * If the user is not allowed to use this service.
807 * @throws IllegalArgumentException
808 * If <code>servicePrincipal</code> is an empty string.
809 */
810 private static void organizationCheck(final String servicePrincipal,
811 final String userOrganization)
812 throws AuthorizationException {
813
814
815 if (servicePrincipal == null || servicePrincipal.equals(""))
816 throw new IllegalArgumentException("Service principal must be a non-empty string");
817
818 try {
819
820
821
822 if (!isOrganizationAllowedForService(servicePrincipal, userOrganization)) {
823
824
825 accessLogger.logService(AccessStatusType.ACCESS_DENIED_USERORG, servicePrincipal, null, null);
826 messageLogger.logInfo("Access to the service '" + servicePrincipal + "' is denied for the organization '" + userOrganization + "'");
827 throw new AuthorizationException("Access to the service '" + servicePrincipal + "' is denied for the organization '" + userOrganization + "'");
828
829 }
830
831 } catch (UnknownServicePrincipalException e) {
832
833
834 messageLogger.logWarn("Unknown service '" + servicePrincipal + "'");
835 throw new AuthorizationException("Unknown service '" + servicePrincipal + "'");
836
837 }
838 }
839
840
841 /***
842 * Check whether a given service may allow users from a given organization.
843 * @param servicePrincipal
844 * The service's unique principal.
845 * @param userOrganization
846 * The home organization, in short form.
847 * @return <code>true</code> if users from this organization can access
848 * this service.
849 * @throws IllegalArgumentException
850 * If <code>servicePrincipal</code> or
851 * <code>userOrganization</code> is <code>null</code> or an
852 * empty string.
853 * @throws UnknownServicePrincipalException
854 * If <code>servicePrincipal</code> is unknown.
855 */
856 public static boolean isOrganizationAllowedForService(final String servicePrincipal,
857 final String userOrganization)
858 throws IllegalArgumentException, UnknownServicePrincipalException {
859
860 if (servicePrincipal == null || servicePrincipal.equals(""))
861 throw new IllegalArgumentException("Service principal must be a non-empty string");
862 if (userOrganization == null || userOrganization.equals(""))
863 throw new IllegalArgumentException("User organization must be a non-empty string");
864
865
866 return (userOrganization != null && authzManager.allowUserorg(servicePrincipal, userOrganization));
867
868 }
869
870
871 /***
872 * Gets the name of the attributes a service requests, based on the
873 * loginTicket.
874 * @param loginTicket
875 * the login ticket
876 * @param servicePrincipal
877 * the name of the service that requested the attributes
878 * @return An array with attribute names.
879 * @throws IllegalInputException
880 * @throws UnknownTicketException
881 * @throws InoperableStateException
882 * @throws AuthorizationException
883 */
884 public static String[] getRequestedAttributes(final String loginTicket,
885 final String servicePrincipal)
886 throws IllegalInputException, UnknownTicketException,
887 InoperableStateException, AuthorizationException {
888
889
890 if (!ready)
891 throw new InoperableStateException(NOT_READY);
892 if (loginTicket == null || loginTicket.equals(""))
893 throw new IllegalInputException("Login ticket ID must be a non-empty string.");
894 if (servicePrincipal == null || servicePrincipal.equals(""))
895 throw new IllegalInputException("Service principal must be a non-empty string.");
896
897 String[] attr = null;
898 try {
899 attr = store.getAuthnAttempt(loginTicket, true, servicePrincipal).getRequestedAttributes();
900 } catch (MoriaStoreException e) {
901 messageLogger.logCritical(CAUGHT_STORE, e);
902 throw new InoperableStateException(STORE_DOWN);
903 } catch (InvalidTicketException e) {
904 throw new UnknownTicketException(NONEXISTENT_TICKET);
905 } catch (NonExistentTicketException e) {
906 throw new UnknownTicketException(NONEXISTENT_TICKET);
907 }
908
909 return attr;
910 }
911
912
913 /***
914 * Retrieves user attributes from an authentication attempt. The method
915 * returns the user attributes stored in the authentication attempt, which
916 * is referenced to by the service ticket. <br>
917 * <br>
918 * Note that this method can only be used once for each non-SSO
919 * authentication attempt. For security reasons, Moria will not cache
920 * attribute values longer than absolutely necessary.
921 * @param serviceTicketId
922 * The ticket associated with the authentication attempt. Cannot
923 * be <code>null</code> or an empty string.
924 * @param servicePrincipal
925 * The principal of the calling service. Cannot be
926 * <code>null</code> or an empty string.
927 * @return A newly instantiated <code>Map</code> object containing the
928 * requested user attributes, if found. Entries have a
929 * <code>String</code> key and a <code>String[]</code> value.
930 * @throws AuthorizationException
931 * If userorg isn't set for ticket, userorg is denied access to
932 * the service or service principal is unknown.
933 * @throws IllegalInputException
934 * If <code>serviceTicketId</code> or
935 * <code>servicePrincipal</code> is <code>null</code> or an
936 * empty string.
937 * @throws UnknownTicketException
938 * If the service ticket does not exist in the store, or is
939 * invalid.
940 * @throws InoperableStateException
941 * If Moria is not ready for use.
942 */
943 public static Map getUserAttributes(final String serviceTicketId,
944 final String servicePrincipal)
945 throws IllegalInputException, UnknownTicketException,
946 InoperableStateException, AuthorizationException {
947
948
949 if (!ready)
950 throw new InoperableStateException(NOT_READY);
951 if (serviceTicketId == null || serviceTicketId.equals(""))
952 throw new IllegalInputException("Service ticket ID must be a non-empty string.");
953 if (servicePrincipal == null || servicePrincipal.equals(""))
954 throw new IllegalInputException("Service principal must be a non-empty string.");
955
956
957 HashMap filteredAttributes = new HashMap();
958 try {
959 String userorg = null;
960 userorg = store.getTicketUserorg(serviceTicketId, MoriaTicketType.SERVICE_TICKET);
961 if (userorg == null) { throw new AuthorizationException("Userorg is not set for ticket"); }
962
963 if (!authzManager.allowUserorg(servicePrincipal, userorg)) {
964 accessLogger.logService(AccessStatusType.ACCESS_DENIED_USERORG, servicePrincipal, serviceTicketId, null);
965 messageLogger.logWarn(CAUGHT_DENIED_USERORG + ", userorg (" + userorg + ") tried to access service (" + servicePrincipal + ")", serviceTicketId);
966 throw new AuthorizationException("Access to the requested service is denied for " + userorg + ".");
967 }
968
969
970 MoriaAuthnAttempt authenticationAttempt = store.getAuthnAttempt(serviceTicketId, false, servicePrincipal);
971 String[] requestedAttributes = authenticationAttempt.getRequestedAttributes();
972 final Map cachedAttributes = authenticationAttempt.getTransientAttributes();
973
974
975
976 for (int i = 0; i < requestedAttributes.length; i++) {
977 if (cachedAttributes.containsKey(requestedAttributes[i]))
978 filteredAttributes.put(requestedAttributes[i], cachedAttributes.get(requestedAttributes[i]));
979 }
980
981
982 } catch (UnknownServicePrincipalException e) {
983 accessLogger.logService(AccessStatusType.GET_USER_ATTRIBUTES_DENIED_INVALID_PRINCIPAL, servicePrincipal, serviceTicketId, null);
984 messageLogger.logInfo("UnknownServicePrincipalException caught", e);
985 throw new AuthorizationException("Unknown service principal: " + servicePrincipal);
986 } catch (NonExistentTicketException e) {
987
988
989 accessLogger.logService(AccessStatusType.NONEXISTENT_SERVICE_TICKET, servicePrincipal, serviceTicketId, null);
990 messageLogger.logWarn(CAUGHT_NONEXISTENT_TICKET + ", service (" + servicePrincipal + ") tried to fetch attributes too late", serviceTicketId, e);
991 throw new UnknownTicketException(NONEXISTENT_TICKET);
992
993 } catch (InvalidTicketException e) {
994
995
996 accessLogger.logService(AccessStatusType.INVALID_SERVICE_TICKET, servicePrincipal, serviceTicketId, null);
997 messageLogger.logWarn(CAUGHT_INVALID_TICKET + ", service (" + servicePrincipal + ") tried to fetch attributes too late", serviceTicketId, e);
998 throw new UnknownTicketException(NONEXISTENT_TICKET);
999
1000 } catch (MoriaStoreException e) {
1001
1002
1003 messageLogger.logCritical(CAUGHT_STORE, serviceTicketId, e);
1004 throw new InoperableStateException(STORE_DOWN);
1005
1006 }
1007
1008
1009 accessLogger.logService(AccessStatusType.SUCCESSFUL_GET_ATTRIBUTES, servicePrincipal, serviceTicketId, null);
1010 return filteredAttributes;
1011 }
1012
1013
1014 /***
1015 * Performs a direct authentication without the use of tickets. The user is
1016 * authenticated directly against the backend, and the attributes retrieved
1017 * are returned to the caller.
1018 * @param requestedAttributes
1019 * The requested attributes.
1020 * @param userId
1021 * The user's username.
1022 * @param password
1023 * The user's password.
1024 * @param servicePrincipal
1025 * The principal (read: username) of the calling service.
1026 * @return Map containing user attributes with <code>String</code>
1027 * (attribute name) as key and <code>String[]</code> (user
1028 * attributes) as value.
1029 * @throws AuthorizationException
1030 * If the service is not allowed to perform this operation, or
1031 * if the user's organization does not allow the use of this
1032 * service.
1033 * @throws IllegalInputException
1034 * If <code>requestedAttributes</code> is <code>null</code>,
1035 * or <code>userId</code>, <code>password</code>, or
1036 * <code>servicePrincipal</code> is <code>null</code> or an
1037 * empty string.
1038 * @throws InoperableStateException
1039 * If Moria is not currently ready for use.
1040 * @throws AuthenticationException
1041 * If the authentication failed due to bad user credentials.
1042 * @throws DirectoryUnavailableException
1043 * If directory of the user's home organization is unavailable.
1044 */
1045 public static Map directNonInteractiveAuthentication(final String[] requestedAttributes,
1046 final String userId,
1047 final String password,
1048 final String servicePrincipal)
1049 throws AuthorizationException, IllegalInputException,
1050 InoperableStateException, AuthenticationException,
1051 DirectoryUnavailableException {
1052
1053
1054 if (!ready) { throw new InoperableStateException(NOT_READY); }
1055
1056
1057 if (requestedAttributes == null)
1058 throw new IllegalInputException("Attributes cannot be null");
1059 if (userId == null || userId.equals(""))
1060 throw new IllegalInputException("Username must be a non-empty string");
1061 if (password == null || password.equals(""))
1062 throw new IllegalInputException("User password must be a non-empty string");
1063 if (servicePrincipal == null || servicePrincipal.equals(""))
1064 throw new IllegalInputException("Service principal must be a non-empty string");
1065
1066
1067 authorizationCheck(servicePrincipal, requestedAttributes, DIRECT_AUTH_OPER);
1068
1069
1070 organizationCheck(servicePrincipal, getUserOrg(userId));
1071
1072
1073 final HashMap attributes;
1074 try {
1075 attributes = authenticate(null, new Credentials(userId, password), requestedAttributes);
1076 } catch (AuthenticationFailedException e) {
1077 accessLogger.logService(AccessStatusType.BAD_USER_CREDENTIALS, servicePrincipal, null, null);
1078 messageLogger.logInfo("AuthenticationFailedException caught", e);
1079 throw new AuthenticationException();
1080 } catch (BackendException e) {
1081 messageLogger.logWarn("Directory is unavailable. Tried to authenticate user: " + userId, e);
1082 throw new DirectoryUnavailableException();
1083 }
1084
1085
1086 accessLogger.logUser(AccessStatusType.SUCCESSFUL_DIRECT_AUTHENTICATION, servicePrincipal, userId, null, null);
1087
1088
1089 return attributes;
1090 }
1091
1092
1093 /***
1094 * Performs a ticket based proxy authentication. A proxy ticket and a set of
1095 * requested attributes are used to retrieve user data. Only cached userdata
1096 * can be retrieved.
1097 * @param requestedAttributes
1098 * The requested attributes to retrieve.
1099 * @param proxyTicketId
1100 * The proxy ticket connected with the cached user data.
1101 * @param servicePrincipal
1102 * The principal of the requesting service.
1103 * @return Map containing user attributes with <code>String</code>
1104 * (attribute name) as key and <code>String[]</code> (user
1105 * attributes) as value.
1106 * @throws AuthorizationException
1107 * If the service is not allowed to perform this operation, or
1108 * if the user's organization does not allow the use of this
1109 * service.
1110 * @throws IllegalInputException
1111 * If <code>requestedAttributes</code> is null, or
1112 * <code>proxyTicketId</code> or <code>servicePrincipal</code>
1113 * is <code>null</code> or an empty string.
1114 * @throws InoperableStateException
1115 * If the controller is not currently ready to use.
1116 * @throws UnknownTicketException
1117 * If the proxy ticket is invalid or does not exist.
1118 */
1119 public static Map proxyAuthentication(final String[] requestedAttributes,
1120 final String proxyTicketId,
1121 final String servicePrincipal)
1122 throws AuthorizationException, IllegalInputException,
1123 InoperableStateException, UnknownTicketException {
1124
1125
1126 if (!ready) { throw new InoperableStateException(NOT_READY); }
1127
1128
1129 if (requestedAttributes == null)
1130 throw new IllegalInputException("Requested attributes cannot be null");
1131 if (proxyTicketId == null || proxyTicketId.equals(""))
1132 throw new IllegalInputException("'proxyTicket' must be a non-empty string.");
1133 if (servicePrincipal == null || servicePrincipal.equals(""))
1134 throw new IllegalInputException("Service principal must be a non-empty string.");
1135
1136
1137 authorizationCheck(servicePrincipal, requestedAttributes, PROXY_AUTH_OPER);
1138
1139
1140 final HashMap result = new HashMap();
1141 final HashMap userData;
1142 final HashSet cachedAttributes = authzManager.getCachableAttributes();
1143
1144 try {
1145
1146
1147 final String userOrg = store.getTicketUserorg(proxyTicketId, MoriaTicketType.PROXY_TICKET);
1148 if (userOrg == null)
1149 throw new InvalidTicketException("User organization is not set in ticket");
1150 organizationCheck(servicePrincipal, userOrg);
1151
1152
1153 userData = store.getUserData(proxyTicketId, servicePrincipal).getAttributes();
1154
1155 } catch (InvalidTicketException e) {
1156
1157
1158 accessLogger.logService(AccessStatusType.INVALID_PROXY_TICKET, servicePrincipal, proxyTicketId, null);
1159 messageLogger.logWarn(CAUGHT_INVALID_TICKET, e);
1160 throw new UnknownTicketException(NONEXISTENT_TICKET);
1161
1162 } catch (NonExistentTicketException e) {
1163
1164
1165 accessLogger.logService(AccessStatusType.NONEXISTENT_PROXY_TICKET, servicePrincipal, proxyTicketId, null);
1166 messageLogger.logDebug(CAUGHT_NONEXISTENT_TICKET, e);
1167 throw new UnknownTicketException(NONEXISTENT_TICKET);
1168
1169 } catch (MoriaStoreException e) {
1170
1171
1172 messageLogger.logCritical(CAUGHT_STORE, e);
1173 throw new InoperableStateException(STORE_DOWN);
1174
1175 }
1176
1177
1178 for (int i = 0; i < requestedAttributes.length; i++) {
1179 final String attr = requestedAttributes[i];
1180 if (!cachedAttributes.contains(attr)) {
1181 accessLogger.logService(AccessStatusType.PROXY_AUTH_DENIED_UNCACHED_ATTRIBUTES, servicePrincipal, proxyTicketId, null);
1182 messageLogger.logInfo("Service (proxy authentication)'" + servicePrincipal + "' requested '" + new HashSet(Arrays.asList(requestedAttributes)) + "', but only the following are cached: '" + cachedAttributes);
1183 throw new AuthorizationException("Requested attributes is not cached: '" + attr + "'");
1184 }
1185 result.put(attr, userData.get(attr));
1186 }
1187
1188 accessLogger.logService(AccessStatusType.SUCCESSFUL_PROXY_AUTHENTICATION, servicePrincipal, proxyTicketId, null);
1189 return result;
1190 }
1191
1192
1193 /***
1194 * Generates a proxy ticket based on a TGT. A new proxy ticket is created,
1195 * referring to the same cached user data as the TGT does. The proxy ticket
1196 * will be owned by the target service, not the one that requested its
1197 * creation.
1198 * @param ticketGrantingTicket
1199 * The TGT to generate a proxy ticket for.
1200 * @param proxyServicePrincipal
1201 * The principal of the service that the proxy ticket is created
1202 * for.
1203 * @param servicePrincipal
1204 * The principal of the service requesting the ticket generation.
1205 * @return A <code>String</code> containing the proxy ticket.
1206 * @throws AuthorizationException
1207 * If the requesting service is not allowed to perform the
1208 * operation, or if the user's organization does not allow the
1209 * use of this service.
1210 * @throws IllegalInputException
1211 * If <code>ticketGrantingTicket</code>,
1212 * <code>proxyServicePrincipal</code> or
1213 * <code>servicePrincipal</code> is <code>null</code> or an
1214 * empty string.
1215 * @throws InoperableStateException
1216 * If Moria is not currently ready for use.
1217 * @throws UnknownTicketException
1218 * If the <code>ticketGrantingTicket</code> is invalid or does
1219 * not exist, or <code>userorg</code> is not set in ticket.
1220 */
1221 public static String getProxyTicket(final String ticketGrantingTicket,
1222 final String proxyServicePrincipal,
1223 final String servicePrincipal)
1224 throws AuthorizationException, IllegalInputException,
1225 InoperableStateException, UnknownTicketException {
1226
1227
1228 if (!ready)
1229 throw new InoperableStateException(NOT_READY);
1230
1231
1232 if (ticketGrantingTicket == null || ticketGrantingTicket.equals(""))
1233 throw new IllegalInputException("Ticket granting ticket must be a non-empty string");
1234 if (proxyServicePrincipal == null || proxyServicePrincipal.equals(""))
1235 throw new IllegalInputException("Proxy service principal must be a non-empty string.");
1236 if (servicePrincipal == null || servicePrincipal.equals(""))
1237 throw new IllegalInputException("Service principal must be a non-empty string");
1238
1239
1240 authorizationCheck(servicePrincipal, new String[] {}, PROXY_AUTH_OPER);
1241
1242
1243 final String proxyTicketId;
1244 try {
1245
1246
1247 final String userOrg = store.getTicketUserorg(ticketGrantingTicket, MoriaTicketType.TICKET_GRANTING_TICKET);
1248 if (userOrg == null)
1249 throw new UnknownTicketException("User organization is not set in ticket");
1250 organizationCheck(servicePrincipal, userOrg);
1251
1252 try {
1253 if (!authzManager.getSubsystems(servicePrincipal).contains(proxyServicePrincipal)) {
1254 accessLogger.logService(AccessStatusType.PROXY_TICKET_GENERATION_DENIED_UNAUTHORIZED, servicePrincipal, ticketGrantingTicket, null);
1255 throw new AuthorizationException("Request for proxy ticket denied.");
1256 }
1257 } catch (UnknownServicePrincipalException e) {
1258 accessLogger.logService(AccessStatusType.PROXY_TICKET_GENERATION_DENIED_INVALID_PRINCIPAL, servicePrincipal, ticketGrantingTicket, null);
1259 messageLogger.logInfo("UnknownServicePrincipalException caught", e);
1260 throw new AuthorizationException("Unknown service principal: " + servicePrincipal);
1261 }
1262 proxyTicketId = store.createProxyTicket(ticketGrantingTicket, servicePrincipal, proxyServicePrincipal);
1263 } catch (InvalidTicketException e) {
1264 accessLogger.logService(AccessStatusType.INVALID_TGT, servicePrincipal, ticketGrantingTicket, null);
1265 messageLogger.logWarn(CAUGHT_INVALID_TICKET, e);
1266 throw new UnknownTicketException(NONEXISTENT_TICKET);
1267 } catch (NonExistentTicketException e) {
1268 accessLogger.logService(AccessStatusType.NONEXISTENT_TGT, servicePrincipal, ticketGrantingTicket, null);
1269 messageLogger.logInfo(CAUGHT_NONEXISTENT_TICKET, e);
1270 throw new UnknownTicketException(NONEXISTENT_TICKET);
1271 } catch (MoriaStoreException e) {
1272 messageLogger.logCritical(CAUGHT_STORE, e);
1273 throw new InoperableStateException(STORE_DOWN);
1274 }
1275
1276 accessLogger.logService(AccessStatusType.SUCCESSFUL_GET_PROXY_TICKET, servicePrincipal, ticketGrantingTicket, proxyTicketId);
1277 return proxyTicketId;
1278 }
1279
1280
1281 /***
1282 * Verifies the existence of a user.
1283 * @param userId
1284 * The username to verify.
1285 * @param servicePrincipal
1286 * The principal of the requesting service.
1287 * @return <code>true</code> if the user exists, otherwise
1288 * <code>false</code>.
1289 * @throws AuthorizationException
1290 * If the requesting service is not allowed to perform the
1291 * operation, or if the user's organization does not allow the
1292 * use of this service.
1293 * @throws IllegalInputException
1294 * If <code>userId</code> or <code>servicePrincipal</code>
1295 * is <code>null</code> or an empty string.
1296 * @throws InoperableStateException
1297 * If the controller is not currently ready to use.
1298 * @throws DirectoryUnavailableException
1299 * If the directory for the user is not available.
1300 */
1301 public static boolean verifyUserExistence(final String userId,
1302 final String servicePrincipal)
1303 throws AuthorizationException, IllegalInputException,
1304 InoperableStateException, DirectoryUnavailableException {
1305
1306
1307 if (!ready) { throw new InoperableStateException(NOT_READY); }
1308
1309
1310 if (userId == null || userId.equals(""))
1311 throw new IllegalInputException("Username must be non-empty string");
1312 if (servicePrincipal == null || servicePrincipal.equals(""))
1313 throw new IllegalInputException("Service principal must be non-empty string");
1314
1315
1316 authorizationCheck(servicePrincipal, new String[] {}, VERIFY_USER_EXISTENCE_OPER);
1317
1318
1319 String userOrganization = null;
1320 if (userId.indexOf("@") != -1)
1321 userOrganization = userId.substring(userId.indexOf("@") + 1, userId.length());
1322 if (userOrganization == null)
1323 throw new AuthorizationException("User organization is unknown");
1324 organizationCheck(servicePrincipal, userOrganization);
1325
1326
1327 final boolean userExistence;
1328 try {
1329 userExistence = directoryManager.userExists(null, userId);
1330 } catch (BackendException e) {
1331 messageLogger.logWarn("BackendException caught", e);
1332 throw new DirectoryUnavailableException();
1333 }
1334
1335
1336 final String resultString;
1337 if (userExistence) {
1338 resultString = "was verified.";
1339 } else {
1340 resultString = "does not exist.";
1341 }
1342 accessLogger.logService(AccessStatusType.SUCCESSFUL_VERIFY_USER, servicePrincipal, null, null);
1343 messageLogger.logInfo("User verification (by " + servicePrincipal + "): " + userId + " " + resultString);
1344
1345 return userExistence;
1346 }
1347
1348
1349 /***
1350 * Sets config for a module. A supplied configuration is transferred to the
1351 * correct module. When all modules have received their config, the
1352 * controller's status becomes ready.
1353 * @param module
1354 * Name of the module to set config for.
1355 * @param properties
1356 * The configuration to transfer to the module.
1357 * @see ConfigurationManager#MODULE_AM
1358 * @see ConfigurationManager#MODULE_DM
1359 * @see ConfigurationManager#MODULE_SM
1360 * @see ConfigurationManager#MODULE_WEB
1361 */
1362 public static synchronized void setConfig(final String module,
1363 final Properties properties) {
1364
1365 if (module.equals(ConfigurationManager.MODULE_AM)) {
1366 if (authzManager != null) {
1367 authzManager.setConfig(properties);
1368 amReady = true;
1369 messageLogger.logInfo("Config set for AM.");
1370 } else {
1371 messageLogger.logCritical("Received authorization config before AM is initialized.");
1372 }
1373 } else if (module.equals(ConfigurationManager.MODULE_DM)) {
1374 if (directoryManager != null) {
1375 directoryManager.setConfig(properties);
1376 dmReady = true;
1377 messageLogger.logInfo("Config set for DM.");
1378 } else {
1379 messageLogger.logCritical("Received directory config before DM is initialized.");
1380 }
1381 } else if (module.equals(ConfigurationManager.MODULE_SM)) {
1382 if (store != null) {
1383 try {
1384 store.setConfig(properties);
1385 smReady = true;
1386 messageLogger.logInfo("Config set for SM.");
1387 } catch (MoriaStoreConfigurationException msce) {
1388 smReady = false;
1389 messageLogger.logCritical("Unable to set config for SM", msce);
1390 }
1391 } else {
1392 messageLogger.logCritical("Received store config before SM is initialized.");
1393 }
1394 } else if (module.equals(ConfigurationManager.MODULE_WEB)) {
1395 if (servletContext != null) {
1396 servletContext.setAttribute("no.feide.moria.web.config", properties);
1397 messageLogger.logInfo("Config set for WEB.");
1398 } else {
1399 messageLogger.logCritical("Received web config before the servlet context is available.");
1400 }
1401 }
1402
1403
1404 if (isInitialized.booleanValue() && amReady && dmReady && smReady) {
1405 ready = true;
1406 messageLogger.logInfo("All config is set. Moria is READY for use.");
1407 }
1408 }
1409
1410
1411 /***
1412 * Starts the controller. The controller expects to be started from a web
1413 * application. The supplied ServletContext will be used to transfer config
1414 * from the configuration manager to the servlets.
1415 * @param sc
1416 * The servletContext from the caller.
1417 * @throws InoperableStateException
1418 * if Moria is not ready for use.
1419 */
1420 public static void initController(final ServletContext sc)
1421 throws InoperableStateException {
1422
1423 if (isInitialized.booleanValue()) {
1424 messageLogger.logDebug("Controller has already been initialized");
1425 return;
1426 }
1427
1428
1429 servletContext = sc;
1430 init();
1431
1432 messageLogger.logInfo("Controller initialized");
1433 }
1434
1435
1436 /***
1437 * Stops the controller.
1438 */
1439 public static void stopController() {
1440
1441 if (!isInitialized.booleanValue()) {
1442 messageLogger = new MessageLogger(MoriaController.class);
1443 messageLogger.logInfo("Attempt to stop uninitialized controller, ignoring");
1444 } else {
1445 stop();
1446 messageLogger.logInfo("Controller stopped");
1447 }
1448 }
1449
1450
1451 /***
1452 * Validates a URL.
1453 * @param url
1454 * The URL to validate.
1455 * @return <code>true</code> if the URL is valid, else <code>false</code>.
1456 * @throws IllegalArgumentException
1457 * if <code>url</code> is <code>null</code> or an empty
1458 * string.
1459 * @see URI
1460 */
1461 static boolean isLegalURL(final String url) {
1462
1463
1464 if (url == null || url.equals(""))
1465 throw new IllegalArgumentException("URL must be a non-empty string");
1466
1467 try {
1468 URI validator = new URI(url);
1469 final String protocol = validator.getScheme();
1470 if (protocol == null || (!protocol.equalsIgnoreCase("http") && !protocol.equalsIgnoreCase("https"))) {
1471 messageLogger.logWarn("Illegal URL protocol '" + url + "'");
1472 return false;
1473 }
1474 } catch (URISyntaxException e) {
1475 messageLogger.logWarn("Illegal URL '" + url + "'");
1476 return false;
1477 }
1478 return true;
1479
1480 }
1481
1482
1483 /***
1484 * Returns the service configuration for the service that created the
1485 * authentication attempt.
1486 * @param loginTicketId
1487 * The login ticket associated with the authentication attempt.
1488 * Cannot be <code>null</code> or an empty string.
1489 * @return A HashMap containing service configuration.
1490 * @throws UnknownTicketException
1491 * If the ticket does not exist in the store, if the ticket is
1492 * invalid, or if the ticket does not correspond to a service.
1493 * @throws InoperableStateException
1494 * If the controller or the store is not ready to use.
1495 * @throws IllegalInputException
1496 * If <code>loginTicketId</code> is <code>null</code> or an
1497 * empty string.
1498 */
1499 public static HashMap getServiceProperties(final String loginTicketId)
1500 throws UnknownTicketException, InoperableStateException,
1501 IllegalInputException {
1502
1503
1504 if (!ready)
1505 throw new InoperableStateException(NOT_READY);
1506 if (loginTicketId == null || loginTicketId.equals(""))
1507 throw new IllegalInputException("Login ticket ID must be a non-empty string");
1508
1509
1510 final MoriaAuthnAttempt authnAttempt;
1511 try {
1512 authnAttempt = store.getAuthnAttempt(loginTicketId, true, null);
1513 } catch (NonExistentTicketException e) {
1514 accessLogger.logUser(AccessStatusType.NONEXISTENT_LOGIN_TICKET, null, null, loginTicketId, null);
1515 messageLogger.logDebug(CAUGHT_NONEXISTENT_TICKET, e);
1516 throw new UnknownTicketException(NONEXISTENT_TICKET);
1517 } catch (InvalidTicketException e) {
1518 accessLogger.logUser(AccessStatusType.INVALID_LOGIN_TICKET, null, null, loginTicketId, null);
1519 messageLogger.logWarn(CAUGHT_INVALID_TICKET, e);
1520 throw new UnknownTicketException(NONEXISTENT_TICKET);
1521 } catch (MoriaStoreException e) {
1522 messageLogger.logCritical(CAUGHT_STORE, e);
1523 throw new InoperableStateException(STORE_DOWN);
1524 }
1525
1526
1527 try {
1528 return authzManager.getServiceProperties(authnAttempt.getServicePrincipal());
1529 } catch (UnknownServicePrincipalException e) {
1530 messageLogger.logWarn("Service '" + authnAttempt.getServicePrincipal() + "' is unknown", loginTicketId, e);
1531 throw new UnknownTicketException("Ticket '" + loginTicketId + "' does not correspond to a known service");
1532 }
1533 }
1534
1535
1536 /***
1537 * Gets the security level of an authentication attempt.
1538 * @param loginTicketId
1539 * The ticket associated with the authentication attempt.
1540 * @return int describing the security level for the requested attributes in
1541 * The authentication attempt.
1542 * @throws UnknownTicketException
1543 * If the ticket does not exist, is invalid, or is not
1544 * associated with a service.
1545 * @throws InoperableStateException
1546 * If Moria is not usable.
1547 * @throws IllegalArgumentException
1548 * If loginTicketId is null or empty.
1549 */
1550 public static int getSecLevel(final String loginTicketId)
1551 throws UnknownTicketException, InoperableStateException {
1552
1553
1554 if (!ready) { throw new InoperableStateException(NOT_READY); }
1555
1556
1557 if (loginTicketId == null || loginTicketId.equals("")) { throw new IllegalArgumentException("'loginTicketId' must be a non-empty string, was: " + loginTicketId); }
1558
1559 final MoriaAuthnAttempt authnAttempt;
1560 try {
1561 authnAttempt = store.getAuthnAttempt(loginTicketId, true, null);
1562 } catch (NonExistentTicketException e) {
1563 accessLogger.logUser(AccessStatusType.NONEXISTENT_LOGIN_TICKET, null, null, loginTicketId, null);
1564 messageLogger.logInfo(CAUGHT_NONEXISTENT_TICKET, e);
1565 throw new UnknownTicketException(NONEXISTENT_TICKET);
1566 } catch (InvalidTicketException e) {
1567 accessLogger.logUser(AccessStatusType.INVALID_LOGIN_TICKET, null, null, loginTicketId, null);
1568 throw new UnknownTicketException(NONEXISTENT_TICKET);
1569 } catch (MoriaStoreException e) {
1570 messageLogger.logCritical(CAUGHT_STORE, e);
1571 throw new InoperableStateException(STORE_DOWN);
1572 }
1573
1574 try {
1575 return authzManager.getSecLevel(authnAttempt.getServicePrincipal(), authnAttempt.getRequestedAttributes());
1576 } catch (UnknownServicePrincipalException e) {
1577 messageLogger.logWarn("UnknownServicePrincipalException caught, has the service been removed from the authorization database?", e);
1578 throw new UnknownTicketException("Ticket is no longer connected to a service.");
1579 } catch (UnknownAttributeException e) {
1580 messageLogger.logWarn("UnknownAttributeException caught, has the attribute been removed from the authorization database?", e);
1581 throw new InoperableStateException("The authentication attempt is unusable.");
1582 }
1583 }
1584
1585
1586 /***
1587 * Invalidates an SSO ticket. After the invalidation, the ticket cannot be
1588 * used any more.
1589 * @param ssoTicketId
1590 * The ticket to be invalidated.
1591 * @throws IllegalInputException
1592 * If <code>ssoTicketId</code> is null or empty.
1593 * @throws InoperableStateException
1594 * If Moria is not ready to use.
1595 */
1596 public static void invalidateSSOTicket(final String ssoTicketId)
1597 throws IllegalInputException, InoperableStateException {
1598
1599
1600 if (!ready) { throw new InoperableStateException(NOT_READY); }
1601
1602
1603 if (ssoTicketId == null || ssoTicketId.equals("")) { throw new IllegalInputException("'ssoTicketId' must be a non-empty string."); }
1604
1605 try {
1606 store.removeSSOTicket(ssoTicketId);
1607 } catch (NonExistentTicketException e) {
1608
1609 messageLogger.logDebug(CAUGHT_NONEXISTENT_TICKET + ", OK since we tried to remove");
1610 } catch (MoriaStoreException e) {
1611 messageLogger.logCritical(CAUGHT_STORE, e);
1612 throw new InoperableStateException(STORE_DOWN);
1613 }
1614
1615 accessLogger.logUser(AccessStatusType.SSO_TICKET_INVALIDATED, null, null, ssoTicketId, null);
1616 }
1617
1618
1619 /***
1620 * Creates a redirect URL for redirecting user back to web service. The URL
1621 * is created by concatenating the URL prefix with the service ticket and
1622 * the URL postfix.
1623 * @param serviceTicketId
1624 * The service ticket to generate redirect URL for.
1625 * @return A <code>String</code> containing the URL.
1626 * @throws InoperableStateException
1627 * If Moria is not ready for use.
1628 * @throws IllegalInputException
1629 * If <code>serviceTicketId</code> is null or empty.
1630 * @throws UnknownTicketException
1631 * If the service ticket is invalid or does not exist.
1632 */
1633 public static String getRedirectURL(final String serviceTicketId)
1634 throws InoperableStateException, IllegalInputException,
1635 UnknownTicketException {
1636
1637
1638 if (!ready) { throw new InoperableStateException(NOT_READY); }
1639
1640
1641 if (serviceTicketId == null || serviceTicketId.equals("")) { throw new IllegalInputException("serviceTicketId must be a non-empty string."); }
1642
1643 final MoriaAuthnAttempt authnAttempt;
1644 try {
1645 authnAttempt = store.getAuthnAttempt(serviceTicketId, true, null);
1646 } catch (InvalidTicketException e) {
1647 accessLogger.logUser(AccessStatusType.INVALID_SERVICE_TICKET, null, null, serviceTicketId, null);
1648 messageLogger.logWarn(CAUGHT_INVALID_TICKET, e);
1649 throw new UnknownTicketException(NONEXISTENT_TICKET);
1650 } catch (NonExistentTicketException e) {
1651 accessLogger.logUser(AccessStatusType.NONEXISTENT_SERVICE_TICKET, null, null, serviceTicketId, null);
1652 messageLogger.logInfo(CAUGHT_NONEXISTENT_TICKET, e);
1653 throw new UnknownTicketException(NONEXISTENT_TICKET);
1654 } catch (MoriaStoreException e) {
1655 messageLogger.logCritical(CAUGHT_STORE, e);
1656 throw new InoperableStateException(STORE_DOWN);
1657 }
1658
1659 return authnAttempt.getReturnURLPrefix() + serviceTicketId + authnAttempt.getReturnURLPostfix();
1660 }
1661
1662
1663 /***
1664 * Resolves a user's home organization through the Directory Manager.
1665 * @param username
1666 * The full username of a user. Must be a non-empty string.
1667 * @return A <code>String</code> containing the user's organization.
1668 * @throws AuthenticationException
1669 * If the user's organization is not found.
1670 * @throws IllegalArgumentException
1671 * If <code>username</code> is <code>null</code> or an empty
1672 * string.
1673 */
1674 public static String getUserOrg(final String username)
1675 throws AuthenticationException {
1676
1677
1678 if ((username == null) || (username.length() == 0))
1679 throw new IllegalArgumentException("User name must be a non-empty string");
1680
1681 String org = directoryManager.getRealm(username);
1682 if (org == null)
1683 throw new AuthenticationException();
1684 return org;
1685 }
1686
1687
1688 /***
1689 * Convenience method to assure certain pre-authentication checks.
1690 * @param sessionTicket
1691 * The session ticket.
1692 * @param userCredentials
1693 * The user's credentials.
1694 * @param attributeRequest
1695 * The attribute request.
1696 * @return The returned attributes.
1697 * @throws AuthenticationFailedException
1698 * If authentication fails.
1699 * @throws BackendException
1700 * If the backend fails to authenticate/retrieve attributes.
1701 * @throws IllegalStateException
1702 * If Moria2 is in an illegal state.
1703 * @see DirectoryManager#authenticate(java.lang.String,
1704 * no.feide.moria.directory.Credentials, java.lang.String[])
1705 */
1706 private static final HashMap authenticate(final String sessionTicket,
1707 final Credentials userCredentials,
1708 final String[] attributeRequest)
1709 throws AuthenticationFailedException, BackendException,
1710 IllegalStateException {
1711
1712
1713
1714 if (messageLogger.isEnabledFor(Level.WARN)) {
1715 String checkPassword = userCredentials.getPassword().replaceAll("[b-zA-Z0-9]", "a");
1716 checkPassword = checkPassword.replaceAll("[^a]", "A");
1717 if (checkPassword.contains("A"))
1718 messageLogger.logWarn("User '" + userCredentials.getUsername() + "' attempts login with suspicious password (should only contain [a-zA-Z0-9])", sessionTicket);
1719 }
1720
1721 return directoryManager.authenticate(sessionTicket, userCredentials, attributeRequest);
1722
1723 }
1724
1725 }