001: /*
002: * JSPWiki - a JSP-based WikiWiki clone. Copyright (C) 2001-2003 Janne Jalkanen
003: * (Janne.Jalkanen@iki.fi) This program is free software; you can redistribute
004: * it and/or modify it under the terms of the GNU Lesser General Public License
005: * as published by the Free Software Foundation; either version 2.1 of the
006: * License, or (at your option) any later version. This program is distributed
007: * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
008: * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
009: * See the GNU Lesser General Public License for more details. You should have
010: * received a copy of the GNU Lesser General Public License along with this
011: * program; if not, write to the Free Software Foundation, Inc., 59 Temple
012: * Place, Suite 330, Boston, MA 02111-1307 USA
013: */
014: package com.ecyrd.jspwiki.auth.authorize;
015:
016: import java.io.IOException;
017: import java.net.URL;
018: import java.security.Principal;
019: import java.util.HashSet;
020: import java.util.Iterator;
021: import java.util.List;
022: import java.util.Properties;
023: import java.util.Set;
024:
025: import javax.servlet.http.HttpServletRequest;
026:
027: import org.apache.log4j.Logger;
028: import org.jdom.Document;
029: import org.jdom.Element;
030: import org.jdom.Namespace;
031: import org.jdom.JDOMException;
032: import org.jdom.input.SAXBuilder;
033: import org.jdom.xpath.XPath;
034: import org.xml.sax.EntityResolver;
035: import org.xml.sax.InputSource;
036: import org.xml.sax.SAXException;
037:
038: import com.ecyrd.jspwiki.InternalWikiException;
039: import com.ecyrd.jspwiki.WikiEngine;
040: import com.ecyrd.jspwiki.WikiSession;
041:
042: /**
043: * Authorizes users by delegating role membership checks to the servlet
044: * container. In addition to implementing methods for the
045: * <code>Authorizer</code> interface, this class also provides a convenience
046: * method {@link #isContainerAuthorized()} that queries the web application
047: * descriptor to determine if the container manages authorization.
048: * @author Andrew Jaquith
049: * @since 2.3
050: */
051: public class WebContainerAuthorizer implements WebAuthorizer {
052: private static final String J2EE_SCHEMA_24_NAMESPACE = "http://java.sun.com/xml/ns/j2ee";
053:
054: protected static final Logger log = Logger
055: .getLogger(WebContainerAuthorizer.class);
056:
057: protected WikiEngine m_engine;
058:
059: /**
060: * A lazily-initialized array of Roles that the container knows about. These
061: * are parsed from JSPWiki's <code>web.xml</code> web application
062: * deployment descriptor. If this file cannot be read for any reason, the
063: * role list will be empty. This is a hack designed to get around the fact
064: * that we have no direct way of querying the web container about which
065: * roles it manages.
066: */
067: protected Role[] m_containerRoles = new Role[0];
068:
069: /**
070: * Lazily-initialized boolean flag indicating whether the web container
071: * protects JSPWiki resources.
072: */
073: protected boolean m_containerAuthorized = false;
074:
075: private Document m_webxml = null;
076:
077: /**
078: * Constructs a new instance of the WebContainerAuthorizer class.
079: */
080: public WebContainerAuthorizer() {
081: super ();
082: }
083:
084: /**
085: * Initializes the authorizer for.
086: * @param engine the current wiki engine
087: * @param props the wiki engine initialization properties
088: */
089: public void initialize(WikiEngine engine, Properties props) {
090: m_engine = engine;
091: m_containerAuthorized = false;
092:
093: // FIXME: Error handling here is not very verbose
094: try {
095: m_webxml = getWebXml();
096: if (m_webxml != null) {
097: // Add the J2EE 2.4 schema namespace
098: m_webxml
099: .getRootElement()
100: .setNamespace(
101: Namespace
102: .getNamespace(J2EE_SCHEMA_24_NAMESPACE));
103:
104: m_containerAuthorized = isConstrained("/Delete.jsp",
105: Role.ALL)
106: && isConstrained("/Login.jsp", Role.ALL);
107: }
108: if (m_containerAuthorized) {
109: m_containerRoles = getRoles(m_webxml);
110: log
111: .info("JSPWiki is using container-managed authentication.");
112: } else {
113: log.info("JSPWiki is using custom authentication.");
114: }
115: } catch (IOException e) {
116: log.error("Initialization failed: ", e);
117: throw new InternalWikiException(e.getClass().getName()
118: + ": " + e.getMessage());
119: } catch (JDOMException e) {
120: log.error("Malformed XML in web.xml", e);
121: throw new InternalWikiException(e.getClass().getName()
122: + ": " + e.getMessage());
123: }
124:
125: if (m_containerRoles.length > 0) {
126: String roles = "";
127: for (int i = 0; i < m_containerRoles.length; i++) {
128: roles = roles + m_containerRoles[i] + " ";
129: }
130: log
131: .info(" JSPWiki determined the web container manages these roles: "
132: + roles);
133: }
134: log
135: .info("Authorizer WebContainerAuthorizer initialized successfully.");
136: }
137:
138: /**
139: * Determines whether a user associated with an HTTP request possesses
140: * a particular role. This method simply delegates to
141: * {@link javax.servlet.http.HttpServletRequest#isUserInRole(String)}
142: * by converting the Principal's name to a String.
143: * @param request the HTTP request
144: * @param role the role to check
145: * @return <code>true</code> if the user is considered to be in the role,
146: * <code>false</code> otherwise
147: */
148: public boolean isUserInRole(HttpServletRequest request,
149: Principal role) {
150: return request.isUserInRole(role.getName());
151: }
152:
153: /**
154: * Determines whether the Subject associated with a WikiSession is in a
155: * particular role. This method takes two parameters: the WikiSession
156: * containing the subject and the desired role ( which may be a Role or a
157: * Group). If either parameter is <code>null</code>, this method must
158: * return <code>false</code>.
159: * This method simply examines the WikiSession subject to see if it
160: * possesses the desired Principal. We assume that the method
161: * {@link com.ecyrd.jspwiki.auth.AuthenticationManager#login(HttpServletRequest)}
162: * previously executed at user login time, and that it has injected
163: * the role Principals that were in force at login time.
164: * This is definitely a hack,
165: * but it eliminates the need for WikiSession to keep dangling
166: * references to the last WikiContext hanging around, just
167: * so we can look up the HttpServletRequest.
168: *
169: * @param session the current WikiSession
170: * @param role the role to check
171: * @return <code>true</code> if the user is considered to be in the role,
172: * <code>false</code> otherwise
173: * @see com.ecyrd.jspwiki.auth.Authorizer#isUserInRole(com.ecyrd.jspwiki.WikiSession, java.security.Principal)
174: */
175: public boolean isUserInRole(WikiSession session, Principal role) {
176: if (session == null || role == null) {
177: return false;
178: }
179: return session.hasPrincipal(role);
180: }
181:
182: /**
183: * Looks up and returns a Role Principal matching a given String. If the
184: * Role does not match one of the container Roles identified during
185: * initialization, this method returns <code>null</code>.
186: * @param role the name of the Role to retrieve
187: * @return a Role Principal, or <code>null</code>
188: * @see com.ecyrd.jspwiki.auth.Authorizer#initialize(WikiEngine, Properties)
189: */
190: public Principal findRole(String role) {
191: for (int i = 0; i < m_containerRoles.length; i++) {
192: if (m_containerRoles[i].getName().equals(role)) {
193: return m_containerRoles[i];
194: }
195: }
196: return null;
197: }
198:
199: /**
200: * <p>
201: * Protected method that identifies whether a particular webapp URL is
202: * constrained to a particular Role. The resource is considered constrained
203: * if:
204: * </p>
205: * <ul>
206: * <li>the web application deployment descriptor contains a
207: * <code>security-constraint</code> with a child
208: * <code>web-resource-collection/url-pattern</code> element matching the
209: * URL, <em>and</em>:</li>
210: * <li>this constraint also contains an
211: * <code>auth-constraint/role-name</code> element equal to the supplied
212: * Role's <code>getName()</code> method. If the supplied Role is Role.ALL,
213: * it matches all roles</li>
214: * </ul>
215: * @param url the web resource
216: * @param role the role
217: * @return <code>true</code> if the resource is constrained to the role,
218: * <code>false</code> otherwise
219: * @throws JDOMException if elements cannot be parsed correctly
220: */
221: public boolean isConstrained(String url, Role role)
222: throws JDOMException {
223: Element root = m_webxml.getRootElement();
224: XPath xpath;
225: String selector;
226:
227: // Get all constraints that have our URL pattern
228: // (Note the crazy j: prefix to denote the 2.4 j2ee schema)
229: selector = "//j:web-app/j:security-constraint[j:web-resource-collection/j:url-pattern=\""
230: + url + "\"]";
231: xpath = XPath.newInstance(selector);
232: xpath.addNamespace("j", J2EE_SCHEMA_24_NAMESPACE);
233: List constraints = xpath.selectNodes(root);
234:
235: // Get all constraints that match our Role pattern
236: selector = "//j:web-app/j:security-constraint[j:auth-constraint/j:role-name=\""
237: + role.getName() + "\"]";
238: xpath = XPath.newInstance(selector);
239: xpath.addNamespace("j", J2EE_SCHEMA_24_NAMESPACE);
240: List roles = xpath.selectNodes(root);
241:
242: // If we can't find either one, we must not be constrained
243: if (constraints.size() == 0) {
244: return false;
245: }
246:
247: // Shortcut: if the role is ALL, we are constrained
248: if (role.equals(Role.ALL)) {
249: return true;
250: }
251:
252: // If no roles, we must not be constrained
253: if (roles.size() == 0) {
254: return false;
255: }
256:
257: // If a constraint is contained in both lists, we must be constrained
258: for (Iterator c = constraints.iterator(); c.hasNext();) {
259: Element constraint = (Element) c.next();
260: for (Iterator r = roles.iterator(); r.hasNext();) {
261: Element roleConstraint = (Element) r.next();
262: if (constraint.equals(roleConstraint)) {
263: return true;
264: }
265: }
266: }
267: return false;
268: }
269:
270: /**
271: * Returns <code>true</code> if the web container is configured to protect
272: * certain JSPWiki resources by requiring authentication. Specifically, this
273: * method parses JSPWiki's web application descriptor (<code>web.xml</code>)
274: * and identifies whether the string representation of
275: * {@link com.ecyrd.jspwiki.auth.authorize.Role#AUTHENTICATED} is required
276: * to access <code>/Delete.jsp</code> and <code>LoginRedirect.jsp</code>.
277: * If the administrator has uncommented the large
278: * <code><security-constraint></code> section of <code>web.xml</code>,
279: * this will be true. This is admittedly an indirect way to go about it, but
280: * it should be an accurate test for default installations, and also in 99%
281: * of customized installs.
282: * @return <code>true</code> if the container protects resources,
283: * <code>false</code> otherwise
284: */
285: public boolean isContainerAuthorized() {
286: return m_containerAuthorized;
287: }
288:
289: /**
290: * Returns an array of role Principals this Authorizer knows about.
291: * This method will return an array of Role objects corresponding to
292: * the logical roles enumerated in the <code>web.xml</code>.
293: * This method actually returns a defensive copy of an internally stored
294: * array.
295: * @return an array of Principals representing the roles
296: */
297: public Principal[] getRoles() {
298: return (Principal[]) m_containerRoles.clone();
299: }
300:
301: /**
302: * Protected method that extracts the roles from JSPWiki's web application
303: * deployment descriptor. Each Role is constructed by using the String
304: * representation of the Role, for example
305: * <code>new Role("Administrator")</code>.
306: * @param webxml the web application deployment descriptor
307: * @return an array of Role objects
308: * @throws JDOMException if elements cannot be parsed correctly
309: */
310: protected Role[] getRoles(Document webxml) throws JDOMException {
311: Set roles = new HashSet();
312: Element root = webxml.getRootElement();
313:
314: // Get roles referred to by constraints
315: String selector = "//j:web-app/j:security-constraint/j:auth-constraint/j:role-name";
316: XPath xpath = XPath.newInstance(selector);
317: xpath.addNamespace("j", J2EE_SCHEMA_24_NAMESPACE);
318: List nodes = xpath.selectNodes(root);
319: for (Iterator it = nodes.iterator(); it.hasNext();) {
320: String role = ((Element) it.next()).getTextTrim();
321: roles.add(new Role(role));
322: }
323:
324: // Get all defined roles
325: selector = "//j:web-app/j:security-role/j:role-name";
326: xpath = XPath.newInstance(selector);
327: xpath.addNamespace("j", J2EE_SCHEMA_24_NAMESPACE);
328: nodes = xpath.selectNodes(root);
329: for (Iterator it = nodes.iterator(); it.hasNext();) {
330: String role = ((Element) it.next()).getTextTrim();
331: roles.add(new Role(role));
332: }
333:
334: return (Role[]) roles.toArray(new Role[roles.size()]);
335: }
336:
337: /**
338: * Returns an {@link org.jdom.Document} representing JSPWiki's web
339: * application deployment descriptor. The document is obtained by calling
340: * the servlet context's <code>getResource()</code> method and requesting
341: * <code>/WEB-INF/web.xml</code>. For non-servlet applications, this
342: * method calls this class'
343: * {@link ClassLoader#getResource(java.lang.String)} and requesting
344: * <code>WEB-INF/web.xml</code>.
345: * @return the descriptor
346: * @throws IOException if the deployment descriptor cannot be found or opened
347: * @throws JDOMException if the deployment descriptor cannot be parsed correctly
348: */
349: protected Document getWebXml() throws JDOMException, IOException {
350: URL url;
351: SAXBuilder builder = new SAXBuilder();
352: builder.setValidation(false);
353: builder.setEntityResolver(new LocalEntityResolver());
354: Document doc = null;
355: if (m_engine.getServletContext() == null) {
356: ClassLoader cl = WebContainerAuthorizer.class
357: .getClassLoader();
358: url = cl.getResource("WEB-INF/web.xml");
359: if (url != null)
360: log.info("Examining " + url.toExternalForm());
361: } else {
362: url = m_engine.getServletContext().getResource(
363: "/WEB-INF/web.xml");
364: if (url != null)
365: log.info("Examining " + url.toExternalForm());
366: }
367: if (url == null)
368: throw new IOException(
369: "Unable to find web.xml for processing.");
370:
371: log.debug("Processing web.xml at " + url.toExternalForm());
372: doc = builder.build(url);
373: return doc;
374: }
375:
376: /**
377: * <p>XML entity resolver that redirects resolution requests by JDOM, JAXP and
378: * other XML parsers to locally-cached copies of the resources. Local
379: * resources are stored in the <code>WEB-INF/dtd</code> directory.</p>
380: * <p>For example, Sun Microsystem's DTD for the webapp 2.3 specification is normally
381: * kept at <code>http://java.sun.com/dtd/web-app_2_3.dtd</code>. The
382: * local copy is stored at <code>WEB-INF/dtd/web-app_2_3.dtd</code>.</p>
383: * @author Andrew Jaquith
384: */
385: public class LocalEntityResolver implements EntityResolver {
386: /**
387: * Returns an XML input source for a requested external resource by
388: * reading the resource instead from local storage. The local resource path
389: * is <code>WEB-INF/dtd</code>, plus the file name of the requested
390: * resource, minus the non-filename path information.
391: * @param publicId the public ID, such as
392: * <code>-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN</code>
393: * @param systemId the system ID, such as
394: * <code>http://java.sun.com/dtd/web-app_2_3.dtd</code>
395: * @return the InputSource containing the resolved resource
396: * @see org.xml.sax.EntityResolver#resolveEntity(java.lang.String,
397: * java.lang.String)
398: * @throws SAXException if the resource cannot be resolved locally
399: * @throws IOException if the resource cannot be opened
400: */
401: public InputSource resolveEntity(String publicId,
402: String systemId) throws SAXException, IOException {
403: String file = systemId
404: .substring(systemId.lastIndexOf('/') + 1);
405: URL url;
406: if (m_engine.getServletContext() == null) {
407: ClassLoader cl = WebContainerAuthorizer.class
408: .getClassLoader();
409: url = cl.getResource("WEB-INF/dtd/" + file);
410: } else {
411: url = m_engine.getServletContext().getResource(
412: "/WEB-INF/dtd/" + file);
413: }
414:
415: if (url != null) {
416: InputSource is = new InputSource(url.openStream());
417: log.debug("Resolved systemID=" + systemId
418: + " using local file " + url);
419: return is;
420: }
421:
422: //
423: // Let's fall back to default behaviour of the container, and let's
424: // also let the user know what is going on. This caught me by surprise
425: // while running JSPWiki on an unconnected laptop...
426: //
427: // The DTD needs to be resolved and read because it contains things like
428: // entity definitions...
429: //
430: log
431: .info("Please note: There are no local DTD references in /WEB-INF/dtd/"
432: + file
433: + "; falling back to default behaviour."
434: + " This may mean that the XML parser will attempt to connect to the internet to find the DTD."
435: + " If you are running JSPWiki locally in an unconnected network, you might want to put the DTD files in place to avoid nasty UnknownHostExceptions.");
436:
437: // Fall back to default behaviour
438: return null;
439: }
440: }
441:
442: }
|