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