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