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     */
019    package org.apache.wiki.auth.authorize;
020    
021    import java.io.IOException;
022    import java.net.URL;
023    import java.security.Principal;
024    import java.util.HashSet;
025    import java.util.Iterator;
026    import java.util.List;
027    import java.util.Properties;
028    import java.util.Set;
029    
030    import javax.servlet.http.HttpServletRequest;
031    
032    import org.apache.log4j.Logger;
033    import org.jdom2.Document;
034    import org.jdom2.Element;
035    import org.jdom2.Namespace;
036    import org.jdom2.JDOMException;
037    import org.jdom2.input.SAXBuilder;
038    import org.jdom2.xpath.XPath;
039    import org.xml.sax.EntityResolver;
040    import org.xml.sax.InputSource;
041    import org.xml.sax.SAXException;
042    
043    import org.apache.wiki.InternalWikiException;
044    import org.apache.wiki.WikiEngine;
045    import 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     */
055    public 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() );
125            }
126            catch ( JDOMException e )
127            {
128                log.error("Malformed XML in web.xml",e);
129                throw new InternalWikiException( e.getClass().getName()+": "+e.getMessage() );
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    }