ref #6794 change FeatureTree to TermTree
[cdmlib.git] / cdmlib-persistence / src / main / java / eu / etaxonomy / cdm / persistence / hibernate / CdmSecurityHibernateInterceptor.java
1 /**
2 * Copyright (C) 2011 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
5 *
6 * The contents of this file are subject to the Mozilla Public License Version 1.1
7 * See LICENSE.TXT at the top of this package for the full license terms.
8 */
9 package eu.etaxonomy.cdm.persistence.hibernate;
10
11 import java.beans.Introspector;
12 import java.io.Serializable;
13 import java.util.ArrayList;
14 import java.util.Collection;
15 import java.util.EnumSet;
16 import java.util.HashMap;
17 import java.util.HashSet;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Set;
21
22 import org.apache.commons.lang.ArrayUtils;
23 import org.apache.log4j.Logger;
24 import org.hibernate.EmptyInterceptor;
25 import org.hibernate.type.Type;
26 import org.springframework.security.core.context.SecurityContextHolder;
27 import org.springframework.stereotype.Component;
28
29 import eu.etaxonomy.cdm.database.PermissionDeniedException;
30 import eu.etaxonomy.cdm.model.CdmBaseType;
31 import eu.etaxonomy.cdm.model.common.CdmBase;
32 import eu.etaxonomy.cdm.model.common.IPublishable;
33 import eu.etaxonomy.cdm.persistence.hibernate.permission.CRUD;
34 import eu.etaxonomy.cdm.persistence.hibernate.permission.ICdmPermissionEvaluator;
35 import eu.etaxonomy.cdm.persistence.hibernate.permission.Operation;
36 import eu.etaxonomy.cdm.persistence.hibernate.permission.Role;
37 import eu.etaxonomy.cdm.persistence.hibernate.permission.TargetEntityStates;
38
39 /**
40 * @author k.luther
41 * @author a.kohlbecker
42 *
43 */
44 @Component
45 public class CdmSecurityHibernateInterceptor extends EmptyInterceptor {
46
47 private static final long serialVersionUID = 8477758472369568074L;
48
49 public static final Logger logger = Logger.getLogger(CdmSecurityHibernateInterceptor.class);
50
51
52 private ICdmPermissionEvaluator permissionEvaluator;
53
54 public ICdmPermissionEvaluator getPermissionEvaluator() {
55 return permissionEvaluator;
56 }
57
58 public void setPermissionEvaluator(ICdmPermissionEvaluator permissionEvaluator) {
59 this.permissionEvaluator = permissionEvaluator;
60 }
61
62 /**
63 * The exculdeMap must map every property to the CdmBase type !!!
64 */
65 public static final Map<Class<? extends CdmBase>, Set<String>> exculdeMap = new HashMap<Class<? extends CdmBase>, Set<String>>();
66
67 static{
68 // disabled since no longer needed, see https://dev.e-taxonomy.eu/trac/ticket/4111#comment:8
69 // exculdeMap.put(TaxonName.class, new HashSet<String>());
70
71 Set<String> defaultExculdes = new HashSet<String>();
72 defaultExculdes.add("createdBy"); //created by is changed by CdmPreDataChangeListener after save. This is handled as a change and therefore throws a security exception during first insert if only CREATE rights exist
73 defaultExculdes.add("created"); // same behavior was not yet observed for "created", but to be on the save side we also exclude "created"
74 defaultExculdes.add("updatedBy");
75 defaultExculdes.add("updated");
76
77 for ( CdmBaseType type: CdmBaseType.values()){
78 exculdeMap.put(type.getBaseClass(), new HashSet<String>());
79 exculdeMap.get(type.getBaseClass()).addAll(defaultExculdes);
80 }
81 exculdeMap.put(CdmBase.class, new HashSet<String>());
82 exculdeMap.get(CdmBase.class).addAll(defaultExculdes);
83
84
85 /*
86 * default fields required for each type for which excludes are defined
87 */
88 // exculdeMap.get(TaxonName.class).add("updatedBy");
89 // exculdeMap.get(TaxonName.class).add("created");
90 // exculdeMap.get(TaxonName.class).add("updated");
91
92 /*
93 * the specific excludes
94 */
95 // exculdeMap.get(TaxonName.class).add("taxonBases");
96 }
97
98 @Override
99 public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] type) {
100
101 if (SecurityContextHolder.getContext().getAuthentication() == null || !(entity instanceof CdmBase)) {
102 return true;
103 }
104 // evaluate throws EvaluationFailedException
105 TargetEntityStates cdmEntityStates = new TargetEntityStates((CdmBase)entity, state, null, propertyNames, type);
106 checkPermissions(cdmEntityStates, Operation.CREATE);
107 logger.debug("permission check suceeded - object creation granted");
108 return true;
109 }
110
111 @Override
112 public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
113
114 if (SecurityContextHolder.getContext().getAuthentication() == null || !(entity instanceof CdmBase)) {
115 return true;
116 }
117 CdmBase cdmEntity = (CdmBase) entity;
118 if (previousState == null){
119 return onSave(cdmEntity, id, currentState, propertyNames, null);
120 }
121
122
123 Set<String> excludes = exculdeMap.get(baseType(cdmEntity));
124 excludes.addAll(unprotectedCacheFields(currentState, previousState, propertyNames));
125 if (isModified(currentState, previousState, propertyNames, excludes)) {
126 // evaluate throws EvaluationFailedException
127 //if(cdmEntity.getCreated())
128 TargetEntityStates cdmEntityStates = new TargetEntityStates(cdmEntity, currentState, previousState, propertyNames, types);
129 checkPermissions(cdmEntityStates, Operation.UPDATE);
130 logger.debug("Operation.UPDATE permission check suceeded - object update granted");
131
132 if(IPublishable.class.isAssignableFrom(entity.getClass())){
133 if(namedPropertyIsModified(currentState, previousState, propertyNames, "publish")){
134 checkRoles(Role.ROLE_PUBLISH, Role.ROLE_ADMIN);
135 logger.debug("Role.ROLE_PUBLISH permission check suceeded - object update granted");
136 }
137 }
138 }
139 return true;
140 }
141
142 /**
143 * Detects all cache fields and the according protection flags. For cache fields which are not
144 * protected the name of the cache field and of the protection flag are returned.
145 * <p>
146 * This method relies on the convention that the protection flag for cache fields are named like
147 * {@code protected{CacheFieldName} } whereas the cache fields a always ending with "Cache"
148 *
149 * @param currentState
150 * @param previousState
151 * @param propertyNames
152 * @return
153 */
154 protected Collection<? extends String> unprotectedCacheFields(Object[] currentState, Object[] previousState,
155 String[] propertyNames) {
156
157 List<String> excludes = new ArrayList<>();
158 for(int i = 0; i < propertyNames.length; i ++){
159 if(propertyNames[i].matches("^protected.*Cache$")){
160 if(currentState[i] instanceof Boolean && ((Boolean)currentState[i]) == false && currentState[i].equals(previousState[i])){
161 excludes.add(propertyNames[i]);
162 String cacheFieldName = propertyNames[i].replace("protected", "");
163 cacheFieldName = Introspector.decapitalize(cacheFieldName);
164 excludes.add(cacheFieldName);
165 }
166 }
167 }
168
169 return excludes;
170 }
171
172 private Class<? extends CdmBase> baseType(CdmBase cdmEntity) {
173 Class<? extends CdmBase> basetype = CdmBaseType.baseTypeFor(cdmEntity.getClass());
174 return basetype == null ? CdmBase.class : basetype;
175 }
176
177
178 @Override
179 public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
180
181 if (SecurityContextHolder.getContext().getAuthentication() == null || !(entity instanceof CdmBase)) {
182 return;
183 }
184 CdmBase cdmEntity = (CdmBase) entity;
185 // evaluate throws EvaluationFailedException
186 TargetEntityStates cdmEntityStates = new TargetEntityStates(cdmEntity, state, null, propertyNames, types);
187 checkPermissions(cdmEntityStates, Operation.DELETE);
188 logger.debug("permission check suceeded - object update granted");
189 return;
190 }
191
192 /**
193 * checks if the current authentication has the <code>expectedPermission</code> on the supplied <code>entity</code>.
194 * Throws an {@link PermissionDeniedException} if the evaluation fails.
195 *
196 * @param entity
197 * @param expectedOperation
198 */
199 private void checkPermissions(CdmBase entity, EnumSet<CRUD> expectedOperation) {
200 checkPermissions(new TargetEntityStates(entity), expectedOperation);
201 }
202
203 // TargetEntityStates
204 private void checkPermissions(TargetEntityStates entityStates, EnumSet<CRUD> expectedOperation) {
205
206 if (!permissionEvaluator.hasPermission(SecurityContextHolder.getContext().getAuthentication(), entityStates, expectedOperation)){
207 throw new PermissionDeniedException(SecurityContextHolder.getContext().getAuthentication(), entityStates.getEntity(), expectedOperation);
208 }
209 }
210
211 /**
212 * checks if the current authentication has at least one of the <code>roles</code>.
213 * Throws an {@link PermissionDeniedException} if the evaluation fails.
214 * @param roles
215 */
216 private void checkRoles(Role ... roles) {
217
218 if (!permissionEvaluator.hasOneOfRoles(SecurityContextHolder.getContext().getAuthentication(), roles)){
219 throw new PermissionDeniedException(SecurityContextHolder.getContext().getAuthentication(), roles);
220 }
221 }
222
223 /**
224 * Checks if the CDM entity as been modified by comparing the current with the previous state.
225 *
226 * @param currentState
227 * @param previousState
228 * @return true if the currentState and previousState differ.
229 */
230 private boolean isModified(Object[] currentState, Object[] previousState, String[] propertyNames, Set<String> excludes) {
231
232 Set<Integer> excludeIds = null;
233
234 if(excludes != null && excludes.size() > 0) {
235 excludeIds = new HashSet<Integer>(excludes.size());
236 int i = 0;
237 for(String prop : propertyNames){
238 if(excludes.contains(prop)){
239 excludeIds.add(i);
240 }
241 if(excludeIds.size() == excludes.size()){
242 // all ids found
243 break;
244 }
245 i++;
246 }
247 }
248
249 for (int i = 0; i<currentState.length; i++){
250 if((excludeIds == null || !excludeIds.contains(i))){
251 if(propertyIsModified(currentState, previousState, i)){
252 if(logger.isDebugEnabled()){
253 logger.debug("modified property found: " + propertyNames[i] + ", previousState: " + previousState[i] + ", currentState: " + currentState[i] );
254 }
255 return true;
256 }
257 }
258 }
259
260 return false;
261 }
262
263 /**
264 * Compares the object states at the property denoted by the key parameter and returns true if they differ in this property
265 *
266 * @param currentState
267 * @param previousState
268 * @param isModified
269 * @param key
270 * @return
271 */
272 private boolean propertyIsModified(Object[] currentState, Object[] previousState, int key) {
273 if (currentState[key]== null ) {
274 if ( previousState[key]!= null) {
275 return true;
276 }
277 }
278 if (currentState[key]!= null ){
279 if (previousState[key] == null){
280 return true;
281 }
282 }
283 if (currentState[key]!= null && previousState[key] != null){
284 if (!currentState[key].equals(previousState[key])) {
285 return true;
286 }
287 }
288 return false;
289 }
290
291 private boolean namedPropertyIsModified(Object[] currentState, Object[] previousState, String[] propertyNames, String propertyNameToTest) {
292
293 int key = ArrayUtils.indexOf(propertyNames, propertyNameToTest);
294 return propertyIsModified(currentState, previousState, key);
295 }
296
297 }