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