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