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 public String getActions() 239 { 240 return m_actionString; 241 } 242 243 /** 244 * Returns the name of the wiki page represented by this permission. 245 * @return the page name 246 */ 247 public String getPage() 248 { 249 return m_page; 250 } 251 252 /** 253 * Returns the name of the wiki containing the page represented by 254 * this permission; may return the wildcard string. 255 * @return the wiki 256 */ 257 public String getWiki() 258 { 259 return m_wiki; 260 } 261 262 /** 263 * Returns the hash code for this PagePermission. 264 * @return {@inheritDoc} 265 */ 266 public int hashCode() { 267 // If the wiki has not been set, uses a dummy value for the hashcode 268 // calculation. This may occur if the page given does not refer 269 // to any particular wiki 270 final String wiki = m_wiki != null ? m_wiki : "dummy_value"; 271 return m_mask + ( ( 13 * m_actionString.hashCode() ) * 23 * wiki.hashCode() ); 272 } 273 274 /** 275 * <p> 276 * PagePermission can only imply other PagePermissions; no other permission 277 * types are implied. One PagePermission implies another if its actions if 278 * three conditions are met: 279 * </p> 280 * <ol> 281 * <li>The other PagePermission's wiki is equal to, or a subset of, that of 282 * this permission. This permission's wiki is considered a superset of the 283 * other if it contains a matching prefix plus a wildcard, or a wildcard 284 * followed by a matching suffix.</li> 285 * <li>The other PagePermission's target is equal to, or a subset of, the 286 * target specified by this permission. This permission's target is 287 * considered a superset of the other if it contains a matching prefix plus 288 * a wildcard, or a wildcard followed by a matching suffix.</li> 289 * <li>All of other PagePermission's actions are equal to, or a subset of, 290 * those of this permission</li> 291 * </ol> 292 * @see java.security.Permission#implies(java.security.Permission) 293 * 294 * @param permission {@inheritDoc} 295 * @return {@inheritDoc} 296 */ 297 public boolean implies( final Permission permission ) 298 { 299 // Permission must be a PagePermission 300 if ( !( permission instanceof PagePermission ) ) 301 { 302 return false; 303 } 304 305 // Build up an "implied mask" 306 final PagePermission p = (PagePermission) permission; 307 final int impliedMask = impliedMask( m_mask ); 308 309 // If actions aren't a proper subset, return false 310 if ( ( impliedMask & p.m_mask ) != p.m_mask ) 311 { 312 return false; 313 } 314 315 // See if the tested permission's wiki is implied 316 final boolean impliedWiki = isSubset( m_wiki, p.m_wiki ); 317 318 // If this page is "*", the tested permission's 319 // page is implied 320 final boolean impliedPage = isSubset( m_page, p.m_page ); 321 322 return impliedWiki && impliedPage; 323 } 324 325 /** 326 * Returns a new {@link AllPermissionCollection}. 327 * @see java.security.Permission#newPermissionCollection() 328 * @return {@inheritDoc} 329 */ 330 @Override 331 public PermissionCollection newPermissionCollection() 332 { 333 return new AllPermissionCollection(); 334 } 335 336 /** 337 * Prints a human-readable representation of this permission. 338 * @see java.lang.Object#toString() 339 * 340 * @return Something human-readable 341 */ 342 public String toString() 343 { 344 final String wiki = ( m_wiki == null ) ? "" : m_wiki; 345 return "(\"" + this.getClass().getName() + "\",\"" + wiki + WIKI_SEPARATOR + m_page + "\",\"" + getActions() + "\")"; 346 } 347 348 /** 349 * Creates an "implied mask" based on the actions originally assigned: for 350 * example, delete implies modify, comment, upload and view. 351 * @param mask binary mask for actions 352 * @return binary mask for implied actions 353 */ 354 static int impliedMask( int mask ) 355 { 356 if ( ( mask & DELETE_MASK ) > 0 ) 357 { 358 mask |= MODIFY_MASK; 359 } 360 if ( ( mask & RENAME_MASK ) > 0 ) 361 { 362 mask |= EDIT_MASK; 363 } 364 if ( ( mask & MODIFY_MASK ) > 0 ) 365 { 366 mask |= EDIT_MASK | UPLOAD_MASK; 367 } 368 if ( ( mask & EDIT_MASK ) > 0 ) 369 { 370 mask |= COMMENT_MASK; 371 } 372 if ( ( mask & COMMENT_MASK ) > 0 ) 373 { 374 mask |= VIEW_MASK; 375 } 376 if ( ( mask & UPLOAD_MASK ) > 0 ) 377 { 378 mask |= VIEW_MASK; 379 } 380 return mask; 381 } 382 383 /** 384 * Determines whether one target string is a logical subset of the other. 385 * @param superSet the prospective superset 386 * @param subSet the prospective subset 387 * @return the results of the test, where <code>true</code> indicates that 388 * <code>subSet</code> is a subset of <code>superSet</code> 389 */ 390 static boolean isSubset( final String superSet, final String subSet ) 391 { 392 // If either is null, return false 393 if ( superSet == null || subSet == null ) 394 { 395 return false; 396 } 397 398 // If targets are identical, it's a subset 399 if ( superSet.equals( subSet ) ) 400 { 401 return true; 402 } 403 404 // If super is "*", it's a subset 405 if ( superSet.equals( WILDCARD ) ) 406 { 407 return true; 408 } 409 410 // If super starts with "*", sub must end with everything after the * 411 if ( superSet.startsWith( WILDCARD ) ) 412 { 413 final String suffix = superSet.substring( 1 ); 414 return subSet.endsWith( suffix ); 415 } 416 417 // If super ends with "*", sub must start with everything before * 418 if ( superSet.endsWith( WILDCARD ) ) 419 { 420 final String prefix = superSet.substring( 0, superSet.length() - 1 ); 421 return subSet.startsWith( prefix ); 422 } 423 424 return false; 425 } 426 427 /** 428 * Private method that creates a binary mask based on the actions specified. 429 * This is used by {@link #implies(Permission)}. 430 * @param actions the actions for this permission, separated by commas 431 * @return the binary actions mask 432 */ 433 static int createMask( final String actions ) 434 { 435 if ( actions == null || actions.isEmpty() ) 436 { 437 throw new IllegalArgumentException( "Actions cannot be blank or null" ); 438 } 439 int mask = 0; 440 final String[] actionList = StringUtils.split( actions, ACTION_SEPARATOR ); 441 for( final String action : actionList ) 442 { 443 if ( action.equalsIgnoreCase( VIEW_ACTION ) ) 444 { 445 mask |= VIEW_MASK; 446 } 447 else if ( action.equalsIgnoreCase( EDIT_ACTION ) ) 448 { 449 mask |= EDIT_MASK; 450 } 451 else if ( action.equalsIgnoreCase( COMMENT_ACTION ) ) 452 { 453 mask |= COMMENT_MASK; 454 } 455 else if ( action.equalsIgnoreCase( MODIFY_ACTION ) ) 456 { 457 mask |= MODIFY_MASK; 458 } 459 else if ( action.equalsIgnoreCase( UPLOAD_ACTION ) ) 460 { 461 mask |= UPLOAD_MASK; 462 } 463 else if ( action.equalsIgnoreCase( DELETE_ACTION ) ) 464 { 465 mask |= DELETE_MASK; 466 } 467 else if ( action.equalsIgnoreCase( RENAME_ACTION ) ) 468 { 469 mask |= RENAME_MASK; 470 } 471 else 472 { 473 throw new IllegalArgumentException( "Unrecognized action: " + action ); 474 } 475 } 476 return mask; 477 } 478}