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    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    public boolean implies(final Permission permission )
273    {
274        // Permission must be a GroupPermission
275        if ( !( permission instanceof GroupPermission ) )
276        {
277            return false;
278        }
279
280        // Build up an "implied mask"
281        final GroupPermission p = (GroupPermission) permission;
282        final int impliedMask = impliedMask( m_mask );
283
284        // If actions aren't a proper subset, return false
285        if ( ( impliedMask & p.m_mask ) != p.m_mask )
286        {
287            return false;
288        }
289
290        // See if the tested permission's wiki is implied
291        final boolean impliedWiki = PagePermission.isSubset( m_wiki, p.m_wiki );
292
293        // If this page is "*", the tested permission's
294        // group is implied, unless implied permission has <groupmember> token
295        final boolean impliedGroup;
296        if ( MEMBER_TOKEN.equals( p.m_group ) )
297        {
298            impliedGroup = MEMBER_TOKEN.equals( m_group );
299        }
300        else
301        {
302            impliedGroup = PagePermission.isSubset( m_group, p.m_group );
303        }
304
305        // See if this permission is <groupmember> and Subject possesses
306        // GroupPrincipal matching the implied GroupPermission's group
307        final boolean impliedMember = impliesMember( p );
308
309        return  impliedWiki && ( impliedGroup || impliedMember );
310    }
311
312    /**
313     * Prints a human-readable representation of this permission.
314     * @return the string
315     * @see java.lang.Object#toString()
316     */
317    public String toString()
318    {
319        final String wiki = ( m_wiki == null ) ? "" : m_wiki;
320        return "(\"" + this.getClass().getName() + "\",\"" + wiki + WIKI_SEPARATOR + m_group + "\",\"" + getActions()
321                + "\")";
322    }
323
324    /**
325     * Creates an &#8220;implied mask&#8221; based on the actions originally assigned: for
326     * example, delete implies edit; edit implies view.
327     * @param mask binary mask for actions
328     * @return binary mask for implied actions
329     */
330    static int impliedMask( int mask )
331    {
332        if ( ( mask & DELETE_MASK ) > 0 )
333        {
334            mask |= EDIT_MASK;
335        }
336        if ( ( mask & EDIT_MASK ) > 0 )
337        {
338            mask |= VIEW_MASK;
339        }
340        return mask;
341    }
342
343    /**
344     * Protected method that creates a binary mask based on the actions specified.
345     * This is used by {@link #implies(Permission)}.
346     * @param actions the actions for this permission, separated by commas
347     * @return the binary actions mask
348     */
349    static int createMask( final String actions )
350    {
351        if ( actions == null || actions.isEmpty() )
352        {
353            throw new IllegalArgumentException( "Actions cannot be blank or null" );
354        }
355        int mask = 0;
356        final String[] actionList = actions.split( ACTION_SEPARATOR );
357        for( final String action : actionList )
358        {
359            if ( action.equalsIgnoreCase( VIEW_ACTION ) )
360            {
361                mask |= VIEW_MASK;
362            }
363            else if ( action.equalsIgnoreCase( EDIT_ACTION ) )
364            {
365                mask |= EDIT_MASK;
366            }
367            else if ( action.equalsIgnoreCase( DELETE_ACTION ) )
368            {
369                mask |= DELETE_MASK;
370            }
371            else
372            {
373                throw new IllegalArgumentException( "Unrecognized action: " + action );
374            }
375        }
376        return mask;
377    }
378
379    /**
380     * <p>
381     * Returns <code>true</code> if this GroupPermission was created with the
382     * token <code>&lt;groupmember&gt;</code>
383     * <em>and</em> the current
384     * thread&#8217;s Subject is a member of the Group indicated by the implied
385     * GroupPermission. Thus, a GroupPermission with the group
386     * <code>&lt;groupmember&gt;</code> implies GroupPermission for group
387     * "TestGroup" only if the Subject is a member of TestGroup.
388     * </p>
389     * <p>
390     * We make this determination by obtaining the current {@link Thread}&#8217;s
391     * {@link java.security.AccessControlContext} and requesting the
392     * {@link javax.security.auth.SubjectDomainCombiner}. If the combiner is
393     * not <code>null</code>, then we know that the access check was
394     * requested using a {@link javax.security.auth.Subject}; that is, that an
395     * upstream caller caused a Subject to be associated with the Thread&#8217;s
396     * ProtectionDomain by executing a
397     * {@link javax.security.auth.Subject#doAs(Subject, java.security.PrivilegedAction)}
398     * operation.
399     * </p>
400     * <p>
401     * If a SubjectDomainCombiner exists, determining group membership is
402     * simple: just iterate through the Subject&#8217;s Principal set and look for all
403     * Principals of type {@link org.apache.wiki.auth.GroupPrincipal}. If the
404     * name of any Principal matches the value of the implied Permission&#8217;s
405     * {@link GroupPermission#getGroup()} value, then the Subject is a member of
406     * this group -- and therefore this <code>impliesMember</code> call
407     * returns <code>true</code>.
408     * </p>
409     * <p>
410     * This may sound complicated, but it really isn&#8217;t. Consider the following
411     * examples:
412     * </p>
413     * <table border="1"> <thead>
414     * <tr>
415     * <th width="25%">This object</th>
416     * <th width="25%"><code>impliesMember</code> parameter</th>
417     * <th width="25%">Calling Subject&#8217;s Principals
418     * <th width="25%">Result</th>
419     * </tr>
420     * <tr>
421     * <td><code>GroupPermission ("&lt;groupmember&gt;")</code></td>
422     * <td><code>GroupPermission ("*:TestGroup")</code></td>
423     * <td><code>WikiPrincipal ("Biff"),<br/>GroupPrincipal ("TestGroup")</code></td>
424     * <td><code>true</code></td>
425     * </tr>
426     * <tr>
427     * <td><code>GroupPermission ("*:TestGroup")</code></td>
428     * <td><code>GroupPermission ("*:TestGroup")</code></td>
429     * <td><code>WikiPrincipal ("Biff"),<br/>GroupPrincipal ("TestGroup")</code></td>
430     * <td><code>false</code> - this object does not contain
431     * <code>&lt;groupmember&gt;</code></td>
432     * </tr>
433     * <tr>
434     * <td><code>GroupPermission ("&lt;groupmember&gt;")</code></td>
435     * <td><code>GroupPermission ("*:TestGroup")</code></td>
436     * <td><code>WikiPrincipal ("Biff"),<br/>GroupPrincipal ("FooGroup")</code></td>
437     * <td><code>false</code> - Subject does not contain GroupPrincipal
438     * matching implied Permission&#8217;s group (TestGroup)</td>
439     * </tr>
440     * <tr>
441     * <td><code>GroupPermission ("&lt;groupmember&gt;")</code></td>
442     * <td><code>WikiPermission ("*:createGroups")</code></td>
443     * <td><code>WikiPrincipal ("Biff"),<br/>GroupPrincipal ("TestGroup")</code></td>
444     * <td><code>false</code> - implied permission not of type
445     * GroupPermission</td>
446     * </tr>
447     * <tr>
448     * <td><code>GroupPermission ("&lt;groupmember&gt;")</code></td>
449     * <td><code>GroupPermission ("*:TestGroup")</code></td>
450     * <td>-</td>
451     * <td><code>false</code> - <code>Subject.doAs()</code> not called
452     * upstream</td>
453     * </tr>
454     * </table>
455     * <p>
456     * Note that JSPWiki&#8217;s access control checks are made inside of
457     * {@link org.apache.wiki.auth.AuthorizationManager#checkPermission(org.apache.wiki.api.core.Session, Permission)},
458     * which performs a <code>Subject.doAs()</code> call. Thus, this
459     * Permission functions exactly the way it should during normal
460     * operations.
461     * </p>
462     * @param permission the implied permission
463     * @return <code>true</code> if the calling Thread&#8217;s Subject contains a
464     *         GroupPrincipal matching the implied GroupPermission&#8217;s group;
465     *         <code>false</code> otherwise
466     */
467    boolean impliesMember(final Permission permission )
468    {
469        if ( !( permission instanceof GroupPermission ) )
470        {
471            return false;
472        }
473        final GroupPermission gp = (GroupPermission) permission;
474        if ( !MEMBER_TOKEN.equals( m_group ) )
475        {
476            return false;
477        }
478
479        // For the current thread, retrieve the SubjectDomainCombiner
480        // (if one was used to create current AccessControlContext )
481        final AccessControlContext acc = AccessController.getContext();
482        final DomainCombiner dc = acc.getDomainCombiner();
483        if ( dc != null && dc instanceof SubjectDomainCombiner )
484        {
485            // <member> implies permission if subject possesses
486            // GroupPrincipal with same name as target
487            final Subject subject = ( (SubjectDomainCombiner) dc ).getSubject();
488            final Set<GroupPrincipal> principals = subject.getPrincipals( GroupPrincipal.class );
489            for( final Principal principal : principals )
490            {
491                if ( principal.getName().equals( gp.m_group ) )
492                {
493                    return true;
494                }
495            }
496        }
497        return false;
498    }
499}