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.util.Arrays; 027import java.util.Set; 028 029import javax.security.auth.Subject; 030import javax.security.auth.SubjectDomainCombiner; 031 032import org.apache.wiki.auth.GroupPrincipal; 033 034/** 035 * <p> 036 * Permission to perform an operation on a group in a given wiki. Permission 037 * actions include: <code>view</code>, <code>edit</code>, <code>delete</code>. 038 * </p> 039 * <p> 040 * The target of a permission is a single group or collection in a given wiki. 041 * The syntax for the target is the wiki name, followed by a colon (:) and the 042 * name of the group. “All wikis” can be specified using a wildcard (*). Group 043 * collections may also be specified using a wildcard. For groups, the wildcard 044 * may be a prefix, suffix, or all by itself. Examples of targets include: 045 * </p> 046 * <blockquote><code>*:*<br/> 047 * *:TestPlanners<br/> 048 * *:*Planners<br/> 049 * *:Test*<br/> 050 * mywiki:TestPlanners<br/> 051 * mywiki:*Planners<br/> 052 * mywiki:Test*</code> 053 * </blockquote> 054 * <p> 055 * For a given target, certain permissions imply others: 056 * </p> 057 * <ul> 058 * <li><code>edit</code> implies <code>view</code></li> 059 * <li><code>delete</code> implies <code>edit</code> and 060 * <code>view</code></li> 061 * </ul> 062 * <P>Targets that do not include a wiki prefix <em>never </em> imply others.</p> 063 * <p> 064 * GroupPermission accepts a special target called 065 * <code><groupmember></code> that means “all groups that a user is a 066 * member of.” When included in a policy file <code>grant</code> block, it 067 * functions like a wildcard. Thus, this block: 068 * 069 * <pre> 070 * grant signedBy "jspwiki", 071 * principal org.apache.wiki.auth.authorize.Role "Authenticated" { 072 * permission org.apache.wiki.auth.permissions.GroupPermission "*:<groupmember>", "edit"; 073 * </pre> 074 * 075 * means, “allow Authenticated users to edit any groups they are members of.” 076 * The wildcard target (*) does <em>not</em> imply <code><groupmember></code>; it 077 * must be granted explicitly. 078 * @since 2.4.17 079 */ 080public final class GroupPermission extends Permission implements Serializable 081{ 082 /** Special target token that denotes all groups that a Subject's Principals are members of. */ 083 public static final String MEMBER_TOKEN = "<groupmember>"; 084 085 private static final long serialVersionUID = 1L; 086 087 /** Action for deleting a group or collection of groups. */ 088 public static final String DELETE_ACTION = "delete"; 089 090 /** Action for editing a group or collection of groups. */ 091 public static final String EDIT_ACTION = "edit"; 092 093 /** Action for viewing a group or collection of groups. */ 094 public static final String VIEW_ACTION = "view"; 095 096 static final int DELETE_MASK = 0x4; 097 098 static final int EDIT_MASK = 0x2; 099 100 static final int VIEW_MASK = 0x1; 101 102 /** Convenience constant that denotes <code>GroupPermission( "*:*, "delete" )</code>. */ 103 public static final GroupPermission DELETE = new GroupPermission( DELETE_ACTION ); 104 105 /** Convenience constant that denotes <code>GroupPermission( "*:*, "edit" )</code>. */ 106 public static final GroupPermission EDIT = new GroupPermission( EDIT_ACTION ); 107 108 /** Convenience constant that denotes <code>GroupPermission( "*:*, "view" )</code>. */ 109 public static final GroupPermission VIEW = new GroupPermission( VIEW_ACTION ); 110 111 private static final String ACTION_SEPARATOR = ","; 112 113 private static final String WILDCARD = "*"; 114 115 private static final String WIKI_SEPARATOR = ":"; 116 117 private final String m_actionString; 118 119 private final int m_mask; 120 121 private final String m_group; 122 123 private final String m_wiki; 124 125 /** For serialization purposes */ 126 GroupPermission() 127 { 128 this(""); 129 } 130 131 /** 132 * Private convenience constructor that creates a new GroupPermission for 133 * all wikis and groups (*:*) and set of actions. 134 * @param actions 135 */ 136 private GroupPermission(final String actions ) 137 { 138 this( WILDCARD + WIKI_SEPARATOR + WILDCARD, actions ); 139 } 140 141 /** 142 * Creates a new GroupPermission for a specified group and set of actions. 143 * Group should include a prepended wiki name followed by a colon (:). If 144 * the wiki name is not supplied or starts with a colon, the group refers to 145 * all wikis. 146 * @param group the wiki group 147 * @param actions the allowed actions for this group 148 */ 149 public GroupPermission(final String group, final String actions ) 150 { 151 super( group ); 152 153 // Parse wiki and group (which may include wiki name and group) 154 // Strip out attachment separator; it is irrelevant. 155 final String[] pathParams = group.split( WIKI_SEPARATOR ); 156 final String groupName; 157 if ( pathParams.length >= 2 ) 158 { 159 m_wiki = !pathParams[0].isEmpty() ? pathParams[0] : null; 160 groupName = pathParams[1]; 161 } 162 else 163 { 164 m_wiki = WILDCARD; 165 groupName = pathParams[0]; 166 } 167 m_group = groupName; 168 169 // Parse actions 170 final String[] groupActions = actions.toLowerCase().split( ACTION_SEPARATOR ); 171 Arrays.sort( groupActions, String.CASE_INSENSITIVE_ORDER ); 172 m_mask = createMask( actions ); 173 final StringBuilder buffer = new StringBuilder(); 174 final int groupActionsLength = groupActions.length; 175 for( int i = 0; i < groupActionsLength; i++ ) 176 { 177 buffer.append( groupActions[i] ); 178 if ( i < ( groupActionsLength - 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(final Object obj ) 194 { 195 if ( !( obj instanceof GroupPermission ) ) 196 { 197 return false; 198 } 199 final 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 @Override 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 @Override 273 public boolean implies(final Permission permission ) 274 { 275 // Permission must be a GroupPermission 276 if ( !( permission instanceof GroupPermission ) ) 277 { 278 return false; 279 } 280 281 // Build up an "implied mask" 282 final GroupPermission p = (GroupPermission) permission; 283 final int impliedMask = impliedMask( m_mask ); 284 285 // If actions aren't a proper subset, return false 286 if ( ( impliedMask & p.m_mask ) != p.m_mask ) 287 { 288 return false; 289 } 290 291 // See if the tested permission's wiki is implied 292 final boolean impliedWiki = PagePermission.isSubset( m_wiki, p.m_wiki ); 293 294 // If this page is "*", the tested permission's 295 // group is implied, unless implied permission has <groupmember> token 296 final boolean impliedGroup; 297 if ( MEMBER_TOKEN.equals( p.m_group ) ) 298 { 299 impliedGroup = MEMBER_TOKEN.equals( m_group ); 300 } 301 else 302 { 303 impliedGroup = PagePermission.isSubset( m_group, p.m_group ); 304 } 305 306 // See if this permission is <groupmember> and Subject possesses 307 // GroupPrincipal matching the implied GroupPermission's group 308 final boolean impliedMember = impliesMember( p ); 309 310 return impliedWiki && ( impliedGroup || impliedMember ); 311 } 312 313 /** 314 * Prints a human-readable representation of this permission. 315 * @return the string 316 * @see java.lang.Object#toString() 317 */ 318 public String toString() 319 { 320 final String wiki = ( m_wiki == null ) ? "" : m_wiki; 321 return "(\"" + this.getClass().getName() + "\",\"" + wiki + WIKI_SEPARATOR + m_group + "\",\"" + getActions() 322 + "\")"; 323 } 324 325 /** 326 * Creates an “implied mask” based on the actions originally assigned: for 327 * example, delete implies edit; edit implies view. 328 * @param mask binary mask for actions 329 * @return binary mask for implied actions 330 */ 331 static int impliedMask( int mask ) 332 { 333 if ( ( mask & DELETE_MASK ) > 0 ) 334 { 335 mask |= EDIT_MASK; 336 } 337 if ( ( mask & EDIT_MASK ) > 0 ) 338 { 339 mask |= VIEW_MASK; 340 } 341 return mask; 342 } 343 344 /** 345 * Protected method that creates a binary mask based on the actions specified. 346 * This is used by {@link #implies(Permission)}. 347 * @param actions the actions for this permission, separated by commas 348 * @return the binary actions mask 349 */ 350 static int createMask( final String actions ) 351 { 352 if ( actions == null || actions.isEmpty() ) 353 { 354 throw new IllegalArgumentException( "Actions cannot be blank or null" ); 355 } 356 int mask = 0; 357 final String[] actionList = actions.split( ACTION_SEPARATOR ); 358 for( final String action : actionList ) 359 { 360 if ( action.equalsIgnoreCase( VIEW_ACTION ) ) 361 { 362 mask |= VIEW_MASK; 363 } 364 else if ( action.equalsIgnoreCase( EDIT_ACTION ) ) 365 { 366 mask |= EDIT_MASK; 367 } 368 else if ( action.equalsIgnoreCase( DELETE_ACTION ) ) 369 { 370 mask |= DELETE_MASK; 371 } 372 else 373 { 374 throw new IllegalArgumentException( "Unrecognized action: " + action ); 375 } 376 } 377 return mask; 378 } 379 380 /** 381 * <p> 382 * Returns <code>true</code> if this GroupPermission was created with the 383 * token <code><groupmember></code> 384 * <em>and</em> the current 385 * thread’s Subject is a member of the Group indicated by the implied 386 * GroupPermission. Thus, a GroupPermission with the group 387 * <code><groupmember></code> implies GroupPermission for group 388 * "TestGroup" only if the Subject is a member of TestGroup. 389 * </p> 390 * <p> 391 * We make this determination by obtaining the current {@link Thread}’s 392 * {@link java.security.AccessControlContext} and requesting the 393 * {@link javax.security.auth.SubjectDomainCombiner}. If the combiner is 394 * not <code>null</code>, then we know that the access check was 395 * requested using a {@link javax.security.auth.Subject}; that is, that an 396 * upstream caller caused a Subject to be associated with the Thread’s 397 * ProtectionDomain by executing a 398 * {@link javax.security.auth.Subject#doAs(Subject, java.security.PrivilegedAction)} 399 * operation. 400 * </p> 401 * <p> 402 * If a SubjectDomainCombiner exists, determining group membership is 403 * simple: just iterate through the Subject’s Principal set and look for all 404 * Principals of type {@link org.apache.wiki.auth.GroupPrincipal}. If the 405 * name of any Principal matches the value of the implied Permission’s 406 * {@link GroupPermission#getGroup()} value, then the Subject is a member of 407 * this group -- and therefore this <code>impliesMember</code> call 408 * returns <code>true</code>. 409 * </p> 410 * <p> 411 * This may sound complicated, but it really isn’t. Consider the following 412 * examples: 413 * </p> 414 * <table border="1"> <thead> 415 * <tr> 416 * <th width="25%">This object</th> 417 * <th width="25%"><code>impliesMember</code> parameter</th> 418 * <th width="25%">Calling Subject’s Principals 419 * <th width="25%">Result</th> 420 * </tr> 421 * <tr> 422 * <td><code>GroupPermission ("<groupmember>")</code></td> 423 * <td><code>GroupPermission ("*:TestGroup")</code></td> 424 * <td><code>WikiPrincipal ("Biff"),<br/>GroupPrincipal ("TestGroup")</code></td> 425 * <td><code>true</code></td> 426 * </tr> 427 * <tr> 428 * <td><code>GroupPermission ("*:TestGroup")</code></td> 429 * <td><code>GroupPermission ("*:TestGroup")</code></td> 430 * <td><code>WikiPrincipal ("Biff"),<br/>GroupPrincipal ("TestGroup")</code></td> 431 * <td><code>false</code> - this object does not contain 432 * <code><groupmember></code></td> 433 * </tr> 434 * <tr> 435 * <td><code>GroupPermission ("<groupmember>")</code></td> 436 * <td><code>GroupPermission ("*:TestGroup")</code></td> 437 * <td><code>WikiPrincipal ("Biff"),<br/>GroupPrincipal ("FooGroup")</code></td> 438 * <td><code>false</code> - Subject does not contain GroupPrincipal 439 * matching implied Permission’s group (TestGroup)</td> 440 * </tr> 441 * <tr> 442 * <td><code>GroupPermission ("<groupmember>")</code></td> 443 * <td><code>WikiPermission ("*:createGroups")</code></td> 444 * <td><code>WikiPrincipal ("Biff"),<br/>GroupPrincipal ("TestGroup")</code></td> 445 * <td><code>false</code> - implied permission not of type 446 * GroupPermission</td> 447 * </tr> 448 * <tr> 449 * <td><code>GroupPermission ("<groupmember>")</code></td> 450 * <td><code>GroupPermission ("*:TestGroup")</code></td> 451 * <td>-</td> 452 * <td><code>false</code> - <code>Subject.doAs()</code> not called 453 * upstream</td> 454 * </tr> 455 * </table> 456 * <p> 457 * Note that JSPWiki’s access control checks are made inside of 458 * {@link org.apache.wiki.auth.AuthorizationManager#checkPermission(org.apache.wiki.api.core.Session, Permission)}, 459 * which performs a <code>Subject.doAs()</code> call. Thus, this 460 * Permission functions exactly the way it should during normal 461 * operations. 462 * </p> 463 * @param permission the implied permission 464 * @return <code>true</code> if the calling Thread’s Subject contains a 465 * GroupPrincipal matching the implied GroupPermission’s group; 466 * <code>false</code> otherwise 467 */ 468 boolean impliesMember(final Permission permission ) 469 { 470 if ( !( permission instanceof GroupPermission ) ) 471 { 472 return false; 473 } 474 final GroupPermission gp = (GroupPermission) permission; 475 if ( !MEMBER_TOKEN.equals( m_group ) ) 476 { 477 return false; 478 } 479 480 // For the current thread, retrieve the SubjectDomainCombiner 481 // (if one was used to create current AccessControlContext ) 482 final AccessControlContext acc = AccessController.getContext(); 483 final DomainCombiner dc = acc.getDomainCombiner(); 484 if ( dc != null && dc instanceof SubjectDomainCombiner ) 485 { 486 // <member> implies permission if subject possesses 487 // GroupPrincipal with same name as target 488 final Subject subject = ( (SubjectDomainCombiner) dc ).getSubject(); 489 final Set<GroupPrincipal> principals = subject.getPrincipals( GroupPrincipal.class ); 490 return principals.stream().anyMatch(principal -> principal.getName().equals(gp.m_group)); 491 } 492 return false; 493 } 494}