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.Permission; 023 import java.security.PermissionCollection; 024 import java.util.Arrays; 025 026 import org.apache.commons.lang.StringUtils; 027 028 import org.apache.wiki.WikiPage; 029 030 /** 031 * <p> 032 * Permission to perform an operation on a single page or collection of pages in 033 * a given wiki. Permission actions include: <code>view</code>, 034 * <code>edit</code> (edit the text of a wiki page), <code>comment</code>, 035 * <code>upload</code>, <code>modify</code> (edit text and upload 036 * attachments), <code>delete</code> 037 * and <code>rename</code>. 038 * </p> 039 * <p> 040 * The target of a permission is a single page 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 page. "All wikis" can be specified using a wildcard (*). Page 043 * collections may also be specified using a wildcard. For pages, the wildcard 044 * may be a prefix, suffix, or all by itself. Examples of targets include: 045 * </p> 046 * <blockquote><code>*:*<br/> 047 * *:JanneJalkanen<br/> 048 * *:Jalkanen<br/> 049 * *:Janne*<br/> 050 * mywiki:JanneJalkanen<br/> 051 * mywiki:*Jalkanen<br/> 052 * mywiki:Janne*</code> 053 * </blockquote> 054 * <p> 055 * For a given target, certain permissions imply others: 056 * </p> 057 * <ul> 058 * <li><code>delete</code> and <code>rename</code> imply <code>edit</code></li> 059 * <li><code>modify</code> implies <code>edit</code> and <code>upload</code></li> 060 * <li><code>edit</code> implies <code>comment</code> and <code>view</code></li> 061 * <li><code>comment</code> and <code>upload</code> imply <code>view</code></li> 062 * Targets that do not include a wiki prefix <i>never </i> imply others. 063 * </ul> 064 * @since 2.3 065 */ 066 public final class PagePermission extends Permission implements Serializable 067 { 068 private static final long serialVersionUID = 2L; 069 070 /** Action name for the comment permission. */ 071 public static final String COMMENT_ACTION = "comment"; 072 073 /** Action name for the delete permission. */ 074 public static final String DELETE_ACTION = "delete"; 075 076 /** Action name for the edit permission. */ 077 public static final String EDIT_ACTION = "edit"; 078 079 /** Action name for the modify permission. */ 080 public static final String MODIFY_ACTION = "modify"; 081 082 /** Action name for the rename permission. */ 083 public static final String RENAME_ACTION = "rename"; 084 085 /** Action name for the upload permission. */ 086 public static final String UPLOAD_ACTION = "upload"; 087 088 /** Action name for the view permission. */ 089 public static final String VIEW_ACTION = "view"; 090 091 protected static final int COMMENT_MASK = 0x4; 092 093 protected static final int DELETE_MASK = 0x10; 094 095 protected static final int EDIT_MASK = 0x2; 096 097 protected static final int MODIFY_MASK = 0x40; 098 099 protected static final int RENAME_MASK = 0x20; 100 101 protected static final int UPLOAD_MASK = 0x8; 102 103 protected static final int VIEW_MASK = 0x1; 104 105 /** A static instance of the comment permission. */ 106 public static final PagePermission COMMENT = new PagePermission( COMMENT_ACTION ); 107 108 /** A static instance of the delete permission. */ 109 public static final PagePermission DELETE = new PagePermission( DELETE_ACTION ); 110 111 /** A static instance of the edit permission. */ 112 public static final PagePermission EDIT = new PagePermission( EDIT_ACTION ); 113 114 /** A static instance of the rename permission. */ 115 public static final PagePermission RENAME = new PagePermission( RENAME_ACTION ); 116 117 /** A static instance of the modify permission. */ 118 public static final PagePermission MODIFY = new PagePermission( MODIFY_ACTION ); 119 120 /** A static instance of the upload permission. */ 121 public static final PagePermission UPLOAD = new PagePermission( UPLOAD_ACTION ); 122 123 /** A static instance of the view permission. */ 124 public static final PagePermission VIEW = new PagePermission( VIEW_ACTION ); 125 126 private static final String ACTION_SEPARATOR = ","; 127 128 private static final String WILDCARD = "*"; 129 130 private static final String WIKI_SEPARATOR = ":"; 131 132 private static final String ATTACHMENT_SEPARATOR = "/"; 133 134 private final String m_actionString; 135 136 private final int m_mask; 137 138 private final String m_page; 139 140 private final String m_wiki; 141 142 /** For serialization purposes. */ 143 protected PagePermission() 144 { 145 this(""); 146 } 147 148 /** 149 * Private convenience constructor that creates a new PagePermission for all wikis and pages 150 * (*:*) and set of actions. 151 * @param actions 152 */ 153 private PagePermission( String actions ) 154 { 155 this( WILDCARD + WIKI_SEPARATOR + WILDCARD, actions ); 156 } 157 158 /** 159 * Creates a new PagePermission for a specified page name and set of 160 * actions. Page should include a prepended wiki name followed by a colon (:). 161 * If the wiki name is not supplied or starts with a colon, the page 162 * refers to no wiki in particular, and will never imply any other 163 * PagePermission. 164 * @param page the wiki page 165 * @param actions the allowed actions for this page 166 */ 167 public PagePermission( String page, String actions ) 168 { 169 super( page ); 170 171 // Parse wiki and page (which may include wiki name and page) 172 // Strip out attachment separator; it is irrelevant. 173 174 // FIXME3.0: Assumes attachment separator is "/". 175 String[] pathParams = StringUtils.split( page, WIKI_SEPARATOR ); 176 String pageName; 177 if ( pathParams.length >= 2 ) 178 { 179 m_wiki = pathParams[0].length() > 0 ? pathParams[0] : null; 180 pageName = pathParams[1]; 181 } 182 else 183 { 184 m_wiki = null; 185 pageName = pathParams[0]; 186 } 187 int pos = pageName.indexOf( ATTACHMENT_SEPARATOR ); 188 m_page = ( pos == -1 ) ? pageName : pageName.substring( 0, pos ); 189 190 // Parse actions 191 String[] pageActions = StringUtils.split( actions.toLowerCase(), ACTION_SEPARATOR ); 192 Arrays.sort( pageActions, String.CASE_INSENSITIVE_ORDER ); 193 m_mask = createMask( actions ); 194 StringBuffer buffer = new StringBuffer(); 195 for( int i = 0; i < pageActions.length; i++ ) 196 { 197 buffer.append( pageActions[i] ); 198 if ( i < ( pageActions.length - 1 ) ) 199 { 200 buffer.append( ACTION_SEPARATOR ); 201 } 202 } 203 m_actionString = buffer.toString(); 204 } 205 206 /** 207 * Creates a new PagePermission for a specified page and set of actions. 208 * @param page The wikipage. 209 * @param actions A set of actions; a comma-separated list of actions. 210 */ 211 public PagePermission( WikiPage page, String actions ) 212 { 213 this( page.getWiki() + WIKI_SEPARATOR + page.getName(), actions ); 214 } 215 216 /** 217 * Two PagePermission objects are considered equal if their actions (after 218 * normalization), wiki and target are equal. 219 * @param obj {@inheritDoc} 220 * @return {@inheritDoc} 221 */ 222 public boolean equals( Object obj ) 223 { 224 if ( !( obj instanceof PagePermission ) ) 225 { 226 return false; 227 } 228 PagePermission p = (PagePermission) obj; 229 return p.m_mask == m_mask && p.m_page.equals( m_page ) 230 && p.m_wiki != null && p.m_wiki.equals( m_wiki ); 231 } 232 233 /** 234 * Returns the actions for this permission: "view", "edit", "comment", 235 * "modify", "upload" or "delete". The actions will always be sorted in alphabetic 236 * order, and will always appear in lower case. 237 * 238 * @return {@inheritDoc} 239 */ 240 public String getActions() 241 { 242 return m_actionString; 243 } 244 245 /** 246 * Returns the name of the wiki page represented by this permission. 247 * @return the page name 248 */ 249 public String getPage() 250 { 251 return m_page; 252 } 253 254 /** 255 * Returns the name of the wiki containing the page represented by 256 * this permission; may return the wildcard string. 257 * @return the wiki 258 */ 259 public String getWiki() 260 { 261 return m_wiki; 262 } 263 264 /** 265 * Returns the hash code for this PagePermission. 266 * @return {@inheritDoc} 267 */ 268 public int hashCode() 269 { 270 // If the wiki has not been set, uses a dummy value for the hashcode 271 // calculation. This may occur if the page given does not refer 272 // to any particular wiki 273 String wiki = m_wiki != null ? m_wiki : "dummy_value"; 274 return m_mask + ( ( 13 * m_actionString.hashCode() ) * 23 * wiki.hashCode() ); 275 } 276 277 /** 278 * <p> 279 * PagePermission can only imply other PagePermissions; no other permission 280 * types are implied. One PagePermission implies another if its actions if 281 * three conditions are met: 282 * </p> 283 * <ol> 284 * <li>The other PagePermission's wiki is equal to, or a subset of, that of 285 * this permission. This permission's wiki is considered a superset of the 286 * other if it contains a matching prefix plus a wildcard, or a wildcard 287 * followed by a matching suffix.</li> 288 * <li>The other PagePermission's target is equal to, or a subset of, the 289 * target specified by this permission. This permission's target is 290 * considered a superset of the other if it contains a matching prefix plus 291 * a wildcard, or a wildcard followed by a matching suffix.</li> 292 * <li>All of other PagePermission's actions are equal to, or a subset of, 293 * those of this permission</li> 294 * </ol> 295 * @see java.security.Permission#implies(java.security.Permission) 296 * 297 * @param permission {@inheritDoc} 298 * @return {@inheritDoc} 299 */ 300 public boolean implies( Permission permission ) 301 { 302 // Permission must be a PagePermission 303 if ( !( permission instanceof PagePermission ) ) 304 { 305 return false; 306 } 307 308 // Build up an "implied mask" 309 PagePermission p = (PagePermission) permission; 310 int impliedMask = impliedMask( m_mask ); 311 312 // If actions aren't a proper subset, return false 313 if ( ( impliedMask & p.m_mask ) != p.m_mask ) 314 { 315 return false; 316 } 317 318 // See if the tested permission's wiki is implied 319 boolean impliedWiki = isSubset( m_wiki, p.m_wiki ); 320 321 // If this page is "*", the tested permission's 322 // page is implied 323 boolean impliedPage = isSubset( m_page, p.m_page ); 324 325 return impliedWiki && impliedPage; 326 } 327 328 /** 329 * Returns a new {@link AllPermissionCollection}. 330 * @see java.security.Permission#newPermissionCollection() 331 * @return {@inheritDoc} 332 */ 333 @Override 334 public PermissionCollection newPermissionCollection() 335 { 336 return new AllPermissionCollection(); 337 } 338 339 /** 340 * Prints a human-readable representation of this permission. 341 * @see java.lang.Object#toString() 342 * 343 * @return Something human-readable 344 */ 345 public String toString() 346 { 347 String wiki = ( m_wiki == null ) ? "" : m_wiki; 348 return "(\"" + this.getClass().getName() + "\",\"" + wiki + WIKI_SEPARATOR + m_page + "\",\"" + getActions() + "\")"; 349 } 350 351 /** 352 * Creates an "implied mask" based on the actions originally assigned: for 353 * example, delete implies modify, comment, upload and view. 354 * @param mask binary mask for actions 355 * @return binary mask for implied actions 356 */ 357 protected static int impliedMask( int mask ) 358 { 359 if ( ( mask & DELETE_MASK ) > 0 ) 360 { 361 mask |= MODIFY_MASK; 362 } 363 if ( ( mask & RENAME_MASK ) > 0 ) 364 { 365 mask |= EDIT_MASK; 366 } 367 if ( ( mask & MODIFY_MASK ) > 0 ) 368 { 369 mask |= EDIT_MASK | UPLOAD_MASK; 370 } 371 if ( ( mask & EDIT_MASK ) > 0 ) 372 { 373 mask |= COMMENT_MASK; 374 } 375 if ( ( mask & COMMENT_MASK ) > 0 ) 376 { 377 mask |= VIEW_MASK; 378 } 379 if ( ( mask & UPLOAD_MASK ) > 0 ) 380 { 381 mask |= VIEW_MASK; 382 } 383 return mask; 384 } 385 386 /** 387 * Determines whether one target string is a logical subset of the other. 388 * @param superSet the prospective superset 389 * @param subSet the prospective subset 390 * @return the results of the test, where <code>true</code> indicates that 391 * <code>subSet</code> is a subset of <code>superSet</code> 392 */ 393 protected static boolean isSubset( String superSet, String subSet ) 394 { 395 // If either is null, return false 396 if ( superSet == null || subSet == null ) 397 { 398 return false; 399 } 400 401 // If targets are identical, it's a subset 402 if ( superSet.equals( subSet ) ) 403 { 404 return true; 405 } 406 407 // If super is "*", it's a subset 408 if ( superSet.equals( WILDCARD ) ) 409 { 410 return true; 411 } 412 413 // If super starts with "*", sub must end with everything after the * 414 if ( superSet.startsWith( WILDCARD ) ) 415 { 416 String suffix = superSet.substring( 1 ); 417 return subSet.endsWith( suffix ); 418 } 419 420 // If super ends with "*", sub must start with everything before * 421 if ( superSet.endsWith( WILDCARD ) ) 422 { 423 String prefix = superSet.substring( 0, superSet.length() - 1 ); 424 return subSet.startsWith( prefix ); 425 } 426 427 return false; 428 } 429 430 /** 431 * Private method that creates a binary mask based on the actions specified. 432 * This is used by {@link #implies(Permission)}. 433 * @param actions the actions for this permission, separated by commas 434 * @return the binary actions mask 435 */ 436 protected static int createMask( String actions ) 437 { 438 if ( actions == null || actions.length() == 0 ) 439 { 440 throw new IllegalArgumentException( "Actions cannot be blank or null" ); 441 } 442 int mask = 0; 443 String[] actionList = StringUtils.split( actions, ACTION_SEPARATOR ); 444 for( String action : actionList ) 445 { 446 if ( action.equalsIgnoreCase( VIEW_ACTION ) ) 447 { 448 mask |= VIEW_MASK; 449 } 450 else if ( action.equalsIgnoreCase( EDIT_ACTION ) ) 451 { 452 mask |= EDIT_MASK; 453 } 454 else if ( action.equalsIgnoreCase( COMMENT_ACTION ) ) 455 { 456 mask |= COMMENT_MASK; 457 } 458 else if ( action.equalsIgnoreCase( MODIFY_ACTION ) ) 459 { 460 mask |= MODIFY_MASK; 461 } 462 else if ( action.equalsIgnoreCase( UPLOAD_ACTION ) ) 463 { 464 mask |= UPLOAD_MASK; 465 } 466 else if ( action.equalsIgnoreCase( DELETE_ACTION ) ) 467 { 468 mask |= DELETE_MASK; 469 } 470 else if ( action.equalsIgnoreCase( RENAME_ACTION ) ) 471 { 472 mask |= RENAME_MASK; 473 } 474 else 475 { 476 throw new IllegalArgumentException( "Unrecognized action: " + action ); 477 } 478 } 479 return mask; 480 } 481 }