2 * Copyright (C) 2011 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
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.
9 package eu
.etaxonomy
.cdm
.persistence
.hibernate
;
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
;
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
;
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
;
41 * @author a.kohlbecker
45 public class CdmSecurityHibernateInterceptor
extends EmptyInterceptor
{
47 private static final long serialVersionUID
= 8477758472369568074L;
49 public static final Logger logger
= Logger
.getLogger(CdmSecurityHibernateInterceptor
.class);
52 private ICdmPermissionEvaluator permissionEvaluator
;
54 public ICdmPermissionEvaluator
getPermissionEvaluator() {
55 return permissionEvaluator
;
58 public void setPermissionEvaluator(ICdmPermissionEvaluator permissionEvaluator
) {
59 this.permissionEvaluator
= permissionEvaluator
;
63 * The exculdeMap must map every property to the CdmBase type !!!
65 public static final Map
<Class
<?
extends CdmBase
>, Set
<String
>> exculdeMap
= new HashMap
<Class
<?
extends CdmBase
>, Set
<String
>>();
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>());
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");
77 for ( CdmBaseType type
: CdmBaseType
.values()){
78 exculdeMap
.put(type
.getBaseClass(), new HashSet
<String
>());
79 exculdeMap
.get(type
.getBaseClass()).addAll(defaultExculdes
);
81 exculdeMap
.put(CdmBase
.class, new HashSet
<String
>());
82 exculdeMap
.get(CdmBase
.class).addAll(defaultExculdes
);
86 * default fields required for each type for which excludes are defined
88 // exculdeMap.get(TaxonName.class).add("updatedBy");
89 // exculdeMap.get(TaxonName.class).add("created");
90 // exculdeMap.get(TaxonName.class).add("updated");
93 * the specific excludes
95 // exculdeMap.get(TaxonName.class).add("taxonBases");
99 public boolean onSave(Object entity
, Serializable id
, Object
[] state
, String
[] propertyNames
, Type
[] type
) {
101 if (SecurityContextHolder
.getContext().getAuthentication() == null || !(entity
instanceof CdmBase
)) {
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");
112 public boolean onFlushDirty(Object entity
, Serializable id
, Object
[] currentState
, Object
[] previousState
, String
[] propertyNames
, Type
[] types
) {
114 if (SecurityContextHolder
.getContext().getAuthentication() == null || !(entity
instanceof CdmBase
)) {
117 CdmBase cdmEntity
= (CdmBase
) entity
;
118 if (previousState
== null){
119 return onSave(cdmEntity
, id
, currentState
, propertyNames
, null);
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");
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");
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.
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"
149 * @param currentState
150 * @param previousState
151 * @param propertyNames
154 protected Collection
<?
extends String
> unprotectedCacheFields(Object
[] currentState
, Object
[] previousState
,
155 String
[] propertyNames
) {
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
);
172 private Class
<?
extends CdmBase
> baseType(CdmBase cdmEntity
) {
173 Class
<?
extends CdmBase
> basetype
= CdmBaseType
.baseTypeFor(cdmEntity
.getClass());
174 return basetype
== null ? CdmBase
.class : basetype
;
179 public void onDelete(Object entity
, Serializable id
, Object
[] state
, String
[] propertyNames
, Type
[] types
) {
181 if (SecurityContextHolder
.getContext().getAuthentication() == null || !(entity
instanceof CdmBase
)) {
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");
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.
197 * @param expectedOperation
199 private void checkPermissions(CdmBase entity
, EnumSet
<CRUD
> expectedOperation
) {
200 checkPermissions(new TargetEntityStates(entity
), expectedOperation
);
203 // TargetEntityStates
204 private void checkPermissions(TargetEntityStates entityStates
, EnumSet
<CRUD
> expectedOperation
) {
206 if (!permissionEvaluator
.hasPermission(SecurityContextHolder
.getContext().getAuthentication(), entityStates
, expectedOperation
)){
207 throw new PermissionDeniedException(SecurityContextHolder
.getContext().getAuthentication(), entityStates
.getEntity(), expectedOperation
);
212 * checks if the current authentication has at least one of the <code>roles</code>.
213 * Throws an {@link PermissionDeniedException} if the evaluation fails.
216 private void checkRoles(Role
... roles
) {
218 if (!permissionEvaluator
.hasOneOfRoles(SecurityContextHolder
.getContext().getAuthentication(), roles
)){
219 throw new PermissionDeniedException(SecurityContextHolder
.getContext().getAuthentication(), roles
);
224 * Checks if the CDM entity as been modified by comparing the current with the previous state.
226 * @param currentState
227 * @param previousState
228 * @return true if the currentState and previousState differ.
230 private boolean isModified(Object
[] currentState
, Object
[] previousState
, String
[] propertyNames
, Set
<String
> excludes
) {
232 Set
<Integer
> excludeIds
= null;
234 if(excludes
!= null && excludes
.size() > 0) {
235 excludeIds
= new HashSet
<Integer
>(excludes
.size());
237 for(String prop
: propertyNames
){
238 if(excludes
.contains(prop
)){
241 if(excludeIds
.size() == excludes
.size()){
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
] );
264 * Compares the object states at the property denoted by the key parameter and returns true if they differ in this property
266 * @param currentState
267 * @param previousState
272 private boolean propertyIsModified(Object
[] currentState
, Object
[] previousState
, int key
) {
273 if (currentState
[key
]== null ) {
274 if ( previousState
[key
]!= null) {
278 if (currentState
[key
]!= null ){
279 if (previousState
[key
] == null){
283 if (currentState
[key
]!= null && previousState
[key
] != null){
284 if (!currentState
[key
].equals(previousState
[key
])) {
291 private boolean namedPropertyIsModified(Object
[] currentState
, Object
[] previousState
, String
[] propertyNames
, String propertyNameToTest
) {
293 int key
= ArrayUtils
.indexOf(propertyNames
, propertyNameToTest
);
294 return propertyIsModified(currentState
, previousState
, key
);