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.apache.wiki.InternalWikiException;
034import org.apache.wiki.WikiEngine;
035import org.apache.wiki.WikiSession;
036import org.jdom2.Document;
037import org.jdom2.Element;
038import org.jdom2.JDOMException;
039import org.jdom2.Namespace;
040import org.jdom2.input.SAXBuilder;
041import org.jdom2.input.sax.XMLReaders;
042import org.jdom2.xpath.XPath;
043import org.xml.sax.EntityResolver;
044import org.xml.sax.InputSource;
045import org.xml.sax.SAXException;
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    @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     * @throws JDOMException if elements cannot be parsed correctly
236     */
237    public boolean isConstrained( String url, Role role ) throws JDOMException
238    {
239        Element root = m_webxml.getRootElement();
240        XPath xpath;
241        String selector;
242
243        // Get all constraints that have our URL pattern
244        // (Note the crazy j: prefix to denote the 2.4 j2ee schema)
245        selector = "//j:web-app/j:security-constraint[j:web-resource-collection/j:url-pattern=\"" + url + "\"]";
246        xpath = XPath.newInstance( selector );
247        xpath.addNamespace( "j", J2EE_SCHEMA_25_NAMESPACE );
248        List<?> constraints = xpath.selectNodes( root );
249
250        // Get all constraints that match our Role pattern
251        selector = "//j:web-app/j:security-constraint[j:auth-constraint/j:role-name=\"" + role.getName() + "\"]";
252        xpath = XPath.newInstance( selector );
253        xpath.addNamespace( "j", J2EE_SCHEMA_25_NAMESPACE );
254        List<?> roles = xpath.selectNodes( root );
255
256        // If we can't find either one, we must not be constrained
257        if ( constraints.size() == 0 )
258        {
259            return false;
260        }
261
262        // Shortcut: if the role is ALL, we are constrained
263        if ( role.equals( Role.ALL ) )
264        {
265            return true;
266        }
267
268        // If no roles, we must not be constrained
269        if ( roles.size() == 0 )
270        {
271            return false;
272        }
273
274        // If a constraint is contained in both lists, we must be constrained
275        for ( Iterator<?> c = constraints.iterator(); c.hasNext(); )
276        {
277            Element constraint = (Element)c.next();
278            for ( Iterator<?> r = roles.iterator(); r.hasNext(); )
279            {
280                Element roleConstraint = (Element)r.next();
281                if ( constraint.equals( roleConstraint ) )
282                {
283                    return true;
284                }
285            }
286        }
287        return false;
288    }
289
290    /**
291     * Returns <code>true</code> if the web container is configured to protect
292     * certain JSPWiki resources by requiring authentication. Specifically, this
293     * method parses JSPWiki's web application descriptor (<code>web.xml</code>)
294     * and identifies whether the string representation of
295     * {@link org.apache.wiki.auth.authorize.Role#AUTHENTICATED} is required
296     * to access <code>/Delete.jsp</code> and <code>LoginRedirect.jsp</code>.
297     * If the administrator has uncommented the large
298     * <code>&lt;security-constraint&gt;</code> section of <code>web.xml</code>,
299     * this will be true. This is admittedly an indirect way to go about it, but
300     * it should be an accurate test for default installations, and also in 99%
301     * of customized installs.
302     * @return <code>true</code> if the container protects resources,
303     *         <code>false</code> otherwise
304     */
305    public boolean isContainerAuthorized()
306    {
307        return m_containerAuthorized;
308    }
309
310    /**
311     * Returns an array of role Principals this Authorizer knows about.
312     * This method will return an array of Role objects corresponding to
313     * the logical roles enumerated in the <code>web.xml</code>.
314     * This method actually returns a defensive copy of an internally stored
315     * array.
316     * @return an array of Principals representing the roles
317     */
318    @Override
319    public Principal[] getRoles()
320    {
321        return m_containerRoles.clone();
322    }
323
324    /**
325     * Protected method that extracts the roles from JSPWiki's web application
326     * deployment descriptor. Each Role is constructed by using the String
327     * representation of the Role, for example
328     * <code>new Role("Administrator")</code>.
329     * @param webxml the web application deployment descriptor
330     * @return an array of Role objects
331     * @throws JDOMException if elements cannot be parsed correctly
332     */
333    protected Role[] getRoles( Document webxml ) throws JDOMException
334    {
335        Set<Role> roles = new HashSet<>();
336        Element root = webxml.getRootElement();
337
338        // Get roles referred to by constraints
339        String selector = "//j:web-app/j:security-constraint/j:auth-constraint/j:role-name";
340        XPath xpath = XPath.newInstance( selector );
341        xpath.addNamespace( "j", J2EE_SCHEMA_25_NAMESPACE );
342        List<?> nodes = xpath.selectNodes( root );
343        for( Iterator<?> it = nodes.iterator(); it.hasNext(); )
344        {
345            String role = ( (Element) it.next() ).getTextTrim();
346            roles.add( new Role( role ) );
347        }
348
349        // Get all defined roles
350        selector = "//j:web-app/j:security-role/j:role-name";
351        xpath = XPath.newInstance( selector );
352        xpath.addNamespace( "j", J2EE_SCHEMA_25_NAMESPACE );
353        nodes = xpath.selectNodes( root );
354        for( Iterator<?> it = nodes.iterator(); it.hasNext(); )
355        {
356            String role = ( (Element) it.next() ).getTextTrim();
357            roles.add( new Role( role ) );
358        }
359
360        return roles.toArray( new Role[roles.size()] );
361    }
362
363    /**
364     * Returns an {@link org.jdom2.Document} representing JSPWiki's web
365     * application deployment descriptor. The document is obtained by calling
366     * the servlet context's <code>getResource()</code> method and requesting
367     * <code>/WEB-INF/web.xml</code>. For non-servlet applications, this
368     * method calls this class'
369     * {@link ClassLoader#getResource(java.lang.String)} and requesting
370     * <code>WEB-INF/web.xml</code>.
371     * @return the descriptor
372     * @throws IOException if the deployment descriptor cannot be found or opened
373     * @throws JDOMException if the deployment descriptor cannot be parsed correctly
374     */
375    protected Document getWebXml() throws JDOMException, IOException
376    {
377        URL url;
378        SAXBuilder builder = new SAXBuilder();
379        builder.setXMLReaderFactory( XMLReaders.NONVALIDATING );
380        builder.setEntityResolver( new LocalEntityResolver() );
381        Document doc = null;
382        if ( m_engine.getServletContext() == null )
383        {
384            ClassLoader cl = WebContainerAuthorizer.class.getClassLoader();
385            url = cl.getResource( "WEB-INF/web.xml" );
386            if( url != null )
387                log.info( "Examining " + url.toExternalForm() );
388        }
389        else
390        {
391            url = m_engine.getServletContext().getResource( "/WEB-INF/web.xml" );
392            if( url != null )
393                log.info( "Examining " + url.toExternalForm() );
394        }
395        if( url == null )
396            throw new IOException("Unable to find web.xml for processing.");
397
398        log.debug( "Processing web.xml at " + url.toExternalForm() );
399        doc = builder.build( url );
400        return doc;
401    }
402
403    /**
404     * <p>XML entity resolver that redirects resolution requests by JDOM, JAXP and
405     * other XML parsers to locally-cached copies of the resources. Local
406     * resources are stored in the <code>WEB-INF/dtd</code> directory.</p>
407     * <p>For example, Sun Microsystem's DTD for the webapp 2.3 specification is normally
408     * kept at <code>http://java.sun.com/dtd/web-app_2_3.dtd</code>. The
409     * local copy is stored at <code>WEB-INF/dtd/web-app_2_3.dtd</code>.</p>
410     */
411    public class LocalEntityResolver implements EntityResolver
412    {
413        /**
414         * Returns an XML input source for a requested external resource by
415         * reading the resource instead from local storage. The local resource path
416         * is <code>WEB-INF/dtd</code>, plus the file name of the requested
417         * resource, minus the non-filename path information.
418         * @param publicId the public ID, such as
419         *            <code>-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN</code>
420         * @param systemId the system ID, such as
421         *            <code>http://java.sun.com/dtd/web-app_2_3.dtd</code>
422         * @return the InputSource containing the resolved resource
423         * @see org.xml.sax.EntityResolver#resolveEntity(java.lang.String,
424         *      java.lang.String)
425         * @throws SAXException if the resource cannot be resolved locally
426         * @throws IOException if the resource cannot be opened
427         */
428        @Override
429        public InputSource resolveEntity( String publicId, String systemId ) throws SAXException, IOException
430        {
431            String file = systemId.substring( systemId.lastIndexOf( '/' ) + 1 );
432            URL url;
433            if ( m_engine.getServletContext() == null )
434            {
435                ClassLoader cl = WebContainerAuthorizer.class.getClassLoader();
436                url = cl.getResource( "WEB-INF/dtd/" + file );
437            }
438            else
439            {
440                url = m_engine.getServletContext().getResource( "/WEB-INF/dtd/" + file );
441            }
442
443            if( url != null )
444            {
445                InputSource is = new InputSource( url.openStream() );
446                log.debug( "Resolved systemID=" + systemId + " using local file " + url );
447                return is;
448            }
449
450            //
451            //  Let's fall back to default behaviour of the container, and let's
452            //  also let the user know what is going on.  This caught me by surprise
453            //  while running JSPWiki on an unconnected laptop...
454            //
455            //  The DTD needs to be resolved and read because it contains things like
456            //  entity definitions...
457            //
458            log.info("Please note: There are no local DTD references in /WEB-INF/dtd/"+file+"; falling back to default behaviour."+
459                     " This may mean that the XML parser will attempt to connect to the internet to find the DTD."+
460                     " If you are running JSPWiki locally in an unconnected network, you might want to put the DTD files in place to avoid nasty UnknownHostExceptions.");
461
462
463            // Fall back to default behaviour
464            return null;
465        }
466    }
467
468}