Project

General

Profile

Download (11.8 KB) Statistics
| Branch: | Tag: | Revision:
1
/**
2
* Copyright (C) 2012 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.permission.voter;
10

    
11
import java.util.Collection;
12
import java.util.EnumSet;
13

    
14
import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;
15
import org.springframework.security.access.AccessDecisionVoter;
16
import org.springframework.security.access.ConfigAttribute;
17
import org.springframework.security.core.Authentication;
18
import org.springframework.security.core.GrantedAuthority;
19

    
20
import eu.etaxonomy.cdm.model.common.CdmBase;
21
import eu.etaxonomy.cdm.model.permission.CRUD;
22
import eu.etaxonomy.cdm.model.permission.PermissionClass;
23
import eu.etaxonomy.cdm.persistence.permission.CdmAuthority;
24
import eu.etaxonomy.cdm.persistence.permission.CdmAuthorityParsingException;
25
import eu.etaxonomy.cdm.persistence.permission.TargetEntityStates;
26

    
27
/**
28
 * The <code>CdmPermissionVoter</code> provides access control votes for {@link CdmBase} objects.
29
 *
30
 * @author andreas kohlbecker
31
 * @since Sep 4, 2012
32
 */
33
public abstract class CdmPermissionVoter implements AccessDecisionVoter <TargetEntityStates> {
34

    
35
    public static final Logger logger = LogManager.getLogger(CdmPermissionVoter.class);
36

    
37
    private static final EnumSet<CRUD> DELETE = EnumSet.of(CRUD.DELETE);
38

    
39
    @Override
40
    public boolean supports(ConfigAttribute attribute) {
41
        // all CdmPermissionVoter support CdmAuthority
42
        return attribute instanceof CdmAuthority;
43
    }
44

    
45
    @Override
46
    public boolean supports(Class<?> clazz) {
47
        /* NOTE!!!
48
         * Do not change this, all CdmPermissionVoters must support CdmBase.class
49
         */
50
        return clazz.isInstance(CdmBase.class);
51
    }
52

    
53
    /**
54
     * Sets the Cdm type, or super type this Voter is responsible for.
55
     */
56
    abstract public Class<? extends CdmBase> getResponsibilityClass();
57

    
58
    protected boolean isResponsibleFor(Object securedObject) {
59
        return getResponsibilityClass().isAssignableFrom(securedObject.getClass());
60
    }
61

    
62
    protected boolean isResponsibleFor(PermissionClass permissionClass) {
63
        return getResponsibility().equals(permissionClass);
64
    }
65

    
66
    /**
67
     * Get the according CdmPermissionClass matching {@link #getResponsibilityClass()} the cdm class this voter is responsible for.
68
     * @return
69
     */
70
    protected PermissionClass getResponsibility() {
71
        return PermissionClass.getValueOf(getResponsibilityClass());
72
    }
73

    
74
    @Override
75
    public int vote(Authentication authentication, TargetEntityStates targetEntityStates, Collection<ConfigAttribute> attributes) {
76

    
77
        if(!isResponsibleFor(targetEntityStates.getEntity())){
78
            logger.debug(voterLoggingLabel() + " class missmatch => ACCESS_ABSTAIN");
79
            return ACCESS_ABSTAIN;
80
        }
81

    
82
        if (logger.isDebugEnabled()){
83
            logger.debug(voterLoggingLabel() + " voting for authentication: " + authentication.getName() + ", object : " + targetEntityStates.getEntity().toString() + ", attribute[0]:" + ((CdmAuthority)attributes.iterator().next()).getAttribute());
84
        }
85

    
86
        int fallThroughVote = ACCESS_DENIED;
87
        boolean deniedByPreviousFurtherVoting = false;
88

    
89
        // loop over all attributes = permissions of which at least one must match
90
        // usually there is only one element in the collection!
91
        for(ConfigAttribute attribute : attributes){
92
            if(!(attribute instanceof CdmAuthority)){
93
                throw new RuntimeException("attributes must contain only CdmAuthority");
94
            }
95
            CdmAuthority evalPermission = (CdmAuthority)attribute;
96

    
97
            for (GrantedAuthority authority: authentication.getAuthorities()){
98

    
99
                CdmAuthority auth;
100
                try {
101
                    auth = CdmAuthority.fromGrantedAuthority(authority);
102
                } catch (CdmAuthorityParsingException e) {
103
                    logger.debug(voterLoggingLabel() + " skipping " + authority.getAuthority() + " due to CdmAuthorityParsingException");
104
                    continue;
105
                }
106

    
107
                // check if the voter is responsible for the permission to be evaluated
108
                if( ! isResponsibleFor(evalPermission.getPermissionClass())){
109
                    logger.debug(voterLoggingLabel() + " not responsible for " + evalPermission.getPermissionClass() + " -> skipping");
110
                    continue;
111
                }
112

    
113
                ValidationResult vr = new ValidationResult();
114

    
115
                boolean isALL = auth.getPermissionClass().equals(PermissionClass.ALL);
116

    
117
                vr.isClassMatch = isALL || auth.getPermissionClass().equals(evalPermission.getPermissionClass());
118
                vr.isPermissionMatch = auth.getOperation().containsAll(evalPermission.getOperation());
119
                vr.isUuidMatch = auth.hasTargetUuid() && auth.getTargetUUID().equals(targetEntityStates.getEntity().getUuid());
120
                vr.isIgnoreUuidMatch = !auth.hasTargetUuid();
121

    
122
                if(logger.isDebugEnabled()){
123
                    logger.debug(voterLoggingLabel() + " " + vr);
124
                }
125

    
126
                // first of all, always allow deleting orphan entities
127
                if(vr.isClassMatch && evalPermission.getOperation().equals(DELETE) && isOrpahn(targetEntityStates.getEntity())) {
128
                    if(logger.isDebugEnabled()){
129
                        logger.debug(voterLoggingLabel() +" entity is considered orphan => ACCESS_GRANTED");
130
                    }
131
                    return ACCESS_GRANTED;
132
                }
133

    
134
                if(!auth.hasProperty()){
135
                    if ( vr.isIgnoreUuidMatch && vr.isClassMatch && vr.isPermissionMatch){
136
                        if(logger.isDebugEnabled()){
137
                            logger.debug(voterLoggingLabel() +" no targetUuid, class & permission match => ACCESS_GRANTED");
138
                        }
139
                        return ACCESS_GRANTED;
140
                    }
141
                    if ( vr.isUuidMatch && vr.isClassMatch && vr.isPermissionMatch ){
142
                        if(logger.isDebugEnabled()){
143
                            logger.debug(voterLoggingLabel() +" permission, class and uuid are matching => ACCESS_GRANTED");
144
                        }
145
                        return ACCESS_GRANTED;
146
                    }
147
                } else {
148
                    //
149
                    // If the authority contains a property AND the voter is responsible for this class
150
                    // we must change the fallThroughVote
151
                    // to ABSTAIN, since no decision can be made in this case at this point
152
                    // the decision will be delegated to the furtherVotingDescisions() method
153
                    if(vr.isClassMatch){
154
                        fallThroughVote = ACCESS_ABSTAIN;
155
                    }
156
                }
157

    
158
                //
159
                // ask subclasses for further voting decisions
160
                // subclasses will cast votes for specific Cdm Types
161
                //
162
                Integer furtherVotingResult = furtherVotingDescisions(auth, targetEntityStates, attributes, vr);
163
                if(furtherVotingResult != null){
164
                    if(logger.isDebugEnabled()){
165
                        logger.debug(voterLoggingLabel() + " furtherVotingResult => " + voteToString(furtherVotingResult));
166
                    }
167
                    switch(furtherVotingResult){
168
                        case ACCESS_GRANTED:
169
                            // no further check needed
170
                            return ACCESS_GRANTED;
171
                        case ACCESS_DENIED:
172
                            // remember the DENIED vote in case none of
173
                            // potentially following furtherVotes are
174
                            // GRANTED
175
                            deniedByPreviousFurtherVoting = true;
176
                        //$FALL-THROUGH$
177
                        case ACCESS_ABSTAIN: /* nothing to do */
178
                            default: /* nothing to do */
179
                    }
180
                }
181
            } // END Authorities loop
182
        } // END attributes loop
183

    
184
        int votingResult = deniedByPreviousFurtherVoting ? ACCESS_DENIED : fallThroughVote;
185
        // the value of fallThroughVote depends on whether the authority had an property or not, see above
186
        if(logger.isDebugEnabled()){
187
            logger.debug(voterLoggingLabel() + " fallThroughVote => " + voteToString(fallThroughVote));
188
            logger.debug(voterLoggingLabel() + " ##votingResult## => " + voteToString(votingResult));
189
        }
190
        return votingResult;
191
    }
192

    
193
    /**
194
     * The AccessDecisionVoter implementing this method can indicate via this method that
195
     * an entity has become orphan in order to allow deleting it. In case the implementing method
196
     * returns <code>false</code> deleting of the entity will be denied.
197
     * <p>
198
     * This is important
199
     * in the context of hierarchic permission propagation like for example in
200
     * tree structures where the permission to delete an entity is given on base
201
     * of the permission on an parent object. Entities which become detached
202
     * from the tree would otherwise no longer be deletable.
203
     *
204
     * @param object
205
     * @return whether the cdm entity is orpahn
206
     */
207
    public abstract boolean isOrpahn(CdmBase object);
208

    
209
    /**
210
     * Override this method to implement specific decisions.
211
     * Implementations of this method will be executed in {@link #vote(Authentication, TargetEntityStates, Collection)}.
212
     *
213
     * @param CdmAuthority
214
     * @param targetEntityStates
215
     * @param attributes
216
     * @param validationResult
217
     * @return A return value of ACCESS_ABSTAIN or null will be ignored in {@link #vote(Authentication, Object, Collection)}
218
     */
219
    protected Integer furtherVotingDescisions(CdmAuthority CdmAuthority, TargetEntityStates targetEntityStates, Collection<ConfigAttribute> attributes,
220
            ValidationResult validationResult) {
221
        return null;
222
    }
223

    
224
    /**
225
     * returns a label for the logging output
226
     * @return
227
     */
228
    protected String voterLoggingLabel(){
229
        return "(" + getResponsibilityClass().getSimpleName() + "-Voter)";
230
    }
231

    
232
    /**
233
     *
234
     * @param vote
235
     * @return string representations for the votes defined in {@link AccessDecisionVoter}
236
     */
237
    protected String voteToString(int vote) {
238
        switch (vote){
239
            case 1: return "ACCESS_GRANTED";
240
            case 0: return "ACCESS_ABSTAIN";
241
            case -1: return "ACCESS_DENIED";
242
            default: return Integer.toString(vote);
243
        }
244
    }
245

    
246
    /**
247
     * Holds various flags with validation results.
248
     * Is used to pass this information from
249
     * {@link CdmPermissionVoter#vote(Authentication, Object, Collection)}
250
     * to {@link CdmPermissionVoter#furtherVotingDescisions(CdmAuthority, Object, Collection, ValidationResult)}
251
     *
252
     * @author andreas kohlbecker
253
     * @since Sep 5, 2012
254
     *
255
     */
256
    protected class ValidationResult {
257

    
258
        /**
259
         * ignore the result of the uuid match test completely
260
         * this flag becomes true when the authority given to
261
         * an authentication has no uuid part
262
         */
263
        public boolean isIgnoreUuidMatch;
264
        boolean isPermissionMatch = false;
265
        boolean isPropertyMatch = false;
266
        boolean isUuidMatch = false;
267
        boolean isClassMatch = false;
268

    
269
        @Override
270
        public String toString(){
271
            return "isClassMatch: " + Boolean.toString(isClassMatch) + ", "
272
                    + "isUuidMatch: " + Boolean.toString(isUuidMatch) + ", "
273
                    + "isPermissionMatch: " + Boolean.toString(isPermissionMatch) + ", "
274
                    + "isPropertyMatch: " + Boolean.toString(isPropertyMatch);
275
        }
276
    }
277
}
(1-1/12)