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