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>&lt;security-constraint&gt;</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}