2 * Copyright (C) 2012 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
.permission
.voter
;
11 import java
.util
.Collection
;
12 import java
.util
.EnumSet
;
14 import org
.apache
.logging
.log4j
.LogManager
;
15 import org
.apache
.logging
.log4j
.Logger
;
16 import org
.springframework
.security
.access
.AccessDecisionVoter
;
17 import org
.springframework
.security
.access
.ConfigAttribute
;
18 import org
.springframework
.security
.core
.Authentication
;
19 import org
.springframework
.security
.core
.GrantedAuthority
;
21 import eu
.etaxonomy
.cdm
.model
.common
.CdmBase
;
22 import eu
.etaxonomy
.cdm
.model
.permission
.CRUD
;
23 import eu
.etaxonomy
.cdm
.model
.permission
.PermissionClass
;
24 import eu
.etaxonomy
.cdm
.persistence
.permission
.CdmAuthority
;
25 import eu
.etaxonomy
.cdm
.persistence
.permission
.CdmAuthorityParsingException
;
26 import eu
.etaxonomy
.cdm
.persistence
.permission
.TargetEntityStates
;
29 * The <code>CdmPermissionVoter</code> provides access control votes for {@link CdmBase} objects.
31 * @author andreas kohlbecker
34 public abstract class CdmPermissionVoter
implements AccessDecisionVoter
<TargetEntityStates
> {
36 private static final Logger logger
= LogManager
.getLogger();
38 private static final EnumSet
<CRUD
> DELETE
= EnumSet
.of(CRUD
.DELETE
);
41 public boolean supports(ConfigAttribute attribute
) {
42 // all CdmPermissionVoter support CdmAuthority
43 return attribute
instanceof CdmAuthority
;
47 public boolean supports(Class
<?
> clazz
) {
49 * Do not change this, all CdmPermissionVoters must support CdmBase.class
51 return clazz
.isInstance(CdmBase
.class);
55 * Sets the Cdm type, or super type this Voter is responsible for.
57 abstract public Class
<?
extends CdmBase
> getResponsibilityClass();
59 protected boolean isResponsibleFor(Object securedObject
) {
60 return getResponsibilityClass().isAssignableFrom(securedObject
.getClass());
63 protected boolean isResponsibleFor(PermissionClass permissionClass
) {
64 return getResponsibility().equals(permissionClass
);
68 * Get the according CdmPermissionClass matching {@link #getResponsibilityClass()} the cdm class this voter is responsible for.
71 protected PermissionClass
getResponsibility() {
72 return PermissionClass
.getValueOf(getResponsibilityClass());
76 public int vote(Authentication authentication
, TargetEntityStates targetEntityStates
, Collection
<ConfigAttribute
> attributes
) {
78 if(!isResponsibleFor(targetEntityStates
.getEntity())){
79 logger
.debug(voterLoggingLabel() + " class missmatch => ACCESS_ABSTAIN");
80 return ACCESS_ABSTAIN
;
83 if (logger
.isDebugEnabled()){
84 logger
.debug(voterLoggingLabel() + " voting for authentication: " + authentication
.getName() + ", object : " + targetEntityStates
.getEntity().toString() + ", attribute[0]:" + ((CdmAuthority
)attributes
.iterator().next()).getAttribute());
87 int fallThroughVote
= ACCESS_DENIED
;
88 boolean deniedByPreviousFurtherVoting
= false;
90 // loop over all attributes = permissions of which at least one must match
91 // usually there is only one element in the collection!
92 for(ConfigAttribute attribute
: attributes
){
93 if(!(attribute
instanceof CdmAuthority
)){
94 throw new RuntimeException("attributes must contain only CdmAuthority");
96 CdmAuthority evalPermission
= (CdmAuthority
)attribute
;
98 for (GrantedAuthority authority
: authentication
.getAuthorities()){
102 auth
= CdmAuthority
.fromGrantedAuthority(authority
);
103 } catch (CdmAuthorityParsingException e
) {
104 logger
.debug(voterLoggingLabel() + " skipping " + authority
.getAuthority() + " due to CdmAuthorityParsingException");
108 // check if the voter is responsible for the permission to be evaluated
109 if( ! isResponsibleFor(evalPermission
.getPermissionClass())){
110 logger
.debug(voterLoggingLabel() + " not responsible for " + evalPermission
.getPermissionClass() + " -> skipping");
114 ValidationResult vr
= new ValidationResult();
116 boolean isALL
= auth
.getPermissionClass().equals(PermissionClass
.ALL
);
118 vr
.isClassMatch
= isALL
|| auth
.getPermissionClass().equals(evalPermission
.getPermissionClass());
119 vr
.isPermissionMatch
= auth
.getOperation().containsAll(evalPermission
.getOperation());
120 vr
.isUuidMatch
= auth
.hasTargetUuid() && auth
.getTargetUUID().equals(targetEntityStates
.getEntity().getUuid());
121 vr
.isIgnoreUuidMatch
= !auth
.hasTargetUuid();
123 if(logger
.isDebugEnabled()){
124 logger
.debug(voterLoggingLabel() + " " + vr
);
127 // first of all, always allow deleting orphan entities
128 if(vr
.isClassMatch
&& evalPermission
.getOperation().equals(DELETE
) && isOrpahn(targetEntityStates
.getEntity())) {
129 if(logger
.isDebugEnabled()){
130 logger
.debug(voterLoggingLabel() +" entity is considered orphan => ACCESS_GRANTED");
132 return ACCESS_GRANTED
;
135 if(!auth
.hasProperty()){
136 if ( vr
.isIgnoreUuidMatch
&& vr
.isClassMatch
&& vr
.isPermissionMatch
){
137 if(logger
.isDebugEnabled()){
138 logger
.debug(voterLoggingLabel() +" no targetUuid, class & permission match => ACCESS_GRANTED");
140 return ACCESS_GRANTED
;
142 if ( vr
.isUuidMatch
&& vr
.isClassMatch
&& vr
.isPermissionMatch
){
143 if(logger
.isDebugEnabled()){
144 logger
.debug(voterLoggingLabel() +" permission, class and uuid are matching => ACCESS_GRANTED");
146 return ACCESS_GRANTED
;
150 // If the authority contains a property AND the voter is responsible for this class
151 // we must change the fallThroughVote
152 // to ABSTAIN, since no decision can be made in this case at this point
153 // the decision will be delegated to the furtherVotingDescisions() method
155 fallThroughVote
= ACCESS_ABSTAIN
;
160 // ask subclasses for further voting decisions
161 // subclasses will cast votes for specific Cdm Types
163 Integer furtherVotingResult
= furtherVotingDescisions(auth
, targetEntityStates
, attributes
, vr
);
164 if(furtherVotingResult
!= null){
165 if(logger
.isDebugEnabled()){
166 logger
.debug(voterLoggingLabel() + " furtherVotingResult => " + voteToString(furtherVotingResult
));
168 switch(furtherVotingResult
){
170 // no further check needed
171 return ACCESS_GRANTED
;
173 // remember the DENIED vote in case none of
174 // potentially following furtherVotes are
176 deniedByPreviousFurtherVoting
= true;
178 case ACCESS_ABSTAIN
: /* nothing to do */
179 default: /* nothing to do */
182 } // END Authorities loop
183 } // END attributes loop
185 int votingResult
= deniedByPreviousFurtherVoting ? ACCESS_DENIED
: fallThroughVote
;
186 // the value of fallThroughVote depends on whether the authority had an property or not, see above
187 if(logger
.isDebugEnabled()){
188 logger
.debug(voterLoggingLabel() + " fallThroughVote => " + voteToString(fallThroughVote
));
189 logger
.debug(voterLoggingLabel() + " ##votingResult## => " + voteToString(votingResult
));
195 * The AccessDecisionVoter implementing this method can indicate via this method that
196 * an entity has become orphan in order to allow deleting it. In case the implementing method
197 * returns <code>false</code> deleting of the entity will be denied.
200 * in the context of hierarchic permission propagation like for example in
201 * tree structures where the permission to delete an entity is given on base
202 * of the permission on an parent object. Entities which become detached
203 * from the tree would otherwise no longer be deletable.
206 * @return whether the cdm entity is orpahn
208 public abstract boolean isOrpahn(CdmBase object
);
211 * Override this method to implement specific decisions.
212 * Implementations of this method will be executed in {@link #vote(Authentication, TargetEntityStates, Collection)}.
214 * @param CdmAuthority
215 * @param targetEntityStates
217 * @param validationResult
218 * @return A return value of ACCESS_ABSTAIN or null will be ignored in {@link #vote(Authentication, Object, Collection)}
220 protected Integer
furtherVotingDescisions(CdmAuthority CdmAuthority
, TargetEntityStates targetEntityStates
, Collection
<ConfigAttribute
> attributes
,
221 ValidationResult validationResult
) {
226 * returns a label for the logging output
229 protected String
voterLoggingLabel(){
230 return "(" + getResponsibilityClass().getSimpleName() + "-Voter)";
236 * @return string representations for the votes defined in {@link AccessDecisionVoter}
238 protected String
voteToString(int vote
) {
240 case 1: return "ACCESS_GRANTED";
241 case 0: return "ACCESS_ABSTAIN";
242 case -1: return "ACCESS_DENIED";
243 default: return Integer
.toString(vote
);
248 * Holds various flags with validation results.
249 * Is used to pass this information from
250 * {@link CdmPermissionVoter#vote(Authentication, Object, Collection)}
251 * to {@link CdmPermissionVoter#furtherVotingDescisions(CdmAuthority, Object, Collection, ValidationResult)}
253 * @author andreas kohlbecker
257 protected class ValidationResult
{
260 * ignore the result of the uuid match test completely
261 * this flag becomes true when the authority given to
262 * an authentication has no uuid part
264 public boolean isIgnoreUuidMatch
;
265 boolean isPermissionMatch
= false;
266 boolean isPropertyMatch
= false;
267 boolean isUuidMatch
= false;
268 boolean isClassMatch
= false;
271 public String
toString(){
272 return "isClassMatch: " + Boolean
.toString(isClassMatch
) + ", "
273 + "isUuidMatch: " + Boolean
.toString(isUuidMatch
) + ", "
274 + "isPermissionMatch: " + Boolean
.toString(isPermissionMatch
) + ", "
275 + "isPropertyMatch: " + Boolean
.toString(isPropertyMatch
);