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. “All wikis” 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> implies <code>view</code></li>
060 * <li><code>delete</code> implies <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><groupmember></code> that means “all groups that a user is a
067 * member of.” 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 "jspwiki",
072 * principal org.apache.wiki.auth.authorize.Role "Authenticated" {
073 * permission org.apache.wiki.auth.permissions.GroupPermission "*:<groupmember>", "edit";
074 * </pre>
075 *
076 * means, “allow Authenticated users to edit any groups they are members of.”
077 * The wildcard target (*) does <em>not</em> imply <code><groupmember></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: “view”, “edit”, or “delete”. 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’s wiki is equal to, or a subset of, that
256 * of this permission. This permission’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’s target is equal to, or a subset of, the
260 * target specified by this permission. This permission’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’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 “implied mask” 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><groupmember></code>
382 * <em>and</em> the current
383 * thread’s Subject is a member of the Group indicated by the implied
384 * GroupPermission. Thus, a GroupPermission with the group
385 * <code><groupmember></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}’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’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’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’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’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’s Principals
417 * <th width="25%">Result</th>
418 * </tr>
419 * <tr>
420 * <td><code>GroupPermission ("<groupmember>")</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><groupmember></code></td>
431 * </tr>
432 * <tr>
433 * <td><code>GroupPermission ("<groupmember>")</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’s group (TestGroup)</td>
438 * </tr>
439 * <tr>
440 * <td><code>GroupPermission ("<groupmember>")</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 ("<groupmember>")</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’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’s Subject contains a
463 * GroupPrincipal matching the implied GroupPermission’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 }