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