1   package com.atlassian.seraph.service;
2   
3   import com.atlassian.seraph.SecurityService;
4   import com.atlassian.seraph.config.SecurityConfig;
5   import com.atlassian.seraph.util.CachedPathMapper;
6   import com.atlassian.seraph.util.XMLUtils;
7   
8   import org.apache.log4j.Category;
9   import org.w3c.dom.Document;
10  import org.w3c.dom.Element;
11  import org.w3c.dom.NodeList;
12  
13  import com.opensymphony.util.ClassLoaderUtil;
14  
15  import java.net.URL;
16  import java.util.Collection;
17  import java.util.Collections;
18  import java.util.HashMap;
19  import java.util.HashSet;
20  import java.util.Map;
21  import java.util.Set;
22  import java.util.StringTokenizer;
23  import java.util.concurrent.ConcurrentHashMap;
24  
25  import javax.servlet.http.HttpServletRequest;
26  import javax.xml.parsers.DocumentBuilderFactory;
27  
28  /**
29   * Configures Seraph to require certain roles to access certain URL paths.
30   * <p>
31   * Single init-param 'config.file' which is the location of the XML config file. Default value is '/seraph-paths.xml' (loaded from classpath - usually
32   * in /WEB-INF/classes)
33   * <p>
34   * Here's a sample of the XML config file. Path names must be unique
35   * <p>
36   * 
37   * <pre>
38   * &lt;seraph-paths&gt;
39   *     &lt;path name=&quot;admin&quot;&gt;
40   *         &lt;url-pattern&gt;/secure/admin/*&lt;/url-pattern&gt;
41   *         &lt;role-name&gt;administrators&lt;/role-name&gt;
42   *     &lt;/path&gt;
43   *     &lt;path name=&quot;secured&quot;&gt;
44   *         &lt;url-pattern&gt;/secure/*&lt;/url-pattern&gt;
45   *         &lt;role-name&gt;users&lt;/role-name&gt;
46   *     &lt;/path&gt;
47   * &lt;/seraph-paths&gt;
48   * </pre>
49   */
50  public class PathService implements SecurityService
51  {
52      private static final Category log = Category.getInstance(PathService.class);
53      static String CONFIG_FILE_PARAM_KEY = "config.file";
54  
55      String configFileLocation = "seraph-paths.xml";
56  
57      // maps url patterns to path names
58      private final CachedPathMapper pathMapper = new CachedPathMapper();
59  
60      // maps roles to path names
61      private final Map<String, String[]> paths = new ConcurrentHashMap<String, String[]>();
62  
63      /**
64       * Init the service - configure it from the config file
65       */
66      public void init(final Map<String, String> params, final SecurityConfig config)
67      {
68          if (params.get(CONFIG_FILE_PARAM_KEY) != null)
69          {
70              configFileLocation = params.get(CONFIG_FILE_PARAM_KEY);
71          }
72  
73          configurePathMapper();
74      }
75  
76      /**
77       * Configure the path mapper
78       */
79      private void configurePathMapper()
80      {
81          try
82          {
83              final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
84              URL fileUrl = ClassLoaderUtil.getResource(configFileLocation, this.getClass());
85  
86              if (fileUrl == null)
87              {
88                  fileUrl = ClassLoaderUtil.getResource("/" + configFileLocation, this.getClass());
89              }
90  
91              if (fileUrl == null)
92              {
93                  return;
94              }
95  
96              // Parse document
97              final Document doc = factory.newDocumentBuilder().parse(fileUrl.toString());
98  
99              // Get list of actions
100             final Element root = doc.getDocumentElement();
101             final NodeList pathNodes = root.getElementsByTagName("path");
102 
103             final Map<String, String> pathMaps = new HashMap<String, String>();
104             // Build list of views
105             for (int i = 0; i < pathNodes.getLength(); i++)
106             {
107                 final Element path = (Element) pathNodes.item(i);
108 
109                 final String pathName = path.getAttribute("name");
110                 final String roleNames = XMLUtils.getContainedText(path, "role-name");
111                 final String urlPattern = XMLUtils.getContainedText(path, "url-pattern");
112 
113                 if ((roleNames != null) && (urlPattern != null))
114                 {
115                     final String[] rolesArr = parseRoles(roleNames);
116                     paths.put(pathName, rolesArr);
117                     pathMaps.put(pathName, urlPattern);
118                 }
119             }
120             pathMapper.set(pathMaps);
121         }
122         catch (final Exception ex)
123         {
124             log.error(ex);
125         }
126     }
127 
128     protected String[] parseRoles(final String roleNames)
129     {
130         final StringTokenizer st = new StringTokenizer(roleNames, ",; \t\n", false);
131         final String[] roles = new String[st.countTokens()];
132         int i = 0;
133         while (st.hasMoreTokens())
134         {
135             roles[i] = st.nextToken();
136             i++;
137         }
138         return roles;
139     }
140 
141     public void destroy()
142     {}
143 
144     public Set<String> getRequiredRoles(final HttpServletRequest request)
145     {
146         final String servletPath = request.getServletPath();
147         return getRequiredRoles(servletPath);
148     }
149 
150     public Set<String> getRequiredRoles(final String servletPath)
151     {
152         final Set<String> requiredRoles = new HashSet<String>();
153 
154         // then check path mappings first and add any required roles
155         final Collection<String> constraintMatches = pathMapper.getAll(servletPath);
156 
157         if (constraintMatches == null)
158         {
159             throw new RuntimeException("No constraints matched for path " + servletPath);
160         }
161         for (final String constraint : constraintMatches)
162         {
163             final String[] rolesForPath = paths.get(constraint);
164             for (int i = 0; i < rolesForPath.length; i++)
165             {
166                 if (!requiredRoles.contains(rolesForPath[i])) // since requiredRoles is a set, isn't this useless?
167                 {
168                     requiredRoles.add(rolesForPath[i]);
169                 }
170             }
171         }
172         return Collections.unmodifiableSet(requiredRoles);
173     }
174 }