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   
7   import org.apache.log4j.Category;
8   import org.w3c.dom.Document;
9   import org.w3c.dom.Element;
10  import org.w3c.dom.NodeList;
11  import org.xml.sax.SAXException;
12  
13  import com.opensymphony.util.ClassLoaderUtil;
14  
15  import java.io.IOException;
16  import java.net.URL;
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  import javax.xml.parsers.ParserConfigurationException;
28  
29  /**
30   * Configures Seraph based on WebWork 1.x configuration file actions.xml. <p/> Takes two optional init parameters:
31   * <dl>
32   * <dt>action.extension (default: "action")</dt>
33   * <dd>The extension of action URLs which are intercepted by this service. If this parameter is not specified, the default is the WebWork default
34   * 'action'. (This should match your servlet mapping in web.xml.)</dd>
35   * <dt>actions.xml.file (default: "actions")</dt>
36   * <dd>The location on the classpath of your actions.xml file (WebWork 1.x style). If this parameter is not specified, the default is the WebWork 1.x
37   * default 'actions'. (This should match the value configured for 'webwork.configuration.xml' in webwork.properties.)</dd>
38   * </dl>
39   * In actions.xml (or other actions XML file, as specified by the init-param) specify roles required per action or command:
40   * 
41   * <pre>
42   *   &lt;action name=&quot;project.ViewProject&quot; alias=&quot;ViewProject&quot; roles-required=&quot;RoleFoo, RoleBar&quot;&gt;
43   * or
44   *   &lt;command name=&quot;Delete&quot; alias=&quot;DeleteProject&quot; roles-required=&quot;RoleBat&quot;&gt;
45   * </pre>
46   * 
47   * Roles can be separated by commas and spaces.
48   */
49  public class WebworkService implements SecurityService
50  {
51      private static final Category log = Category.getInstance(WebworkService.class);
52      private static final String ROLES_REQUIRED_ATTR = "roles-required";
53      private static final String ACTION_EXTENSION_INIT_PARAM = "action.extension";
54      private static final String ACTIONS_XML_FILE_INIT_PARAM = "actions.xml.file";
55  
56      // Same as the WebWork default (i.e. action URLs end with '.action')
57      private static final String ACTION_EXTENSION_DEFAULT = "action";
58  
59      // Same as the WebWork 1.x default (i.e. actions.xml is the mapping file)
60      private static final String ACTIONS_XML_FILE_DEFAULT = "actions";
61  
62      // used to check which actions match the current path
63      // This class only uses the "get" method of the pathmapper, so initialise the map
64      // that caches its results to a large value. The getAll method of the PathMapper is not used
65      // by this class so make the second caching map small.
66      private final CachedPathMapper actionMapper = new CachedPathMapper();
67  
68      // maps current action to roles required
69      private final Map<String, String> rolesMap = new ConcurrentHashMap<String, String>();
70  
71      public void init(final Map<String, String> params, final SecurityConfig config)
72      {
73          try
74          {
75              // the extension of webwork actions (should match web.xml servlet mapping)
76              String extension = params.get(ACTION_EXTENSION_INIT_PARAM);
77              if (extension == null)
78              {
79                  extension = ACTION_EXTENSION_DEFAULT;
80              }
81  
82              // the XML file containing the action mappings (should match 'webwork.configuration.xml' property)
83  
84              String actionsXmlFile = params.get(ACTIONS_XML_FILE_INIT_PARAM);
85              if (actionsXmlFile == null)
86              {
87                  actionsXmlFile = ACTIONS_XML_FILE_DEFAULT;
88              }
89  
90              configureActionMapper(extension, actionsXmlFile);
91          }
92          catch (final RuntimeException e)
93          {
94              log.error("Failed to initialise WebworkService", e);
95          }
96      }
97  
98      private void configureActionMapper(final String extension, final String actionsXmlFile)
99      {
100         final Document doc = parseActionsXmlFile(actionsXmlFile);
101 
102         // Get list of actions
103         final NodeList actions = doc.getElementsByTagName("action");
104 
105         final String rootRolesRequired = overrideRoles(null, doc.getDocumentElement());
106 
107         final Map<String, String> pathMaps = new HashMap<String, String>();
108 
109         // Build list of views
110         for (int i = 0; i < actions.getLength(); i++)
111         {
112             final Element action = (Element) actions.item(i);
113             final String actionName = action.getAttribute("name");
114             final String actionAlias = action.getAttribute("alias");
115             final String actionRolesRequired = overrideRoles(rootRolesRequired, action);
116 
117             if (actionRolesRequired != null)
118             {
119 
120                 if (actionAlias != null)
121                 {
122                     pathMaps.put(actionAlias, "/" + actionAlias + "." + extension);
123                     rolesMap.put(actionAlias, actionRolesRequired);
124                     pathMaps.put(actionAlias + "!*", "/" + actionAlias + "!*." + extension);
125                     rolesMap.put(actionAlias + "!*", actionRolesRequired);
126                 }
127 
128                 if (actionName != null)
129                 {
130                     pathMaps.put(actionName, "/" + actionName + "." + extension);
131                     rolesMap.put(actionName, actionRolesRequired);
132                     pathMaps.put(actionName + "!*", "/" + actionName + "!*." + extension);
133                     rolesMap.put(actionName + "!*", actionRolesRequired);
134                 }
135             }
136 
137             // Get list of commands
138             final NodeList commands = action.getElementsByTagName("command");
139             for (int j = 0; j < commands.getLength(); j++)
140             {
141                 final Element command = (Element) commands.item(j);
142                 final String cmdRolesRequired = overrideRoles(actionRolesRequired, command);
143 
144                 final String commandAlias = command.getAttribute("alias");
145 
146                 if ((commandAlias != null) && (cmdRolesRequired != null))
147                 {
148                     pathMaps.put(commandAlias, "/" + commandAlias + "." + extension);
149                     rolesMap.put(commandAlias, cmdRolesRequired);
150                 }
151             }
152         }
153         actionMapper.set(pathMaps);
154     }
155 
156     private Document parseActionsXmlFile(final String actionsXmlFile)
157     {
158         final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
159         URL fileUrl = ClassLoaderUtil.getResource(actionsXmlFile + ".xml", this.getClass());
160 
161         if (fileUrl == null)
162         {
163             fileUrl = ClassLoaderUtil.getResource("/" + actionsXmlFile + ".xml", this.getClass());
164         }
165 
166         if (fileUrl == null)
167         {
168             throw new IllegalArgumentException("No such XML file:/" + actionsXmlFile + ".xml");
169         }
170 
171         try
172         {
173             return factory.newDocumentBuilder().parse(fileUrl.toString());
174         }
175         catch (final SAXException e)
176         {
177             throw new RuntimeException(e);
178         }
179         catch (final IOException e)
180         {
181             throw new RuntimeException(e);
182         }
183         catch (final ParserConfigurationException e)
184         {
185             throw new RuntimeException(e);
186         }
187     }
188 
189     /**
190      * Returns newRolesRequired if it isn't empty, and rolesRequired otherwise.
191      */
192     private String overrideRoles(final String rolesRequired, final Element action)
193     {
194         if (action.hasAttribute(ROLES_REQUIRED_ATTR))
195         {
196             return action.getAttribute(ROLES_REQUIRED_ATTR);
197         }
198         else
199         {
200             return rolesRequired;
201         }
202     }
203 
204     public void destroy()
205     {}
206 
207     public Set<String> getRequiredRoles(final HttpServletRequest request)
208     {
209         final Set<String> requiredRoles = new HashSet<String>();
210 
211         final String currentURL = request.getRequestURI();
212 
213         final int lastSlash = currentURL.lastIndexOf('/');
214         String targetURL;
215 
216         // then check webwork mappings
217         if (lastSlash > -1)
218         {
219             targetURL = currentURL.substring(lastSlash);
220         }
221         else
222         {
223             targetURL = currentURL;
224         }
225 
226         final String actionMatch = actionMapper.get(targetURL);
227 
228         if (actionMatch != null)
229         {
230             final String rolesStr = rolesMap.get(actionMatch);
231 
232             final StringTokenizer st = new StringTokenizer(rolesStr, ", ");
233             while (st.hasMoreTokens())
234             {
235                 requiredRoles.add(st.nextToken());
236             }
237         }
238 
239         return Collections.unmodifiableSet(requiredRoles);
240     }
241 }