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.cookie.CookieFactory;
11  import com.atlassian.seraph.elevatedsecurity.ElevatedSecurityGuard;
12  import com.atlassian.seraph.elevatedsecurity.NoopElevatedSecurityGuard;
13  import com.atlassian.seraph.interceptor.Interceptor;
14  import com.atlassian.seraph.ioc.ComponentLocator;
15  import com.atlassian.seraph.ioc.NoopComponentLocator;
16  import com.atlassian.seraph.service.rememberme.NoopRememberMeService;
17  import com.atlassian.seraph.service.rememberme.RememberMeService;
18  import com.atlassian.seraph.util.XMLUtils;
19  import com.opensymphony.util.ClassLoaderUtil;
20  import org.apache.log4j.Logger;
21  import org.w3c.dom.Document;
22  import org.w3c.dom.Element;
23  import org.w3c.dom.Node;
24  import org.w3c.dom.NodeList;
25  import org.xml.sax.SAXException;
26  
27  import java.io.IOException;
28  import java.io.Serializable;
29  import java.net.URL;
30  import java.util.ArrayList;
31  import java.util.Collections;
32  import java.util.HashMap;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.concurrent.CopyOnWriteArrayList;
36  import javax.xml.parsers.DocumentBuilderFactory;
37  import javax.xml.parsers.ParserConfigurationException;
38  
39  /**
40   * The main class of Seraph's configuration.
41   * <p/>
42   * This class is a Singleton, access it using SecurityConfigFactory.getInstance().
43   */
44  public class SecurityConfigImpl implements Serializable, SecurityConfig
45  {
46      private static final Logger log = Logger.getLogger(SecurityConfigImpl.class);
47  
48      public static final String DEFAULT_CONFIG_LOCATION = "seraph-config.xml";
49  
50      private static final int YEAR_IN_SECONDS = 365 * 24 * 60 * 60;
51  
52      private final Authenticator authenticator;
53      private final ElevatedSecurityGuard elevatedSecurityGuard;
54      private final ComponentLocator componentLocator;
55      private final RoleMapper roleMapper;
56      private final SecurityController controller;
57      private final List<SecurityService> services;
58      private final List<Interceptor> interceptors = new CopyOnWriteArrayList<Interceptor>();
59  
60      private final String loginURL;
61      private final String logoutURL;
62      private final String originalURLKey;
63      private final String cookieEncoding;
64      private final String loginCookieKey;
65      private final String linkLoginURL;
66      private final String authType;
67      private RedirectPolicy redirectPolicy;
68  
69      private boolean insecureCookie;
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             // be VERY careful about changing order here, THIS reference is passed out while initializing and so we are partially constructed when
138             // clients call us
139 
140             authenticator = configureAuthenticator(rootEl);
141             controller = configureController(rootEl);
142             roleMapper = configureRoleMapper(rootEl);
143             services = Collections.unmodifiableList(configureServices(rootEl));
144             configureInterceptors(rootEl);
145             loginUrlStrategy = configureLoginUrlStrategy(rootEl);
146             configureRedirectPolicy(rootEl);
147             CookieFactory.init(this);
148             elevatedSecurityGuard = configureElevatedSecurityGuard(rootEl);
149             componentLocator = configureComponentLocator(rootEl);
150 
151         }
152         catch (final Exception e)
153         {
154             throw new ConfigurationException("Exception configuring from '" + configFileLocation, e);
155         }
156     }
157 
158     private Element loadConfigXml(final String configFileLocation)
159             throws SAXException, IOException, ParserConfigurationException
160     {
161         final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
162         final URL fileUrl = ClassLoaderUtil.getResource(configFileLocation, getClass());
163 
164         if (fileUrl == null)
165         {
166             throw new IllegalArgumentException("No such XML file: " + configFileLocation);
167         }
168 
169         // Parse document
170         final Document doc = factory.newDocumentBuilder().parse(fileUrl.toString());
171         return doc.getDocumentElement();
172     }
173 
174     protected void configureRedirectPolicy(final Element rootEl) throws ConfigurationException
175     {
176         // If no redirect-policy is configured, then this will be null which is valid.
177         redirectPolicy = (RedirectPolicy) configureClass(rootEl, "redirect-policy", this);
178 
179         // If none is configured, then use the default.
180         if (redirectPolicy == null)
181         {
182             redirectPolicy = new DefaultRedirectPolicy();
183         }
184     }
185 
186     private LoginUrlStrategy configureLoginUrlStrategy(final Element rootEl) throws ConfigurationException
187     {
188         LoginUrlStrategy loginUrlStrategy = (LoginUrlStrategy) configureClass(rootEl, "login-url-strategy", this);
189 
190         if (loginUrlStrategy == null)
191         {
192             loginUrlStrategy = new DefaultLoginUrlStrategy();
193         }
194         return loginUrlStrategy;
195     }
196 
197     private Authenticator configureAuthenticator(final Element rootEl) throws ConfigurationException
198     {
199         Authenticator authenticator = (Authenticator) configureClass(rootEl, "authenticator", this);
200 
201         try
202         {
203             if (authenticator == null)
204             {
205                 authenticator = (Authenticator) ClassLoaderUtil.loadClass(Authenticator.DEFAULT_AUTHENTICATOR, getClass()).newInstance();
206                 authenticator.init(Collections.<String, String>emptyMap(), this);
207             }
208         }
209         catch (final Exception e)
210         {
211             throw new ConfigurationException("Could not lookup class: " + Authenticator.DEFAULT_AUTHENTICATOR, e);
212         }
213         return authenticator;
214     }
215 
216     private ElevatedSecurityGuard configureElevatedSecurityGuard(final Element rootEl) throws ConfigurationException
217     {
218         ElevatedSecurityGuard elevatedSecurityGuard = (ElevatedSecurityGuard) configureClass(rootEl, "elevatedsecurityguard", this);
219         if (elevatedSecurityGuard == null)
220         {
221             elevatedSecurityGuard = NoopElevatedSecurityGuard.INSTANCE;
222         }
223         return elevatedSecurityGuard;
224     }
225 
226     private ComponentLocator configureComponentLocator(final Element rootEl) throws ConfigurationException
227     {
228         ComponentLocator componentLocator = (ComponentLocator) configureClass(rootEl, "componentlocator", this);
229         if (componentLocator == null)
230         {
231             componentLocator = NoopComponentLocator.INSTANCE;
232         }
233         return componentLocator;
234     }
235 
236     private SecurityController configureController(final Element rootEl) throws ConfigurationException
237     {
238         SecurityController controller = (SecurityController) configureClass(rootEl, "controller", this);
239 
240         try
241         {
242             if (controller == null)
243             {
244                 controller = (SecurityController) ClassLoaderUtil.loadClass(SecurityController.NULL_CONTROLLER, getClass()).newInstance();
245             }
246         }
247         catch (final Exception e)
248         {
249             throw new ConfigurationException("Could not lookup class: " + SecurityController.NULL_CONTROLLER, e);
250         }
251         return controller;
252     }
253 
254     private RoleMapper configureRoleMapper(final Element rootEl) throws ConfigurationException
255     {
256         return (RoleMapper) configureClass(rootEl, "rolemapper", this);
257     }
258 
259     private static Initable configureClass(final Element rootEl, final String tagname, final SecurityConfig owner)
260             throws ConfigurationException
261     {
262         try
263         {
264             final NodeList elementList = rootEl.getElementsByTagName(tagname);
265 
266             if (elementList.getLength() > 0)
267             {
268                 final Element authEl = (Element) elementList.item(0);
269                 final String clazz = authEl.getAttribute("class");
270 
271                 final Initable initable = (Initable) ClassLoaderUtil.loadClass(clazz, owner.getClass()).newInstance();
272 
273                 initable.init(getInitParameters(authEl), owner);
274                 return initable;
275             }
276             return null;
277         }
278         catch (final Exception e)
279         {
280             throw new ConfigurationException("Could not create: " + tagname, e);
281         }
282     }
283 
284     // only called from the constructor
285     private List<SecurityService> configureServices(final Element rootEl) throws ConfigurationException
286     {
287         final NodeList nl = rootEl.getElementsByTagName("services");
288         final List<SecurityService> result = new ArrayList<SecurityService>();
289 
290         if ((nl != null) && (nl.getLength() > 0))
291         {
292             final Element servicesEl = (Element) nl.item(0);
293             final NodeList serviceList = servicesEl.getElementsByTagName("service");
294 
295             for (int i = 0; i < serviceList.getLength(); i++)
296             {
297                 final Element serviceEl = (Element) serviceList.item(i);
298                 final String serviceClazz = serviceEl.getAttribute("class");
299 
300                 if ((serviceClazz == null) || "".equals(serviceClazz))
301                 {
302                     throw new ConfigurationException("Service element with bad class attribute");
303                 }
304 
305                 try
306                 {
307                     SecurityConfigImpl.log.debug("Adding seraph service of class: " + serviceClazz);
308                     final SecurityService service = (SecurityService) ClassLoaderUtil.loadClass(serviceClazz, getClass()).newInstance();
309 
310                     service.init(getInitParameters(serviceEl), this);
311 
312                     result.add(service);
313                 }
314                 catch (final Exception e)
315                 {
316                     throw new ConfigurationException("Could not getRequest service: " + serviceClazz, e);
317                 }
318             }
319         }
320         return result;
321     }
322 
323     private void configureInterceptors(final Element rootEl) throws ConfigurationException
324     {
325         final NodeList nl = rootEl.getElementsByTagName("interceptors");
326 
327         if ((nl != null) && (nl.getLength() > 0))
328         {
329             final Element interceptorsEl = (Element) nl.item(0);
330             final NodeList interceptorList = interceptorsEl.getElementsByTagName("interceptor");
331 
332             for (int i = 0; i < interceptorList.getLength(); i++)
333             {
334                 final Element interceptorEl = (Element) interceptorList.item(i);
335                 final String interceptorClazz = interceptorEl.getAttribute("class");
336 
337                 if ((interceptorClazz == null) || "".equals(interceptorClazz))
338                 {
339                     throw new ConfigurationException("Interceptor element with bad class attribute");
340                 }
341 
342                 try
343                 {
344                     SecurityConfigImpl.log.debug("Adding interceptor of class: " + interceptorClazz);
345                     final Interceptor interceptor = (Interceptor) ClassLoaderUtil.loadClass(interceptorClazz, getClass()).newInstance();
346 
347                     interceptor.init(getInitParameters(interceptorEl), this);
348 
349                     interceptors.add(interceptor);
350                 }
351                 catch (final Exception e)
352                 {
353                     throw new ConfigurationException("Could not getRequest service: " + interceptorClazz, e);
354                 }
355             }
356         }
357     }
358 
359     /**
360      * Returns a Map of the "init-param" properties under the given element. The map could be empty, but is guaranteed
361      * not to be null.
362      *
363      * @param el The XML config element.
364      *
365      * @return a Map of the "init-param" properties under the given element.
366      */
367     private static Map<String, String> getInitParameters(final Element el)
368     {
369         final Map<String, String> params = new HashMap<String, String>();
370         final NodeList nl = el.getElementsByTagName("init-param");
371 
372         for (int i = 0; i < nl.getLength(); i++)
373         {
374             final Node initParam = nl.item(i);
375             final String paramName = XMLUtils.getContainedText(initParam, "param-name");
376             final String paramValue = XMLUtils.getContainedText(initParam, "param-value");
377             params.put(paramName, paramValue);
378         }
379         return Collections.unmodifiableMap(params);
380     }
381 
382     public void destroy()
383     {
384         for (final Object element : services)
385         {
386             final SecurityService securityService = (SecurityService) element;
387             securityService.destroy();
388         }
389 
390         for (final Object element : interceptors)
391         {
392             ((Interceptor) element).destroy();
393         }
394     }
395 
396     /**
397      * Do not use in production! Only used in tests, will be removed.
398      *
399      * @param interceptor the Interceptor to add
400      */
401     public void addInterceptor(final Interceptor interceptor)
402     {
403         interceptors.add(interceptor);
404     }
405 
406     public List<SecurityService> getServices()
407     {
408         return services;
409     }
410 
411     public String getLoginURL()
412     {
413         return loginUrlStrategy.getLoginURL(this, loginURL);
414     }
415 
416     public String getLinkLoginURL()
417     {
418         return loginUrlStrategy.getLinkLoginURL(this, linkLoginURL);
419     }
420 
421     public String getLogoutURL()
422     {
423         return loginUrlStrategy.getLogoutURL(this, logoutURL);
424     }
425 
426     public String getOriginalURLKey()
427     {
428         return originalURLKey;
429     }
430 
431     public Authenticator getAuthenticator()
432     {
433         return authenticator;
434     }
435 
436     public AuthenticationContext getAuthenticationContext()
437     {
438         return new AuthenticationContextImpl();
439     }
440 
441     public SecurityController getController()
442     {
443         return controller;
444     }
445 
446     public RoleMapper getRoleMapper()
447     {
448         return roleMapper;
449     }
450 
451     public RedirectPolicy getRedirectPolicy()
452     {
453         return redirectPolicy;
454     }
455 
456     public <T extends Interceptor> List<T> getInterceptors(final Class<T> desiredInterceptorClass)
457     {
458         final List<T> result = new ArrayList<T>();
459         for (final Interceptor interceptor : interceptors)
460         {
461             if (desiredInterceptorClass.isAssignableFrom(interceptor.getClass()))
462             {
463                 result.add(desiredInterceptorClass.cast(interceptor));
464             }
465         }
466         return Collections.unmodifiableList(result);
467     }
468 
469     public String getCookieEncoding()
470     {
471         return cookieEncoding;
472     }
473 
474     public String getLoginCookiePath()
475     {
476         return loginCookiePath;
477     }
478 
479     public String getLoginCookieKey()
480     {
481         return loginCookieKey;
482     }
483 
484     public String getAuthType()
485     {
486         return authType;
487     }
488 
489     public boolean isInsecureCookie()
490     {
491         return insecureCookie;
492     }
493 
494     public int getAutoLoginCookieAge()
495     {
496         return autoLoginCookieAge;
497     }
498 
499     public ElevatedSecurityGuard getElevatedSecurityGuard()
500     {
501         return elevatedSecurityGuard;
502     }
503 
504     public ComponentLocator getComponentLocator()
505     {
506         return componentLocator;
507     }
508 
509     /**
510      * This implementation will always try to get a live RememberMeService by calling back to the ComponentLocator.  if
511      * null is returned, it will swap in a NoopRememberMeService implementation
512      *
513      * @return a NON NULL RememberMeService implementation
514      */
515     public RememberMeService getRememberMeService()
516     {
517         RememberMeService service = null;
518         if (componentLocator != null)
519         {
520             service = componentLocator.getComponent(RememberMeService.class);
521         }
522         if (service == null)
523         {
524             service = NoopRememberMeService.INSTANCE;
525         }
526         return service;
527     }
528 }