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