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;
020    
021    import java.io.File;
022    import java.io.IOException;
023    import java.net.MalformedURLException;
024    import java.net.URL;
025    import java.security.*;
026    import java.util.LinkedHashSet;
027    import java.util.List;
028    import java.util.Set;
029    
030    import javax.security.auth.Subject;
031    import javax.security.auth.spi.LoginModule;
032    
033    import org.apache.commons.lang.ArrayUtils;
034    import org.apache.log4j.Logger;
035    import org.apache.wiki.InternalWikiException;
036    import org.apache.wiki.WikiEngine;
037    import org.apache.wiki.WikiSession;
038    import org.apache.wiki.api.exceptions.WikiException;
039    import org.apache.wiki.auth.authorize.*;
040    import org.apache.wiki.auth.permissions.AllPermission;
041    import org.apache.wiki.auth.permissions.GroupPermission;
042    import org.apache.wiki.auth.permissions.PermissionFactory;
043    import org.apache.wiki.auth.permissions.WikiPermission;
044    import org.apache.wiki.auth.user.UserDatabase;
045    import org.apache.wiki.auth.user.UserProfile;
046    import org.freshcookies.security.policy.PolicyReader;
047    import org.jdom2.JDOMException;
048    
049    /**
050     * Helper class for verifying JSPWiki's security configuration. Invoked by
051     * <code>admin/SecurityConfig.jsp</code>.
052     * @since 2.4
053     */
054    public final class SecurityVerifier
055    {
056        private static final long     serialVersionUID             = -3859563355089169941L;
057    
058        private WikiEngine            m_engine;
059    
060        private boolean               m_isSecurityPolicyConfigured = false;
061    
062        private Principal[]           m_policyPrincipals           = new Principal[0];
063    
064        private WikiSession           m_session;
065    
066        /** Message prefix for errors. */
067        public static final String    ERROR                        = "Error.";
068    
069        /** Message prefix for warnings. */
070        public static final String    WARNING                      = "Warning.";
071    
072        /** Message prefix for information messages. */
073        public static final String    INFO                         = "Info.";
074    
075        /** Message topic for policy errors. */
076        public static final String    ERROR_POLICY                 = "Error.Policy";
077    
078        /** Message topic for policy warnings. */
079        public static final String    WARNING_POLICY               = "Warning.Policy";
080    
081        /** Message topic for policy information messages. */
082        public static final String    INFO_POLICY                  = "Info.Policy";
083    
084        /** Message topic for JAAS errors. */
085        public static final String    ERROR_JAAS                   = "Error.Jaas";
086    
087        /** Message topic for JAAS warnings. */
088        public static final String    WARNING_JAAS                 = "Warning.Jaas";
089    
090        /** Message topic for role-checking errors. */
091        public static final String    ERROR_ROLES                  = "Error.Roles";
092    
093        /** Message topic for role-checking information messages. */
094        public static final String    INFO_ROLES                   = "Info.Roles";
095    
096        /** Message topic for user database errors. */
097        public static final String    ERROR_DB                     = "Error.UserDatabase";
098    
099        /** Message topic for user database warnings. */
100        public static final String    WARNING_DB                   = "Warning.UserDatabase";
101    
102        /** Message topic for user database information messages. */
103        public static final String    INFO_DB                      = "Info.UserDatabase";
104    
105        /** Message topic for group database errors. */
106        public static final String    ERROR_GROUPS                 = "Error.GroupDatabase";
107    
108        /** Message topic for group database warnings. */
109        public static final String    WARNING_GROUPS               = "Warning.GroupDatabase";
110    
111        /** Message topic for group database information messages. */
112        public static final String    INFO_GROUPS                  = "Info.GroupDatabase";
113    
114        /** Message topic for JAAS information messages. */
115        public static final String    INFO_JAAS                    = "Info.Jaas";
116    
117        private static final String[] CONTAINER_ACTIONS            = new String[]
118                                                                   { "View pages", "Comment on existing pages",
119                "Edit pages", "Upload attachments", "Create a new group", "Rename an existing page", "Delete pages" };
120    
121        private static final String[] CONTAINER_JSPS               = new String[]
122                                                                   { "/Wiki.jsp", "/Comment.jsp", "/Edit.jsp",
123                "/Upload.jsp", "/NewGroup.jsp", "/Rename.jsp", "/Delete.jsp" };
124    
125        private static final String   BG_GREEN                     = "bgcolor=\"#c0ffc0\"";
126    
127        private static final String   BG_RED                       = "bgcolor=\"#ffc0c0\"";
128    
129        private static final Logger LOG                          = Logger.getLogger( SecurityVerifier.class.getName() );
130    
131        /**
132         * Constructs a new SecurityVerifier for a supplied WikiEngine and WikiSession.
133         * @param engine the wiki engine
134         * @param session the wiki session (typically, that of an administrator)
135         */
136        public SecurityVerifier( WikiEngine engine, WikiSession session )
137        {
138            super();
139            m_engine = engine;
140            m_session = session;
141            m_session.clearMessages();
142            verifyJaas();
143            verifyPolicy();
144            try
145            {
146                verifyPolicyAndContainerRoles();
147            }
148            catch ( WikiException e )
149            {
150                m_session.addMessage( ERROR_ROLES, e.getMessage() );
151            }
152            verifyGroupDatabase();
153            verifyUserDatabase();
154        }
155    
156        /**
157         * Returns an array of unique Principals from the JSPWIki security policy
158         * file. This array will be zero-length if the policy file was not
159         * successfully located, or if the file did not specify any Principals in
160         * the policy.
161         * @return the array of principals
162         */
163        public Principal[] policyPrincipals()
164        {
165            return m_policyPrincipals;
166        }
167    
168        /**
169         * Formats and returns an HTML table containing sample permissions and what
170         * roles are allowed to have them. This method will throw an
171         * {@link IllegalStateException} if the authorizer is not of type
172         * {@link org.apache.wiki.auth.authorize.WebContainerAuthorizer}
173         * @return the formatted HTML table containing the result of the tests
174         */
175        public String policyRoleTable()
176        {
177            Principal[] roles = m_policyPrincipals;
178            String wiki = m_engine.getApplicationName();
179    
180            String[] pages = new String[]
181            { "Main", "Index", "GroupTest", "GroupAdmin" };
182            String[] pageActions = new String[]
183            { "view", "edit", "modify", "rename", "delete" };
184    
185            String[] groups = new String[]
186            { "Admin", "TestGroup", "Foo" };
187            String[] groupActions = new String[]
188            { "view", "edit", null, null, "delete" };
189    
190            // Calculate column widths
191            String colWidth;
192            if ( pageActions.length > 0 && roles.length > 0 )
193            {
194                colWidth =  (67f / ( pageActions.length * roles.length )) + "%";
195            }
196            else
197            {
198                colWidth = "67%";
199            }
200    
201            StringBuffer s = new StringBuffer();
202    
203            // Write the table header
204            s.append( "<table class=\"wikitable\" border=\"1\">\n" );
205            s.append( "  <colgroup span=\"1\" width=\"33%\"/>\n" );
206            s.append( "  <colgroup span=\"" + pageActions.length * roles.length + "\" width=\"" + colWidth
207                    + "\" align=\"center\"/>\n" );
208            s.append( "  <tr>\n" );
209            s.append( "    <th rowspan=\"2\" valign=\"bottom\">Permission</th>\n" );
210            for( int i = 0; i < roles.length; i++ )
211            {
212                s.append( "    <th colspan=\"" + pageActions.length + "\" title=\"" + roles[i].getClass().getName() + "\">"
213                        + roles[i].getName() + "</th>\n" );
214            }
215            s.append( "  </tr>\n" );
216    
217            // Print a column for each role
218            s.append( "  <tr>\n" );
219            for( int i = 0; i < roles.length; i++ )
220            {
221                for( String pageAction : pageActions )
222                {
223                    String action = pageAction.substring( 0, 1 );
224                    s.append( "    <th title=\"" + pageAction + "\">" + action + "</th>\n" );
225                }
226            }
227            s.append( "  </tr>\n" );
228    
229            // Write page permission tests first
230            for( String page : pages )
231            {
232                s.append( "  <tr>\n" );
233                s.append( "    <td>PagePermission \"" + wiki + ":" + page + "\"</td>\n" );
234                for( Principal role : roles )
235                {
236                    for( String pageAction : pageActions )
237                    {
238                        Permission permission = PermissionFactory.getPagePermission( wiki + ":" + page, pageAction );
239                        s.append( printPermissionTest( permission, role, 1 ) );
240                    }
241                }
242                s.append( "  </tr>\n" );
243            }
244    
245            // Now do the group tests
246            for( String group : groups )
247            {
248                s.append( "  <tr>\n" );
249                s.append( "    <td>GroupPermission \"" + wiki + ":" + group + "\"</td>\n" );
250                for( Principal role : roles )
251                {
252                    for( String groupAction : groupActions )
253                    {
254                        Permission permission = null;
255                        if ( groupAction != null)
256                        {
257                            permission = new GroupPermission( wiki + ":" + group, groupAction );
258                        }
259                        s.append( printPermissionTest( permission, role, 1 ) );
260                    }
261                }
262                s.append( "  </tr>\n" );
263            }
264    
265    
266            // Now check the wiki-wide permissions
267            String[] wikiPerms = new String[]
268            { "createGroups", "createPages", "login", "editPreferences", "editProfile" };
269            for( String wikiPerm : wikiPerms )
270            {
271                s.append( "  <tr>\n" );
272                s.append( "    <td>WikiPermission \"" + wiki + "\",\"" + wikiPerm + "\"</td>\n" );
273                for( Principal role : roles )
274                {
275                    Permission permission = new WikiPermission( wiki, wikiPerm );
276                    s.append( printPermissionTest( permission, role, pageActions.length ) );
277                }
278                s.append( "  </tr>\n" );
279            }
280    
281            // Lastly, check for AllPermission
282            s.append( "  <tr>\n" );
283            s.append( "    <td>AllPermission \"" + wiki + "\"</td>\n" );
284            for( Principal role : roles )
285            {
286                Permission permission = new AllPermission( wiki );
287                s.append( printPermissionTest( permission, role, pageActions.length ) );
288            }
289            s.append( "  </tr>\n" );
290    
291            // We're done!
292            s.append( "</table>" );
293            return s.toString();
294        }
295    
296        /**
297         * Prints a &lt;td&gt; HTML element with the results of a permission test.
298         * @param permission the permission to format
299         * @param principal
300         * @param cols
301         */
302        private String printPermissionTest( Permission permission, Principal principal, int cols )
303        {
304            StringBuffer s = new StringBuffer();
305            if ( permission == null )
306            {
307                s.append( "    <td colspan=\"" + cols + "\" align=\"center\" title=\"N/A\">" );
308                s.append( "&nbsp;</td>\n" );
309            }
310            else
311            {
312                boolean allowed = verifyStaticPermission( principal, permission );
313                s.append( "    <td colspan=\"" + cols + "\" align=\"center\" title=\"" );
314                s.append( allowed ? "ALLOW: " : "DENY: " );
315                s.append( permission.getClass().getName() );
316                s.append( " &quot;" );
317                s.append( permission.getName() );
318                s.append( "&quot;" );
319                if ( permission.getName() != null )
320                {
321                    s.append( ",&quot;" );
322                    s.append( permission.getActions() );
323                    s.append( "&quot;" );
324                }
325                s.append( " " );
326                s.append( principal.getClass().getName() );
327                s.append( " &quot;" );
328                s.append( principal.getName() );
329                s.append( "&quot;" );
330                s.append( "\"" );
331                s.append( allowed ? BG_GREEN + ">" : BG_RED + ">" );
332                s.append( "&nbsp;</td>\n" );
333            }
334            return s.toString();
335        }
336    
337        /**
338         * Formats and returns an HTML table containing the roles the web container
339         * is aware of, and whether each role maps to particular JSPs. This method
340         * throws an {@link IllegalStateException} if the authorizer is not of type
341         * {@link org.apache.wiki.auth.authorize.WebContainerAuthorizer}
342         * @return the formatted HTML table containing the result of the tests
343         * @throws WikiException if tests fail for unexpected reasons
344         */
345        public String containerRoleTable() throws WikiException
346        {
347    
348            AuthorizationManager authorizationManager = m_engine.getAuthorizationManager();
349            Authorizer authorizer = authorizationManager.getAuthorizer();
350    
351            // If authorizer not WebContainerAuthorizer, print error message
352            if ( !( authorizer instanceof WebContainerAuthorizer ) )
353            {
354                throw new IllegalStateException( "Authorizer should be WebContainerAuthorizer" );
355            }
356    
357            // Now, print a table with JSP pages listed on the left, and
358            // an evaluation of each pages' constraints for each role
359            // we discovered
360            StringBuffer s = new StringBuffer();
361            Principal[] roles = authorizer.getRoles();
362            s.append( "<table class=\"wikitable\" border=\"1\">\n" );
363            s.append( "<thead>\n" );
364            s.append( "  <tr>\n" );
365            s.append( "    <th rowspan=\"2\">Action</th>\n" );
366            s.append( "    <th rowspan=\"2\">Page</th>\n" );
367            s.append( "    <th colspan=\"" + roles.length + 1 + "\">Roles</th>\n" );
368            s.append( "  </tr>\n" );
369            s.append( "  <tr>\n" );
370            s.append( "    <th>Anonymous</th>\n" );
371            for( Principal role : roles )
372            {
373                s.append( "    <th>" + role.getName() + "</th>\n" );
374            }
375            s.append( "</tr>\n" );
376            s.append( "</thead>\n" );
377            s.append( "<tbody>\n" );
378    
379            try
380            {
381                WebContainerAuthorizer wca = (WebContainerAuthorizer) authorizer;
382                for( int i = 0; i < CONTAINER_ACTIONS.length; i++ )
383                {
384                    String action = CONTAINER_ACTIONS[i];
385                    String jsp = CONTAINER_JSPS[i];
386    
387                    // Print whether the page is constrained for each role
388                    boolean allowsAnonymous = !wca.isConstrained( jsp, Role.ALL );
389                    s.append( "  <tr>\n" );
390                    s.append( "    <td>" + action + "</td>\n" );
391                    s.append( "    <td>" + jsp + "</td>\n" );
392                    s.append( "    <td title=\"" );
393                    s.append( allowsAnonymous ? "ALLOW: " : "DENY: " );
394                    s.append( jsp );
395                    s.append( " Anonymous" );
396                    s.append( "\"" );
397                    s.append( allowsAnonymous ? BG_GREEN + ">" : BG_RED + ">" );
398                    s.append( "&nbsp;</td>\n" );
399                    for( Principal role : roles )
400                    {
401                        boolean allowed = allowsAnonymous || wca.isConstrained( jsp, (Role)role );
402                        s.append( "    <td title=\"" );
403                        s.append( allowed ? "ALLOW: " : "DENY: " );
404                        s.append( jsp );
405                        s.append( " " );
406                        s.append( role.getClass().getName() );
407                        s.append( " &quot;" );
408                        s.append( role.getName() );
409                        s.append( "&quot;" );
410                        s.append( "\"" );
411                        s.append( allowed ? BG_GREEN + ">" : BG_RED + ">" );
412                        s.append( "&nbsp;</td>\n" );
413                    }
414                    s.append( "  </tr>\n" );
415                }
416            }
417            catch( JDOMException e )
418            {
419                // If we couldn't evaluate constraints it means
420                // there's some sort of IO mess or parsing issue
421                LOG.error( "Malformed XML in web.xml", e );
422                throw new InternalWikiException( e.getClass().getName() + ": " + e.getMessage() );
423            }
424    
425            s.append( "</tbody>\n" );
426            s.append( "</table>\n" );
427            return s.toString();
428        }
429    
430        /**
431         * Returns <code>true</code> if the Java security policy is configured
432         * correctly, and it verifies as valid.
433         * @return the result of the configuration check
434         */
435        public boolean isSecurityPolicyConfigured()
436        {
437            return m_isSecurityPolicyConfigured;
438        }
439    
440        /**
441         * If the active Authorizer is the WebContainerAuthorizer, returns the roles
442         * it knows about; otherwise, a zero-length array.
443         * @return the roles parsed from <code>web.xml</code>, or a zero-length array
444         * @throws WikiException if the web authorizer cannot obtain the list of roles
445         */
446        public Principal[] webContainerRoles() throws WikiException
447        {
448            Authorizer authorizer = m_engine.getAuthorizationManager().getAuthorizer();
449            if ( authorizer instanceof WebContainerAuthorizer )
450            {
451                return ( (WebContainerAuthorizer) authorizer ).getRoles();
452            }
453            return new Principal[0];
454        }
455    
456        /**
457         * Verifies that the roles given in the security policy are reflected by the
458         * container <code>web.xml</code> file.
459         * @throws WikiException if the web authorizer cannot verify the roles
460         */
461        protected void verifyPolicyAndContainerRoles() throws WikiException
462        {
463            Authorizer authorizer = m_engine.getAuthorizationManager().getAuthorizer();
464            Principal[] containerRoles = authorizer.getRoles();
465            boolean missing = false;
466            for( Principal principal : m_policyPrincipals )
467            {
468                if ( principal instanceof Role )
469                {
470                    Role role = (Role) principal;
471                    boolean isContainerRole = ArrayUtils.contains( containerRoles, role );
472                    if ( !Role.isBuiltInRole( role ) && !isContainerRole )
473                    {
474                        m_session.addMessage( ERROR_ROLES, "Role '" + role.getName() + "' is defined in security policy but not in web.xml." );
475                        missing = true;
476                    }
477                }
478            }
479            if ( !missing )
480            {
481                m_session.addMessage( INFO_ROLES, "Every non-standard role defined in the security policy was also found in web.xml." );
482            }
483        }
484    
485        /**
486         * Verifies that the group datbase was initialized properly, and that
487         * user add and delete operations work as they should.
488         */
489        protected void verifyGroupDatabase()
490        {
491            GroupManager mgr = m_engine.getGroupManager();
492            GroupDatabase db = null;
493            try
494            {
495                db = m_engine.getGroupManager().getGroupDatabase();
496            }
497            catch ( WikiSecurityException e )
498            {
499                m_session.addMessage( ERROR_GROUPS, "Could not retrieve GroupManager: " + e.getMessage() );
500            }
501    
502            // Check for obvious error conditions
503            if ( mgr == null || db == null )
504            {
505                if ( mgr == null )
506                {
507                    m_session.addMessage( ERROR_GROUPS, "GroupManager is null; JSPWiki could not " +
508                            "initialize it. Check the error logs." );
509                }
510                if ( db == null )
511                {
512                    m_session.addMessage( ERROR_GROUPS, "GroupDatabase is null; JSPWiki could not " +
513                            "initialize it. Check the error logs." );
514                }
515                return;
516            }
517    
518            // Everything initialized OK...
519    
520            // Tell user what class of database this is.
521            m_session.addMessage( INFO_GROUPS, "GroupDatabase is of type '" + db.getClass().getName() +
522                    "'. It appears to be initialized properly." );
523    
524            // Now, see how many groups we have.
525            int oldGroupCount = 0;
526            try
527            {
528                Group[] groups = db.groups();
529                oldGroupCount = groups.length;
530                m_session.addMessage( INFO_GROUPS, "The group database contains " + oldGroupCount + " groups." );
531            }
532            catch ( WikiSecurityException e )
533            {
534                m_session.addMessage( ERROR_GROUPS, "Could not obtain a list of current groups: " + e.getMessage() );
535                return;
536            }
537    
538            // Try adding a bogus group with random name
539            String name = "TestGroup" + System.currentTimeMillis();
540            Group group = null;
541            try
542            {
543                // Create dummy test group
544                group = mgr.parseGroup( name, "", true );
545                Principal user = new WikiPrincipal( "TestUser" );
546                group.add( user );
547                db.save( group, new WikiPrincipal("SecurityVerifier") );
548    
549                // Make sure the group saved successfully
550                if ( db.groups().length == oldGroupCount )
551                {
552                    m_session.addMessage( ERROR_GROUPS, "Could not add a test group to the database." );
553                    return;
554                }
555                m_session.addMessage( INFO_GROUPS, "The group database allows new groups to be created, as it should." );
556            }
557            catch ( WikiSecurityException e )
558            {
559                m_session.addMessage( ERROR_GROUPS, "Could not add a group to the database: " + e.getMessage() );
560                return;
561            }
562    
563            // Now delete the group; should be back to old count
564            try
565            {
566                db.delete( group );
567                if ( db.groups().length != oldGroupCount )
568                {
569                    m_session.addMessage( ERROR_GROUPS, "Could not delete a test group from the database." );
570                    return;
571                }
572                m_session.addMessage( INFO_GROUPS, "The group database allows groups to be deleted, as it should." );
573            }
574            catch ( WikiSecurityException e )
575            {
576                m_session.addMessage( ERROR_GROUPS, "Could not delete a test group from the database: " + e.getMessage() );
577                return;
578            }
579    
580            m_session.addMessage( INFO_GROUPS, "The group database configuration looks fine." );
581        }
582    
583        /**
584         * Verfies the JAAS configuration. The configuration is valid if value of the
585         * <code>jspwiki.properties<code> property
586         * {@value org.apache.wiki.auth.AuthenticationManager#PROP_LOGIN_MODULE}
587         * resolves to a valid class on the classpath.
588         */
589        protected void verifyJaas()
590        {
591            // See if JAAS is on
592            AuthorizationManager authMgr = m_engine.getAuthorizationManager();
593            if ( !authMgr.isJAASAuthorized() )
594            {
595                m_session.addMessage( ERROR_JAAS, "JSPWiki's JAAS-based authentication " +
596                        "and authorization system is turned off (your jspwiki.properties file " +
597                        "contains the setting 'jspwiki.security = container'. This " +
598                        "setting disables authorization checks and is meant for testing " +
599                        "and troubleshooting only. The test results on this page will not " +
600                        "be reliable as a result. You should set this to 'jaas' " +
601                        "so that security works properly." );
602            }
603            
604            // Verify that the specified JAAS moduie corresponds to a class we can load successfully.
605            String jaasClass = m_engine.getWikiProperties().getProperty( AuthenticationManager.PROP_LOGIN_MODULE );
606            if ( jaasClass == null || jaasClass.length() == 0 )
607            {
608                m_session.addMessage( ERROR_JAAS, "The value of the '" + AuthenticationManager.PROP_LOGIN_MODULE +
609                        "' property was null or blank. This is a fatal error. This value should be set to a valid LoginModule implementation " +
610                        "on the classpath." );
611                return;
612            }
613            
614            // See if we can find the LoginModule on the classpath
615            Class c = null;
616            try
617            {
618                m_session.addMessage( INFO_JAAS, "The property '" + AuthenticationManager.PROP_LOGIN_MODULE +
619                                      "' specified the class '" + jaasClass + ".'" );
620                c = Class.forName( jaasClass );
621            }
622            catch( ClassNotFoundException e )
623            {
624                m_session.addMessage( ERROR_JAAS, "We could not find the the class '" + jaasClass + "' on the " +
625                "classpath. This is fatal error." );
626            }
627            
628            // Is the specified class actually a LoginModule?
629            if ( LoginModule.class.isAssignableFrom( c ) )
630            {
631                m_session.addMessage( INFO_JAAS, "We found the the class '" + jaasClass + "' on the " +
632                        "classpath, and it is a LoginModule implementation. Good!" );
633            }
634            else
635            {
636                m_session.addMessage( ERROR_JAAS, "We found the the class '" + jaasClass + "' on the " +
637                "classpath, but it does not seem to be LoginModule implementation! This is fatal error." );
638            }
639        }
640    
641        /**
642         * Looks up a file name based on a JRE system property and returns the associated
643         * File object if it exists. This method adds messages with the topic prefix 
644         * {@link #ERROR} and {@link #INFO} as appropriate, with the suffix matching the 
645         * supplied property.
646         * @param property the system property to look up
647         * @return the file object, or <code>null</code> if not found
648         */
649        protected File getFileFromProperty( String property )
650        {
651            String propertyValue = null;
652            try
653            {
654                propertyValue = System.getProperty( property );
655                if ( propertyValue == null )
656                {
657                    m_session.addMessage( "Error." + property, "The system property '" + property + "' is null." );
658                    return null;
659                }
660    
661                //
662                //  It's also possible to use "==" to mark a property.  We remove that
663                //  here so that we can actually find the property file, then.
664                //
665                if( propertyValue.startsWith("=") )
666                {
667                    propertyValue = propertyValue.substring(1);
668                }
669    
670                try
671                {
672                    m_session.addMessage( "Info." + property, "The system property '" + property + "' is set to: "
673                            + propertyValue + "." );
674    
675                    // Prepend a file: prefix if not there already
676                    if ( !propertyValue.startsWith( "file:" ) )
677                    {
678                      propertyValue = "file:" + propertyValue;
679                    }
680                    URL url = new URL( propertyValue );
681                    File file = new File( url.getPath() );
682                    if ( file.exists() )
683                    {
684                        m_session.addMessage( "Info." + property, "File '" + propertyValue + "' exists in the filesystem." );
685                        return file;
686                    }
687                }
688                catch( MalformedURLException e )
689                {
690                    // Swallow exception because we can't find it anyway
691                }
692                m_session.addMessage( "Error." + property, "File '" + propertyValue
693                        + "' doesn't seem to exist. This might be a problem." );
694                return null;
695            }
696            catch( SecurityException e )
697            {
698                m_session.addMessage( "Error." + property, "We could not read system property '" + property
699                        + "'. This is probably because you are running with a security manager." );
700                return null;
701            }
702        }
703    
704        /**
705         * Verfies the Java security policy configuration. The configuration is
706         * valid if value of the local policy (at <code>WEB-INF/jspwiki.policy</code>
707         * resolves to an existing file, and the policy file contained therein
708         * represents a valid policy.
709         */
710        @SuppressWarnings("unchecked")
711        protected void verifyPolicy()
712        {
713            // Look up the policy file and set the status text.
714            URL policyURL = AuthenticationManager.findConfigFile( m_engine, AuthorizationManager.DEFAULT_POLICY );
715            String path = policyURL.getPath();
716            if ( path.startsWith("file:") )
717            {
718                path = path.substring( 5 );
719            }
720            File policyFile = new File( path );
721    
722            // Next, verify the policy
723            try
724            {
725                // Get the file
726                PolicyReader policy = new PolicyReader( policyFile );
727                m_session.addMessage( INFO_POLICY, "The security policy '" + policy.getFile() + "' exists." );
728    
729                // See if there is a keystore that's valid
730                KeyStore ks = policy.getKeyStore();
731                if ( ks == null )
732                {
733                    m_session.addMessage( WARNING_POLICY,
734                        "Policy file does not have a keystore... at least not one that we can locate. If your policy file " +
735                        "does not contain any 'signedBy' blocks, this is probably ok." );
736                }
737                else
738                {
739                    m_session.addMessage( INFO_POLICY,
740                        "The security policy specifies a keystore, and we were able to locate it in the filesystem." );
741                }
742    
743                // Verify the file
744                policy.read();
745                List<Exception> errors = policy.getMessages();
746                if ( errors.size() > 0 )
747                {
748                    for( Exception e : errors )
749                    {
750                        m_session.addMessage( ERROR_POLICY, e.getMessage() );
751                    }
752                }
753                else
754                {
755                    m_session.addMessage( INFO_POLICY, "The security policy looks fine." );
756                    m_isSecurityPolicyConfigured = true;
757                }
758    
759                // Stash the unique principals mentioned in the file,
760                // plus our standard roles.
761                Set<Principal> principals = new LinkedHashSet<Principal>();
762                principals.add( Role.ALL );
763                principals.add( Role.ANONYMOUS );
764                principals.add( Role.ASSERTED );
765                principals.add( Role.AUTHENTICATED );
766                ProtectionDomain[] domains = policy.getProtectionDomains();
767                for ( ProtectionDomain domain : domains )
768                {
769                    for( Principal principal : domain.getPrincipals() )
770                    {
771                        principals.add( principal );
772                    }
773                }
774                m_policyPrincipals = principals.toArray( new Principal[principals.size()] );
775            }
776            catch( IOException e )
777            {
778                m_session.addMessage( ERROR_POLICY, e.getMessage() );
779            }
780        }
781    
782        /**
783         * Verifies that a particular Principal possesses a Permission, as defined
784         * in the security policy file.
785         * @param principal the principal
786         * @param permission the permission
787         * @return the result, based on consultation with the active Java security
788         *         policy
789         */
790        protected boolean verifyStaticPermission( Principal principal, final Permission permission )
791        {
792            Subject subject = new Subject();
793            subject.getPrincipals().add( principal );
794            boolean allowedByGlobalPolicy = ((Boolean)
795                Subject.doAsPrivileged( subject, new PrivilegedAction<Object>()
796                {
797                    public Object run()
798                    {
799                        try
800                        {
801                            AccessController.checkPermission( permission );
802                            return Boolean.TRUE;
803                        }
804                        catch ( AccessControlException e )
805                        {
806                            return Boolean.FALSE;
807                        }
808                    }
809                }, null )).booleanValue();
810    
811            if ( allowedByGlobalPolicy )
812            {
813                return true;
814            }
815    
816            // Check local policy
817            Principal[] principals = new Principal[]{ principal };
818            return m_engine.getAuthorizationManager().allowedByLocalPolicy( principals, permission );
819        }
820    
821        /**
822         * Verifies that the user datbase was initialized properly, and that
823         * user add and delete operations work as they should.
824         */
825        protected void verifyUserDatabase()
826        {
827            UserDatabase db = m_engine.getUserManager().getUserDatabase();
828    
829            // Check for obvious error conditions
830            if ( db == null )
831            {
832                m_session.addMessage( ERROR_DB, "UserDatabase is null; JSPWiki could not " +
833                        "initialize it. Check the error logs." );
834                return;
835            }
836    
837            if ( db instanceof UserManager.DummyUserDatabase )
838            {
839                m_session.addMessage( ERROR_DB, "UserDatabase is DummyUserDatabase; JSPWiki " +
840                        "may not have been able to initialize the database you supplied in " +
841                        "jspwiki.properties, or you left the 'jspwiki.userdatabase' property " +
842                        "blank. Check the error logs." );
843            }
844    
845            // Tell user what class of database this is.
846            m_session.addMessage( INFO_DB, "UserDatabase is of type '" + db.getClass().getName() +
847                    "'. It appears to be initialized properly." );
848    
849            // Now, see how many users we have.
850            int oldUserCount = 0;
851            try
852            {
853                Principal[] users = db.getWikiNames();
854                oldUserCount = users.length;
855                m_session.addMessage( INFO_DB, "The user database contains " + oldUserCount + " users." );
856            }
857            catch ( WikiSecurityException e )
858            {
859                m_session.addMessage( ERROR_DB, "Could not obtain a list of current users: " + e.getMessage() );
860                return;
861            }
862    
863            // Try adding a bogus user with random name
864            String loginName = "TestUser" + System.currentTimeMillis();
865            try
866            {
867                UserProfile profile = db.newProfile();
868                profile.setEmail( "jspwiki.tests@mailinator.com" );
869                profile.setLoginName( loginName );
870                profile.setFullname( "FullName"+loginName );
871                profile.setPassword( "password" );
872                db.save(profile);
873    
874                // Make sure the profile saved successfully
875                if ( db.getWikiNames().length == oldUserCount )
876                {
877                    m_session.addMessage( ERROR_DB, "Could not add a test user to the database." );
878                    return;
879                }
880                m_session.addMessage( INFO_DB, "The user database allows new users to be created, as it should." );
881            }
882            catch ( WikiSecurityException e )
883            {
884                m_session.addMessage( ERROR_DB, "Could not add a test user to the database: " + e.getMessage() );
885                return;
886            }
887    
888            // Now delete the profile; should be back to old count
889            try
890            {
891                db.deleteByLoginName( loginName );
892                if ( db.getWikiNames().length != oldUserCount )
893                {
894                    m_session.addMessage( ERROR_DB, "Could not delete a test user from the database." );
895                    return;
896                }
897                m_session.addMessage( INFO_DB, "The user database allows users to be deleted, as it should." );
898            }
899            catch ( WikiSecurityException e )
900            {
901                m_session.addMessage( ERROR_DB, "Could not delete a test user to the database: " + e.getMessage() );
902                return;
903            }
904    
905            m_session.addMessage( INFO_DB, "The user database configuration looks fine." );
906        }
907    }