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 org.apache.log4j.Logger; 022import org.apache.wiki.InternalWikiException; 023import org.apache.wiki.WikiEngine; 024import org.apache.wiki.WikiSession; 025import org.jdom2.Document; 026import org.jdom2.Element; 027import org.jdom2.JDOMException; 028import org.jdom2.Namespace; 029import org.jdom2.filter.Filters; 030import org.jdom2.input.SAXBuilder; 031import org.jdom2.input.sax.XMLReaders; 032import org.jdom2.xpath.XPathFactory; 033import org.xml.sax.EntityResolver; 034import org.xml.sax.InputSource; 035import org.xml.sax.SAXException; 036 037import javax.servlet.http.HttpServletRequest; 038import java.io.IOException; 039import java.net.URL; 040import java.security.Principal; 041import java.util.HashSet; 042import java.util.Iterator; 043import java.util.List; 044import java.util.Properties; 045import java.util.Set; 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://xmlns.jcp.org/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 */ 236 public boolean isConstrained( final String url, final Role role ) { 237 final Element root = m_webxml.getRootElement(); 238 final Namespace jeeNs = Namespace.getNamespace( "j", J2EE_SCHEMA_25_NAMESPACE ); 239 240 // Get all constraints that have our URL pattern 241 // (Note the crazy j: prefix to denote the 2.4 j2ee schema) 242 final String constrainsSelector = "//j:web-app/j:security-constraint[j:web-resource-collection/j:url-pattern=\"" + url + "\"]"; 243 final List< Element > constraints = XPathFactory.instance() 244 .compile( constrainsSelector, Filters.element(), null, jeeNs ) 245 .evaluate( root ); 246 247 // Get all constraints that match our Role pattern 248 final String rolesSelector = "//j:web-app/j:security-constraint[j:auth-constraint/j:role-name=\"" + role.getName() + "\"]"; 249 final List< Element > roles = XPathFactory.instance() 250 .compile( rolesSelector, Filters.element(), null, jeeNs ) 251 .evaluate( root ); 252 253 // If we can't find either one, we must not be constrained 254 if ( constraints.size() == 0 ) { 255 return false; 256 } 257 258 // Shortcut: if the role is ALL, we are constrained 259 if ( role.equals( Role.ALL ) ) { 260 return true; 261 } 262 263 // If no roles, we must not be constrained 264 if ( roles.size() == 0 ) { 265 return false; 266 } 267 268 // If a constraint is contained in both lists, we must be constrained 269 for ( Iterator< Element > c = constraints.iterator(); c.hasNext(); ) { 270 final Element constraint = c.next(); 271 for ( Iterator< Element > r = roles.iterator(); r.hasNext(); ) { 272 final Element roleConstraint = r.next(); 273 if ( constraint.equals( roleConstraint ) ) { 274 return true; 275 } 276 } 277 } 278 return false; 279 } 280 281 /** 282 * Returns <code>true</code> if the web container is configured to protect 283 * certain JSPWiki resources by requiring authentication. Specifically, this 284 * method parses JSPWiki's web application descriptor (<code>web.xml</code>) 285 * and identifies whether the string representation of 286 * {@link org.apache.wiki.auth.authorize.Role#AUTHENTICATED} is required 287 * to access <code>/Delete.jsp</code> and <code>LoginRedirect.jsp</code>. 288 * If the administrator has uncommented the large 289 * <code><security-constraint></code> section of <code>web.xml</code>, 290 * this will be true. This is admittedly an indirect way to go about it, but 291 * it should be an accurate test for default installations, and also in 99% 292 * of customized installs. 293 * @return <code>true</code> if the container protects resources, 294 * <code>false</code> otherwise 295 */ 296 public boolean isContainerAuthorized() 297 { 298 return m_containerAuthorized; 299 } 300 301 /** 302 * Returns an array of role Principals this Authorizer knows about. 303 * This method will return an array of Role objects corresponding to 304 * the logical roles enumerated in the <code>web.xml</code>. 305 * This method actually returns a defensive copy of an internally stored 306 * array. 307 * @return an array of Principals representing the roles 308 */ 309 @Override 310 public Principal[] getRoles() 311 { 312 return m_containerRoles.clone(); 313 } 314 315 /** 316 * Protected method that extracts the roles from JSPWiki's web application 317 * deployment descriptor. Each Role is constructed by using the String 318 * representation of the Role, for example 319 * <code>new Role("Administrator")</code>. 320 * @param webxml the web application deployment descriptor 321 * @return an array of Role objects 322 */ 323 protected Role[] getRoles( final Document webxml ) { 324 final Set<Role> roles = new HashSet<>(); 325 final Element root = webxml.getRootElement(); 326 final Namespace jeeNs = Namespace.getNamespace( "j", J2EE_SCHEMA_25_NAMESPACE ); 327 328 // Get roles referred to by constraints 329 final String constrainsSelector = "//j:web-app/j:security-constraint/j:auth-constraint/j:role-name"; 330 final List< Element > constraints = XPathFactory.instance() 331 .compile( constrainsSelector, Filters.element(), null, jeeNs ) 332 .evaluate( root ); 333 for( final Iterator< Element > it = constraints.iterator(); it.hasNext(); ) { 334 final String role = ( it.next() ).getTextTrim(); 335 roles.add( new Role( role ) ); 336 } 337 338 // Get all defined roles 339 final String rolesSelector = "//j:web-app/j:security-role/j:role-name"; 340 final List< Element > nodes = XPathFactory.instance() 341 .compile( rolesSelector, Filters.element(), null, jeeNs ) 342 .evaluate( root ); 343 for( final Iterator< Element > it = nodes.iterator(); it.hasNext(); ) { 344 final String role = ( it.next() ).getTextTrim(); 345 roles.add( new Role( role ) ); 346 } 347 348 return roles.toArray( new Role[roles.size()] ); 349 } 350 351 /** 352 * Returns an {@link org.jdom2.Document} representing JSPWiki's web 353 * application deployment descriptor. The document is obtained by calling 354 * the servlet context's <code>getResource()</code> method and requesting 355 * <code>/WEB-INF/web.xml</code>. For non-servlet applications, this 356 * method calls this class' 357 * {@link ClassLoader#getResource(java.lang.String)} and requesting 358 * <code>WEB-INF/web.xml</code>. 359 * @return the descriptor 360 * @throws IOException if the deployment descriptor cannot be found or opened 361 * @throws JDOMException if the deployment descriptor cannot be parsed correctly 362 */ 363 protected Document getWebXml() throws JDOMException, IOException 364 { 365 URL url; 366 SAXBuilder builder = new SAXBuilder(); 367 builder.setXMLReaderFactory( XMLReaders.NONVALIDATING ); 368 builder.setEntityResolver( new LocalEntityResolver() ); 369 Document doc = null; 370 if ( m_engine.getServletContext() == null ) 371 { 372 ClassLoader cl = WebContainerAuthorizer.class.getClassLoader(); 373 url = cl.getResource( "WEB-INF/web.xml" ); 374 if( url != null ) 375 log.info( "Examining " + url.toExternalForm() ); 376 } 377 else 378 { 379 url = m_engine.getServletContext().getResource( "/WEB-INF/web.xml" ); 380 if( url != null ) 381 log.info( "Examining " + url.toExternalForm() ); 382 } 383 if( url == null ) 384 throw new IOException("Unable to find web.xml for processing."); 385 386 log.debug( "Processing web.xml at " + url.toExternalForm() ); 387 doc = builder.build( url ); 388 return doc; 389 } 390 391 /** 392 * <p>XML entity resolver that redirects resolution requests by JDOM, JAXP and 393 * other XML parsers to locally-cached copies of the resources. Local 394 * resources are stored in the <code>WEB-INF/dtd</code> directory.</p> 395 * <p>For example, Sun Microsystem's DTD for the webapp 2.3 specification is normally 396 * kept at <code>http://java.sun.com/dtd/web-app_2_3.dtd</code>. The 397 * local copy is stored at <code>WEB-INF/dtd/web-app_2_3.dtd</code>.</p> 398 */ 399 public class LocalEntityResolver implements EntityResolver 400 { 401 /** 402 * Returns an XML input source for a requested external resource by 403 * reading the resource instead from local storage. The local resource path 404 * is <code>WEB-INF/dtd</code>, plus the file name of the requested 405 * resource, minus the non-filename path information. 406 * @param publicId the public ID, such as 407 * <code>-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN</code> 408 * @param systemId the system ID, such as 409 * <code>http://java.sun.com/dtd/web-app_2_3.dtd</code> 410 * @return the InputSource containing the resolved resource 411 * @see org.xml.sax.EntityResolver#resolveEntity(java.lang.String, 412 * java.lang.String) 413 * @throws SAXException if the resource cannot be resolved locally 414 * @throws IOException if the resource cannot be opened 415 */ 416 @Override 417 public InputSource resolveEntity( String publicId, String systemId ) throws SAXException, IOException 418 { 419 String file = systemId.substring( systemId.lastIndexOf( '/' ) + 1 ); 420 URL url; 421 if ( m_engine.getServletContext() == null ) 422 { 423 ClassLoader cl = WebContainerAuthorizer.class.getClassLoader(); 424 url = cl.getResource( "WEB-INF/dtd/" + file ); 425 } 426 else 427 { 428 url = m_engine.getServletContext().getResource( "/WEB-INF/dtd/" + file ); 429 } 430 431 if( url != null ) 432 { 433 InputSource is = new InputSource( url.openStream() ); 434 log.debug( "Resolved systemID=" + systemId + " using local file " + url ); 435 return is; 436 } 437 438 // 439 // Let's fall back to default behaviour of the container, and let's 440 // also let the user know what is going on. This caught me by surprise 441 // while running JSPWiki on an unconnected laptop... 442 // 443 // The DTD needs to be resolved and read because it contains things like 444 // entity definitions... 445 // 446 log.info("Please note: There are no local DTD references in /WEB-INF/dtd/"+file+"; falling back to default behaviour."+ 447 " This may mean that the XML parser will attempt to connect to the internet to find the DTD."+ 448 " If you are running JSPWiki locally in an unconnected network, you might want to put the DTD files in place to avoid nasty UnknownHostExceptions."); 449 450 451 // Fall back to default behaviour 452 return null; 453 } 454 } 455 456}