001: /*
002: jGuard is a security framework based on top of jaas (java authentication and authorization security).
003: it is written for web applications, to resolve simply, access control problems.
004: version $Name$
005: http://sourceforge.net/projects/jguard/
006:
007: Copyright (C) 2004 Charles GAY
008:
009: This library is free software; you can redistribute it and/or
010: modify it under the terms of the GNU Lesser General Public
011: License as published by the Free Software Foundation; either
012: version 2.1 of the License, or (at your option) any later version.
013:
014: This library is distributed in the hope that it will be useful,
015: but WITHOUT ANY WARRANTY; without even the implied warranty of
016: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017: Lesser General Public License for more details.
018:
019: You should have received a copy of the GNU Lesser General Public
020: License along with this library; if not, write to the Free Software
021: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
022:
023:
024: jGuard project home page:
025: http://sourceforge.net/projects/jguard/
026:
027: */
028: package net.sf.jguard.core.authorization.permissions;
029:
030: import java.lang.reflect.Constructor;
031: import java.lang.reflect.InvocationTargetException;
032: import java.security.Permission;
033: import java.security.PermissionCollection;
034:
035: import java.security.Principal;
036: import java.security.ProtectionDomain;
037: import java.util.ArrayList;
038: import java.util.Enumeration;
039: import java.util.HashMap;
040: import java.util.HashSet;
041: import java.util.Iterator;
042: import java.util.List;
043: import java.util.Map;
044: import java.util.Set;
045: import java.util.logging.Level;
046: import java.util.logging.Logger;
047: import java.util.regex.Matcher;
048: import java.util.regex.Pattern;
049:
050: import net.sf.ehcache.Cache;
051: import net.sf.ehcache.CacheException;
052: import net.sf.ehcache.CacheManager;
053: import net.sf.ehcache.Element;
054: import net.sf.jguard.core.principals.RolePrincipal;
055: import net.sf.jguard.core.principals.UserPrincipal;
056:
057: import org.apache.commons.jexl.Expression;
058: import org.apache.commons.jexl.ExpressionFactory;
059: import org.apache.commons.jexl.JexlContext;
060: import org.apache.commons.jexl.JexlHelper;
061:
062: /**
063: * java.security.Permission related utility class.
064: * @author <a href="mailto:diabolo512@users.sourceforge.net">Charles Gay</a>
065: * @author <a href="mailto:vberetti@users.sourceforge.net">Vincent Beretti</a>
066: * @author <a href="mailto:tandilero@users.sourceforge.net">Maximiliano Batelli</a>
067: */
068: public class PermissionUtils {
069:
070: private static final Logger logger = Logger
071: .getLogger(PermissionUtils.class.getName());
072:
073: private static CacheManager manager;
074: private static Cache unresolvedPermToNeededExpressions;
075: private static Cache unresolvedPermAndValuesToResolvedPerm;
076: private static boolean cachesEnabled;
077: private static Pattern JEXL_PATTERN = Pattern
078: .compile("(\\$\\{[^\\}]+\\})");
079:
080: /**
081: * instantiate a java.security.Permission subclass.
082: * @param className class name
083: * @param name permission name
084: * @param actions actions name splitted by comma ','
085: * @return a java.security.Permission subclass, or a java.security.BasicPermission subclass
086: * (which inherit java.security.Permission)
087: * @throws ClassNotFoundException
088: */
089: public static Permission getPermission(String className,
090: String name, String actions) throws ClassNotFoundException {
091: // TODO VBE : check if className is instanceof BasicPermission to speed up the method
092: // if (permission instanceof BasicPermission) {
093: // Class[] classes = {String.class, String.class};
094: // String[] initArgs = new String[2];
095: // initArgs[0] = resolvedName;
096: // initArgs[1] = resolvedActions;
097: // Permission resolvedPermission = (Permission) permission.getClass().getConstructor(classes).newInstance(initArgs);
098: //
099: // resolvedPc.add(resolvedPermission);
100: // }
101: Class clazz = null;
102: try {
103: clazz = Thread.currentThread().getContextClassLoader()
104: .loadClass(className);
105: } catch (ClassNotFoundException e1) {
106: logger
107: .log(
108: Level.SEVERE,
109: " class "
110: + className
111: + " is not found please check your classPath \n and the permission set in the Datasource \n(either database or JGuardPrincipalsPermissions.xml file) ",
112: e1);
113: throw e1;
114: }
115: Class[] permArgsBasicPermClass = { String.class, String.class };
116: Class[] permArgsPermClass = { String.class };
117: Object[] objBasicArray = { name, actions };
118: Object[] objArray = { name };
119: Permission newPerm = null;
120: Constructor[] constructors = clazz.getConstructors();
121: boolean constructorWithActions = false;
122: for (int i = 0; i < constructors.length; i++) {
123: Constructor tempConstructor = constructors[i];
124: Class[] classes = tempConstructor.getParameterTypes();
125: if (classes.length == 2 && classes[0].equals(String.class)
126: && classes[1].equals(String.class)) {
127: constructorWithActions = true;
128: break;
129: }
130: }
131: try {
132: if (constructorWithActions == true) {
133: newPerm = (Permission) clazz.getConstructor(
134: permArgsBasicPermClass).newInstance(
135: objBasicArray);
136: } else {
137:
138: //Permission subclass which has got a constructor with name argument
139: newPerm = (Permission) clazz.getConstructor(
140: permArgsPermClass).newInstance(objArray);
141: }
142: } catch (IllegalArgumentException e) {
143: logger.log(Level.SEVERE, " illegal argument ", e);
144: } catch (SecurityException e) {
145: logger.log(Level.SEVERE, "className=" + className);
146: logger.log(Level.SEVERE, "name=" + name);
147: logger.log(Level.SEVERE, "actions=" + actions);
148: logger
149: .log(
150: Level.SEVERE,
151: " you don't have right to instantiate a permission ",
152: e);
153: } catch (InstantiationException e) {
154: logger.log(Level.SEVERE, "className=" + className);
155: logger.log(Level.SEVERE, "name=" + name);
156: logger.log(Level.SEVERE, "actions=" + actions);
157: logger.log(Level.SEVERE,
158: " you cannot instantiate a permission ", e);
159: } catch (IllegalAccessException e) {
160: logger.log(Level.SEVERE, "className=" + className);
161: logger.log(Level.SEVERE, "name=" + name);
162: logger.log(Level.SEVERE, "actions=" + actions);
163: } catch (InvocationTargetException e) {
164: logger.log(Level.SEVERE, "className=" + className);
165: logger.log(Level.SEVERE, "name=" + name);
166: logger.log(Level.SEVERE, "actions=" + actions);
167: } catch (NoSuchMethodException e) {
168: logger.log(Level.SEVERE, "method not found =", e);
169: }
170: return newPerm;
171: }
172:
173: /**
174: * Evaluate jexlExpression using UserPrincipal as context.<br>
175: * and return <strong>true</strong> if expression is valid,
176: * <strong>false</strong> otherwise.
177: * @param jexlExpression
178: * @param userPrincipal
179: * @return boolean
180: */
181: private static boolean evaluateDefinition(String jexlExpression,
182: UserPrincipal userPrincipal) {
183: if (jexlExpression == null)
184: return false;
185: if ("true".equalsIgnoreCase(jexlExpression))
186: return true;
187: if ("false".equalsIgnoreCase(jexlExpression))
188: return false;
189: if (jexlExpression != null && userPrincipal == null) {
190: logger
191: .warning("evaluateDefinition() no UserPrincipal defined, can not use regex definition");
192: }
193:
194: jexlExpression = jexlExpression.substring(2, jexlExpression
195: .length() - 1);
196: JexlContext jexlContext = JexlHelper.createContext();
197: jexlContext.getVars().put("subject.roles",
198: userPrincipal.getRoles());
199: jexlContext.getVars().put("subject.publicCredentials",
200: userPrincipal.getPublicCredentials());
201: jexlContext.getVars().put("subject.privateCredentials",
202: userPrincipal.getPrivateCredentials());
203:
204: Object resolvedExpression = null;
205: try {
206: Expression expression = ExpressionFactory
207: .createExpression(jexlExpression);
208: resolvedExpression = expression.evaluate(jexlContext);
209: } catch (Exception e) {
210: logger.warning("Failed to evaluate : " + jexlExpression);
211: }
212:
213: if (resolvedExpression == null
214: || !(resolvedExpression instanceof Boolean)) {
215: logger
216: .warning("Subject does not have the required credentials to resolve the role activation : "
217: + jexlExpression);
218: return false;
219: } else {
220: Boolean val = (Boolean) resolvedExpression;
221: return val.booleanValue();
222: }
223: }
224:
225: /**
226: * Evaluate principal definition attr and active attr.<br>
227: * To resolve definition attr, this method uses a particular Principal (UserPrincipal)
228: * set to the user during authentication. If this principal is not present and the definition attr != null, the
229: * definition attr is not evaluated and the function returns false.
230: * definition attr take precedence against active attr, so
231: * if definition evaluate to false but active is true, then evaluatePrincipal return false
232: * @param ppal
233: * @param userPrincipal
234: * @return boolean
235: */
236: public static boolean evaluatePrincipal(RolePrincipal ppal,
237: UserPrincipal userPrincipal) {
238: if (!evaluateDefinition(ppal.getDefinition(), userPrincipal)) {
239: if (logger.isLoggable(Level.FINEST)) {
240: logger
241: .finest("evaluatePrincipal() - user's principal definition attr evaluates to false="
242: + ppal.getLocalName());
243: }
244: return false;
245: } else if (!ppal.isActive()) {
246: if (logger.isLoggable(Level.FINEST)) {
247: logger
248: .finest("evaluatePrincipal() - user's principal active attr is false="
249: + ppal.getLocalName());
250: }
251: return false;
252: } else
253: return true;
254:
255: }
256:
257: /**
258: * Resolve permission collection containing regular expressions.<br>
259: * To resolve the permissions, this method uses a particular Principal (UserPrincipal)
260: * set to the user during authentication. If this principal is not present, the
261: * permission collection given in parameters is returned with no modifications. If
262: * the UserPrincipal is present but does not contain the required data to resolved the regex,
263: * the permission is removed from the permission collection.
264: * @param protectionDomain
265: * @param pc
266: * @return
267: */
268: public static PermissionCollection evaluatePermissionCollection(
269: ProtectionDomain protectionDomain, PermissionCollection pc) {
270: // resolve regular expressions in permissions
271: Principal[] ppals = protectionDomain.getPrincipals();
272: boolean hasJexlPrincipal = false;
273: int i = 0;
274:
275: //we are looking for UserPrincipal to resolve regexp with JEXL
276: while (!hasJexlPrincipal && i < ppals.length) {
277: hasJexlPrincipal = ppals[i] instanceof UserPrincipal;
278: i++;
279: }
280: if (!hasJexlPrincipal) {
281: logger
282: .warning("no UserPrincipal defined, can not use regex permissions");
283: return pc;
284: } else {
285: PermissionCollection resolvedPc = new JGPositivePermissionCollection();
286:
287: UserPrincipal subjectPrincipal = (UserPrincipal) ppals[i - 1];
288: JexlContext jc = JexlHelper.createContext();
289: Map vars = jc.getVars();
290: vars.put("subject.roles", subjectPrincipal.getRoles());
291: vars.put("subject.publicCredentials", subjectPrincipal
292: .getPublicCredentials());
293: vars.put("subject.privateCredentials", subjectPrincipal
294: .getPrivateCredentials());
295:
296: //TODO CGA add time-based permissions with DurationDecorator class
297:
298: Enumeration permissionsEnum = pc.elements();
299:
300: Map subjectResolvedExpressions = new HashMap();
301: // stores every already resolved expressions inside this method i.e. for a subject principal
302: while (permissionsEnum.hasMoreElements()) {
303: Permission permission = (Permission) permissionsEnum
304: .nextElement();
305: logger.finest("Resolving permission = " + permission);
306: PermissionCollection pcFromPermission = resolvePermission(
307: permission, subjectResolvedExpressions, jc);
308: Enumeration enumPermissions = pcFromPermission
309: .elements();
310: while (enumPermissions.hasMoreElements()) {
311: Permission p = (Permission) enumPermissions
312: .nextElement();
313: resolvedPc.add(p);
314: }
315: }
316:
317: return resolvedPc;
318: }
319: }
320:
321: private static HashSet createKey(Permission unresolvedPermission,
322: Map values) {
323:
324: HashSet key = new HashSet();
325: key.add(unresolvedPermission);
326: key.add(values);
327:
328: return key;
329: }
330:
331: /**
332: * return all the permissions which match the regexp expression present in the
333: * permission passed as a parameter.
334: * @param permission
335: * @param subjectResolvedExpressions
336: * @param jexlContext
337: * @return
338: */
339: private static PermissionCollection resolvePermission(
340: Permission permission, Map subjectResolvedExpressions,
341: JexlContext jexlContext) {
342:
343: PermissionCollection resolvedPermissions = new JGPositivePermissionCollection();
344:
345: // try to get the resolved permissions from the cache
346: if (cachesEnabled) {
347: try {
348: // check in cache if unresolved permission -> needed expressions exists
349: Element expressionsCacheEntry = unresolvedPermToNeededExpressions
350: .get(permission);
351: if (expressionsCacheEntry != null) {
352:
353: Set neededExpressions = (Set) expressionsCacheEntry
354: .getValue();
355:
356: if (neededExpressions.isEmpty()) {
357: // no need to resolve this permission
358: resolvedPermissions.add(permission);
359: logger
360: .finest("get permission from cache with no resolution needed");
361: return resolvedPermissions;
362: }
363:
364: Iterator itExpressions = neededExpressions
365: .iterator();
366: Map permissionResolvedExpressions = new HashMap();
367: boolean hasNull = false;
368: while (itExpressions.hasNext()) {
369: String jexlExpression = (String) itExpressions
370: .next();
371: Object resolvedExpression = null;
372:
373: if (subjectResolvedExpressions
374: .containsKey(jexlExpression)) {
375: resolvedExpression = subjectResolvedExpressions
376: .get(jexlExpression);
377: permissionResolvedExpressions.put(
378: jexlExpression, resolvedExpression);
379: } else {
380: try {
381: Expression expression = ExpressionFactory
382: .createExpression(jexlExpression);
383: resolvedExpression = expression
384: .evaluate(jexlContext);
385: subjectResolvedExpressions.put(
386: jexlExpression,
387: resolvedExpression);
388: permissionResolvedExpressions.put(
389: jexlExpression,
390: resolvedExpression);
391: } catch (Exception e) {
392: logger.warning("Failed to evaluate : "
393: + jexlExpression);
394: }
395: }
396:
397: if (resolvedExpression == null
398: || (resolvedExpression instanceof List && ((List) resolvedExpression)
399: .isEmpty())) {
400: hasNull = true;
401: break;
402: }
403:
404: }
405:
406: if (hasNull) {
407: logger
408: .warning("Subject does not have the required credentials to resolve the permission : "
409: + permission);
410: //skip this unresolvable permission
411: resolvedPermissions.add(permission);
412: return resolvedPermissions;
413: }
414:
415: // check in cache if (needed values + unresolvedPermission) -> resolved permission exists
416: HashSet key = createKey(permission,
417: permissionResolvedExpressions);
418: Element permissionCacheEntry = unresolvedPermAndValuesToResolvedPerm
419: .get(key);
420:
421: if (permissionCacheEntry != null) {
422: PermissionCollection permissionsFromCache = (PermissionCollection) permissionCacheEntry
423: .getValue();
424: logger
425: .finest("get resolved permission from cache");
426: Enumeration enumeration = permissionsFromCache
427: .elements();
428: while (enumeration.hasMoreElements()) {
429: Permission permissionFromCache = (Permission) enumeration
430: .nextElement();
431: resolvedPermissions
432: .add(permissionFromCache);
433: }
434: return resolvedPermissions;
435: }
436: }
437: } catch (CacheException e) {
438: logger.log(Level.WARNING, "Failed using caches : "
439: + e.getMessage());
440: }
441: }
442:
443: // if permission is not yet resolved continue
444: // resolution will be fast because jexlExpression -> value
445: // has already been resolved and stored in resolvedValues
446:
447: // resolution is combinative so one unresolved permission
448: // may imply n resolved permissions
449: List unresolvedPermissions = new ArrayList();
450: unresolvedPermissions.add(permission);
451: Map resolvedExpressionsByPermission = new HashMap();
452:
453: while (!unresolvedPermissions.isEmpty()) {
454:
455: Permission unresolvedPermission = (Permission) unresolvedPermissions
456: .remove(0);
457:
458: String name = unresolvedPermission.getName();
459: Set partiallyResolvedNames = resolvePartiallyExpression(
460: name, JEXL_PATTERN, jexlContext,
461: resolvedExpressionsByPermission,
462: subjectResolvedExpressions);
463: if (partiallyResolvedNames == null) {
464: // unresolvable permission
465: return new JGPositivePermissionCollection();
466: }
467:
468: boolean matchesInName = (partiallyResolvedNames.size() != 1 || !partiallyResolvedNames
469: .contains(name));
470: if (matchesInName) {
471: Iterator itNames = partiallyResolvedNames.iterator();
472: while (itNames.hasNext()) {
473: String resolvedName = (String) itNames.next();
474: Permission partiallyResolvedPermission;
475: try {
476: partiallyResolvedPermission = PermissionUtils
477: .getPermission(permission.getClass()
478: .getName(), resolvedName,
479: unresolvedPermission
480: .getActions());
481: } catch (ClassNotFoundException e) {
482: logger.warning(e.getMessage());
483: continue;
484: }
485: unresolvedPermissions
486: .add(partiallyResolvedPermission);
487: }
488: continue;
489: }
490:
491: String actions = unresolvedPermission.getActions();
492: String[] actionsArray = actions.split(",");
493: String action = actionsArray[0];
494: Set partiallyResolvedActions = resolvePartiallyExpression(
495: action, JEXL_PATTERN, jexlContext,
496: resolvedExpressionsByPermission,
497: subjectResolvedExpressions);
498: if (partiallyResolvedActions == null) {
499: // unresolvable permission
500: return new JGPositivePermissionCollection();
501: }
502:
503: boolean matchesInActions = (partiallyResolvedActions.size() != 1 || !partiallyResolvedActions
504: .contains(action));
505: if (matchesInActions) {
506: Iterator itActions = partiallyResolvedActions
507: .iterator();
508: while (itActions.hasNext()) {
509: String resolvedAction = (String) itActions.next();
510: Permission partiallyResolvedPermission;
511: try {
512: partiallyResolvedPermission = PermissionUtils
513: .getPermission(permission.getClass()
514: .getName(),
515: unresolvedPermission.getName(),
516: resolvedAction);
517: } catch (ClassNotFoundException e) {
518: logger.warning(e.getMessage());
519: continue;
520: }
521: unresolvedPermissions
522: .add(partiallyResolvedPermission);
523: }
524: continue;
525: }
526:
527: // if this code is reached, there is no match in name and actions
528: // the permission is resolved
529: resolvedPermissions.add(unresolvedPermission);
530: }
531:
532: if (cachesEnabled) {
533: try {
534: // store permissions needed expressions in cache
535: List unresolvedKeys = unresolvedPermToNeededExpressions
536: .getKeys();
537: if (!unresolvedKeys.contains(permission)) {
538:
539: HashSet permissionNeededExpressions = new HashSet(
540: resolvedExpressionsByPermission.keySet());
541: unresolvedPermToNeededExpressions.put(new Element(
542: permission, permissionNeededExpressions));
543: }
544: } catch (CacheException e) {
545: logger.log(Level.WARNING, "Failed using caches : "
546: + e.getMessage());
547: }
548:
549: // store mapping (values + unresolved permission ) -> resolved permission in cache
550: Element cacheEntry = new Element(createKey(permission,
551: resolvedExpressionsByPermission),
552: resolvedPermissions);
553: unresolvedPermAndValuesToResolvedPerm.put(cacheEntry);
554: logger.finest("add resolved permissions to cache");
555: }
556:
557: return resolvedPermissions;
558: }
559:
560: /**
561: * /**
562: * resolves first occurence of jexl expression. The other expressions remain unresolved
563: * @param expression
564: * @param pattern
565: * @param jexlContext
566: * @param resolvedExpressionsByPermission
567: * @param subjectResolvedExpressions
568: * @return
569: */
570: private static Set resolvePartiallyExpression(String expression,
571: Pattern pattern, JexlContext jexlContext,
572: Map resolvedExpressionsByPermission,
573: Map subjectResolvedExpressions) {
574:
575: boolean hasMatches = false;
576: boolean hasNull = false;
577:
578: Set resolvedExpressionsSet = new HashSet();
579:
580: Matcher matcher = pattern.matcher(expression);
581: if (matcher.find()) {
582: hasMatches = true;
583: String matchedExpression = matcher.group();
584:
585: String jexlExpression = matchedExpression.substring(2,
586: matchedExpression.length() - 1);
587: Object resolvedExpression = null;
588:
589: if (subjectResolvedExpressions.containsKey(jexlExpression)) {
590: resolvedExpression = (Set) subjectResolvedExpressions
591: .get(jexlExpression);
592: } else {
593: try {
594: Expression expr = ExpressionFactory
595: .createExpression(jexlExpression);
596: resolvedExpression = expr.evaluate(jexlContext);
597: subjectResolvedExpressions.put(jexlExpression,
598: resolvedExpression);
599:
600: } catch (Exception e) {
601: logger.warning("Failed to resolve expression : "
602: + jexlExpression);
603: }
604: }
605:
606: if (!(resolvedExpressionsByPermission
607: .containsKey(jexlExpression))) {
608: resolvedExpressionsByPermission.put(jexlExpression,
609: resolvedExpression);
610: }
611:
612: if (resolvedExpression == null) {
613: // expression can not be resolved
614: hasNull = true;
615: } else if (resolvedExpression instanceof Set) {
616: Iterator it = ((Set) resolvedExpression).iterator();
617: while (it.hasNext()) {
618: StringBuffer builder = new StringBuffer(expression);
619: builder.replace(matcher.start(), matcher.end(),
620: (String) it.next());
621: resolvedExpressionsSet.add(builder.toString());
622: }
623: } else if (resolvedExpression instanceof String) {
624: StringBuffer builder = new StringBuffer(expression);
625: builder.replace(matcher.start(), matcher.end(),
626: (String) resolvedExpression);
627: resolvedExpressionsSet.add(builder.toString());
628: }
629: }
630:
631: if (!hasMatches) {
632: // no jexl expression in part, return original part
633: resolvedExpressionsSet.add(expression);
634: }
635: if (hasNull) {
636: // can not be resolved
637: return null;
638: }
639:
640: return resolvedExpressionsSet;
641: }
642:
643: public static void createCaches() throws CacheException {
644: // gets ehcache.xml as a resource in the classpath
645: if (unresolvedPermToNeededExpressions == null
646: || unresolvedPermAndValuesToResolvedPerm == null) {
647: logger.info("Creating caches for permissions evaluations");
648: manager = CacheManager.create();
649: unresolvedPermToNeededExpressions = manager
650: .getCache("unresolvedPermToNeededExpressions");
651: unresolvedPermAndValuesToResolvedPerm = manager
652: .getCache("unresolvedPermAndValuesToResolvedPerm");
653:
654: if (unresolvedPermToNeededExpressions == null
655: || unresolvedPermAndValuesToResolvedPerm == null) {
656: logger
657: .warning("Failed to create caches for permissions evaluations, use non-caching evaluation");
658: setCachesEnabled(false);
659: }
660: }
661: }
662:
663: public static boolean isCachesEnabled() {
664: return cachesEnabled;
665: }
666:
667: public static void setCachesEnabled(boolean cachesEnabled) {
668: PermissionUtils.cachesEnabled = cachesEnabled;
669: }
670: }
|