1   package com.atlassian.seraph.config;
2   
3   import com.atlassian.seraph.Initable;
4   import com.atlassian.seraph.SecurityService;
5   import com.atlassian.seraph.auth.AuthenticationContext;
6   import com.atlassian.seraph.auth.AuthenticationContextImpl;
7   import com.atlassian.seraph.auth.Authenticator;
8   import com.atlassian.seraph.auth.RoleMapper;
9   import com.atlassian.seraph.controller.SecurityController;
10  import com.atlassian.seraph.elevatedsecurity.ElevatedSecurityGuard;
11  import com.atlassian.seraph.elevatedsecurity.NoopElevatedSecurityGuard;
12  import com.atlassian.seraph.interceptor.Interceptor;
13  import com.atlassian.seraph.ioc.ApplicationServicesRegistry;
14  import com.atlassian.seraph.service.rememberme.RememberMeService;
15  import com.atlassian.seraph.util.XMLUtils;
16  import com.opensymphony.util.ClassLoaderUtil;
17  import org.apache.log4j.Logger;
18  import org.w3c.dom.Document;
19  import org.w3c.dom.Element;
20  import org.w3c.dom.Node;
21  import org.w3c.dom.NodeList;
22  import org.xml.sax.SAXException;
23  
24  import java.io.IOException;
25  import java.io.Serializable;
26  import java.net.URL;
27  import java.util.ArrayList;
28  import java.util.Arrays;
29  import java.util.Collections;
30  import java.util.HashMap;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.concurrent.CopyOnWriteArrayList;
34  import javax.xml.parsers.DocumentBuilderFactory;
35  import javax.xml.parsers.ParserConfigurationException;
36  
37  /**
38   * The main implementation of Seraph's configuration - reads from seraph-config.xml.
39   * <p/>
40   * This class is a Singleton, access it using SecurityConfigFactory.getInstance().
41   */
42  public class SecurityConfigImpl implements Serializable, SecurityConfig
43  {
44      private static final Logger log = Logger.getLogger(SecurityConfigImpl.class);
45  
46      public static final String DEFAULT_CONFIG_LOCATION = "seraph-config.xml";
47  
48      private static final int YEAR_IN_SECONDS = 365 * 24 * 60 * 60;
49  
50      private final Authenticator authenticator;
51      private final ElevatedSecurityGuard elevatedSecurityGuard;
52      private final RoleMapper roleMapper;
53      private final SecurityController controller;
54      private final List<SecurityService> services;
55      private final List<Interceptor> interceptors = new CopyOnWriteArrayList<Interceptor>();
56  
57      private final String loginURL;
58      private final String logoutURL;
59      private final String originalURLKey;
60      private final String cookieEncoding;
61      private final String loginCookieKey;
62      private final String linkLoginURL;
63      private final String authType;
64      private RedirectPolicy redirectPolicy;
65  
66      private boolean insecureCookie;
67      private final boolean invalidateSessionOnLogin;
68      private final List<String> invalidateSessionExcludeList;
69  
70      // the age of the auto-login cookie - default = 1 year (in seconds)
71      private final int autoLoginCookieAge;
72  
73      private final LoginUrlStrategy loginUrlStrategy;
74      private final String loginCookiePath;
75  
76      public SecurityConfigImpl(String configFileLocation) throws ConfigurationException
77      {
78          if (configFileLocation != null)
79          {
80              if (SecurityConfigImpl.log.isDebugEnabled())
81              {
82                  SecurityConfigImpl.log.debug("Config file location passed.  Location: " + configFileLocation);
83              }
84          }
85          else
86          {
87              configFileLocation = "seraph-config.xml";
88              if (SecurityConfigImpl.log.isDebugEnabled())
89              {
90                  SecurityConfigImpl.log.debug("Initialising securityConfig using default configFile: " + configFileLocation);
91              }
92          }
93  
94          try
95          {
96              final Element rootEl = loadConfigXml(configFileLocation);
97  
98              final NodeList nl = rootEl.getElementsByTagName("parameters");
99              final Element parametersEl = (Element) nl.item(0);
100             final Map<String, String> globalParams = getInitParameters(parametersEl);
101 
102             loginURL = globalParams.get("login.url");
103             linkLoginURL = globalParams.get("link.login.url");
104             logoutURL = globalParams.get("logout.url");
105             cookieEncoding = globalParams.get("cookie.encoding");
106             loginCookiePath = globalParams.get("login.cookie.path");
107             authType = globalParams.get("authentication.type");
108             insecureCookie = "true".equals(globalParams.get("insecure.cookie"));
109 
110             if (globalParams.get("original.url.key") != null)
111             {
112                 originalURLKey = globalParams.get("original.url.key");
113             }
114             else
115             {
116                 originalURLKey = "seraph_originalurl";
117             }
118 
119             if (globalParams.get("login.cookie.key") != null)
120             {
121                 loginCookieKey = globalParams.get("login.cookie.key");
122             }
123             else
124             {
125                 loginCookieKey = "seraph.os.cookie";
126             }
127 
128             if (globalParams.get("autologin.cookie.age") != null)
129             {
130                 autoLoginCookieAge = Integer.parseInt(globalParams.get("autologin.cookie.age"));
131             }
132             else
133             {
134                 autoLoginCookieAge = SecurityConfigImpl.YEAR_IN_SECONDS;
135             }
136 
137             if (globalParams.get("invalidate.session.on.login") != null)
138             {
139                 invalidateSessionOnLogin = "true".equalsIgnoreCase(globalParams.get("invalidate.session.on.login"));
140                 if (globalParams.get("invalidate.session.exclude.list") != null)
141                 {
142                     final String[] excludes = globalParams.get("invalidate.session.exclude.list").split(",");
143                     invalidateSessionExcludeList = Arrays.asList(excludes);
144                 }
145                 else
146                 {
147                     invalidateSessionExcludeList = Collections.emptyList();
148                 }
149             }
150             else
151             {
152                 invalidateSessionOnLogin = false;
153                 invalidateSessionExcludeList = Collections.emptyList();
154             }
155 
156             // be VERY careful about changing order here, THIS reference is passed out while initializing and so we are partially constructed when
157             // clients call us
158 
159             authenticator = configureAuthenticator(rootEl);
160             controller = configureController(rootEl);
161             roleMapper = configureRoleMapper(rootEl);
162             services = Collections.unmodifiableList(configureServices(rootEl));
163             configureInterceptors(rootEl);
164             loginUrlStrategy = configureLoginUrlStrategy(rootEl);
165             configureRedirectPolicy(rootEl);
166             elevatedSecurityGuard = configureElevatedSecurityGuard(rootEl);
167         }
168         catch (final Exception ex)
169         {
170             throw new ConfigurationException("Exception configuring from '" + configFileLocation + "'.", ex);
171         }
172     }
173 
174     private Element loadConfigXml(final String configFileLocation)
175             throws SAXException, IOException, ParserConfigurationException
176     {
177         final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
178         final URL fileUrl = ClassLoaderUtil.getResource(configFileLocation, getClass());
179 
180         if (fileUrl == null)
181         {
182             throw new IllegalArgumentException("No such XML file: " + configFileLocation);
183         }
184 
185         // Parse document
186         final Document doc = factory.newDocumentBuilder().parse(fileUrl.toString());
187         return doc.getDocumentElement();
188     }
189 
190     protected void configureRedirectPolicy(final Element rootEl) throws ConfigurationException
191     {
192         // If no redirect-policy is configured, then this will be null which is valid.
193         redirectPolicy = (RedirectPolicy) configureClass(rootEl, "redirect-policy", this);
194 
195         // If none is configured, then use the default.
196         if (redirectPolicy == null)
197         {
198             redirectPolicy = new DefaultRedirectPolicy();
199         }
200     }
201 
202     private LoginUrlStrategy configureLoginUrlStrategy(final Element rootEl) throws ConfigurationException
203     {
204         LoginUrlStrategy loginUrlStrategy = (LoginUrlStrategy) configureClass(rootEl, "login-url-strategy", this);
205 
206         if (loginUrlStrategy == null)
207         {
208             loginUrlStrategy = new DefaultLoginUrlStrategy();
209         }
210         return loginUrlStrategy;
211     }
212 
213     private Authenticator configureAuthenticator(final Element rootEl) throws ConfigurationException
214     {
215         Authenticator authenticator = (Authenticator) configureClass(rootEl, "authenticator", this);
216 
217         if (authenticator == null)
218         {
219             // We used to try to load the DefaultAuthenticator here, but it is now abstract so we can only fail.
220             throw new ConfigurationException("No authenticator implementation was configured in SecurityConfig.");
221         }
222         return authenticator;
223     }
224 
225     private ElevatedSecurityGuard configureElevatedSecurityGuard(final Element rootEl) throws ConfigurationException
226     {
227         ElevatedSecurityGuard elevatedSecurityGuard = (ElevatedSecurityGuard) configureClass(rootEl, "elevatedsecurityguard", this);
228         if (elevatedSecurityGuard == null)
229         {
230             elevatedSecurityGuard = NoopElevatedSecurityGuard.INSTANCE;
231         }
232         return elevatedSecurityGuard;
233     }
234 
235     private SecurityController configureController(final Element rootEl) throws ConfigurationException
236     {
237         SecurityController controller = (SecurityController) configureClass(rootEl, "controller", this);
238 
239         try
240         {
241             if (controller == null)
242             {
243                 controller = (SecurityController) ClassLoaderUtil.loadClass(SecurityController.NULL_CONTROLLER, getClass()).newInstance();
244             }
245         }
246         catch (final Exception e)
247         {
248             throw new ConfigurationException("Could not lookup class: " + SecurityController.NULL_CONTROLLER, e);
249         }
250         return controller;
251     }
252 
253     private RoleMapper configureRoleMapper(final Element rootEl) throws ConfigurationException
254     {
255         return (RoleMapper) configureClass(rootEl, "rolemapper", this);
256     }
257 
258     private static Initable configureClass(final Element rootEl, final String tagname, final SecurityConfig owner)
259             throws ConfigurationException
260     {
261         final NodeList elementList = rootEl.getElementsByTagName(tagname);
262 
263         if (elementList.getLength() == 0)
264         {
265             return null;
266         }
267         final Element authEl = (Element) elementList.item(0);
268         final String clazz = authEl.getAttribute("class");
269         if (clazz == null || clazz.trim().length() == 0)
270         {
271             return null;
272         }
273 
274         final Initable initable;
275         try
276         {
277             initable = (Initable) ClassLoaderUtil.loadClass(clazz, owner.getClass()).newInstance();
278         }
279         catch (InstantiationException ex)
280         {
281             // Java's has a null message - add something useful
282             throw new ConfigurationException("Unable to instantiate class '" + clazz + "'", ex);
283         }
284         catch (Exception ex)
285         {
286             final String message = "Unable to load " + tagname + " class '" + clazz + "': " + ex.getMessage();
287             // SER-173: log before rethrow, else the interesting part of the stacktrace is buried too deep and gets discarded
288             log.error(message, ex);
289             throw new ConfigurationException(message, ex);
290         }
291 
292         try
293         {
294             initable.init(getInitParameters(authEl), owner);
295             return initable;
296         }
297         catch (Exception ex)
298         {
299             final String message = "Error caught in initialisation of " + tagname + " class '" + clazz + "': " + ex.getMessage();
300             // SER-173: log before rethrow, else the interesting part of the stacktrace is buried too deep and gets discarded or ignored
301             log.error(message, ex);
302             throw new ConfigurationException(message, ex);
303         }
304     }
305 
306     // only called from the constructor
307     private List<SecurityService> configureServices(final Element rootEl) throws ConfigurationException
308     {
309         final NodeList nl = rootEl.getElementsByTagName("services");
310         final List<SecurityService> result = new ArrayList<SecurityService>();
311 
312         if ((nl != null) && (nl.getLength() > 0))
313         {
314             final Element servicesEl = (Element) nl.item(0);
315             final NodeList serviceList = servicesEl.getElementsByTagName("service");
316 
317             for (int i = 0; i < serviceList.getLength(); i++)
318             {
319                 final Element serviceEl = (Element) serviceList.item(i);
320                 final String serviceClazz = serviceEl.getAttribute("class");
321 
322                 if ((serviceClazz == null) || "".equals(serviceClazz))
323                 {
324                     throw new ConfigurationException("Service element with bad class attribute");
325                 }
326 
327                 try
328                 {
329                     SecurityConfigImpl.log.debug("Adding seraph service of class: " + serviceClazz);
330                     final SecurityService service = (SecurityService) ClassLoaderUtil.loadClass(serviceClazz, getClass()).newInstance();
331 
332                     service.init(getInitParameters(serviceEl), this);
333 
334                     result.add(service);
335                 }
336                 catch (final Exception e)
337                 {
338                     throw new ConfigurationException("Could not getRequest service: " + serviceClazz, e);
339                 }
340             }
341         }
342         return result;
343     }
344 
345     private void configureInterceptors(final Element rootEl) throws ConfigurationException
346     {
347         final NodeList nl = rootEl.getElementsByTagName("interceptors");
348 
349         if ((nl != null) && (nl.getLength() > 0))
350         {
351             final Element interceptorsEl = (Element) nl.item(0);
352             final NodeList interceptorList = interceptorsEl.getElementsByTagName("interceptor");
353 
354             for (int i = 0; i < interceptorList.getLength(); i++)
355             {
356                 final Element interceptorEl = (Element) interceptorList.item(i);
357                 final String interceptorClazz = interceptorEl.getAttribute("class");
358 
359                 if ((interceptorClazz == null) || "".equals(interceptorClazz))
360                 {
361                     throw new ConfigurationException("Interceptor element with bad class attribute");
362                 }
363 
364                 try
365                 {
366                     SecurityConfigImpl.log.debug("Adding interceptor of class: " + interceptorClazz);
367                     final Interceptor interceptor = (Interceptor) ClassLoaderUtil.loadClass(interceptorClazz, getClass()).newInstance();
368 
369                     interceptor.init(getInitParameters(interceptorEl), this);
370 
371                     interceptors.add(interceptor);
372                 }
373                 catch (final Exception e)
374                 {
375                     throw new ConfigurationException("Could not getRequest service: " + interceptorClazz, e);
376                 }
377             }
378         }
379     }
380 
381     /**
382      * Returns a Map of the "init-param" properties under the given element. The map could be empty, but is guaranteed
383      * not to be null.
384      *
385      * @param el The XML config element.
386      *
387      * @return a Map of the "init-param" properties under the given element.
388      */
389     private static Map<String, String> getInitParameters(final Element el)
390     {
391         final Map<String, String> params = new HashMap<String, String>();
392         final NodeList nl = el.getElementsByTagName("init-param");
393 
394         for (int i = 0; i < nl.getLength(); i++)
395         {
396             final Node initParam = nl.item(i);
397             final String paramName = XMLUtils.getContainedText(initParam, "param-name");
398             final String paramValue = XMLUtils.getContainedText(initParam, "param-value");
399             params.put(paramName, paramValue);
400         }
401         return Collections.unmodifiableMap(params);
402     }
403 
404     public void destroy()
405     {
406         for (final Object element : services)
407         {
408             final SecurityService securityService = (SecurityService) element;
409             securityService.destroy();
410         }
411 
412         for (final Object element : interceptors)
413         {
414             ((Interceptor) element).destroy();
415         }
416     }
417 
418     /**
419      * Do not use in production! Only used in tests, will be removed.
420      *
421      * @param interceptor the Interceptor to add
422      */
423     public void addInterceptor(final Interceptor interceptor)
424     {
425         interceptors.add(interceptor);
426     }
427 
428     public List<SecurityService> getServices()
429     {
430         return services;
431     }
432 
433     public String getLoginURL()
434     {
435         return loginUrlStrategy.getLoginURL(this, loginURL);
436     }
437 
438     public String getLinkLoginURL()
439     {
440         return loginUrlStrategy.getLinkLoginURL(this, linkLoginURL);
441     }
442 
443     public String getLogoutURL()
444     {
445         return loginUrlStrategy.getLogoutURL(this, logoutURL);
446     }
447 
448     public String getOriginalURLKey()
449     {
450         return originalURLKey;
451     }
452 
453     public Authenticator getAuthenticator()
454     {
455         return authenticator;
456     }
457 
458     public AuthenticationContext getAuthenticationContext()
459     {
460         return new AuthenticationContextImpl();
461     }
462 
463     public SecurityController getController()
464     {
465         return controller;
466     }
467 
468     public RoleMapper getRoleMapper()
469     {
470         return roleMapper;
471     }
472 
473     public RedirectPolicy getRedirectPolicy()
474     {
475         return redirectPolicy;
476     }
477 
478     public <T extends Interceptor> List<T> getInterceptors(final Class<T> desiredInterceptorClass)
479     {
480         final List<T> result = new ArrayList<T>();
481         for (final Interceptor interceptor : interceptors)
482         {
483             if (desiredInterceptorClass.isAssignableFrom(interceptor.getClass()))
484             {
485                 result.add(desiredInterceptorClass.cast(interceptor));
486             }
487         }
488         return Collections.unmodifiableList(result);
489     }
490 
491     public String getCookieEncoding()
492     {
493         return cookieEncoding;
494     }
495 
496     public String getLoginCookiePath()
497     {
498         return loginCookiePath;
499     }
500 
501     public String getLoginCookieKey()
502     {
503         return loginCookieKey;
504     }
505 
506     public String getAuthType()
507     {
508         return authType;
509     }
510 
511     public boolean isInsecureCookie()
512     {
513         return insecureCookie;
514     }
515 
516     public int getAutoLoginCookieAge()
517     {
518         return autoLoginCookieAge;
519     }
520 
521     public ElevatedSecurityGuard getElevatedSecurityGuard()
522     {
523         return elevatedSecurityGuard;
524     }
525 
526     /**
527      * @return a NON NULL RememberMeService implementation
528      */
529     public RememberMeService getRememberMeService()
530     {
531         return ApplicationServicesRegistry.getRememberMeService();
532     }
533 
534     public boolean isInvalidateSessionOnLogin()
535     {
536         return invalidateSessionOnLogin;
537     }
538 
539     public List<String> getInvalidateSessionExcludeList()
540     {
541         return invalidateSessionExcludeList;
542     }
543 }