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