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