cleanup
[cdmlib.git] / cdmlib-persistence / src / main / java / eu / etaxonomy / cdm / persistence / permission / voter / CdmPermissionVoter.java
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;
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;
20
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;
27
28 /**
29 * The <code>CdmPermissionVoter</code> provides access control votes for {@link CdmBase} objects.
30 *
31 * @author andreas kohlbecker
32 * @since Sep 4, 2012
33 */
34 public abstract class CdmPermissionVoter implements AccessDecisionVoter <TargetEntityStates> {
35
36 private static final Logger logger = LogManager.getLogger();
37
38 private static final EnumSet<CRUD> DELETE = EnumSet.of(CRUD.DELETE);
39
40 @Override
41 public boolean supports(ConfigAttribute attribute) {
42 // all CdmPermissionVoter support CdmAuthority
43 return attribute instanceof CdmAuthority;
44 }
45
46 @Override
47 public boolean supports(Class<?> clazz) {
48 /* NOTE!!!
49 * Do not change this, all CdmPermissionVoters must support CdmBase.class
50 */
51 return clazz.isInstance(CdmBase.class);
52 }
53
54 /**
55 * Sets the Cdm type, or super type this Voter is responsible for.
56 */
57 abstract public Class<? extends CdmBase> getResponsibilityClass();
58
59 protected boolean isResponsibleFor(Object securedObject) {
60 return getResponsibilityClass().isAssignableFrom(securedObject.getClass());
61 }
62
63 protected boolean isResponsibleFor(PermissionClass permissionClass) {
64 return getResponsibility().equals(permissionClass);
65 }
66
67 /**
68 * Get the according CdmPermissionClass matching {@link #getResponsibilityClass()} the cdm class this voter is responsible for.
69 * @return
70 */
71 protected PermissionClass getResponsibility() {
72 return PermissionClass.getValueOf(getResponsibilityClass());
73 }
74
75 @Override
76 public int vote(Authentication authentication, TargetEntityStates targetEntityStates, Collection<ConfigAttribute> attributes) {
77
78 if(!isResponsibleFor(targetEntityStates.getEntity())){
79 logger.debug(voterLoggingLabel() + " class missmatch => ACCESS_ABSTAIN");
80 return ACCESS_ABSTAIN;
81 }
82
83 if (logger.isDebugEnabled()){
84 logger.debug(voterLoggingLabel() + " voting for authentication: " + authentication.getName() + ", object : " + targetEntityStates.getEntity().toString() + ", attribute[0]:" + ((CdmAuthority)attributes.iterator().next()).getAttribute());
85 }
86
87 int fallThroughVote = ACCESS_DENIED;
88 boolean deniedByPreviousFurtherVoting = false;
89
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");
95 }
96 CdmAuthority evalPermission = (CdmAuthority)attribute;
97
98 for (GrantedAuthority authority: authentication.getAuthorities()){
99
100 CdmAuthority auth;
101 try {
102 auth = CdmAuthority.fromGrantedAuthority(authority);
103 } catch (CdmAuthorityParsingException e) {
104 logger.debug(voterLoggingLabel() + " skipping " + authority.getAuthority() + " due to CdmAuthorityParsingException");
105 continue;
106 }
107
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");
111 continue;
112 }
113
114 ValidationResult vr = new ValidationResult();
115
116 boolean isALL = auth.getPermissionClass().equals(PermissionClass.ALL);
117
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();
122
123 if(logger.isDebugEnabled()){
124 logger.debug(voterLoggingLabel() + " " + vr);
125 }
126
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");
131 }
132 return ACCESS_GRANTED;
133 }
134
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");
139 }
140 return ACCESS_GRANTED;
141 }
142 if ( vr.isUuidMatch && vr.isClassMatch && vr.isPermissionMatch ){
143 if(logger.isDebugEnabled()){
144 logger.debug(voterLoggingLabel() +" permission, class and uuid are matching => ACCESS_GRANTED");
145 }
146 return ACCESS_GRANTED;
147 }
148 } else {
149 //
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
154 if(vr.isClassMatch){
155 fallThroughVote = ACCESS_ABSTAIN;
156 }
157 }
158
159 //
160 // ask subclasses for further voting decisions
161 // subclasses will cast votes for specific Cdm Types
162 //
163 Integer furtherVotingResult = furtherVotingDescisions(auth, targetEntityStates, attributes, vr);
164 if(furtherVotingResult != null){
165 if(logger.isDebugEnabled()){
166 logger.debug(voterLoggingLabel() + " furtherVotingResult => " + voteToString(furtherVotingResult));
167 }
168 switch(furtherVotingResult){
169 case ACCESS_GRANTED:
170 // no further check needed
171 return ACCESS_GRANTED;
172 case ACCESS_DENIED:
173 // remember the DENIED vote in case none of
174 // potentially following furtherVotes are
175 // GRANTED
176 deniedByPreviousFurtherVoting = true;
177 //$FALL-THROUGH$
178 case ACCESS_ABSTAIN: /* nothing to do */
179 default: /* nothing to do */
180 }
181 }
182 } // END Authorities loop
183 } // END attributes loop
184
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));
190 }
191 return votingResult;
192 }
193
194 /**
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.
198 * <p>
199 * This is important
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.
204 *
205 * @param object
206 * @return whether the cdm entity is orpahn
207 */
208 public abstract boolean isOrpahn(CdmBase object);
209
210 /**
211 * Override this method to implement specific decisions.
212 * Implementations of this method will be executed in {@link #vote(Authentication, TargetEntityStates, Collection)}.
213 *
214 * @param CdmAuthority
215 * @param targetEntityStates
216 * @param attributes
217 * @param validationResult
218 * @return A return value of ACCESS_ABSTAIN or null will be ignored in {@link #vote(Authentication, Object, Collection)}
219 */
220 protected Integer furtherVotingDescisions(CdmAuthority CdmAuthority, TargetEntityStates targetEntityStates, Collection<ConfigAttribute> attributes,
221 ValidationResult validationResult) {
222 return null;
223 }
224
225 /**
226 * returns a label for the logging output
227 * @return
228 */
229 protected String voterLoggingLabel(){
230 return "(" + getResponsibilityClass().getSimpleName() + "-Voter)";
231 }
232
233 /**
234 *
235 * @param vote
236 * @return string representations for the votes defined in {@link AccessDecisionVoter}
237 */
238 protected String voteToString(int vote) {
239 switch (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);
244 }
245 }
246
247 /**
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)}
252 *
253 * @author andreas kohlbecker
254 * @since Sep 5, 2012
255 *
256 */
257 protected class ValidationResult {
258
259 /**
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
263 */
264 public boolean isIgnoreUuidMatch;
265 boolean isPermissionMatch = false;
266 boolean isPropertyMatch = false;
267 boolean isUuidMatch = false;
268 boolean isClassMatch = false;
269
270 @Override
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);
276 }
277 }
278 }