001 package org.trails.hibernate;
002
003 import java.io.Serializable;
004
005 import ognl.Ognl;
006 import ognl.OgnlException;
007
008 import org.acegisecurity.AuthenticationCredentialsNotFoundException;
009 import org.acegisecurity.GrantedAuthority;
010 import org.acegisecurity.context.SecurityContext;
011 import org.acegisecurity.context.SecurityContextHolder;
012 import org.acegisecurity.userdetails.UserDetails;
013 import org.apache.commons.logging.Log;
014 import org.apache.commons.logging.LogFactory;
015 import org.hibernate.CallbackException;
016 import org.hibernate.Transaction;
017 import org.hibernate.type.Type;
018 import org.trails.security.EntityModificationInterception;
019 import org.trails.security.RestrictionType;
020 import org.trails.security.TrailsSecurityException;
021 import org.trails.security.annotation.RemoveRequiresAssociation;
022 import org.trails.security.annotation.RemoveRequiresRole;
023 import org.trails.security.annotation.UpdateRequiresAssociation;
024 import org.trails.security.annotation.UpdateRequiresRole;
025
026 public class TrailsSecurityInterceptor extends TrailsInterceptor {
027 private static final Log log = LogFactory.getLog(TrailsSecurityInterceptor.class);
028
029 private void checkRestriction(final Object entity, final RestrictionType restrictionType) {
030 log.info("Check restriction for entity : " + entity);
031 // Role-base restrictions override association based
032 if (entity == null || restrictionType == null) return;
033 SecurityContext context = SecurityContextHolder.getContext();
034 // Assume that context should have been established for each request, and if it's not,
035 // it's an internal service call
036 if (context == null || context.getAuthentication() == null) return;
037
038 boolean roleRestriction = false;
039
040 String[] requiredRole = null;
041 switch (restrictionType) {
042 case UPDATE :
043 UpdateRequiresRole updateRestriction = entity.getClass().getAnnotation(UpdateRequiresRole.class );
044 if (updateRestriction != null) requiredRole = updateRestriction.value();
045 break;
046 case REMOVE :
047 RemoveRequiresRole removeRestriction = entity.getClass().getAnnotation(RemoveRequiresRole.class );
048 if (removeRestriction != null) requiredRole = removeRestriction.value();
049 break;
050 }
051 if (requiredRole != null) {
052 GrantedAuthority[] authorities = context.getAuthentication().getAuthorities();
053 for (GrantedAuthority authority : authorities)
054 for (String role : requiredRole) if (role.equals(authority.getAuthority()) ) return;
055 roleRestriction = true;
056 }
057
058 String ownerPropertyAssociation = null;
059 switch (restrictionType) {
060 case UPDATE :
061 UpdateRequiresAssociation updateRestriction = entity.getClass().getAnnotation(UpdateRequiresAssociation.class );
062 if (updateRestriction != null) ownerPropertyAssociation = updateRestriction.value();
063 break;
064 case REMOVE :
065 RemoveRequiresAssociation removeRestriction = entity.getClass().getAnnotation(RemoveRequiresAssociation.class );
066 if (removeRestriction != null) ownerPropertyAssociation = removeRestriction.value();
067 break;
068 }
069 // If check returns true, exit immediately, ignoring possible role restriction
070 if (ownerPropertyAssociation != null) if (checkOwnershipRestriction(entity, ownerPropertyAssociation) ) return;
071
072 // Note that because we place no restriction if no annotations are specified, but check the ownership restriction
073 // only after role restriction, we need to delay throwing an exception if the role isn't available
074 if (roleRestriction) throw new EntityModificationInterception(entity, "Authenticated user does not have a required role or ownership");
075
076 }
077
078 private boolean checkOwnershipRestriction(final Object entity, final String associatedProperty) {
079 if (entity == null || associatedProperty == null) return false;
080
081 try {
082 SecurityContext context = SecurityContextHolder.getContext();
083 if (context.getAuthentication() == null) throw new AuthenticationCredentialsNotFoundException("Entity requires an authenticated user as owner");
084 String currentUserName = context.getAuthentication().getName();
085 if (currentUserName == null) currentUserName = "";
086
087 // Empty string as associated property denotes entity itself
088 if ("".equals(associatedProperty) ) {
089 if (!(entity instanceof UserDetails) ) throw new TrailsSecurityException("Entity is not of type UserDetails");
090 UserDetails userDetails = (UserDetails)entity;
091 if (currentUserName.equals(userDetails.getUsername()) ) return true;
092 else throw new EntityModificationInterception(entity, "Entity does not represent the authenticated user");
093 }
094
095 Object value = Ognl.getValue(associatedProperty, entity);
096 if (value == null) throw new EntityModificationInterception(entity, "Associated owner property is null");
097
098 if (value instanceof Iterable) {
099 try {
100 Iterable<UserDetails> iterable = (Iterable<UserDetails>)value;
101 for (UserDetails userDetails : iterable) {
102 if (currentUserName.equals(userDetails.getUsername()) ) {
103 value = null;
104 break;
105 }
106 }
107 }
108 catch (ClassCastException e) {
109 throw new TrailsSecurityException("Associated collection doesn't contain UserDetails objects");
110 }
111
112 if (value != null) throw new EntityModificationInterception(entity, "Authenticated user is not in the owners collection");
113 }
114 else {
115 if (!(value instanceof UserDetails) ) throw new TrailsSecurityException("Associate property is not of type UserDetails");
116 UserDetails userDetails = (UserDetails)value;
117 if (!currentUserName.equals(userDetails.getUsername()) ) throw new EntityModificationInterception(entity, "Authenticated user is not the owner");
118 }
119 }
120 catch(OgnlException e) {
121 throw new TrailsSecurityException("Could not evaluate the owner association", e);
122 }
123 return true;
124 }
125
126 /* (non-Javadoc)
127 * @see org.hibernate.Interceptor#onFlushDirty(java.lang.Object, java.io.Serializable, java.lang.Object[], java.lang.Object[], java.lang.String[], org.hibernate.type.Type[])
128 */
129 public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState,
130 Object[] previousState, String[] propertyNames, Type[] types) throws CallbackException
131 {
132 checkRestriction(entity, RestrictionType.UPDATE);
133 return super.onFlushDirty(entity, id, currentState, previousState, propertyNames, types);
134 }
135
136 /* (non-Javadoc)
137 * @see org.hibernate.Interceptor#onSave(java.lang.Object, java.io.Serializable, java.lang.Object[], java.lang.String[], org.hibernate.type.Type[])
138 */
139 public boolean onSave(Object entity, Serializable id, Object[] state,
140 String[] propertyNames, Type[] types) throws CallbackException
141 {
142 checkRestriction(entity, RestrictionType.UPDATE);
143 return super.onSave(entity, id, state, propertyNames, types);
144 }
145
146 /* (non-Javadoc)
147 * @see org.hibernate.Interceptor#onDelete(java.lang.Object, java.io.Serializable, java.lang.Object[], java.lang.String[], org.hibernate.type.Type[])
148 */
149 public void onDelete(Object entity, Serializable arg1, Object[] arg2,
150 String[] arg3, Type[] arg4) throws CallbackException
151 {
152 checkRestriction(entity, RestrictionType.REMOVE);
153 super.onDelete(entity, arg1, arg2, arg3, arg4);
154 }
155 }