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 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: “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}