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