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