001: /*
002: JSPWiki - a JSP-based WikiWiki clone.
003:
004: Copyright (C) 2001-2006 JSPWiki Development Team
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.AccessControlContext;
023: import java.security.AccessController;
024: import java.security.DomainCombiner;
025: import java.security.Permission;
026: import java.security.Principal;
027: import java.util.Arrays;
028: import java.util.Iterator;
029: import java.util.Set;
030:
031: import javax.security.auth.Subject;
032: import javax.security.auth.SubjectDomainCombiner;
033:
034: import com.ecyrd.jspwiki.auth.GroupPrincipal;
035:
036: /**
037: * <p>
038: * Permission to perform an operation on a group in a given wiki. Permission
039: * actions include: <code>view</code>, <code>edit</code>, <code>delete</code>.
040: * </p>
041: * <p>
042: * The target of a permission is a single group or collection in a given wiki.
043: * The syntax for the target is the wiki name, followed by a colon (:) and the
044: * name of the group. “All wikis” can be specified using a wildcard (*). Group
045: * collections may also be specified using a wildcard. For groups, the wildcard
046: * may be a prefix, suffix, or all by itself. Examples of targets include:
047: * </p>
048: * <blockquote><code>*:*<br/>
049: * *:TestPlanners<br/>
050: * *:*Planners<br/>
051: * *:Test*<br/>
052: * mywiki:TestPlanners<br/>
053: * mywiki:*Planners<br/>
054: * mywiki:Test*</code>
055: * </blockquote>
056: * <p>
057: * For a given target, certain permissions imply others:
058: * </p>
059: * <ul>
060: * <li><code>edit</code> implies <code>view</code></li>
061: * <li><code>delete</code> implies <code>edit</code> and
062: * <code>view</code></li>
063: * </ul>
064: * <P>Targets that do not include a wiki prefix <em>never </em> imply others.</p>
065: * <p>
066: * GroupPermission accepts a special target called
067: * <code><groupmember></code> that means “all groups that a user is a
068: * member of.” When included in a policy file <code>grant</code> block, it
069: * functions like a wildcard. Thus, this block:
070: *
071: * <pre>
072: * grant signedBy "jspwiki",
073: * principal com.ecyrd.jspwiki.auth.authorize.Role "Authenticated" {
074: * permission com.ecyrd.jspwiki.auth.permissions.GroupPermission "*:<groupmember>", "edit";
075: * </pre>
076: *
077: * means, “allow Authenticated users to edit any groups they are members of.”
078: * The wildcard target (*) does <em>not</em> imply <code><groupmember></code>; it
079: * must be granted explicitly.
080: * @author Andrew Jaquith
081: * @since 2.4.17
082: */
083: public final class GroupPermission extends Permission {
084: /** Special target token that denotes all groups that a Subject's Principals are members of. */
085: public static final String MEMBER_TOKEN = "<groupmember>";
086:
087: private static final long serialVersionUID = 1L;
088:
089: /** Action for deleting a group or collection of groups. */
090: public static final String DELETE_ACTION = "delete";
091:
092: /** Action for editing a group or collection of groups. */
093: public static final String EDIT_ACTION = "edit";
094:
095: /** Action for viewing a group or collection of groups. */
096: public static final String VIEW_ACTION = "view";
097:
098: protected static final int DELETE_MASK = 0x4;
099:
100: protected static final int EDIT_MASK = 0x2;
101:
102: protected static final int VIEW_MASK = 0x1;
103:
104: /** Convenience constant that denotes <code>GroupPermission( "*:*, "delete" )</code>. */
105: public static final GroupPermission DELETE = new GroupPermission(
106: DELETE_ACTION);
107:
108: /** Convenience constant that denotes <code>GroupPermission( "*:*, "edit" )</code>. */
109: public static final GroupPermission EDIT = new GroupPermission(
110: EDIT_ACTION);
111:
112: /** Convenience constant that denotes <code>GroupPermission( "*:*, "view" )</code>. */
113: public static final GroupPermission VIEW = new GroupPermission(
114: VIEW_ACTION);
115:
116: private static final String ACTION_SEPARATOR = ",";
117:
118: private static final String WILDCARD = "*";
119:
120: private static final String WIKI_SEPARATOR = ":";
121:
122: private final String m_actionString;
123:
124: private final int m_mask;
125:
126: private final String m_group;
127:
128: private final String m_wiki;
129:
130: /**
131: * Private convenience constructor that creates a new GroupPermission for
132: * all wikis and groups (*:*) and set of actions.
133: * @param actions
134: */
135: private GroupPermission(String actions) {
136: this (WILDCARD + WIKI_SEPARATOR + WILDCARD, actions);
137: }
138:
139: /**
140: * Creates a new GroupPermission for a specified group and set of actions.
141: * Group should include a prepended wiki name followed by a colon (:). If
142: * the wiki name is not supplied or starts with a colon, the group refers to
143: * all wikis.
144: * @param group the wiki group
145: * @param actions the allowed actions for this group
146: */
147: public GroupPermission(String group, String actions) {
148: super (group);
149:
150: // Parse wiki and group (which may include wiki name and group)
151: // Strip out attachment separator; it is irrelevant.
152: String[] pathParams = group.split(WIKI_SEPARATOR);
153: String groupName;
154: if (pathParams.length >= 2) {
155: m_wiki = pathParams[0].length() > 0 ? pathParams[0] : null;
156: groupName = pathParams[1];
157: } else {
158: m_wiki = WILDCARD;
159: groupName = pathParams[0];
160: }
161: m_group = groupName;
162:
163: // Parse actions
164: String[] groupActions = actions.toLowerCase().split(
165: ACTION_SEPARATOR);
166: Arrays.sort(groupActions, String.CASE_INSENSITIVE_ORDER);
167: m_mask = createMask(actions);
168: StringBuffer buffer = new StringBuffer();
169: for (int i = 0; i < groupActions.length; i++) {
170: buffer.append(groupActions[i]);
171: if (i < (groupActions.length - 1)) {
172: buffer.append(ACTION_SEPARATOR);
173: }
174: }
175: m_actionString = buffer.toString();
176: }
177:
178: /**
179: * Two PagePermission objects are considered equal if their actions (after
180: * normalization), wiki and target are equal.
181: * @param obj the object to compare
182: * @return the result of the comparison
183: * @see java.lang.Object#equals(java.lang.Object)
184: */
185: public final boolean equals(Object obj) {
186: if (!(obj instanceof GroupPermission)) {
187: return false;
188: }
189: GroupPermission p = (GroupPermission) obj;
190: return p.m_mask == m_mask && p.m_group.equals(m_group)
191: && p.m_wiki != null && p.m_wiki.equals(m_wiki);
192: }
193:
194: /**
195: * Returns the actions for this permission: “view”, “edit”, or “delete”. The
196: * actions will always be sorted in alphabetic order, and will always appear
197: * in lower case.
198: * @return the actions
199: * @see java.security.Permission#getActions()
200: */
201: public final String getActions() {
202: return m_actionString;
203: }
204:
205: /**
206: * Returns the name of the wiki group represented by this permission.
207: * @return the page name
208: */
209: public final String getGroup() {
210: return m_group;
211: }
212:
213: /**
214: * Returns the name of the wiki containing the group represented by this
215: * permission; may return the wildcard string.
216: * @return the wiki
217: */
218: public final String getWiki() {
219: return m_wiki;
220: }
221:
222: /**
223: * Returns the hash code for this GroupPermission.
224: * @return the hash code
225: * @see java.lang.Object#hashCode()
226: */
227: public final int hashCode() {
228: // If the wiki has not been set, uses a dummy value for the hashcode
229: // calculation. This may occur if the page given does not refer
230: // to any particular wiki
231: String wiki = m_wiki != null ? m_wiki : "dummy_value";
232: return m_mask
233: + ((13 * m_actionString.hashCode()) * 23 * wiki
234: .hashCode());
235: }
236:
237: /**
238: * <p>
239: * GroupPermissions can only imply other GroupPermissions; no other
240: * permission types are implied. One GroupPermission implies another if its
241: * actions if three conditions are met:
242: * </p>
243: * <ol>
244: * <li>The other GroupPermission’s wiki is equal to, or a subset of, that
245: * of this permission. This permission’s wiki is considered a superset of
246: * the other if it contains a matching prefix plus a wildcard, or a wildcard
247: * followed by a matching suffix.</li>
248: * <li>The other GroupPermission’s target is equal to, or a subset of, the
249: * target specified by this permission. This permission’s target is
250: * considered a superset of the other if it contains a matching prefix plus
251: * a wildcard, or a wildcard followed by a matching suffix.</li>
252: * <li>All of other GroupPermission’s actions are equal to, or a subset of,
253: * those of this permission</li>
254: * </ol>
255: * @param permission the Permission to examine
256: * @return <code>true</code> if the GroupPermission implies the
257: * supplied Permission; <code>false</code> otherwise
258: * @see java.security.Permission#implies(java.security.Permission)
259: */
260: public final boolean implies(Permission permission) {
261: // Permission must be a GroupPermission
262: if (!(permission instanceof GroupPermission)) {
263: return false;
264: }
265:
266: // Build up an "implied mask"
267: GroupPermission p = (GroupPermission) permission;
268: int impliedMask = impliedMask(m_mask);
269:
270: // If actions aren't a proper subset, return false
271: if ((impliedMask & p.m_mask) != p.m_mask) {
272: return false;
273: }
274:
275: // See if the tested permission's wiki is implied
276: boolean impliedWiki = PagePermission.isSubset(m_wiki, p.m_wiki);
277:
278: // If this page is "*", the tested permission's
279: // group is implied, unless implied permission has <groupmember> token
280: boolean impliedGroup;
281: if (MEMBER_TOKEN.equals(p.m_group)) {
282: impliedGroup = MEMBER_TOKEN.equals(m_group);
283: } else {
284: impliedGroup = PagePermission.isSubset(m_group, p.m_group);
285: }
286:
287: // See if this permission is <groupmember> and Subject possesses
288: // GroupPrincipal matching the implied GroupPermission's group
289: boolean impliedMember = impliesMember(p);
290:
291: return impliedWiki && (impliedGroup || impliedMember);
292: }
293:
294: /**
295: * Prints a human-readable representation of this permission.
296: * @return the string
297: * @see java.lang.Object#toString()
298: */
299: public final String toString() {
300: String wiki = (m_wiki == null) ? "" : m_wiki;
301: return "(\"" + this .getClass().getName() + "\",\"" + wiki
302: + WIKI_SEPARATOR + m_group + "\",\"" + getActions()
303: + "\")";
304: }
305:
306: /**
307: * Creates an “implied mask” based on the actions originally assigned: for
308: * example, delete implies edit; edit implies view.
309: * @param mask binary mask for actions
310: * @return binary mask for implied actions
311: */
312: protected static final int impliedMask(int mask) {
313: if ((mask & DELETE_MASK) > 0) {
314: mask |= EDIT_MASK;
315: }
316: if ((mask & EDIT_MASK) > 0) {
317: mask |= VIEW_MASK;
318: }
319: return mask;
320: }
321:
322: /**
323: * Protected method that creates a binary mask based on the actions specified.
324: * This is used by {@link #implies(Permission)}.
325: * @param actions the actions for this permission, separated by commas
326: * @return the binary actions mask
327: */
328: protected static final int createMask(String actions) {
329: if (actions == null || actions.length() == 0) {
330: throw new IllegalArgumentException(
331: "Actions cannot be blank or null");
332: }
333: int mask = 0;
334: String[] actionList = actions.split(ACTION_SEPARATOR);
335: for (int i = 0; i < actionList.length; i++) {
336: String action = actionList[i];
337: if (action.equalsIgnoreCase(VIEW_ACTION)) {
338: mask |= VIEW_MASK;
339: } else if (action.equalsIgnoreCase(EDIT_ACTION)) {
340: mask |= EDIT_MASK;
341: } else if (action.equalsIgnoreCase(DELETE_ACTION)) {
342: mask |= DELETE_MASK;
343: } else {
344: throw new IllegalArgumentException(
345: "Unrecognized action: " + action);
346: }
347: }
348: return mask;
349: }
350:
351: /**
352: * <p>
353: * Returns <code>true</code> if this GroupPermission was created with the
354: * token <code><groupmember></code>
355: * <em>and</em> the current
356: * thread’s Subject is a member of the Group indicated by the implied
357: * GroupPermission. Thus, a GroupPermission with the group
358: * <code><groupmember></code> implies GroupPermission for group
359: * "TestGroup" only if the Subject is a member of TestGroup.
360: * </p>
361: * <p>
362: * We make this determination by obtaining the current {@link Thread}’s
363: * {@link java.security.AccessControlContext} and requesting the
364: * {@link javax.security.auth.SubjectDomainCombiner}. If the combiner is
365: * not <code>null</code>, then we know that the access check was
366: * requested using a {@link javax.security.auth.Subject}; that is, that an
367: * upstream caller caused a Subject to be associated with the Thread’s
368: * ProtectionDomain by executing a
369: * {@link javax.security.auth.Subject#doAs(Subject, java.security.PrivilegedAction)}
370: * operation.
371: * </p>
372: * <p>
373: * If a SubjectDomainCombiner exists, determining group membership is
374: * simple: just iterate through the Subject’s Principal set and look for all
375: * Principals of type {@link com.ecyrd.jspwiki.auth.GroupPrincipal}. If the
376: * name of any Principal matches the value of the implied Permission’s
377: * {@link GroupPermission#getGroup()} value, then the Subject is a member of
378: * this group -- and therefore this <code>impliesMember</code> call
379: * returns <code>true</code>.
380: * </p>
381: * <p>
382: * This may sound complicated, but it really isn’t. Consider the following
383: * examples:
384: * </p>
385: * <table border="1"> <thead>
386: * <tr>
387: * <th width="25%">This object</th>
388: * <th width="25%"><code>impliesMember</code> parameter</th>
389: * <th width="25%">Calling Subject’s Principals
390: * <th width="25%">Result</th>
391: * </tr>
392: * <tr>
393: * <td><code>GroupPermission ("<groupmember>")</code></td>
394: * <td><code>GroupPermission ("*:TestGroup")</code></td>
395: * <td><code>WikiPrincipal ("Biff"),<br/>GroupPrincipal ("TestGroup")</code></td>
396: * <td><code>true</code></td>
397: * </tr>
398: * <tr>
399: * <td><code>GroupPermission ("*:TestGroup")</code></td>
400: * <td><code>GroupPermission ("*:TestGroup")</code></td>
401: * <td><code>WikiPrincipal ("Biff"),<br/>GroupPrincipal ("TestGroup")</code></td>
402: * <td><code>false</code> - this object does not contain
403: * <code><groupmember></code></td>
404: * </tr>
405: * <tr>
406: * <td><code>GroupPermission ("<groupmember>")</code></td>
407: * <td><code>GroupPermission ("*:TestGroup")</code></td>
408: * <td><code>WikiPrincipal ("Biff"),<br/>GroupPrincipal ("FooGroup")</code></td>
409: * <td><code>false</code> - Subject does not contain GroupPrincipal
410: * matching implied Permission’s group (TestGroup)</td>
411: * </tr>
412: * <tr>
413: * <td><code>GroupPermission ("<groupmember>")</code></td>
414: * <td><code>WikiPermission ("*:createGroups")</code></td>
415: * <td><code>WikiPrincipal ("Biff"),<br/>GroupPrincipal ("TestGroup")</code></td>
416: * <td><code>false</code> - implied permission not of type
417: * GroupPermission</td>
418: * </tr>
419: * <tr>
420: * <td><code>GroupPermission ("<groupmember>")</code></td>
421: * <td><code>GroupPermission ("*:TestGroup")</code></td>
422: * <td>-</td>
423: * <td><code>false</code> - <code>Subject.doAs()</code> not called
424: * upstream</td>
425: * </tr>
426: * </table>
427: * <p>
428: * Note that JSPWiki’s access control checks are made inside of
429: * {@link com.ecyrd.jspwiki.auth.AuthorizationManager#checkPermission(com.ecyrd.jspwiki.WikiSession, Permission)},
430: * which performs a <code>Subject.doAs()</code> call. Thus, this
431: * Permission functions exactly the way it should during normal
432: * operations.
433: * </p>
434: * @param permission the implied permission
435: * @return <code>true</code> if the calling Thread’s Subject contains a
436: * GroupPrincipal matching the implied GroupPermission’s group;
437: * <code>false</code> otherwise
438: */
439: protected final boolean impliesMember(Permission permission) {
440: if (!(permission instanceof GroupPermission)) {
441: return false;
442: }
443: GroupPermission gp = (GroupPermission) permission;
444: if (!MEMBER_TOKEN.equals(m_group)) {
445: return false;
446: }
447:
448: // For the current thread, retrieve the SubjectDomainCombiner
449: // (if one was used to create current AccessControlContext )
450: AccessControlContext acc = AccessController.getContext();
451: DomainCombiner dc = acc.getDomainCombiner();
452: if (dc != null && dc instanceof SubjectDomainCombiner) {
453: // <member> implies permission if subject possesses
454: // GroupPrincipal with same name as target
455: Subject subject = ((SubjectDomainCombiner) dc).getSubject();
456: Set principals = subject
457: .getPrincipals(GroupPrincipal.class);
458: for (Iterator it = principals.iterator(); it.hasNext();) {
459: Principal principal = (Principal) it.next();
460: if (principal.getName().equals(gp.m_group)) {
461: return true;
462: }
463: }
464: }
465: return false;
466: }
467: }
|