    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.permissions;
    020
    021import java.io.Serializable;
    022import java.security.AccessControlContext;
    023import java.security.AccessController;
    024import java.security.DomainCombiner;
    025import java.security.Permission;
    026import java.util.Arrays;
    027import java.util.Set;
    028
    029import javax.security.auth.Subject;
    030import javax.security.auth.SubjectDomainCombiner;
    031
    032import org.apache.wiki.auth.GroupPrincipal;
    033
    034/**
    035 * <p>
    036 * Permission to perform an operation on a group in a given wiki. Permission
    037 * actions include: <code>view</code>, <code>edit</code>, <code>delete</code>.
    038 * </p>
    039 * <p>
    040 * The target of a permission is a single group or collection in a given wiki.
    041 * The syntax for the target is the wiki name, followed by a colon (:) and the
    042 * name of the group. &#8220;All wikis&#8221; can be specified using a wildcard (*). Group
    043 * collections may also be specified using a wildcard. For groups, the wildcard
    044 * may be a prefix, suffix, or all by itself. Examples of targets include:
    045 * </p>
    046 * <blockquote><code>*:*<br/>
    047 * *:TestPlanners<br/>
    048 * *:*Planners<br/>
    049 * *:Test*<br/>
    050 * mywiki:TestPlanners<br/>
    051 * mywiki:*Planners<br/>
    052 * mywiki:Test*</code>
    053 * </blockquote>
    054 * <p>
    055 * For a given target, certain permissions imply others:
    056 * </p>
    057 * <ul>
    058 * <li><code>edit</code>&nbsp;implies&nbsp;<code>view</code></li>
    059 * <li><code>delete</code>&nbsp;implies&nbsp;<code>edit</code> and
    060 * <code>view</code></li>
    061 * </ul>
    062 * <P>Targets that do not include a wiki prefix <em>never </em> imply others.</p>
    063 * <p>
    064 * GroupPermission accepts a special target called
    065 * <code>&lt;groupmember&gt;</code> that means &#8220;all groups that a user is a
    066 * member of.&#8221; When included in a policy file <code>grant</code> block, it
    067 * functions like a wildcard. Thus, this block:
    068 *
    069 * <pre>
    070 *  grant signedBy &quot;jspwiki&quot;,
    071 *    principal org.apache.wiki.auth.authorize.Role &quot;Authenticated&quot; {
    072 *      permission org.apache.wiki.auth.permissions.GroupPermission &quot;*:&lt;groupmember&gt;&quot;, &quot;edit&quot;;
    073 * </pre>
    074 *
    075 * means, &#8220;allow Authenticated users to edit any groups they are members of.&#8221;
    076 * The wildcard target (*) does <em>not</em> imply <code>&lt;groupmember&gt;</code>; it
    077 * must be granted explicitly.
    078 * @since 2.4.17
    079 */
    080public final class GroupPermission extends Permission implements Serializable
    081{
    082    /** Special target token that denotes all groups that a Subject's Principals are members of. */
    083    public static final String         MEMBER_TOKEN     = "<groupmember>";
    084
    085    private static final long           serialVersionUID = 1L;
    086
    087    /** Action for deleting a group or collection of groups. */
    088    public static final String          DELETE_ACTION    = "delete";
    089
    090    /** Action for editing a group or collection of groups. */
    091    public static final String          EDIT_ACTION      = "edit";
    092
    093    /** Action for viewing a group or collection of groups. */
    094    public static final String          VIEW_ACTION      = "view";
    095
    096    static final int          DELETE_MASK      = 0x4;
    097
    098    static final int          EDIT_MASK        = 0x2;
    099
    100    static final int          VIEW_MASK        = 0x1;
    101
    102    /** Convenience constant that denotes <code>GroupPermission( "*:*, "delete" )</code>. */
    103    public static final GroupPermission DELETE           = new GroupPermission( DELETE_ACTION );
    104
    105    /** Convenience constant that denotes <code>GroupPermission( "*:*, "edit" )</code>. */
    106    public static final GroupPermission EDIT             = new GroupPermission( EDIT_ACTION );
    107
    108    /** Convenience constant that denotes <code>GroupPermission( "*:*, "view" )</code>. */
    109    public static final GroupPermission VIEW             = new GroupPermission( VIEW_ACTION );
    110
    111    private static final String         ACTION_SEPARATOR = ",";
    112
    113    private static final String         WILDCARD         = "*";
    114
    115    private static final String         WIKI_SEPARATOR   = ":";
    116
    117    private final String                m_actionString;
    118
    119    private final int                   m_mask;
    120
    121    private final String                m_group;
    122
    123    private final String                m_wiki;
    124
    125    /** For serialization purposes */
    126    GroupPermission()
    127    {
    128        this("");
    129    }
    130
    131    /**
    132     * Private convenience constructor that creates a new GroupPermission for
    133     * all wikis and groups (*:*) and set of actions.
    134     * @param actions
    135     */
    136    private GroupPermission(final String actions )
    137    {
    138        this( WILDCARD + WIKI_SEPARATOR + WILDCARD, actions );
    139    }
    140
    141    /**
    142     * Creates a new GroupPermission for a specified group and set of actions.
    143     * Group should include a prepended wiki name followed by a colon (:). If
    144     * the wiki name is not supplied or starts with a colon, the group refers to
    145     * all wikis.
    146     * @param group the wiki group
    147     * @param actions the allowed actions for this group
    148     */
    149    public GroupPermission(final String group, final String actions )
    150    {
    151        super( group );
    152
    153        // Parse wiki and group (which may include wiki name and group)
    154        // Strip out attachment separator; it is irrelevant.
    155        final String[] pathParams = group.split( WIKI_SEPARATOR );
    156        final String groupName;
    157        if ( pathParams.length >= 2 )
    158        {
    159            m_wiki = !pathParams[0].isEmpty() ? pathParams[0] : null;
    160            groupName = pathParams[1];
    161        }
    162        else
    163        {
    164            m_wiki = WILDCARD;
    165            groupName = pathParams[0];
    166        }
    167        m_group = groupName;
    168
    169        // Parse actions
    170        final String[] groupActions = actions.toLowerCase().split( ACTION_SEPARATOR );
    171        Arrays.sort( groupActions, String.CASE_INSENSITIVE_ORDER );
    172        m_mask = createMask( actions );
    173        final StringBuilder buffer = new StringBuilder();
    174        final int groupActionsLength = groupActions.length;
    175        for( int i = 0; i < groupActionsLength; i++ )
    176        {
    177            buffer.append( groupActions[i] );
    178            if ( i < ( groupActionsLength - 1 ) )
    179            {
    180                buffer.append( ACTION_SEPARATOR );
    181            }
    182        }
    183        m_actionString = buffer.toString();
    184    }
    185
    186    /**
    187     * Two PagePermission objects are considered equal if their actions (after
    188     * normalization), wiki and target are equal.
    189     * @param obj the object to compare
    190     * @return the result of the comparison
    191     * @see java.lang.Object#equals(java.lang.Object)
    192     */
    193    public boolean equals(final Object obj )
    194    {
    195        if ( !( obj instanceof GroupPermission ) )
    196        {
    197            return false;
    198        }
    199        final GroupPermission p = (GroupPermission) obj;
    200        return  p.m_mask == m_mask && p.m_group.equals( m_group ) && p.m_wiki != null && p.m_wiki.equals( m_wiki );
    201    }
    202
    203    /**
    204     * Returns the actions for this permission: &#8220;view&#8221;, &#8220;edit&#8221;, or &#8220;delete&#8221;. The
    205     * actions will always be sorted in alphabetic order, and will always appear
    206     * in lower case.
    207     * @return the actions
    208     * @see java.security.Permission#getActions()
    209     */
    210    @Override
    211    public String getActions()
    212    {
    213        return m_actionString;
    214    }
    215
    216    /**
    217     * Returns the name of the wiki group represented by this permission.
    218     * @return the page name
    219     */
    220    public String getGroup()
    221    {
    222        return m_group;
    223    }
    224
    225    /**
    226     * Returns the name of the wiki containing the group represented by this
    227     * permission; may return the wildcard string.
    228     * @return the wiki
    229     */
    230    public String getWiki()
    231    {
    232        return m_wiki;
    233    }
    234
    235    /**
    236     * Returns the hash code for this GroupPermission.
    237     * @return the hash code
    238     * @see java.lang.Object#hashCode()
    239     */
    240    public int hashCode()
    241    {
    242        // If the wiki has not been set, uses a dummy value for the hashcode
    243        // calculation. This may occur if the page given does not refer
    244        // to any particular wiki
    245        final String wiki =  m_wiki != null ? m_wiki : "dummy_value";
    246        return m_mask + ( ( 13 * m_actionString.hashCode() ) * 23 * wiki.hashCode() );
    247    }
    248
    249    /**
    250     * <p>
    251     * GroupPermissions can only imply other GroupPermissions; no other
    252     * permission types are implied. One GroupPermission implies another if its
    253     * actions if three conditions are met:
    254     * </p>
    255     * <ol>
    256     * <li>The other GroupPermission&#8217;s wiki is equal to, or a subset of, that
    257     * of this permission. This permission&#8217;s wiki is considered a superset of
    258     * the other if it contains a matching prefix plus a wildcard, or a wildcard
    259     * followed by a matching suffix.</li>
    260     * <li>The other GroupPermission&#8217;s target is equal to, or a subset of, the
    261     * target specified by this permission. This permission&#8217;s target is
    262     * considered a superset of the other if it contains a matching prefix plus
    263     * a wildcard, or a wildcard followed by a matching suffix.</li>
    264     * <li>All of other GroupPermission&#8217;s actions are equal to, or a subset of,
    265     * those of this permission</li>
    266     * </ol>
    267     * @param permission the Permission to examine
    268     * @return <code>true</code> if the GroupPermission implies the
    269     * supplied Permission; <code>false</code> otherwise
    270     * @see java.security.Permission#implies(java.security.Permission)
    271     */
    272    @Override
    273    public boolean implies(final Permission permission )
    274    {
    275        // Permission must be a GroupPermission
    276        if ( !( permission instanceof GroupPermission ) )
    277        {
    278            return false;
    279        }
    280
    281        // Build up an "implied mask"
    282        final GroupPermission p = (GroupPermission) permission;
    283        final int impliedMask = impliedMask( m_mask );
    284
    285        // If actions aren't a proper subset, return false
    286        if ( ( impliedMask & p.m_mask ) != p.m_mask )
    287        {
    288            return false;
    289        }
    290
    291        // See if the tested permission's wiki is implied
    292        final boolean impliedWiki = PagePermission.isSubset( m_wiki, p.m_wiki );
    293
    294        // If this page is "*", the tested permission's
    295        // group is implied, unless implied permission has <groupmember> token
    296        final boolean impliedGroup;
    297        if ( MEMBER_TOKEN.equals( p.m_group ) )
    298        {
    299            impliedGroup = MEMBER_TOKEN.equals( m_group );
    300        }
    301        else
    302        {
    303            impliedGroup = PagePermission.isSubset( m_group, p.m_group );
    304        }
    305
    306        // See if this permission is <groupmember> and Subject possesses
    307        // GroupPrincipal matching the implied GroupPermission's group
    308        final boolean impliedMember = impliesMember( p );
    309
    310        return  impliedWiki && ( impliedGroup || impliedMember );
    311    }
    312
    313    /**
    314     * Prints a human-readable representation of this permission.
    315     * @return the string
    316     * @see java.lang.Object#toString()
    317     */
    318    public String toString()
    319    {
    320        final String wiki = ( m_wiki == null ) ? "" : m_wiki;
    321        return "(\"" + this.getClass().getName() + "\",\"" + wiki + WIKI_SEPARATOR + m_group + "\",\"" + getActions()
    322                + "\")";
    323    }
    324
    325    /**
    326     * Creates an &#8220;implied mask&#8221; based on the actions originally assigned: for
    327     * example, delete implies edit; edit implies view.
    328     * @param mask binary mask for actions
    329     * @return binary mask for implied actions
    330     */
    331    static int impliedMask( int mask )
    332    {
    333        if ( ( mask & DELETE_MASK ) > 0 )
    334        {
    335            mask |= EDIT_MASK;
    336        }
    337        if ( ( mask & EDIT_MASK ) > 0 )
    338        {
    339            mask |= VIEW_MASK;
    340        }
    341        return mask;
    342    }
    343
    344    /**
    345     * Protected method that creates a binary mask based on the actions specified.
    346     * This is used by {@link #implies(Permission)}.
    347     * @param actions the actions for this permission, separated by commas
    348     * @return the binary actions mask
    349     */
    350    static int createMask( final String actions )
    351    {
    352        if ( actions == null || actions.isEmpty() )
    353        {
    354            throw new IllegalArgumentException( "Actions cannot be blank or null" );
    355        }
    356        int mask = 0;
    357        final String[] actionList = actions.split( ACTION_SEPARATOR );
    358        for( final String action : actionList )
    359        {
    360            if ( action.equalsIgnoreCase( VIEW_ACTION ) )
    361            {
    362                mask |= VIEW_MASK;
    363            }
    364            else if ( action.equalsIgnoreCase( EDIT_ACTION ) )
    365            {
    366                mask |= EDIT_MASK;
    367            }
    368            else if ( action.equalsIgnoreCase( DELETE_ACTION ) )
    369            {
    370                mask |= DELETE_MASK;
    371            }
    372            else
    373            {
    374                throw new IllegalArgumentException( "Unrecognized action: " + action );
    375            }
    376        }
    377        return mask;
    378    }
    379
    380    /**
    381     * <p>
    382     * Returns <code>true</code> if this GroupPermission was created with the
    383     * token <code>&lt;groupmember&gt;</code>
    384     * <em>and</em> the current
    385     * thread&#8217;s Subject is a member of the Group indicated by the implied
    386     * GroupPermission. Thus, a GroupPermission with the group
    387     * <code>&lt;groupmember&gt;</code> implies GroupPermission for group
    388     * "TestGroup" only if the Subject is a member of TestGroup.
    389     * </p>
    390     * <p>
    391     * We make this determination by obtaining the current {@link Thread}&#8217;s
    392     * {@link java.security.AccessControlContext} and requesting the
    393     * {@link javax.security.auth.SubjectDomainCombiner}. If the combiner is
    394     * not <code>null</code>, then we know that the access check was
    395     * requested using a {@link javax.security.auth.Subject}; that is, that an
    396     * upstream caller caused a Subject to be associated with the Thread&#8217;s
    397     * ProtectionDomain by executing a
    398     * {@link javax.security.auth.Subject#doAs(Subject, java.security.PrivilegedAction)}
    399     * operation.
    400     * </p>
    401     * <p>
    402     * If a SubjectDomainCombiner exists, determining group membership is
    403     * simple: just iterate through the Subject&#8217;s Principal set and look for all
    404     * Principals of type {@link org.apache.wiki.auth.GroupPrincipal}. If the
    405     * name of any Principal matches the value of the implied Permission&#8217;s
    406     * {@link GroupPermission#getGroup()} value, then the Subject is a member of
    407     * this group -- and therefore this <code>impliesMember</code> call
    408     * returns <code>true</code>.
    409     * </p>
    410     * <p>
    411     * This may sound complicated, but it really isn&#8217;t. Consider the following
    412     * examples:
    413     * </p>
    414     * <table border="1"> <thead>
    415     * <tr>
    416     * <th width="25%">This object</th>
    417     * <th width="25%"><code>impliesMember</code> parameter</th>
    418     * <th width="25%">Calling Subject&#8217;s Principals
    419     * <th width="25%">Result</th>
    420     * </tr>
    421     * <tr>
    422     * <td><code>GroupPermission ("&lt;groupmember&gt;")</code></td>
    423     * <td><code>GroupPermission ("*:TestGroup")</code></td>
    424     * <td><code>WikiPrincipal ("Biff"),<br/>GroupPrincipal ("TestGroup")</code></td>
    425     * <td><code>true</code></td>
    426     * </tr>
    427     * <tr>
    428     * <td><code>GroupPermission ("*:TestGroup")</code></td>
    429     * <td><code>GroupPermission ("*:TestGroup")</code></td>
    430     * <td><code>WikiPrincipal ("Biff"),<br/>GroupPrincipal ("TestGroup")</code></td>
    431     * <td><code>false</code> - this object does not contain
    432     * <code>&lt;groupmember&gt;</code></td>
    433     * </tr>
    434     * <tr>
    435     * <td><code>GroupPermission ("&lt;groupmember&gt;")</code></td>
    436     * <td><code>GroupPermission ("*:TestGroup")</code></td>
    437     * <td><code>WikiPrincipal ("Biff"),<br/>GroupPrincipal ("FooGroup")</code></td>
    438     * <td><code>false</code> - Subject does not contain GroupPrincipal
    439     * matching implied Permission&#8217;s group (TestGroup)</td>
    440     * </tr>
    441     * <tr>
    442     * <td><code>GroupPermission ("&lt;groupmember&gt;")</code></td>
    443     * <td><code>WikiPermission ("*:createGroups")</code></td>
    444     * <td><code>WikiPrincipal ("Biff"),<br/>GroupPrincipal ("TestGroup")</code></td>
    445     * <td><code>false</code> - implied permission not of type
    446     * GroupPermission</td>
    447     * </tr>
    448     * <tr>
    449     * <td><code>GroupPermission ("&lt;groupmember&gt;")</code></td>
    450     * <td><code>GroupPermission ("*:TestGroup")</code></td>
    451     * <td>-</td>
    452     * <td><code>false</code> - <code>Subject.doAs()</code> not called
    453     * upstream</td>
    454     * </tr>
    455     * </table>
    456     * <p>
    457     * Note that JSPWiki&#8217;s access control checks are made inside of
    458     * {@link org.apache.wiki.auth.AuthorizationManager#checkPermission(org.apache.wiki.api.core.Session, Permission)},
    459     * which performs a <code>Subject.doAs()</code> call. Thus, this
    460     * Permission functions exactly the way it should during normal
    461     * operations.
    462     * </p>
    463     * @param permission the implied permission
    464     * @return <code>true</code> if the calling Thread&#8217;s Subject contains a
    465     *         GroupPrincipal matching the implied GroupPermission&#8217;s group;
    466     *         <code>false</code> otherwise
    467     */
    468    boolean impliesMember(final Permission permission )
    469    {
    470        if ( !( permission instanceof GroupPermission ) )
    471        {
    472            return false;
    473        }
    474        final GroupPermission gp = (GroupPermission) permission;
    475        if ( !MEMBER_TOKEN.equals( m_group ) )
    476        {
    477            return false;
    478        }
    479
    480        // For the current thread, retrieve the SubjectDomainCombiner
    481        // (if one was used to create current AccessControlContext )
    482        final AccessControlContext acc = AccessController.getContext();
    483        final DomainCombiner dc = acc.getDomainCombiner();
    484        if ( dc != null && dc instanceof SubjectDomainCombiner )
    485        {
    486            // <member> implies permission if subject possesses
    487            // GroupPrincipal with same name as target
    488            final Subject subject = ( (SubjectDomainCombiner) dc ).getSubject();
    489            final Set<GroupPrincipal> principals = subject.getPrincipals( GroupPrincipal.class );
    490            return principals.stream().anyMatch(principal -> principal.getName().equals(gp.m_group));
    491        }
    492        return false;
    493    }
    494}