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     */
019    package org.apache.wiki.auth.permissions;
020    
021    import java.io.Serializable;
022    import java.security.AccessControlContext;
023    import java.security.AccessController;
024    import java.security.DomainCombiner;
025    import java.security.Permission;
026    import java.security.Principal;
027    import java.util.Arrays;
028    import java.util.Set;
029    
030    import javax.security.auth.Subject;
031    import javax.security.auth.SubjectDomainCombiner;
032    
033    import 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     */
081    public 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            StringBuffer buffer = new StringBuffer();
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    }