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