/**
* Copyright (C) 2015 Valkyrie RCP
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.valkyriercp.security.support;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.valkyriercp.core.Authorizable;
import org.valkyriercp.security.SecurityController;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
/**
* Abstract implementation of a security controller. Derived classes are responsible for
* providing the {@link ConfigAttribute}s and any secured object that will be
* used by the decision manager to make the decision to authorize the controlled objects.
* <p>
* This class uses weak references to track the the controlled objects, so they can be
* GCed as needed.
* <p>
* If a subclass provides a new post-processor action, then it needs to call
* {@link #registerPostProcessorAction(String)} during construction and it must override
* {@link #doPostProcessorAction(String, Object, boolean)}. It is <b>critical</b> that
* the overridden doPostProcessorAction method call
* <code>super.doPostProcessorAction</code> for any action id it does not directly
* handle.
* <p>
* This base class provides the following post-processor actions:
* <p>
* <b>visibleTracksAuthorized</b> - if the controlled object has a
* <code>setVisible(boolean)</code> method then it is called with the authorized value.
* Thus, if the object is not authorized, it will have <code>setVisible(false)</code>
* called on it.
*
* @author Larry Streepy
* @see #getSecuredObject()
* @see #getConfigAttributeDefinition(Object)
*
*/
public abstract class AbstractSecurityController implements SecurityController, InitializingBean {
private final Log logger = LogFactory.getLog(getClass());
/** The list of objects that we are controlling. */
private List controlledObjects = new ArrayList();
/** The AccessDecisionManager used to make the "authorize" decision. */
private AccessDecisionManager accessDecisionManager;
/** Last known authentication token. */
private Authentication lastAuthentication = null;
/** All registered post-processor action ids. */
private HashSet postProcessorActionIds = new HashSet();
/** Comma-separated list of post-processor actions to run. */
private String postProcessorActionsToRun = "";
public static final String VISIBLE_TRACKS_AUTHORIZED_ACTION = "visibleTracksAuthorized";
/**
* Constructor.
*/
protected AbstractSecurityController() {
registerPostProcessorAction( VISIBLE_TRACKS_AUTHORIZED_ACTION );
}
/**
* Get the secured object on which we are making the authorization decision. This may
* be null if no specific object is to be considered in the decision.
* @return secured object
*/
protected abstract Object getSecuredObject();
/**
* Get the ConfigAttributeDefinition for the secured object. This will provide the
* authorization information to the access decision manager.
* @param securedObject Secured object for whom the config attribute definition is to
* be rretrieved. This may be null.
* @return attribute definition for the provided secured object
*/
protected abstract List<ConfigAttribute> getConfigAttributeDefinition(Object securedObject);
/**
* Set the list of post-processor actions to be run. This must be a comma-separated
* list of action names.
* @param actions Comma-separated list of post-processor action names
*/
public void setPostProcessorActionsToRun(String actions) {
postProcessorActionsToRun = actions;
}
/**
* Get the list of post-processor actions to run.
* @return Comma-separated list of post-processor action names
*/
public String getPostProcessorActionsToRun() {
return postProcessorActionsToRun;
}
/**
* Register a post-processor action. The action id specified must not conflict with
* any other action registered. Subclasses that provide additional post-processor
* actions MUST call this method to register them.
* @param actionId Id of post-processor action to register
*/
protected void registerPostProcessorAction(String actionId) {
if( postProcessorActionIds.contains( actionId ) ) {
throw new IllegalArgumentException( "Post-processor Action '" + actionId + "' is already registered" );
}
postProcessorActionIds.add( actionId );
}
/**
* The authentication token for the current user has changed. Update all our
* controlled objects accordingly.
* @param authentication now in effect, may be null
*/
public void setAuthenticationToken(Authentication authentication) {
setLastAuthentication( authentication ); // Keep it for later
runAuthorization();
}
/**
* Update the authorization of all controlled objects.
*/
protected void runAuthorization() {
boolean authorize = shouldAuthorize( getLastAuthentication() );
// Install the decision
for( Iterator iter = controlledObjects.iterator(); iter.hasNext(); ) {
WeakReference ref = (WeakReference) iter.next();
Authorizable controlledObject = (Authorizable) ref.get();
if( controlledObject == null ) {
// Has been GCed, remove from our list
iter.remove();
} else {
updateControlledObject( controlledObject, authorize );
}
}
}
/**
* Update a controlled object based on the given authorization state.
* @param controlledObject Object being controlled
* @param authorized state that has been installed on controlledObject
*/
protected void updateControlledObject(Authorizable controlledObject, boolean authorized) {
if( logger.isDebugEnabled() ) {
logger.debug( "setAuthorized( " + authorized + ") on: " + controlledObject );
}
controlledObject.setAuthorized( authorized );
runPostProcessorActions( controlledObject, authorized );
}
/**
* Run all the requested post-processor actions.
* @param controlledObject Object being controlled
* @param authorized state that has been installed on controlledObject
*/
protected void runPostProcessorActions(Object controlledObject, boolean authorized) {
String actions = getPostProcessorActionsToRun();
if( logger.isDebugEnabled() ) {
logger.debug( "Run post-processors actions: " + actions );
}
String[] actionIds = StringUtils.commaDelimitedListToStringArray(actions);
for( int i = 0; i < actionIds.length; i++ ) {
doPostProcessorAction( actionIds[i], controlledObject, authorized );
}
}
/**
* Post-process a controlled object after its authorization state has been updated.
* Subclasses that override this method MUST ensure that this method is called id they
* do not process the given action id.
*
* @param actionId Id of the post-processor action to run
* @param controlledObject Object being controlled
* @param authorized state that has been installed on controlledObject
*/
protected void doPostProcessorAction(String actionId, Object controlledObject, boolean authorized) {
if( VISIBLE_TRACKS_AUTHORIZED_ACTION.equals( actionId ) ) {
setVisibilityOnControlledObject( controlledObject, authorized );
}
}
/**
* Set the visible property on a controlled action according to the provided
* authorization.
*/
private void setVisibilityOnControlledObject(Object controlledObject, boolean authorized) {
try {
Method method = controlledObject.getClass().getMethod( "setVisible", new Class[] { boolean.class } );
method.invoke( controlledObject, new Object[] { new Boolean( authorized ) } );
} catch( NoSuchMethodException ignored ) {
System.out.println( "NO setVisible method on object: " + controlledObject );
// No method to call, so nothing to do
} catch( IllegalAccessException ignored ) {
logger.error( "Could not call setVisible", ignored );
} catch( InvocationTargetException ignored ) {
logger.error( "Could not call setVisible", ignored );
}
}
/**
* Determine if our controlled objects should be authorized based on the provided
* authentication token.
* @param authentication token
* @return true if should authorize
*/
protected boolean shouldAuthorize(Authentication authentication) {
Assert.state(getAccessDecisionManager() != null, "The AccessDecisionManager can not be null!");
boolean authorize = false;
try {
if( authentication != null ) {
Object securedObject = getSecuredObject();
List<ConfigAttribute> cad = getConfigAttributeDefinition( securedObject );
getAccessDecisionManager().decide( authentication, getSecuredObject(), cad );
authorize = true;
}
} catch( AccessDeniedException e ) {
// This means the secured objects should not be authorized
}
return authorize;
}
/**
* Set the access decision manager to use
* @param accessDecisionManager
*/
public void setAccessDecisionManager(AccessDecisionManager accessDecisionManager) {
this.accessDecisionManager = accessDecisionManager;
}
/**
* Get the access decision manager in use
*/
public AccessDecisionManager getAccessDecisionManager() {
return accessDecisionManager;
}
/**
* Set the objects that are to be controlled. Only beans that implement the
* {@link org.springframework.security.access.event.AuthorizedEvent} interface are processed.
* @param secured List of objects to control
*/
public void setControlledObjects(List secured) {
controlledObjects = new ArrayList( secured.size() );
// Convert to weak references and validate the object types
for( Iterator iter = secured.iterator(); iter.hasNext(); ) {
Object o = iter.next();
// Ensure that we got something we can control
if( !(o instanceof Authorizable) ) {
throw new IllegalArgumentException( "Controlled object must implement Authorizable, got "
+ o.getClass() );
}
addAndPrepareControlledObject( (Authorizable) o );
}
}
/**
* Add an object to our controlled set.
* @param object to control
*/
public void addControlledObject(Authorizable object) {
addAndPrepareControlledObject( object );
}
/**
* Add a new object to the list of controlled objects. Install our last known
* authorization decision so newly created objects will reflect the current security
* state.
* @param controlledObject to add
*/
private void addAndPrepareControlledObject(Authorizable controlledObject) {
controlledObjects.add( new WeakReference( controlledObject ) );
// Properly configure the new object
boolean authorize = shouldAuthorize( getLastAuthentication() );
updateControlledObject( controlledObject, authorize );
}
/**
* Remove an object from our controlled set.
* @param object to remove
* @return object removed or null if not found
*/
public Object removeControlledObject(Authorizable object) {
Object removed = null;
for( Iterator iter = controlledObjects.iterator(); iter.hasNext(); ) {
WeakReference ref = (WeakReference) iter.next();
Authorizable controlledObject = (Authorizable) ref.get();
if( controlledObject == null ) {
// Has been GCed, remove from our list
iter.remove();
} else if( controlledObject.equals( object ) ) {
removed = controlledObject;
iter.remove();
}
}
return removed;
}
protected void setLastAuthentication(Authentication authentication) {
lastAuthentication = authentication;
}
protected Authentication getLastAuthentication() {
return lastAuthentication;
}
/**
* Validate our configuration.
* @throws Exception
*/
public void afterPropertiesSet() throws Exception {
// Ensure that all post-processors requested are registered
String[] actions = StringUtils.commaDelimitedListToStringArray( getPostProcessorActionsToRun() );
for( int i = 0; i < actions.length; i++ ) {
if( !postProcessorActionIds.contains( actions[i] ) ) {
throw new IllegalArgumentException( "Requested post-processor action '" + actions[i]
+ "' is not registered." );
}
}
}
}