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