Project

General

Profile

Download (11.6 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.model.permission.CRUD;
34
import eu.etaxonomy.cdm.model.permission.Operation;
35
import eu.etaxonomy.cdm.persistence.permission.ICdmPermissionEvaluator;
36
import eu.etaxonomy.cdm.persistence.permission.Role;
37
import eu.etaxonomy.cdm.persistence.permission.TargetEntityStates;
38

    
39
/**
40
 * @author k.luther
41
 * @author a.kohlbecker
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<>());
69

    
70
        Set<String> defaultExculdes = new HashSet<>();
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
        defaultExculdes.add("updatedBy");
74
        defaultExculdes.add("updated");
75

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

    
83

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

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

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

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

    
110
    @Override
111
    public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
112

    
113
        if (SecurityContextHolder.getContext().getAuthentication() == null || !(entity instanceof CdmBase)) {
114
            return true;
115
        }
116
        CdmBase cdmEntity = (CdmBase) entity;
117
        if (previousState == null){
118
            return onSave(cdmEntity, id, currentState, propertyNames, null);
119
        }
120

    
121
        Set<String> excludes = exculdeMap.get(baseType(cdmEntity));
122
        excludes.addAll(unprotectedCacheFields(currentState, previousState, propertyNames));
123
        if (isModified(currentState, previousState, propertyNames, excludes)) {
124
            // evaluate throws EvaluationFailedException
125
            //if(cdmEntity.getCreated())
126
            TargetEntityStates cdmEntityStates = new TargetEntityStates(cdmEntity, currentState, previousState, propertyNames);
127
            checkPermissions(cdmEntityStates, Operation.UPDATE);
128
            logger.debug("Operation.UPDATE permission check suceeded - object update granted");
129

    
130
            if(IPublishable.class.isAssignableFrom(entity.getClass())){
131
                if(namedPropertyIsModified(currentState, previousState, propertyNames, "publish")){
132
                    checkRoles(Role.ROLE_PUBLISH, Role.ROLE_ADMIN);
133
                    logger.debug("Role.ROLE_PUBLISH permission check suceeded - object update granted");
134
                }
135
            }
136
        }
137
        return true;
138
    }
139

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

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

    
167
        return excludes;
168
    }
169

    
170
    private Class<? extends CdmBase> baseType(CdmBase cdmEntity) {
171
        Class<? extends CdmBase> basetype = CdmBaseType.baseTypeFor(cdmEntity.getClass());
172
        return basetype == null ? CdmBase.class : basetype;
173
    }
174

    
175
    @Override
176
    public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
177

    
178
        if (SecurityContextHolder.getContext().getAuthentication() == null || !(entity instanceof CdmBase)) {
179
            return;
180
        }
181
        CdmBase cdmEntity = (CdmBase) entity;
182
        // evaluate throws EvaluationFailedException
183
        TargetEntityStates cdmEntityStates = new TargetEntityStates(cdmEntity, state, null, propertyNames);
184
        checkPermissions(cdmEntityStates, Operation.DELETE);
185
        logger.debug("permission check suceeded - object update granted");
186
        return;
187
    }
188

    
189
    /**
190
     * checks if the current authentication has the <code>expectedPermission</code> on the supplied <code>entity</code>.
191
     * Throws an {@link PermissionDeniedException} if the evaluation fails.
192
     *
193
     * @param entity
194
     * @param expectedOperation
195
     */
196
    private void checkPermissions(CdmBase entity, EnumSet<CRUD> expectedOperation) {
197
        checkPermissions(new TargetEntityStates(entity), expectedOperation);
198
    }
199

    
200
    // TargetEntityStates
201
    private void checkPermissions(TargetEntityStates entityStates, EnumSet<CRUD> expectedOperation) {
202

    
203
        if (!permissionEvaluator.hasPermission(SecurityContextHolder.getContext().getAuthentication(), entityStates, expectedOperation)){
204
            throw new PermissionDeniedException(SecurityContextHolder.getContext().getAuthentication(), entityStates.getEntity(), expectedOperation);
205
        }
206
    }
207

    
208
    /**
209
     * checks if the current authentication has at least one of the <code>roles</code>.
210
     * Throws an {@link PermissionDeniedException} if the evaluation fails.
211
     * @param roles
212
     */
213
    private void checkRoles(Role ... roles) {
214

    
215
        if (!permissionEvaluator.hasOneOfRoles(SecurityContextHolder.getContext().getAuthentication(), roles)){
216
            throw new PermissionDeniedException(SecurityContextHolder.getContext().getAuthentication(), roles);
217
        }
218
    }
219

    
220
    /**
221
     * Checks if the CDM entity as been modified by comparing the current with the previous state.
222
     *
223
     * @param currentState
224
     * @param previousState
225
     * @return true if the currentState and previousState differ.
226
     */
227
    private boolean isModified(Object[] currentState, Object[] previousState, String[] propertyNames, Set<String> excludes) {
228

    
229
        Set<Integer> excludeIds = null;
230

    
231
        if(excludes != null && excludes.size() > 0) {
232
            excludeIds = new HashSet<Integer>(excludes.size());
233
            int i = 0;
234
            for(String prop : propertyNames){
235
                if(excludes.contains(prop)){
236
                    excludeIds.add(i);
237
                }
238
                if(excludeIds.size() == excludes.size()){
239
                    // all ids found
240
                    break;
241
                }
242
                i++;
243
            }
244
        }
245

    
246
        for (int i = 0; i<currentState.length; i++){
247
            if((excludeIds == null || !excludeIds.contains(i))){
248
                if(propertyIsModified(currentState, previousState, i)){
249
                    if(logger.isDebugEnabled()){
250
                        logger.debug("modified property found: " + propertyNames[i] + ", previousState: " + previousState[i] + ", currentState: " + currentState[i] );
251
                    }
252
                    return true;
253
                }
254
            }
255
        }
256

    
257
        return false;
258
    }
259

    
260
    /**
261
     * Compares the object states at the property denoted by the key parameter and returns true if they differ in this property
262
     *
263
     * @param currentState
264
     * @param previousState
265
     * @param isModified
266
     * @param key
267
     * @return
268
     */
269
    private boolean propertyIsModified(Object[] currentState, Object[] previousState, int key) {
270
        if (currentState[key]== null ) {
271
            if ( previousState[key]!= null) {
272
                return true;
273
            }
274
        }
275
        if (currentState[key]!= null ){
276
            if (previousState[key] == null){
277
                return true;
278
            }
279
        }
280
        if (currentState[key]!= null && previousState[key] != null){
281
            if (!currentState[key].equals(previousState[key])) {
282
                return true;
283
            }
284
        }
285
        return false;
286
    }
287

    
288
    private boolean namedPropertyIsModified(Object[] currentState, Object[] previousState, String[] propertyNames, String propertyNameToTest) {
289

    
290
        int key = ArrayUtils.indexOf(propertyNames, propertyNameToTest);
291
        return propertyIsModified(currentState, previousState, key);
292
    }
293

    
294
}
(10-10/23)