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