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( 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 final WebContainerAuthorizer wca ) ) {
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        for( int i = 0; i < CONTAINER_ACTIONS.length; i++ ) {
373            final String action = CONTAINER_ACTIONS[i];
374            final String jsp = CONTAINER_JSPS[i];
375
376            // Print whether the page is constrained for each role
377            final boolean allowsAnonymous = !wca.isConstrained( jsp, Role.ALL );
378            s.append( "  <tr>\n" );
379            s.append( "    <td>" ).append( action ).append( "</td>\n" );
380            s.append( "    <td>" ).append( jsp ).append( "</td>\n" );
381            s.append( "    <td title=\"" );
382            s.append( allowsAnonymous ? "ALLOW: " : "DENY: " );
383            s.append( jsp );
384            s.append( " Anonymous" );
385            s.append( "\"" );
386            s.append( allowsAnonymous ? BG_GREEN + ">" : BG_RED + ">" );
387            s.append( "&nbsp;</td>\n" );
388            for( final Principal role : roles )
389            {
390                final boolean allowed = allowsAnonymous || wca.isConstrained( jsp, (Role)role );
391                s.append( "    <td title=\"" );
392                s.append( allowed ? "ALLOW: " : "DENY: " );
393                s.append( jsp );
394                s.append( " " );
395                s.append( role.getClass().getName() );
396                s.append( " &quot;" );
397                s.append( role.getName() );
398                s.append( "&quot;" );
399                s.append( "\"" );
400                s.append( allowed ? BG_GREEN + ">" : BG_RED + ">" );
401                s.append( "&nbsp;</td>\n" );
402            }
403            s.append( "  </tr>\n" );
404        }
405
406        s.append( "</tbody>\n" );
407        s.append( "</table>\n" );
408        return s.toString();
409    }
410
411    /**
412     * Returns <code>true</code> if the Java security policy is configured
413     * correctly, and it verifies as valid.
414     * @return the result of the configuration check
415     */
416    public boolean isSecurityPolicyConfigured()
417    {
418        return m_isSecurityPolicyConfigured;
419    }
420
421    /**
422     * If the active Authorizer is the WebContainerAuthorizer, returns the roles it knows about; otherwise, a zero-length array.
423     *
424     * @return the roles parsed from <code>web.xml</code>, or a zero-length array
425     * @throws WikiException if the web authorizer cannot obtain the list of roles
426     */
427    public Principal[] webContainerRoles() throws WikiException {
428        final Authorizer authorizer = m_engine.getManager( AuthorizationManager.class ).getAuthorizer();
429        if ( authorizer instanceof WebContainerAuthorizer ) {
430            return authorizer.getRoles();
431        }
432        return new Principal[0];
433    }
434
435    /**
436     * Verifies that the roles given in the security policy are reflected by the
437     * container <code>web.xml</code> file.
438     * @throws WikiException if the web authorizer cannot verify the roles
439     */
440    void verifyPolicyAndContainerRoles() throws WikiException {
441        final Authorizer authorizer = m_engine.getManager( AuthorizationManager.class ).getAuthorizer();
442        final Principal[] containerRoles = authorizer.getRoles();
443        boolean missing = false;
444        for( final Principal principal : m_policyPrincipals ) {
445            if( principal instanceof final Role role ) {
446                final boolean isContainerRole = ArrayUtils.contains( containerRoles, role );
447                if ( !Role.isBuiltInRole( role ) && !isContainerRole ) {
448                    m_session.addMessage( ERROR_ROLES, "Role '" + role.getName() + "' is defined in security policy but not in web.xml." );
449                    missing = true;
450                }
451            }
452        }
453        if ( !missing ) {
454            m_session.addMessage( INFO_ROLES, "Every non-standard role defined in the security policy was also found in web.xml." );
455        }
456    }
457
458    /**
459     * Verifies that the group datbase was initialized properly, and that
460     * user add and delete operations work as they should.
461     */
462    void verifyGroupDatabase() {
463        final GroupManager mgr = m_engine.getManager( GroupManager.class );
464        GroupDatabase db = null;
465        try {
466            db = m_engine.getManager( GroupManager.class ).getGroupDatabase();
467        } catch ( final WikiSecurityException e ) {
468            m_session.addMessage( ERROR_GROUPS, "Could not retrieve GroupManager: " + e.getMessage() );
469        }
470
471        // Check for obvious error conditions
472        if ( mgr == null || db == null ) {
473            if ( mgr == null ) {
474                m_session.addMessage( ERROR_GROUPS, "GroupManager is null; JSPWiki could not initialize it. Check the error logs." );
475            }
476            if ( db == null ) {
477                m_session.addMessage( ERROR_GROUPS, "GroupDatabase is null; JSPWiki could not initialize it. Check the error logs." );
478            }
479            return;
480        }
481
482        // Everything initialized OK...
483
484        // Tell user what class of database this is.
485        m_session.addMessage( INFO_GROUPS, "GroupDatabase is of type '" + db.getClass().getName() + "'. It appears to be initialized properly." );
486
487        // Now, see how many groups we have.
488        final int oldGroupCount;
489        try {
490            final Group[] groups = db.groups();
491            oldGroupCount = groups.length;
492            m_session.addMessage( INFO_GROUPS, "The group database contains " + oldGroupCount + " groups." );
493        } catch( final WikiSecurityException e ) {
494            m_session.addMessage( ERROR_GROUPS, "Could not obtain a list of current groups: " + e.getMessage() );
495            return;
496        }
497
498        // Try adding a bogus group with random name
499        final String name = "TestGroup" + System.currentTimeMillis();
500        final Group group;
501        try {
502            // Create dummy test group
503            group = mgr.parseGroup( name, "", true );
504            final Principal user = new WikiPrincipal( "TestUser" );
505            group.add( user );
506            db.save( group, new WikiPrincipal( "SecurityVerifier" ) );
507
508            // Make sure the group saved successfully
509            if( db.groups().length == oldGroupCount ) {
510                m_session.addMessage( ERROR_GROUPS, "Could not add a test group to the database." );
511                return;
512            }
513            m_session.addMessage( INFO_GROUPS, "The group database allows new groups to be created, as it should." );
514        } catch( final WikiSecurityException e ) {
515            m_session.addMessage( ERROR_GROUPS, "Could not add a group to the database: " + e.getMessage() );
516            return;
517        }
518
519        // Now delete the group; should be back to old count
520        try {
521            db.delete( group );
522            if( db.groups().length != oldGroupCount ) {
523                m_session.addMessage( ERROR_GROUPS, "Could not delete a test group from the database." );
524                return;
525            }
526            m_session.addMessage( INFO_GROUPS, "The group database allows groups to be deleted, as it should." );
527        } catch( final WikiSecurityException e ) {
528            m_session.addMessage( ERROR_GROUPS, "Could not delete a test group from the database: " + e.getMessage() );
529            return;
530        }
531
532        m_session.addMessage( INFO_GROUPS, "The group database configuration looks fine." );
533    }
534
535    /**
536     * Verfies the JAAS configuration. The configuration is valid if value of the
537     * <code>jspwiki.properties<code> property
538     * {@value org.apache.wiki.auth.AuthenticationManager#PROP_LOGIN_MODULE}
539     * resolves to a valid class on the classpath.
540     */
541    void verifyJaas() {
542        // Verify that the specified JAAS moduie corresponds to a class we can load successfully.
543        final String jaasClass = m_engine.getWikiProperties().getProperty( AuthenticationManager.PROP_LOGIN_MODULE );
544        if( jaasClass == null || jaasClass.isEmpty() ) {
545            m_session.addMessage( ERROR_JAAS, "The value of the '" + AuthenticationManager.PROP_LOGIN_MODULE
546                    + "' property was null or blank. This is a fatal error. This value should be set to a valid LoginModule implementation "
547                    + "on the classpath." );
548            return;
549        }
550
551        // See if we can find the LoginModule on the classpath
552        Class< ? > c = null;
553        try {
554            m_session.addMessage( INFO_JAAS,
555                    "The property '" + AuthenticationManager.PROP_LOGIN_MODULE + "' specified the class '" + jaasClass + ".'" );
556            c = Class.forName( jaasClass );
557        } catch( final ClassNotFoundException e ) {
558            m_session.addMessage( ERROR_JAAS, "We could not find the the class '" + jaasClass + "' on the " + "classpath. This is fatal error." );
559        }
560
561        // Is the specified class actually a LoginModule?
562        if( LoginModule.class.isAssignableFrom( c ) ) {
563            m_session.addMessage( INFO_JAAS, "We found the the class '" + jaasClass + "' on the classpath, and it is a LoginModule implementation. Good!" );
564        } else {
565            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." );
566        }
567    }
568
569    /**
570     * Looks up a file name based on a JRE system property and returns the associated
571     * File object if it exists. This method adds messages with the topic prefix 
572     * {@link #ERROR} and {@link #INFO} as appropriate, with the suffix matching the 
573     * supplied property.
574     * @param property the system property to look up
575     * @return the file object, or <code>null</code> if not found
576     */
577    File getFileFromProperty( final String property )
578    {
579        String propertyValue;
580        try
581        {
582            propertyValue = System.getProperty( property );
583            if ( propertyValue == null )
584            {
585                m_session.addMessage( "Error." + property, "The system property '" + property + "' is null." );
586                return null;
587            }
588
589            //
590            //  It's also possible to use "==" to mark a property.  We remove that
591            //  here so that we can actually find the property file, then.
592            //
593            if( propertyValue.startsWith("=") )
594            {
595                propertyValue = propertyValue.substring(1);
596            }
597
598            try
599            {
600                m_session.addMessage( "Info." + property, "The system property '" + property + "' is set to: "
601                        + propertyValue + "." );
602
603                // Prepend a file: prefix if not there already
604                if ( !propertyValue.startsWith( "file:" ) )
605                {
606                  propertyValue = "file:" + propertyValue;
607                }
608                final URL url = new URL( propertyValue );
609                final File file = new File( url.getPath() );
610                if ( file.exists() )
611                {
612                    m_session.addMessage( "Info." + property, "File '" + propertyValue + "' exists in the filesystem." );
613                    return file;
614                }
615            }
616            catch( final MalformedURLException e )
617            {
618                LOG.debug(e.getMessage(), e);
619                // Swallow exception because we can't find it anyway
620            }
621            m_session.addMessage( "Error." + property, "File '" + propertyValue
622                    + "' doesn't seem to exist. This might be a problem." );
623            return null;
624        }
625        catch( final SecurityException e )
626        {
627            LOG.debug(e.getMessage(), e);
628            m_session.addMessage( "Error." + property, "We could not read system property '" + property
629                    + "'. This is probably because you are running with a security manager." );
630            return null;
631        }
632    }
633
634    /**
635     * Verfies the Java security policy configuration. The configuration is
636     * valid if value of the local policy (at <code>WEB-INF/jspwiki.policy</code>
637     * resolves to an existing file, and the policy file contained therein
638     * represents a valid policy.
639     */
640    @SuppressWarnings("unchecked")
641    void verifyPolicy() {
642        // Look up the policy file and set the status text.
643        final URL policyURL = m_engine.findConfigFile( AuthorizationManager.DEFAULT_POLICY );
644        String path = policyURL.getPath();
645        if ( path.startsWith("file:") ) {
646            path = path.substring( 5 );
647        }
648        final File policyFile = new File( path );
649
650        // Next, verify the policy
651        try {
652            // Get the file
653            final PolicyReader policy = new PolicyReader( policyFile );
654            m_session.addMessage( INFO_POLICY, "The security policy '" + policy.getFile() + "' exists." );
655
656            // See if there is a keystore that's valid
657            final KeyStore ks = policy.getKeyStore();
658            if ( ks == null ) {
659                m_session.addMessage( WARNING_POLICY,
660                    "Policy file does not have a keystore... at least not one that we can locate. If your policy file " +
661                    "does not contain any 'signedBy' blocks, this is probably ok." );
662            } else {
663                m_session.addMessage( INFO_POLICY,
664                    "The security policy specifies a keystore, and we were able to locate it in the filesystem." );
665            }
666
667            // Verify the file
668            policy.read();
669            final List<Exception> errors = policy.getMessages();
670            if (!errors.isEmpty()) {
671                for( final Exception e : errors ) {
672                    m_session.addMessage( ERROR_POLICY, e.getMessage() );
673                }
674            } else {
675                m_session.addMessage( INFO_POLICY, "The security policy looks fine." );
676                m_isSecurityPolicyConfigured = true;
677            }
678
679            // Stash the unique principals mentioned in the file,
680            // plus our standard roles.
681            final Set<Principal> principals = new LinkedHashSet<>();
682            principals.add( Role.ALL );
683            principals.add( Role.ANONYMOUS );
684            principals.add( Role.ASSERTED );
685            principals.add( Role.AUTHENTICATED );
686            final ProtectionDomain[] domains = policy.getProtectionDomains();
687            for ( final ProtectionDomain domain : domains ) {
688                principals.addAll(Arrays.asList(domain.getPrincipals()));
689            }
690            m_policyPrincipals = principals.toArray( new Principal[0] );
691        } catch( final IOException e ) {
692            m_session.addMessage( ERROR_POLICY, e.getMessage() );
693        }
694    }
695
696    /**
697     * Verifies that a particular Principal possesses a Permission, as defined
698     * in the security policy file.
699     * @param principal the principal
700     * @param permission the permission
701     * @return the result, based on consultation with the active Java security
702     *         policy
703     */
704    boolean verifyStaticPermission( final Principal principal, final Permission permission )
705    {
706        final Subject subject = new Subject();
707        subject.getPrincipals().add( principal );
708        final boolean allowedByGlobalPolicy = (Boolean)
709            Subject.doAsPrivileged( subject, ( PrivilegedAction< Object > )() -> {
710                try {
711                    AccessController.checkPermission( permission );
712                    return Boolean.TRUE;
713                } catch( final AccessControlException e ) {
714                    return Boolean.FALSE;
715                }
716            }, null );
717
718        if ( allowedByGlobalPolicy )
719        {
720            return true;
721        }
722
723        // Check local policy
724        final Principal[] principals = new Principal[]{ principal };
725        return m_engine.getManager( AuthorizationManager.class ).allowedByLocalPolicy( principals, permission );
726    }
727
728    /**
729     * Verifies that the user datbase was initialized properly, and that
730     * user add and delete operations work as they should.
731     */
732    void verifyUserDatabase() {
733        final UserDatabase db = m_engine.getManager( UserManager.class ).getUserDatabase();
734
735        // Check for obvious error conditions
736        if ( db == null ) {
737            m_session.addMessage( ERROR_DB, "UserDatabase is null; JSPWiki could not initialize it. Check the error logs." );
738            return;
739        }
740
741        if ( db instanceof DummyUserDatabase ) {
742            m_session.addMessage( ERROR_DB, "UserDatabase is DummyUserDatabase; JSPWiki " +
743                    "may not have been able to initialize the database you supplied in " +
744                    "jspwiki.properties, or you left the 'jspwiki.userdatabase' property " +
745                    "blank. Check the error logs." );
746        }
747
748        // Tell user what class of database this is.
749        m_session.addMessage( INFO_DB, "UserDatabase is of type '" + db.getClass().getName() +
750                                       "'. It appears to be initialized properly." );
751
752        // Now, see how many users we have.
753        final int oldUserCount;
754        try {
755            final Principal[] users = db.getWikiNames();
756            oldUserCount = users.length;
757            m_session.addMessage( INFO_DB, "The user database contains " + oldUserCount + " users." );
758        } catch( final WikiSecurityException e ) {
759            m_session.addMessage( ERROR_DB, "Could not obtain a list of current users: " + e.getMessage() );
760            return;
761        }
762
763        // Try adding a bogus user with random name
764        final String loginName = "TestUser" + System.currentTimeMillis();
765        try {
766            final UserProfile profile = db.newProfile();
767            profile.setEmail( "jspwiki.tests@mailinator.com" );
768            profile.setLoginName( loginName );
769            profile.setFullname( "FullName" + loginName );
770            profile.setPassword( "password" );
771            db.save( profile );
772
773            // Make sure the profile saved successfully
774            if( db.getWikiNames().length == oldUserCount ) {
775                m_session.addMessage( ERROR_DB, "Could not add a test user to the database." );
776                return;
777            }
778            m_session.addMessage( INFO_DB, "The user database allows new users to be created, as it should." );
779        } catch( final WikiSecurityException e ) {
780            m_session.addMessage( ERROR_DB, "Could not add a test user to the database: " + e.getMessage() );
781            return;
782        }
783
784        // Now delete the profile; should be back to old count
785        try {
786            db.deleteByLoginName( loginName );
787            if( db.getWikiNames().length != oldUserCount ) {
788                m_session.addMessage( ERROR_DB, "Could not delete a test user from the database." );
789                return;
790            }
791            m_session.addMessage( INFO_DB, "The user database allows users to be deleted, as it should." );
792        } catch( final WikiSecurityException e ) {
793            m_session.addMessage( ERROR_DB, "Could not delete a test user to the database: " + e.getMessage() );
794            return;
795        }
796
797        m_session.addMessage( INFO_DB, "The user database configuration looks fine." );
798    }
799}