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