    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}