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}