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         try
262         {
263             final NodeList elementList = rootEl.getElementsByTagName(tagname);
264 
265             if (elementList.getLength() > 0)
266             {
267                 final Element authEl = (Element) elementList.item(0);
268                 final String clazz = authEl.getAttribute("class");
269 
270                 final Initable initable;
271                 try
272                 {
273                     initable = (Initable) ClassLoaderUtil.loadClass(clazz, owner.getClass()).newInstance();
274                 }
275                 catch (InstantiationException e)
276                 {
277                     // Java's has a null message - add something useful
278                     throw new InstantiationException("Unable to instantiate class '" + clazz + "'");
279                 }
280 
281                 initable.init(getInitParameters(authEl), owner);
282                 return initable;
283             }
284             return null;
285         }
286         catch (final Exception e)
287         {
288             throw new ConfigurationException("Could not create: " + tagname, e);
289         }
290     }
291 
292     // only called from the constructor
293     private List<SecurityService> configureServices(final Element rootEl) throws ConfigurationException
294     {
295         final NodeList nl = rootEl.getElementsByTagName("services");
296         final List<SecurityService> result = new ArrayList<SecurityService>();
297 
298         if ((nl != null) && (nl.getLength() > 0))
299         {
300             final Element servicesEl = (Element) nl.item(0);
301             final NodeList serviceList = servicesEl.getElementsByTagName("service");
302 
303             for (int i = 0; i < serviceList.getLength(); i++)
304             {
305                 final Element serviceEl = (Element) serviceList.item(i);
306                 final String serviceClazz = serviceEl.getAttribute("class");
307 
308                 if ((serviceClazz == null) || "".equals(serviceClazz))
309                 {
310                     throw new ConfigurationException("Service element with bad class attribute");
311                 }
312 
313                 try
314                 {
315                     SecurityConfigImpl.log.debug("Adding seraph service of class: " + serviceClazz);
316                     final SecurityService service = (SecurityService) ClassLoaderUtil.loadClass(serviceClazz, getClass()).newInstance();
317 
318                     service.init(getInitParameters(serviceEl), this);
319 
320                     result.add(service);
321                 }
322                 catch (final Exception e)
323                 {
324                     throw new ConfigurationException("Could not getRequest service: " + serviceClazz, e);
325                 }
326             }
327         }
328         return result;
329     }
330 
331     private void configureInterceptors(final Element rootEl) throws ConfigurationException
332     {
333         final NodeList nl = rootEl.getElementsByTagName("interceptors");
334 
335         if ((nl != null) && (nl.getLength() > 0))
336         {
337             final Element interceptorsEl = (Element) nl.item(0);
338             final NodeList interceptorList = interceptorsEl.getElementsByTagName("interceptor");
339 
340             for (int i = 0; i < interceptorList.getLength(); i++)
341             {
342                 final Element interceptorEl = (Element) interceptorList.item(i);
343                 final String interceptorClazz = interceptorEl.getAttribute("class");
344 
345                 if ((interceptorClazz == null) || "".equals(interceptorClazz))
346                 {
347                     throw new ConfigurationException("Interceptor element with bad class attribute");
348                 }
349 
350                 try
351                 {
352                     SecurityConfigImpl.log.debug("Adding interceptor of class: " + interceptorClazz);
353                     final Interceptor interceptor = (Interceptor) ClassLoaderUtil.loadClass(interceptorClazz, getClass()).newInstance();
354 
355                     interceptor.init(getInitParameters(interceptorEl), this);
356 
357                     interceptors.add(interceptor);
358                 }
359                 catch (final Exception e)
360                 {
361                     throw new ConfigurationException("Could not getRequest service: " + interceptorClazz, e);
362                 }
363             }
364         }
365     }
366 
367     /**
368      * Returns a Map of the "init-param" properties under the given element. The map could be empty, but is guaranteed
369      * not to be null.
370      *
371      * @param el The XML config element.
372      *
373      * @return a Map of the "init-param" properties under the given element.
374      */
375     private static Map<String, String> getInitParameters(final Element el)
376     {
377         final Map<String, String> params = new HashMap<String, String>();
378         final NodeList nl = el.getElementsByTagName("init-param");
379 
380         for (int i = 0; i < nl.getLength(); i++)
381         {
382             final Node initParam = nl.item(i);
383             final String paramName = XMLUtils.getContainedText(initParam, "param-name");
384             final String paramValue = XMLUtils.getContainedText(initParam, "param-value");
385             params.put(paramName, paramValue);
386         }
387         return Collections.unmodifiableMap(params);
388     }
389 
390     public void destroy()
391     {
392         for (final Object element : services)
393         {
394             final SecurityService securityService = (SecurityService) element;
395             securityService.destroy();
396         }
397 
398         for (final Object element : interceptors)
399         {
400             ((Interceptor) element).destroy();
401         }
402     }
403 
404     /**
405      * Do not use in production! Only used in tests, will be removed.
406      *
407      * @param interceptor the Interceptor to add
408      */
409     public void addInterceptor(final Interceptor interceptor)
410     {
411         interceptors.add(interceptor);
412     }
413 
414     public List<SecurityService> getServices()
415     {
416         return services;
417     }
418 
419     public String getLoginURL()
420     {
421         return loginUrlStrategy.getLoginURL(this, loginURL);
422     }
423 
424     public String getLinkLoginURL()
425     {
426         return loginUrlStrategy.getLinkLoginURL(this, linkLoginURL);
427     }
428 
429     public String getLogoutURL()
430     {
431         return loginUrlStrategy.getLogoutURL(this, logoutURL);
432     }
433 
434     public String getOriginalURLKey()
435     {
436         return originalURLKey;
437     }
438 
439     public Authenticator getAuthenticator()
440     {
441         return authenticator;
442     }
443 
444     public AuthenticationContext getAuthenticationContext()
445     {
446         return new AuthenticationContextImpl();
447     }
448 
449     public SecurityController getController()
450     {
451         return controller;
452     }
453 
454     public RoleMapper getRoleMapper()
455     {
456         return roleMapper;
457     }
458 
459     public RedirectPolicy getRedirectPolicy()
460     {
461         return redirectPolicy;
462     }
463 
464     public <T extends Interceptor> List<T> getInterceptors(final Class<T> desiredInterceptorClass)
465     {
466         final List<T> result = new ArrayList<T>();
467         for (final Interceptor interceptor : interceptors)
468         {
469             if (desiredInterceptorClass.isAssignableFrom(interceptor.getClass()))
470             {
471                 result.add(desiredInterceptorClass.cast(interceptor));
472             }
473         }
474         return Collections.unmodifiableList(result);
475     }
476 
477     public String getCookieEncoding()
478     {
479         return cookieEncoding;
480     }
481 
482     public String getLoginCookiePath()
483     {
484         return loginCookiePath;
485     }
486 
487     public String getLoginCookieKey()
488     {
489         return loginCookieKey;
490     }
491 
492     public String getAuthType()
493     {
494         return authType;
495     }
496 
497     public boolean isInsecureCookie()
498     {
499         return insecureCookie;
500     }
501 
502     public int getAutoLoginCookieAge()
503     {
504         return autoLoginCookieAge;
505     }
506 
507     public ElevatedSecurityGuard getElevatedSecurityGuard()
508     {
509         return elevatedSecurityGuard;
510     }
511 
512     /**
513      * @return a NON NULL RememberMeService implementation
514      */
515     public RememberMeService getRememberMeService()
516     {
517         return ApplicationServicesRegistry.getRememberMeService();
518     }
519 
520     public boolean isInvalidateSessionOnLogin()
521     {
522         return invalidateSessionOnLogin;
523     }
524 
525     public List<String> getInvalidateSessionExcludeList()
526     {
527         return invalidateSessionExcludeList;
528     }
529 }