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