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