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