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