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. “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 */ 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: “view”, “edit”, or “delete”. 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’s wiki is equal to, or a subset of, that 257 * of this permission. This permission’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’s target is equal to, or a subset of, the 261 * target specified by this permission. This permission’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’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 “implied mask” 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><groupmember></code> 383 * <em>and</em> the current 384 * thread’s Subject is a member of the Group indicated by the implied 385 * GroupPermission. Thus, a GroupPermission with the group 386 * <code><groupmember></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}’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’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’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’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’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’s Principals 418 * <th width="25%">Result</th> 419 * </tr> 420 * <tr> 421 * <td><code>GroupPermission ("<groupmember>")</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><groupmember></code></td> 432 * </tr> 433 * <tr> 434 * <td><code>GroupPermission ("<groupmember>")</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’s group (TestGroup)</td> 439 * </tr> 440 * <tr> 441 * <td><code>GroupPermission ("<groupmember>")</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 ("<groupmember>")</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’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’s Subject contains a 464 * GroupPrincipal matching the implied GroupPermission’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}