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    protected static final int          DELETE_MASK      = 0x4;
098
099    protected static final int          EDIT_MASK        = 0x2;
100
101    protected 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    protected 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( 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( String group, 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        String[] pathParams = group.split( WIKI_SEPARATOR );
157        String groupName;
158        if ( pathParams.length >= 2 )
159        {
160            m_wiki = pathParams[0].length() > 0 ? 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        String[] groupActions = actions.toLowerCase().split( ACTION_SEPARATOR );
172        Arrays.sort( groupActions, String.CASE_INSENSITIVE_ORDER );
173        m_mask = createMask( actions );
174        StringBuilder buffer = new StringBuilder();
175        for( int i = 0; i < groupActions.length; i++ )
176        {
177            buffer.append( groupActions[i] );
178            if ( i < ( groupActions.length - 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( Object obj )
194    {
195        if ( !( obj instanceof GroupPermission ) )
196        {
197            return false;
198        }
199        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    public String getActions()
211    {
212        return m_actionString;
213    }
214
215    /**
216     * Returns the name of the wiki group represented by this permission.
217     * @return the page name
218     */
219    public String getGroup()
220    {
221        return m_group;
222    }
223
224    /**
225     * Returns the name of the wiki containing the group represented by this
226     * permission; may return the wildcard string.
227     * @return the wiki
228     */
229    public String getWiki()
230    {
231        return m_wiki;
232    }
233
234    /**
235     * Returns the hash code for this GroupPermission.
236     * @return the hash code
237     * @see java.lang.Object#hashCode()
238     */
239    public int hashCode()
240    {
241        // If the wiki has not been set, uses a dummy value for the hashcode
242        // calculation. This may occur if the page given does not refer
243        // to any particular wiki
244        String wiki =  m_wiki != null ? m_wiki : "dummy_value";
245        return m_mask + ( ( 13 * m_actionString.hashCode() ) * 23 * wiki.hashCode() );
246    }
247
248    /**
249     * <p>
250     * GroupPermissions can only imply other GroupPermissions; no other
251     * permission types are implied. One GroupPermission implies another if its
252     * actions if three conditions are met:
253     * </p>
254     * <ol>
255     * <li>The other GroupPermission&#8217;s wiki is equal to, or a subset of, that
256     * of this permission. This permission&#8217;s wiki is considered a superset of
257     * the other if it contains a matching prefix plus a wildcard, or a wildcard
258     * followed by a matching suffix.</li>
259     * <li>The other GroupPermission&#8217;s target is equal to, or a subset of, the
260     * target specified by this permission. This permission&#8217;s target is
261     * considered a superset of the other if it contains a matching prefix plus
262     * a wildcard, or a wildcard followed by a matching suffix.</li>
263     * <li>All of other GroupPermission&#8217;s actions are equal to, or a subset of,
264     * those of this permission</li>
265     * </ol>
266     * @param permission the Permission to examine
267     * @return <code>true</code> if the GroupPermission implies the
268     * supplied Permission; <code>false</code> otherwise
269     * @see java.security.Permission#implies(java.security.Permission)
270     */
271    public boolean implies( Permission permission )
272    {
273        // Permission must be a GroupPermission
274        if ( !( permission instanceof GroupPermission ) )
275        {
276            return false;
277        }
278
279        // Build up an "implied mask"
280        GroupPermission p = (GroupPermission) permission;
281        int impliedMask = impliedMask( m_mask );
282
283        // If actions aren't a proper subset, return false
284        if ( ( impliedMask & p.m_mask ) != p.m_mask )
285        {
286            return false;
287        }
288
289        // See if the tested permission's wiki is implied
290        boolean impliedWiki = PagePermission.isSubset( m_wiki, p.m_wiki );
291
292        // If this page is "*", the tested permission's
293        // group is implied, unless implied permission has <groupmember> token
294        boolean impliedGroup;
295        if ( MEMBER_TOKEN.equals( p.m_group ) )
296        {
297            impliedGroup = MEMBER_TOKEN.equals( m_group );
298        }
299        else
300        {
301            impliedGroup = PagePermission.isSubset( m_group, p.m_group );
302        }
303
304        // See if this permission is <groupmember> and Subject possesses
305        // GroupPrincipal matching the implied GroupPermission's group
306        boolean impliedMember = impliesMember( p );
307
308        return  impliedWiki && ( impliedGroup || impliedMember );
309    }
310
311    /**
312     * Prints a human-readable representation of this permission.
313     * @return the string
314     * @see java.lang.Object#toString()
315     */
316    public String toString()
317    {
318        String wiki = ( m_wiki == null ) ? "" : m_wiki;
319        return "(\"" + this.getClass().getName() + "\",\"" + wiki + WIKI_SEPARATOR + m_group + "\",\"" + getActions()
320                + "\")";
321    }
322
323    /**
324     * Creates an &#8220;implied mask&#8221; based on the actions originally assigned: for
325     * example, delete implies edit; edit implies view.
326     * @param mask binary mask for actions
327     * @return binary mask for implied actions
328     */
329    protected static int impliedMask( int mask )
330    {
331        if ( ( mask & DELETE_MASK ) > 0 )
332        {
333            mask |= EDIT_MASK;
334        }
335        if ( ( mask & EDIT_MASK ) > 0 )
336        {
337            mask |= VIEW_MASK;
338        }
339        return mask;
340    }
341
342    /**
343     * Protected method that creates a binary mask based on the actions specified.
344     * This is used by {@link #implies(Permission)}.
345     * @param actions the actions for this permission, separated by commas
346     * @return the binary actions mask
347     */
348    protected static int createMask( String actions )
349    {
350        if ( actions == null || actions.length() == 0 )
351        {
352            throw new IllegalArgumentException( "Actions cannot be blank or null" );
353        }
354        int mask = 0;
355        String[] actionList = actions.split( ACTION_SEPARATOR );
356        for( String action : actionList )
357        {
358            if ( action.equalsIgnoreCase( VIEW_ACTION ) )
359            {
360                mask |= VIEW_MASK;
361            }
362            else if ( action.equalsIgnoreCase( EDIT_ACTION ) )
363            {
364                mask |= EDIT_MASK;
365            }
366            else if ( action.equalsIgnoreCase( DELETE_ACTION ) )
367            {
368                mask |= DELETE_MASK;
369            }
370            else
371            {
372                throw new IllegalArgumentException( "Unrecognized action: " + action );
373            }
374        }
375        return mask;
376    }
377
378    /**
379     * <p>
380     * Returns <code>true</code> if this GroupPermission was created with the
381     * token <code>&lt;groupmember&gt;</code>
382     * <em>and</em> the current
383     * thread&#8217;s Subject is a member of the Group indicated by the implied
384     * GroupPermission. Thus, a GroupPermission with the group
385     * <code>&lt;groupmember&gt;</code> implies GroupPermission for group
386     * "TestGroup" only if the Subject is a member of TestGroup.
387     * </p>
388     * <p>
389     * We make this determination by obtaining the current {@link Thread}&#8217;s
390     * {@link java.security.AccessControlContext} and requesting the
391     * {@link javax.security.auth.SubjectDomainCombiner}. If the combiner is
392     * not <code>null</code>, then we know that the access check was
393     * requested using a {@link javax.security.auth.Subject}; that is, that an
394     * upstream caller caused a Subject to be associated with the Thread&#8217;s
395     * ProtectionDomain by executing a
396     * {@link javax.security.auth.Subject#doAs(Subject, java.security.PrivilegedAction)}
397     * operation.
398     * </p>
399     * <p>
400     * If a SubjectDomainCombiner exists, determining group membership is
401     * simple: just iterate through the Subject&#8217;s Principal set and look for all
402     * Principals of type {@link org.apache.wiki.auth.GroupPrincipal}. If the
403     * name of any Principal matches the value of the implied Permission&#8217;s
404     * {@link GroupPermission#getGroup()} value, then the Subject is a member of
405     * this group -- and therefore this <code>impliesMember</code> call
406     * returns <code>true</code>.
407     * </p>
408     * <p>
409     * This may sound complicated, but it really isn&#8217;t. Consider the following
410     * examples:
411     * </p>
412     * <table border="1"> <thead>
413     * <tr>
414     * <th width="25%">This object</th>
415     * <th width="25%"><code>impliesMember</code> parameter</th>
416     * <th width="25%">Calling Subject&#8217;s Principals
417     * <th width="25%">Result</th>
418     * </tr>
419     * <tr>
420     * <td><code>GroupPermission ("&lt;groupmember&gt;")</code></td>
421     * <td><code>GroupPermission ("*:TestGroup")</code></td>
422     * <td><code>WikiPrincipal ("Biff"),<br/>GroupPrincipal ("TestGroup")</code></td>
423     * <td><code>true</code></td>
424     * </tr>
425     * <tr>
426     * <td><code>GroupPermission ("*:TestGroup")</code></td>
427     * <td><code>GroupPermission ("*:TestGroup")</code></td>
428     * <td><code>WikiPrincipal ("Biff"),<br/>GroupPrincipal ("TestGroup")</code></td>
429     * <td><code>false</code> - this object does not contain
430     * <code>&lt;groupmember&gt;</code></td>
431     * </tr>
432     * <tr>
433     * <td><code>GroupPermission ("&lt;groupmember&gt;")</code></td>
434     * <td><code>GroupPermission ("*:TestGroup")</code></td>
435     * <td><code>WikiPrincipal ("Biff"),<br/>GroupPrincipal ("FooGroup")</code></td>
436     * <td><code>false</code> - Subject does not contain GroupPrincipal
437     * matching implied Permission&#8217;s group (TestGroup)</td>
438     * </tr>
439     * <tr>
440     * <td><code>GroupPermission ("&lt;groupmember&gt;")</code></td>
441     * <td><code>WikiPermission ("*:createGroups")</code></td>
442     * <td><code>WikiPrincipal ("Biff"),<br/>GroupPrincipal ("TestGroup")</code></td>
443     * <td><code>false</code> - implied permission not of type
444     * GroupPermission</td>
445     * </tr>
446     * <tr>
447     * <td><code>GroupPermission ("&lt;groupmember&gt;")</code></td>
448     * <td><code>GroupPermission ("*:TestGroup")</code></td>
449     * <td>-</td>
450     * <td><code>false</code> - <code>Subject.doAs()</code> not called
451     * upstream</td>
452     * </tr>
453     * </table>
454     * <p>
455     * Note that JSPWiki&#8217;s access control checks are made inside of
456     * {@link org.apache.wiki.auth.AuthorizationManager#checkPermission(org.apache.wiki.WikiSession, Permission)},
457     * which performs a <code>Subject.doAs()</code> call. Thus, this
458     * Permission functions exactly the way it should during normal
459     * operations.
460     * </p>
461     * @param permission the implied permission
462     * @return <code>true</code> if the calling Thread&#8217;s Subject contains a
463     *         GroupPrincipal matching the implied GroupPermission&#8217;s group;
464     *         <code>false</code> otherwise
465     */
466    protected boolean impliesMember( Permission permission )
467    {
468        if ( !( permission instanceof GroupPermission ) )
469        {
470            return false;
471        }
472        GroupPermission gp = (GroupPermission) permission;
473        if ( !MEMBER_TOKEN.equals( m_group ) )
474        {
475            return false;
476        }
477
478        // For the current thread, retrieve the SubjectDomainCombiner
479        // (if one was used to create current AccessControlContext )
480        AccessControlContext acc = AccessController.getContext();
481        DomainCombiner dc = acc.getDomainCombiner();
482        if ( dc != null && dc instanceof SubjectDomainCombiner )
483        {
484            // <member> implies permission if subject possesses
485            // GroupPrincipal with same name as target
486            Subject subject = ( (SubjectDomainCombiner) dc ).getSubject();
487            Set<GroupPrincipal> principals = subject.getPrincipals( GroupPrincipal.class );
488            for( Principal principal : principals )
489            {
490                if ( principal.getName().equals( gp.m_group ) )
491                {
492                    return true;
493                }
494            }
495        }
496        return false;
497    }
498}