    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>,&nbsp;
    033 * <code>edit</code> (edit the text of a wiki page),&nbsp;<code>comment</code>,&nbsp;
    034 * <code>upload</code>,&nbsp;<code>modify</code>&nbsp;(edit text and upload
    035 * attachments),&nbsp;<code>delete</code>&nbsp;
    036 * and&nbsp;<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>&nbsp;and&nbsp;<code>rename</code>&nbsp;imply&nbsp;<code>edit</code></li>
    058 * <li><code>modify</code>&nbsp;implies&nbsp;<code>edit</code>&nbsp;and&nbsp;<code>upload</code></li>
    059 * <li><code>edit</code>&nbsp;implies&nbsp;<code>comment</code>&nbsp;and&nbsp;<code>view</code></li>
    060 * <li><code>comment</code>&nbsp;and&nbsp;<code>upload</code>&nbsp;imply&nbsp;<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}