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