Project

General

Profile

Download (10.9 KB) Statistics
| Branch: | Tag: | Revision:
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

    
38
/**
39
 * @author k.luther
40
 * @author a.kohlbecker
41
 *
42
 */
43
@Component
44
public class CdmSecurityHibernateInterceptor extends EmptyInterceptor {
45

    
46
    private static final long serialVersionUID = 8477758472369568074L;
47

    
48
    public static final Logger logger = Logger.getLogger(CdmSecurityHibernateInterceptor.class);
49

    
50

    
51
    private ICdmPermissionEvaluator permissionEvaluator;
52

    
53
    public ICdmPermissionEvaluator getPermissionEvaluator() {
54
        return permissionEvaluator;
55
    }
56

    
57
    public void setPermissionEvaluator(ICdmPermissionEvaluator permissionEvaluator) {
58
        this.permissionEvaluator = permissionEvaluator;
59
    }
60

    
61
    /**
62
     * The exculdeMap must map every property to the CdmBase type !!!
63
     */
64
    public static final Map<Class<? extends CdmBase>, Set<String>> exculdeMap = new HashMap<Class<? extends CdmBase>, Set<String>>();
65

    
66
    static{
67
//        disabled since no longer needed, see https://dev.e-taxonomy.eu/trac/ticket/4111#comment:8
68
//        exculdeMap.put(TaxonName.class, new HashSet<String>());
69

    
70
        Set<String> defaultExculdes = new HashSet<String>();
71
        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
72
        defaultExculdes.add("created");  // same behavior was not yet observed for "created", but to be on the save side we also exclude "created"
73

    
74
        for ( CdmBaseType type: CdmBaseType.values()){
75
            exculdeMap.put(type.getBaseClass(), new HashSet<String>());
76
            exculdeMap.get(type.getBaseClass()).addAll(defaultExculdes);
77
        }
78
        exculdeMap.put(CdmBase.class, new HashSet<String>());
79
        exculdeMap.get(CdmBase.class).addAll(defaultExculdes);
80

    
81

    
82
        /*
83
         * default fields required for each type for which excludes are defined
84
         */
85
//        exculdeMap.get(TaxonName.class).add("updatedBy");
86
//        exculdeMap.get(TaxonName.class).add("created");
87
//        exculdeMap.get(TaxonName.class).add("updated");
88

    
89
        /*
90
         * the specific excludes
91
         */
92
//        exculdeMap.get(TaxonName.class).add("taxonBases");
93
    }
94

    
95
    @Override
96
    public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] type) {
97

    
98
        if (SecurityContextHolder.getContext().getAuthentication() == null || !(entity instanceof CdmBase)) {
99
            return true;
100
        }
101
        // evaluate throws EvaluationFailedException
102
        checkPermissions((CdmBase) entity, Operation.CREATE);
103
        logger.debug("permission check suceeded - object creation granted");
104
        return true;
105
    }
106

    
107
    @Override
108
    public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
109

    
110
        if (SecurityContextHolder.getContext().getAuthentication() == null || !(entity instanceof CdmBase)) {
111
            return true;
112
        }
113
        CdmBase cdmEntity = (CdmBase) entity;
114
        if (previousState == null){
115
            return onSave(cdmEntity, id, currentState, propertyNames, null);
116
        }
117
        Set<String> excludes = exculdeMap.get(baseType(cdmEntity));
118
        excludes.addAll(unprotectedCacheFields(currentState, previousState, propertyNames));
119
        if (isModified(currentState, previousState, propertyNames, excludes)) {
120
            // evaluate throws EvaluationFailedException
121
            //if(cdmEntity.getCreated())
122
            checkPermissions(cdmEntity, Operation.UPDATE);
123
            logger.debug("Operation.UPDATE permission check suceeded - object update granted");
124

    
125
            if(IPublishable.class.isAssignableFrom(entity.getClass())){
126
                if(namedPropertyIsModified(currentState, previousState, propertyNames, "publish")){
127
                    checkRoles(Role.ROLE_PUBLISH, Role.ROLE_ADMIN);
128
                    logger.debug("Role.ROLE_PUBLISH permission check suceeded - object update granted");
129
                }
130
            }
131
        }
132
        return true;
133
    }
134

    
135
    /**
136
     * Detects all cache fields and the according protection flags. For cache fields which are not
137
     * protected the name of the cache field and of the protection flag are returned.
138
     * <p>
139
     * This method relies on  the convention that the protection flag for cache fields are named like
140
     * {@code protected{CacheFieldName} } whereas the cache fields a always ending with "Cache"
141
     *
142
     * @param currentState
143
     * @param previousState
144
     * @param propertyNames
145
     * @return
146
     */
147
    protected Collection<? extends String> unprotectedCacheFields(Object[] currentState, Object[] previousState,
148
            String[] propertyNames) {
149

    
150
        List<String> excludes = new ArrayList<>();
151
        for(int i = 0; i < propertyNames.length; i ++){
152
            if(propertyNames[i].matches("^protected.*Cache$")){
153
                if(currentState[i] instanceof Boolean && ((Boolean)currentState[i]) == false && currentState[i].equals(previousState[i])){
154
                    excludes.add(propertyNames[i]);
155
                    String cacheFieldName = propertyNames[i].replace("protected", "");
156
                    cacheFieldName = Introspector.decapitalize(cacheFieldName);
157
                    excludes.add(cacheFieldName);
158
                }
159
            }
160
        }
161

    
162
        return excludes;
163
    }
164

    
165
    private Class<? extends CdmBase> baseType(CdmBase cdmEntity) {
166
        Class<? extends CdmBase> basetype = CdmBaseType.baseTypeFor(cdmEntity.getClass());
167
        return basetype == null ? CdmBase.class : basetype;
168
    }
169

    
170

    
171
    @Override
172
    public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
173

    
174
        if (SecurityContextHolder.getContext().getAuthentication() == null || !(entity instanceof CdmBase)) {
175
            return;
176
        }
177
        CdmBase cdmEntity = (CdmBase) entity;
178
        // evaluate throws EvaluationFailedException
179
        checkPermissions(cdmEntity, Operation.DELETE);
180
        logger.debug("permission check suceeded - object update granted");
181
        return;
182
    }
183

    
184
    /**
185
     * checks if the current authentication has the <code>expectedPermission</code> on the supplied <code>entity</code>.
186
     * Throws an {@link PermissionDeniedException} if the evaluation fails.
187
     *
188
     * @param entity
189
     * @param expectedOperation
190
     */
191
    private void checkPermissions(CdmBase entity, EnumSet<CRUD> expectedOperation) {
192

    
193
        if (!permissionEvaluator.hasPermission(SecurityContextHolder.getContext().getAuthentication(), entity, expectedOperation)){
194
            throw new PermissionDeniedException(SecurityContextHolder.getContext().getAuthentication(), entity, expectedOperation);
195
        }
196
    }
197

    
198
    /**
199
     * checks if the current authentication has at least one of the <code>roles</code>.
200
     * Throws an {@link PermissionDeniedException} if the evaluation fails.
201
     * @param roles
202
     */
203
    private void checkRoles(Role ... roles) {
204

    
205
        if (!permissionEvaluator.hasOneOfRoles(SecurityContextHolder.getContext().getAuthentication(), roles)){
206
            throw new PermissionDeniedException(SecurityContextHolder.getContext().getAuthentication(), roles);
207
        }
208
    }
209

    
210
    /**
211
     * Checks if the CDM entity as been modified by comparing the current with the previous state.
212
     *
213
     * @param currentState
214
     * @param previousState
215
     * @return true if the currentState and previousState differ.
216
     */
217
    private boolean isModified(Object[] currentState, Object[] previousState, String[] propertyNames, Set<String> excludes) {
218

    
219
        Set<Integer> excludeIds = null;
220

    
221
        if(excludes != null && excludes.size() > 0) {
222
            excludeIds = new HashSet<Integer>(excludes.size());
223
            int i = 0;
224
            for(String prop : propertyNames){
225
                if(excludes.contains(prop)){
226
                    excludeIds.add(i);
227
                }
228
                if(excludeIds.size() == excludes.size()){
229
                    // all ids found
230
                    break;
231
                }
232
                i++;
233
            }
234
        }
235

    
236
        for (int i = 0; i<currentState.length; i++){
237
            if((excludeIds == null || !excludeIds.contains(i))){
238
                if(propertyIsModified(currentState, previousState, i)){
239
                    if(logger.isDebugEnabled()){
240
                        logger.debug("modified property found: " + propertyNames[i] + ", previousState: " + previousState[i] + ", currentState: " + currentState[i] );
241
                    }
242
                    return true;
243
                }
244
            }
245
        }
246

    
247
        return false;
248
    }
249

    
250
    /**
251
     * Compares the object states at the property denoted by the key parameter and returns true if they differ in this property
252
     *
253
     * @param currentState
254
     * @param previousState
255
     * @param isModified
256
     * @param key
257
     * @return
258
     */
259
    private boolean propertyIsModified(Object[] currentState, Object[] previousState, int key) {
260
        if (currentState[key]== null ) {
261
            if ( previousState[key]!= null) {
262
                return true;
263
            }
264
        }
265
        if (currentState[key]!= null ){
266
            if (previousState[key] == null){
267
                return true;
268
            }
269
        }
270
        if (currentState[key]!= null && previousState[key] != null){
271
            if (!currentState[key].equals(previousState[key])) {
272
                return true;
273
            }
274
        }
275
        return false;
276
    }
277

    
278
    private boolean namedPropertyIsModified(Object[] currentState, Object[] previousState, String[] propertyNames, String propertyNameToTest) {
279

    
280
        int key = ArrayUtils.indexOf(propertyNames, propertyNameToTest);
281
        return propertyIsModified(currentState, previousState, key);
282
    }
283

    
284
}
(10-10/21)