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    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}