001: /*
002: JSPWiki - a JSP-based WikiWiki clone.
003:
004: Copyright (C) 2001-2007 JSPWiki development group
005:
006: This program is free software; you can redistribute it and/or modify
007: it under the terms of the GNU Lesser General Public License as published by
008: the Free Software Foundation; either version 2.1 of the License, or
009: (at your option) any later version.
010:
011: This program is distributed in the hope that it will be useful,
012: but WITHOUT ANY WARRANTY; without even the implied warranty of
013: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014: GNU Lesser General Public License for more details.
015:
016: You should have received a copy of the GNU Lesser General Public License
017: along with this program; if not, write to the Free Software
018: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
019: */
020: package com.ecyrd.jspwiki.auth.permissions;
021:
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 com.ecyrd.jspwiki.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>,
034: * <code>edit</code> (edit the text of a wiki page), <code>comment</code>,
035: * <code>upload</code>, <code>modify</code> (edit text and upload
036: * attachments), <code>delete</code>
037: * and <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> and <code>rename</code> imply <code>edit</code></li>
059: * <li><code>modify</code> implies <code>edit</code> and <code>upload</code></li>
060: * <li><code>edit</code> implies <code>comment</code> and <code>view</code></li>
061: * <li><code>comment</code> and <code>upload</code> imply <code>view</code></li>
062: * Targets that do not include a wiki prefix <i>never </i> imply others.
063: * </ul>
064: * @author Andrew Jaquith
065: * @since 2.3
066: */
067: public final class PagePermission extends Permission {
068: private static final long serialVersionUID = 2L;
069:
070: public static final String COMMENT_ACTION = "comment";
071:
072: public static final String DELETE_ACTION = "delete";
073:
074: public static final String EDIT_ACTION = "edit";
075:
076: public static final String MODIFY_ACTION = "modify";
077:
078: public static final String RENAME_ACTION = "rename";
079:
080: public static final String UPLOAD_ACTION = "upload";
081:
082: public static final String VIEW_ACTION = "view";
083:
084: protected static final int COMMENT_MASK = 0x4;
085:
086: protected static final int DELETE_MASK = 0x10;
087:
088: protected static final int EDIT_MASK = 0x2;
089:
090: protected static final int MODIFY_MASK = 0x40;
091:
092: protected static final int RENAME_MASK = 0x20;
093:
094: protected static final int UPLOAD_MASK = 0x8;
095:
096: protected static final int VIEW_MASK = 0x1;
097:
098: public static final PagePermission COMMENT = new PagePermission(
099: COMMENT_ACTION);
100:
101: public static final PagePermission DELETE = new PagePermission(
102: DELETE_ACTION);
103:
104: public static final PagePermission EDIT = new PagePermission(
105: EDIT_ACTION);
106:
107: public static final PagePermission RENAME = new PagePermission(
108: RENAME_ACTION);
109:
110: public static final PagePermission MODIFY = new PagePermission(
111: MODIFY_ACTION);
112:
113: public static final PagePermission UPLOAD = new PagePermission(
114: UPLOAD_ACTION);
115:
116: public static final PagePermission VIEW = new PagePermission(
117: VIEW_ACTION);
118:
119: private static final String ACTION_SEPARATOR = ",";
120:
121: private static final String WILDCARD = "*";
122:
123: private static final String WIKI_SEPARATOR = ":";
124:
125: private static final String ATTACHMENT_SEPARATOR = "/";
126:
127: private final String m_actionString;
128:
129: private final int m_mask;
130:
131: private final String m_page;
132:
133: private final String m_wiki;
134:
135: /**
136: * Private convenience constructor that creates a new PagePermission for all wikis and pages
137: * (*:*) and set of actions.
138: * @param actions
139: */
140: private PagePermission(String actions) {
141: this (WILDCARD + WIKI_SEPARATOR + WILDCARD, actions);
142: }
143:
144: /**
145: * Creates a new PagePermission for a specified page name and set of
146: * actions. Page should include a prepended wiki name followed by a colon (:).
147: * If the wiki name is not supplied or starts with a colon, the page
148: * refers to no wiki in particular, and will never imply any other
149: * PagePermission.
150: * @param page the wiki page
151: * @param actions the allowed actions for this page
152: */
153: public PagePermission(String page, String actions) {
154: super (page);
155:
156: // Parse wiki and page (which may include wiki name and page)
157: // Strip out attachment separator; it is irrelevant.
158:
159: // FIXME3.0: Assumes attachment separator is "/".
160: String[] pathParams = StringUtils.split(page, WIKI_SEPARATOR);
161: String pageName;
162: if (pathParams.length >= 2) {
163: m_wiki = pathParams[0].length() > 0 ? pathParams[0] : null;
164: pageName = pathParams[1];
165: } else {
166: m_wiki = null;
167: pageName = pathParams[0];
168: }
169: int pos = pageName.indexOf(ATTACHMENT_SEPARATOR);
170: m_page = (pos == -1) ? pageName : pageName.substring(0, pos);
171:
172: // Parse actions
173: String[] pageActions = StringUtils.split(actions.toLowerCase(),
174: ACTION_SEPARATOR);
175: Arrays.sort(pageActions, String.CASE_INSENSITIVE_ORDER);
176: m_mask = createMask(actions);
177: StringBuffer buffer = new StringBuffer();
178: for (int i = 0; i < pageActions.length; i++) {
179: buffer.append(pageActions[i]);
180: if (i < (pageActions.length - 1)) {
181: buffer.append(ACTION_SEPARATOR);
182: }
183: }
184: m_actionString = buffer.toString();
185: }
186:
187: /**
188: * Creates a new PagePermission for a specified page and set of actions.
189: * @param page The wikipage.
190: * @param actions A set of actions; a comma-separated list of actions.
191: */
192: public PagePermission(WikiPage page, String actions) {
193: this (page.getWiki() + WIKI_SEPARATOR + page.getName(), actions);
194: }
195:
196: /**
197: * Two PagePermission objects are considered equal if their actions (after
198: * normalization), wiki and target are equal.
199: * @see java.lang.Object#equals(java.lang.Object)
200: */
201: public final boolean equals(Object obj) {
202: if (!(obj instanceof PagePermission)) {
203: return false;
204: }
205: PagePermission p = (PagePermission) obj;
206: return p.m_mask == m_mask && p.m_page.equals(m_page)
207: && p.m_wiki != null && p.m_wiki.equals(m_wiki);
208: }
209:
210: /**
211: * Returns the actions for this permission: "view", "edit", "comment",
212: * "modify", "upload" or "delete". The actions will always be sorted in alphabetic
213: * order, and will always appear in lower case.
214: * @see java.security.Permission#getActions()
215: */
216: public final String getActions() {
217: return m_actionString;
218: }
219:
220: /**
221: * Returns the name of the wiki page represented by this permission.
222: * @return the page name
223: */
224: public final String getPage() {
225: return m_page;
226: }
227:
228: /**
229: * Returns the name of the wiki containing the page represented by
230: * this permission; may return the wildcard string.
231: * @return the wiki
232: */
233: public final String getWiki() {
234: return m_wiki;
235: }
236:
237: /**
238: * Returns the hash code for this PagePermission.
239: * @see java.lang.Object#hashCode()
240: */
241: public final int hashCode() {
242: // If the wiki has not been set, uses a dummy value for the hashcode
243: // calculation. This may occur if the page given does not refer
244: // to any particular wiki
245: String wiki = m_wiki != null ? m_wiki : "dummy_value";
246: return m_mask
247: + ((13 * m_actionString.hashCode()) * 23 * wiki
248: .hashCode());
249: }
250:
251: /**
252: * <p>
253: * PagePermission can only imply other PagePermissions; no other permission
254: * types are implied. One PagePermission implies another if its actions if
255: * three conditions are met:
256: * </p>
257: * <ol>
258: * <li>The other PagePermission's wiki is equal to, or a subset of, that of
259: * this permission. This permission's wiki is considered a superset of the
260: * other if it contains a matching prefix plus a wildcard, or a wildcard
261: * followed by a matching suffix.</li>
262: * <li>The other PagePermission's target is equal to, or a subset of, the
263: * target specified by this permission. This permission's target is
264: * considered a superset of the other if it contains a matching prefix plus
265: * a wildcard, or a wildcard followed by a matching suffix.</li>
266: * <li>All of other PagePermission's actions are equal to, or a subset of,
267: * those of this permission</li>
268: * </ol>
269: * @see java.security.Permission#implies(java.security.Permission)
270: */
271: public final boolean implies(Permission permission) {
272: // Permission must be a PagePermission
273: if (!(permission instanceof PagePermission)) {
274: return false;
275: }
276:
277: // Build up an "implied mask"
278: PagePermission p = (PagePermission) permission;
279: int impliedMask = impliedMask(m_mask);
280:
281: // If actions aren't a proper subset, return false
282: if ((impliedMask & p.m_mask) != p.m_mask) {
283: return false;
284: }
285:
286: // See if the tested permission's wiki is implied
287: boolean impliedWiki = isSubset(m_wiki, p.m_wiki);
288:
289: // If this page is "*", the tested permission's
290: // page is implied
291: boolean impliedPage = isSubset(m_page, p.m_page);
292:
293: return impliedWiki && impliedPage;
294: }
295:
296: /**
297: * Returns a new {@link AllPermissionCollection}.
298: * @see java.security.Permission#newPermissionCollection()
299: */
300: public PermissionCollection newPermissionCollection() {
301: return new AllPermissionCollection();
302: }
303:
304: /**
305: * Prints a human-readable representation of this permission.
306: * @see java.lang.Object#toString()
307: */
308: public final String toString() {
309: String wiki = (m_wiki == null) ? "" : m_wiki;
310: return "(\"" + this .getClass().getName() + "\",\"" + wiki
311: + WIKI_SEPARATOR + m_page + "\",\"" + getActions()
312: + "\")";
313: }
314:
315: /**
316: * Creates an "implied mask" based on the actions originally assigned: for
317: * example, delete implies modify, comment, upload and view.
318: * @param mask binary mask for actions
319: * @return binary mask for implied actions
320: */
321: protected static final int impliedMask(int mask) {
322: if ((mask & DELETE_MASK) > 0) {
323: mask |= MODIFY_MASK;
324: }
325: if ((mask & RENAME_MASK) > 0) {
326: mask |= EDIT_MASK;
327: }
328: if ((mask & MODIFY_MASK) > 0) {
329: mask |= EDIT_MASK | UPLOAD_MASK;
330: }
331: if ((mask & EDIT_MASK) > 0) {
332: mask |= COMMENT_MASK;
333: }
334: if ((mask & COMMENT_MASK) > 0) {
335: mask |= VIEW_MASK;
336: }
337: if ((mask & UPLOAD_MASK) > 0) {
338: mask |= VIEW_MASK;
339: }
340: return mask;
341: }
342:
343: /**
344: * Determines whether one target string is a logical subset of the other.
345: * @param superSet the prospective superset
346: * @param subSet the prospective subset
347: * @return the results of the test, where <code>true</code> indicates that
348: * <code>subSet</code> is a subset of <code>superSet</code>
349: */
350: protected static final boolean isSubset(String super Set,
351: String subSet) {
352: // If either is null, return false
353: if (super Set == null || subSet == null) {
354: return false;
355: }
356:
357: // If targets are identical, it's a subset
358: if (super Set.equals(subSet)) {
359: return true;
360: }
361:
362: // If super is "*", it's a subset
363: if (super Set.equals(WILDCARD)) {
364: return true;
365: }
366:
367: // If super starts with "*", sub must end with everything after the *
368: if (super Set.startsWith(WILDCARD)) {
369: String suffix = super Set.substring(1);
370: return subSet.endsWith(suffix);
371: }
372:
373: // If super ends with "*", sub must start with everything before *
374: if (super Set.endsWith(WILDCARD)) {
375: String prefix = super Set
376: .substring(0, super Set.length() - 1);
377: return subSet.startsWith(prefix);
378: }
379:
380: return false;
381: }
382:
383: /**
384: * Private method that creates a binary mask based on the actions specified.
385: * This is used by {@link #implies(Permission)}.
386: * @param actions the actions for this permission, separated by commas
387: * @return the binary actions mask
388: */
389: protected static final int createMask(String actions) {
390: if (actions == null || actions.length() == 0) {
391: throw new IllegalArgumentException(
392: "Actions cannot be blank or null");
393: }
394: int mask = 0;
395: String[] actionList = StringUtils.split(actions,
396: ACTION_SEPARATOR);
397: for (int i = 0; i < actionList.length; i++) {
398: String action = actionList[i];
399: if (action.equalsIgnoreCase(VIEW_ACTION)) {
400: mask |= VIEW_MASK;
401: } else if (action.equalsIgnoreCase(EDIT_ACTION)) {
402: mask |= EDIT_MASK;
403: } else if (action.equalsIgnoreCase(COMMENT_ACTION)) {
404: mask |= COMMENT_MASK;
405: } else if (action.equalsIgnoreCase(MODIFY_ACTION)) {
406: mask |= MODIFY_MASK;
407: } else if (action.equalsIgnoreCase(UPLOAD_ACTION)) {
408: mask |= UPLOAD_MASK;
409: } else if (action.equalsIgnoreCase(DELETE_ACTION)) {
410: mask |= DELETE_MASK;
411: } else if (action.equalsIgnoreCase(RENAME_ACTION)) {
412: mask |= RENAME_MASK;
413: } else {
414: throw new IllegalArgumentException(
415: "Unrecognized action: " + action);
416: }
417: }
418: return mask;
419: }
420: }
|