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