001    /*
002     * Copyright 2002-2005 the original author or authors.
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005     * use this file except in compliance with the License. You may obtain a copy of
006     * the License at
007     *
008     * http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013     * License for the specific language governing permissions and limitations under
014     * the License.
015     */
016    package org.springframework.richclient.security.support;
017    
018    import java.lang.ref.WeakReference;
019    import java.lang.reflect.InvocationTargetException;
020    import java.lang.reflect.Method;
021    import java.util.ArrayList;
022    import java.util.HashSet;
023    import java.util.Iterator;
024    import java.util.List;
025    
026    import org.apache.commons.logging.Log;
027    import org.apache.commons.logging.LogFactory;
028    import org.springframework.beans.factory.InitializingBean;
029    import org.springframework.richclient.core.Authorizable;
030    import org.springframework.richclient.security.SecurityController;
031    import org.springframework.security.AccessDecisionManager;
032    import org.springframework.security.AccessDeniedException;
033    import org.springframework.security.Authentication;
034    import org.springframework.security.ConfigAttributeDefinition;
035    import org.springframework.util.Assert;
036    import org.springframework.util.StringUtils;
037    
038    /**
039     * Abstract implementation of a security controller. Derived classes are responsible for
040     * providing the {@link ConfigAttributeDefinition} and any secured object that will be
041     * used by the decision manager to make the decision to authorize the controlled objects.
042     * <p>
043     * This class uses weak references to track the the controlled objects, so they can be
044     * GCed as needed.
045     * <p>
046     * If a subclass provides a new post-processor action, then it needs to call
047     * {@link #registerPostProcessorAction(String)} during construction and it must override
048     * {@link #doPostProcessorAction(String, Object, boolean)}. It is <b>critical</b> that
049     * the overridden doPostProcessorAction method call
050     * <code>super.doPostProcessorAction</code> for any action id it does not directly
051     * handle.
052     * <p>
053     * This base class provides the following post-processor actions:
054     * <p>
055     * <b>visibleTracksAuthorized</b> - if the controlled object has a
056     * <code>setVisible(boolean)</code> method then it is called with the authorized value.
057     * Thus, if the object is not authorized, it will have <code>setVisible(false)</code>
058     * called on it.
059     * 
060     * @author Larry Streepy
061     * @see #getSecuredObject()
062     * @see #getConfigAttributeDefinition(Object)
063     * 
064     */
065    public abstract class AbstractSecurityController implements SecurityController, InitializingBean {
066    
067        private final Log logger = LogFactory.getLog( getClass() );
068    
069        /** The list of objects that we are controlling. */
070        private List controlledObjects = new ArrayList();
071    
072        /** The AccessDecisionManager used to make the "authorize" decision. */
073        private AccessDecisionManager accessDecisionManager;
074    
075        /** Last known authentication token. */
076        private Authentication lastAuthentication = null;
077    
078        /** All registered post-processor action ids. */
079        private HashSet postProcessorActionIds = new HashSet();
080    
081        /** Comma-separated list of post-processor actions to run. */
082        private String postProcessorActionsToRun = "";
083    
084        public static final String VISIBLE_TRACKS_AUTHORIZED_ACTION = "visibleTracksAuthorized";
085    
086        /**
087         * Constructor.
088         */
089        protected AbstractSecurityController() {
090            registerPostProcessorAction( VISIBLE_TRACKS_AUTHORIZED_ACTION );
091        }
092    
093        /**
094         * Get the secured object on which we are making the authorization decision. This may
095         * be null if no specific object is to be considered in the decision.
096         * @return secured object
097         */
098        protected abstract Object getSecuredObject();
099    
100        /**
101         * Get the ConfigAttributeDefinition for the secured object. This will provide the
102         * authorization information to the access decision manager.
103         * @param securedObject Secured object for whom the config attribute definition is to
104         *            be rretrieved. This may be null.
105         * @return attribute definition for the provided secured object
106         */
107        protected abstract ConfigAttributeDefinition getConfigAttributeDefinition(Object securedObject);
108    
109        /**
110         * Set the list of post-processor actions to be run. This must be a comma-separated
111         * list of action names.
112         * @param actions Comma-separated list of post-processor action names
113         */
114        public void setPostProcessorActionsToRun(String actions) {
115            postProcessorActionsToRun = actions;
116        }
117    
118        /**
119         * Get the list of post-processor actions to run.
120         * @return Comma-separated list of post-processor action names
121         */
122        public String getPostProcessorActionsToRun() {
123            return postProcessorActionsToRun;
124        }
125    
126        /**
127         * Register a post-processor action. The action id specified must not conflict with
128         * any other action registered. Subclasses that provide additional post-processor
129         * actions MUST call this method to register them.
130         * @param actionId Id of post-processor action to register
131         */
132        protected void registerPostProcessorAction(String actionId) {
133            if( postProcessorActionIds.contains( actionId ) ) {
134                throw new IllegalArgumentException( "Post-processor Action '" + actionId + "' is already registered" );
135            }
136            postProcessorActionIds.add( actionId );
137        }
138    
139        /**
140         * The authentication token for the current user has changed. Update all our
141         * controlled objects accordingly.
142         * @param authentication now in effect, may be null
143         */
144        public void setAuthenticationToken(Authentication authentication) {
145            setLastAuthentication( authentication ); // Keep it for later
146            runAuthorization();
147        }
148    
149        /**
150         * Update the authorization of all controlled objects.
151         */
152        protected void runAuthorization() {
153            boolean authorize = shouldAuthorize( getLastAuthentication() );
154    
155            // Install the decision
156            for( Iterator iter = controlledObjects.iterator(); iter.hasNext(); ) {
157                WeakReference ref = (WeakReference) iter.next();
158    
159                Authorizable controlledObject = (Authorizable) ref.get();
160                if( controlledObject == null ) {
161                    // Has been GCed, remove from our list
162                    iter.remove();
163                } else {
164                    updateControlledObject( controlledObject, authorize );
165                }
166            }
167        }
168    
169        /**
170         * Update a controlled object based on the given authorization state.
171         * @param controlledObject Object being controlled
172         * @param authorized state that has been installed on controlledObject
173         */
174        protected void updateControlledObject(Authorizable controlledObject, boolean authorized) {
175            if( logger.isDebugEnabled() ) {
176                logger.debug( "setAuthorized( " + authorized + ") on: " + controlledObject );
177            }
178            controlledObject.setAuthorized( authorized );
179            runPostProcessorActions( controlledObject, authorized );
180        }
181    
182        /**
183         * Run all the requested post-processor actions.
184         * @param controlledObject Object being controlled
185         * @param authorized state that has been installed on controlledObject
186         */
187        protected void runPostProcessorActions(Object controlledObject, boolean authorized) {
188            String actions = getPostProcessorActionsToRun();
189            if( logger.isDebugEnabled() ) {
190                logger.debug( "Run post-processors actions: " + actions );
191            }
192    
193            String[] actionIds = StringUtils.commaDelimitedListToStringArray( actions );
194            for( int i = 0; i < actionIds.length; i++ ) {
195                doPostProcessorAction( actionIds[i], controlledObject, authorized );
196            }
197        }
198    
199        /**
200         * Post-process a controlled object after its authorization state has been updated.
201         * Subclasses that override this method MUST ensure that this method is called id they
202         * do not process the given action id.
203         * 
204         * @param actionId Id of the post-processor action to run
205         * @param controlledObject Object being controlled
206         * @param authorized state that has been installed on controlledObject
207         */
208        protected void doPostProcessorAction(String actionId, Object controlledObject, boolean authorized) {
209            if( VISIBLE_TRACKS_AUTHORIZED_ACTION.equals( actionId ) ) {
210                setVisibilityOnControlledObject( controlledObject, authorized );
211            }
212        }
213    
214        /**
215         * Set the visible property on a controlled action according to the provided
216         * authorization.
217         */
218        private void setVisibilityOnControlledObject(Object controlledObject, boolean authorized) {
219            try {
220                Method method = controlledObject.getClass().getMethod( "setVisible", new Class[] { boolean.class } );
221                method.invoke( controlledObject, new Object[] { new Boolean( authorized ) } );
222            } catch( NoSuchMethodException ignored ) {
223                System.out.println( "NO setVisible method on object: " + controlledObject );
224                // No method to call, so nothing to do
225            } catch( IllegalAccessException ignored ) {
226                logger.error( "Could not call setVisible", ignored );
227            } catch( InvocationTargetException ignored ) {
228                logger.error( "Could not call setVisible", ignored );
229            }
230        }
231    
232        /**
233         * Determine if our controlled objects should be authorized based on the provided
234         * authentication token.
235         * @param authentication token
236         * @return true if should authorize
237         */
238        protected boolean shouldAuthorize(Authentication authentication) {
239            Assert.state( getAccessDecisionManager() != null, "The AccessDecisionManager can not be null!" );
240            boolean authorize = false;
241            try {
242                if( authentication != null ) {
243                    Object securedObject = getSecuredObject();
244                    ConfigAttributeDefinition cad = getConfigAttributeDefinition( securedObject );
245                    getAccessDecisionManager().decide( authentication, getSecuredObject(), cad );
246                    authorize = true;
247                }
248            } catch( AccessDeniedException e ) {
249                // This means the secured objects should not be authorized
250            }
251            return authorize;
252        }
253    
254        /**
255         * Set the access decision manager to use
256         * @param accessDecisionManager
257         */
258        public void setAccessDecisionManager(AccessDecisionManager accessDecisionManager) {
259            this.accessDecisionManager = accessDecisionManager;
260        }
261    
262        /**
263         * Get the access decision manager in use
264         */
265        public AccessDecisionManager getAccessDecisionManager() {
266            return accessDecisionManager;
267        }
268    
269        /**
270         * Set the objects that are to be controlled. Only beans that implement the
271         * {@link Authorized} interface are processed.
272         * @param secured List of objects to control
273         */
274        public void setControlledObjects(List secured) {
275            controlledObjects = new ArrayList( secured.size() );
276    
277            // Convert to weak references and validate the object types
278            for( Iterator iter = secured.iterator(); iter.hasNext(); ) {
279                Object o = iter.next();
280    
281                // Ensure that we got something we can control
282                if( !(o instanceof Authorizable) ) {
283                    throw new IllegalArgumentException( "Controlled object must implement Authorizable, got "
284                            + o.getClass() );
285                }
286                addAndPrepareControlledObject( (Authorizable) o );
287            }
288        }
289    
290        /**
291         * Add an object to our controlled set.
292         * @param object to control
293         */
294        public void addControlledObject(Authorizable object) {
295            addAndPrepareControlledObject( object );
296        }
297    
298        /**
299         * Add a new object to the list of controlled objects. Install our last known
300         * authorization decision so newly created objects will reflect the current security
301         * state.
302         * @param controlledObject to add
303         */
304        private void addAndPrepareControlledObject(Authorizable controlledObject) {
305            controlledObjects.add( new WeakReference( controlledObject ) );
306    
307            // Properly configure the new object
308            boolean authorize = shouldAuthorize( getLastAuthentication() );
309            updateControlledObject( controlledObject, authorize );
310        }
311    
312        /**
313         * Remove an object from our controlled set.
314         * @param object to remove
315         * @return object removed or null if not found
316         */
317        public Object removeControlledObject(Authorizable object) {
318            Object removed = null;
319    
320            for( Iterator iter = controlledObjects.iterator(); iter.hasNext(); ) {
321                WeakReference ref = (WeakReference) iter.next();
322    
323                Authorizable controlledObject = (Authorizable) ref.get();
324                if( controlledObject == null ) {
325                    // Has been GCed, remove from our list
326                    iter.remove();
327                } else if( controlledObject.equals( object ) ) {
328                    removed = controlledObject;
329                    iter.remove();
330                }
331            }
332            return removed;
333        }
334    
335        protected void setLastAuthentication(Authentication authentication) {
336            lastAuthentication = authentication;
337        }
338    
339        protected Authentication getLastAuthentication() {
340            return lastAuthentication;
341        }
342    
343        /**
344         * Validate our configuration.
345         * @throws Exception
346         */
347        public void afterPropertiesSet() throws Exception {
348            // Ensure that all post-processors requested are registered
349            String[] actions = StringUtils.commaDelimitedListToStringArray( getPostProcessorActionsToRun() );
350            for( int i = 0; i < actions.length; i++ ) {
351                if( !postProcessorActionIds.contains( actions[i] ) ) {
352                    throw new IllegalArgumentException( "Requested post-processor action '" + actions[i]
353                            + "' is not registered." );
354                }
355            }
356        }
357    }