Project

General

Profile

Download (11.7 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
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
}
(10-10/23)