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 }