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