PDA

View Full Version : Dynamic Authorization


mavisakal
Sep 8th, 2004, 03:54 AM
Hi,
Is it possible to make ROLE-METHOD mapping in a database table... For example:
<bean id="bankManagerSecurity" class="net.sf.acegisecurity.intercept.method.MethodSecuri tyInterceptor">
...
<property name="objectDefinitionSource">
<value> net.sf.acegisecurity.context.BankManager.delete*=R OLE_SUPERVISOR,RUN_AS_SERVER
net.sf.acegisecurity.context.BankManager.getBalanc e=ROLE_TELLER,ROLE_SUPERVISOR,BANKSECURITY_CUSTOME R,RUN_AS_SERVER
</value>
</property>
</bean>
Can't we hold net.sf.acegisecurity.context.BankManager.delete* and roles that which were permitted to invoke it, in a database table...

We want to make such a thing because in our application 'Administrator' can create new roles at runtime and can map these roles to the methods... According to the example above 'Administrator' must add method-role mapping definition in applicationContext.xml after creating roles...

Or is there a better way of implementing this kind of dynamic authorization.
We exactly want to do this:
At runtime 'Administrator' will give permissions to users for reaching appropriate jsp pages. How can we implement this?

Thanks in advance.




[/b]

Ben Alex
Sep 8th, 2004, 05:55 PM
You can implement your own ObjectDefinitionSource and refer to it via <ref bean="myDatabaseDrivenObjectDefinitionSource"/>.

mavisakal
Sep 22nd, 2004, 10:18 AM
Can you give some more information about creating database driven ObjectDefinitionSource. API docs and ReferenceManual don't give any information about this.
From reference manual:
The MethodSecurityInterceptor can be configured with configuration attributes in three ways. The first is .....
The third is via writing your own ObjectDefinitionSource, although this is beyond the scope of this document.
Where can we find information about creating custom ObjectDefinitionSource...
Thanks...

Ben Alex
Sep 23rd, 2004, 07:17 PM
You're interested in method interception, meaning you're using MethodSecurityInterceptor. It uses a marker interface which is derived from ObjectDefinitionSource called MethodDefinitionSource.

To write a custom MethodDefinitionSource you'll need to read the JavaDocs for ObjectDefinitionSource. There are three methods, each well defined by the JavaDocs. Indeed there are two concrete implementations - MethodDefinitionMap and MethodDefinitionAttributes - which implement MethodDefinitionSource. These both use AbstractMethodDefinitionSource, which implementes two of the three methods you need to handle from the ObjectDefinitionSource contract. You're only left to handle:


/**
* Performs the actual lookup of the relevant
* <code>ConfigAttributeDefinition</code> for the specified
* <code>MethodInvocation</code>.
*
* <P>
* Provided so subclasses need only to provide one basic method to properly
* interface with the <code>MethodDefinitionSource</code>.
* </p>
*
* @return the <code>ConfigAttributeDefinition</code> that applies to the
* specified <code>MethodInvocation</code>
*/
protected abstract ConfigAttributeDefinition lookupAttributes(
MethodInvocation mi);


Hope that's enough info. If not, please post a specifc question.

mavisakal
Sep 24th, 2004, 12:35 PM
Thanks Ben you explained the relationships very well.
There are some points that I don't understand and want your help.
1-> How the MethodDefinitionSource is constructed from the value of objectDefinitionSource in the following example.
<bean id="securityInterceptor" class="net.sf.acegisecurity.intercept.method.MethodSecuri tyInterceptor">
<property name="authenticationManager"><ref bean="authenticationManager"/></property>
<property name="accessDecisionManager"><ref bean="businessAccessDecisionManager"/></property>
<property name="runAsManager"><ref bean="runAsManager"/></property>
<property name="objectDefinitionSource">
<value>
sample.user.service.UserManager.save*=ROLE_SUPERVI SOR,RUN_AS_SERVER
sample.user.service.UserManager.remove*=ROLE_SUPER VISOR,RUN_AS_SERVER
</value>
</property>
</bean>

2-> Rather than writing the method=ConfigAttributeDefinition mapping in the applicationConfig.xml, I want to hold these values in a database table. For example, MethodAuthorizations table holds a MethodName field (which holds values like sample.user.service.UserManager.save*) and a ConfigAttributeDefinition field(which holds the values like ROLE_SUPERVISOR,RUN_AS_SERVER). After retrieving these values from database I will re-construct them as shown in the above example and I will pass them to MethodDefinitionMap for retrieving ConfigAttributeDefinition for MethodSecurityInterceptor. But I don't know how to handle lookupAttributes() method of MyCustomMethodDefinition class which will extend from AbstractMethodDefinitionSource. Can you help me for accomplishing this subject.
Thanks for your helps.

Ben Alex
Sep 24th, 2004, 06:49 PM
To answer question 1, this works because Spring automatically applies PropertyEditors. Because MethodSecurityInterceptor.objectDefinitionSource is a MethodDefinitionSource, Spring uses the MethodDefinitionSourceEditor. The MethodDefinitionSourceEditor gets passed the entire multi-line String that comprises the entire object definition. It then tokenizes it as if it were a standard Java Properties file. This results in the property name of each line (ie anything to the left of the equals sign) being treated as the method name and passed to the newly created MethodDefinitionMap, with the property value of each line (ie anything to the right of the equals sign) being passed to a ConfigAttributeEditor. ConfigAttributeEditor uses the comma delimiter to build a SecurityConfig instance of each token, putting them all into a ConfigAttributeDefinition that is then returned. The returned ConfigAttributeDefinition is passed to the MethodDefinitionMap.

To answer question 2, I'd normalise the database so you have a SECURE_OBJECT table, a ROLES table, and a SECURE_OBJECT-ROLES table. Thus your DatabaseObjectDefinitionSource can be wired to either a MethodSecurityInterceptor or a FilterSecurityInterceptor. The SECURE_OBJECT table should have a TYPE column to denote whether it's for a MethodInvocation or a FilterInvocation declaration. Once you've got the schema sorted out, it's a simple matter to build a ConfigAttributeDefinition in response to a request. I'd consider returning null to the getConfigAttributeDefinitions() method, as it's only used by AbstractSecurityInterceptor to help with inital configuration checking.

mavisakal
Sep 25th, 2004, 02:54 PM
Thanks again Ben, I implemented my DatabaseDrivenMethodDefinitionSource with your helps.

I created the schema as you defined. Here is my protected ConfigAttributeDefinition lookupAttributes(MethodInvocation mi) method;

protected ConfigAttributeDefinition lookupAttributes(MethodInvocation mi) {
//construct secureObjectName
String secureObjectName=mi.getMethod().getDeclaringClass( ).getName() +"."+ mi.getMethod().getName();
SecureObject secureObject=securityManager.getSecureObject(secur eObjectName);
if(secureObject==null)//if secure object not exist in database
return null;
//retrieving roles associated with this secure object
List secureObjectRoles=(List)securityManager.getSecureO bjectRoles(secureObject);
//creating ConfigAttributeDefinition
if(!secureObjectRoles.isEmpty()){
ConfigAttributeEditor configAttrEditor=new ConfigAttributeEditor();
StringBuffer rolesStr=new StringBuffer();
for(int i=0;i<secureObjectRoles.size();i++){
SecureObjectRole sor=(SecureObjectRole)secureObjectRoles.get(i);
rolesStr.append(sor.getRole().getRoleName()).appen d(",");
}
configAttrEditor.setAsText( rolesStr.toString().substring(0,rolesStr.length()-1) );
ConfigAttributeDefinition configAttrDef=(ConfigAttributeDefinition)configAtt rEditor.getValue();
return configAttrDef;
}
return null;

}

-I have one more question about this subject. For every declared method, lookupAttributes() method reaches database for retrieving secureObjectRoles. This may decrease the performance of the application... Can you suggest anything better.
very happy now...

Ben Alex
Sep 25th, 2004, 05:38 PM
Sure, just use a caching layer like EH-CACHE. Take a look at the implementation of DaoAuthenticationProvider and UserCache for a guide.

smccrory
Sep 26th, 2004, 07:11 PM
Wow, cool thread - I think I'll use this approach in my app too.

Ben, I'll summarize this if you're interested in adding it to the user's manual.

Scott

Ben Alex
Sep 27th, 2004, 02:56 AM
Hi Scott - no problemo. I always welcome contributions. Feel free to email me any diffs etc.

Thanks
Ben

smccrory
Oct 6th, 2004, 10:44 AM
mavisaka or Ben,

If I use a daoAuthenticationProvider as my authenticationManager, and I hang my role set off of the user's object graph, I get a LazyInstantiationException when the DatabaseDrivenMethodDefinitionSource tries to iterate through the roles. I think this is because the lifespan of the Hibernate Session ends after the authentication process, making the roles unavailable for instantiation. I've tried to make the role set non-lazy to no avail, and I've also tried to use BeanUtils.copyProperties() in my daoAuthenticationProvider to return a more fully instantiated user object, but that doesn't work either.

Any recommendations? Big thanks in advance...
Scott

Ben Alex
Oct 6th, 2004, 05:16 PM
Have you tried the Hibernate.initialize(Object) static method?

smccrory
Oct 8th, 2004, 09:55 PM
I tried (thanks for the tip) but it didn't work, so I ended up creating an additional non-lazy object and Hibernate mapping specifically for authentication and authorization.

yfmoan
Dec 12th, 2005, 09:55 PM
Hi ,

protected ConfigAttributeDefinition lookupAttributes(MethodInvocation mi) {
//construct secureObjectName
String secureObjectName=mi.getMethod().getDeclaringClass( ).getName() +"."+ mi.getMethod().getName();
SecureObject secureObject=securityManager.getSecureObject(secur eObjectName);
if(secureObject==null)//if secure object not exist in database
return null;
//retrieving roles associated with this secure object
List secureObjectRoles=(List)securityManager.getSecureO bjectRoles(secureObject);
//creating ConfigAttributeDefinition
if(!secureObjectRoles.isEmpty()){
ConfigAttributeEditor configAttrEditor=new ConfigAttributeEditor();
StringBuffer rolesStr=new StringBuffer();
for(int i=0;i<secureObjectRoles.size();i++){
SecureObjectRole sor=(SecureObjectRole)secureObjectRoles.get(i);
rolesStr.append(sor.getRole().getRoleName()).appen d(",");
}
configAttrEditor.setAsText( rolesStr.toString().substring(0,rolesStr.length()-1) );
ConfigAttributeDefinition configAttrDef=(ConfigAttributeDefinition)configAtt rEditor.getValue();
return configAttrDef;
}
return null;

}

It seem to me that the implementation above still cannot avoid hitting the database , even you use a cache suggested by Ben for every method invocation (OR what object to cache in this implementation ?).

Sure, just use a caching layer like EH-CACHE. Take a look at the implementation of DaoAuthenticationProvider and UserCache for a guide.


moon

Ben Alex
Dec 13th, 2005, 09:16 PM
Why are you even writing a database-driven MethodDefinitionSource? I can't see a strong need for database metadata for method invocations - unlike web URIs where I can see a lot of potential value in some applications.

yfmoan
Dec 14th, 2005, 01:39 AM
Hi Ben,

Why are you even writing a database-driven MethodDefinitionSource? I can't see a strong need for database metadata for method invocations - unlike web URIs where I can see a lot of potential value in some applications.

From my point of view , URIs interception is coarse grained security and method invocation is fine grained.
Once you use URIs interception , you have to categorize the similar functions to a page , each page look like a function's (method) category , sometime you can do this , but sometime you cannot because of the cross boundary business logic , so method blocking is a way out.

l do think that database-driven MethodDefinitionSource is a normal need. AspectJ in action use a whole chapter (Chapter 10) to discuss Authentication and authorization , so let's see some code snipplet in pg 339 ,

public void credit(float amount) {

AccessController.checkPermission(new BankingPermission("credit"));
...
}
public void debit(float amount)
throws InsufficientBalanceException {
AccessController.checkPermission(new BankingPermission("debit"));
...
}

this is a typical JAAS sample code , AccessController.checkPermission(..) look like a "method interception" for me , and this is a need in JAAS , :) . Further , the view no. of this thread show a lot people interested in dynamic authorization.

Back to the question of database-driven MethodDefinitionSource , l think it is nice to have this feature support in acegi security , but how to do it ?

l think there are some criteria for the implemention . let's assume that l have 3 tables,

Users <--M:M--> Roles <--M:M--> AccessMethods

Criteria :

1. the whole relationship (Roles and AccessMethods) must be retrieved from the database to form the (method = ROLE_1,ROLE_2,...) pairs once the application start , that means l have the whole authorization table from the start.

2. we then caching authorization table after that.

3. if some of the relationship change , the authorization table have to be refresh , and cache it agian.

But , it seem to me that , neither MethodDefinitionMap nor MethodDefinitionAttributes provide support for this .

moon

yfmoan
Dec 14th, 2005, 11:45 PM
l have make some code , but l don't know whether it is "save" or not , so comments is needed , :)

DBDrivenMethodDefinitionSource.java

package org.acegi.intercept.method;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.acegi.providers.dao.MethodMapCache;
import org.acegi.providers.dao.cache.NullMethodMapCache;
import org.acegisecurity.ConfigAttribute;
import org.acegisecurity.ConfigAttributeEditor;
import org.acegisecurity.ConfigAttributeDefinition;
import org.acegisecurity.intercept.method.AbstractMethodD efinitionSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.yourschool.library.domain.AccessMethod;
import org.yourschool.library.domain.Role;
import org.yourschool.library.domain.logic.SecurityFacade ;

public class DBDrivenMethodDefinitionSource extends AbstractMethodDefinitionSource {

private static final Log logger = LogFactory.getLog(DBDrivenMethodDefinitionSource.c lass);

private static final String METHOD_MAP = "methodMap";
private Map methodMap;
private Map nameMap;
private SecurityFacade securityManager;
private MethodMapCache methodMapCache = new NullMethodMapCache();

public SecurityFacade getSecurityManager() {
return securityManager;
}

public void setSecurityManager(SecurityFacade securityManager) {
this.securityManager = securityManager;
}

public MethodMapCache getMethodMapCache() {
return methodMapCache;
}

public void setMethodMapCache(MethodMapCache methodMapCache) {
this.methodMapCache = methodMapCache;
}

public void buildMethodMapFromDB(){

nameMap = new HashMap();
methodMap = new HashMap();

List accessMethods = (List) this.securityManager.loadAllAccessMethod();

ConfigAttributeEditor configAttrEditor = new ConfigAttributeEditor();

for(int i = 0 ; i < accessMethods.size() ; i++){

AccessMethod accessMethod = (AccessMethod)accessMethods.get(i);
Iterator it = accessMethod.getRoles().iterator();

StringBuffer rolesStr = new StringBuffer();

while(it.hasNext()) {
Role role = (Role)it.next();
rolesStr.append(role.getRoleName()).append(",");
}

String accessMethodName = accessMethod.getAccessMethodName();

// System.out.println(rolesStr.toString().substring(0 ,rolesStr.length()-1));
configAttrEditor.setAsText(rolesStr.toString().sub string(0,rolesStr.length()-1));
ConfigAttributeDefinition attr =
(ConfigAttributeDefinition)configAttrEditor.getVal ue();

// Register name and attribute
addSecureMethod(accessMethodName, attr);

}
// put it into cache
this.methodMapCache.putMethodMapInCache(METHOD_MAP ,this.methodMap);

}

public Iterator getConfigAttributeDefinitions() {

Map mMap = this.methodMapCache.getMethodMapFromCache(METHOD_M AP);
if(mMap != null)
return mMap.values().iterator();
return null;

}

public int getMethodMapSize() {

Map mMap = this.methodMapCache.getMethodMapFromCache(METHOD_M AP);
if(mMap != null)
return mMap.size();
return 0;

}

private void addSecureMethod(Method method, ConfigAttributeDefinition attr) {
logger.info("Adding secure method [" + method + "] with attributes ["
+ attr + "]");
this.methodMap.put(method, attr);
}

private void addSecureMethod(String name, ConfigAttributeDefinition attr) {
int lastDotIndex = name.lastIndexOf(".");

if (lastDotIndex == -1) {
throw new IllegalArgumentException("'" + name
+ "' is not a valid method name: format is FQN.methodName");
}

String className = name.substring(0, lastDotIndex);
String methodName = name.substring(lastDotIndex + 1);

try {
Class clazz = Class.forName(className, true,
Thread.currentThread().getContextClassLoader());
addSecureMethod(clazz, methodName, attr);
} catch (ClassNotFoundException ex) {
throw new IllegalArgumentException("Class '" + className
+ "' not found");
}
}

private void addSecureMethod(Class clazz, String mappedName,
ConfigAttributeDefinition attr) {
String name = clazz.getName() + '.' + mappedName;

if (logger.isDebugEnabled()) {
logger.debug("Adding secure method [" + name
+ "] with attributes [" + attr + "]");
}

Method[] methods = clazz.getDeclaredMethods();
List matchingMethods = new ArrayList();

for (int i = 0; i < methods.length; i++) {
if (methods[i].getName().equals(mappedName)
|| isMatch(methods[i].getName(), mappedName)) {
matchingMethods.add(methods[i]);
}
}

if (matchingMethods.isEmpty()) {
throw new IllegalArgumentException("Couldn't find method '"
+ mappedName + "' on " + clazz);
}

// register all matching methods
for (Iterator it = matchingMethods.iterator(); it.hasNext();) {
Method method = (Method) it.next();
String regMethodName = (String) this.nameMap.get(method);

if ((regMethodName == null)
|| (!regMethodName.equals(name)
&& (regMethodName.length() <= name.length()))) {
// no already registered method name, or more specific
// method name specification now -> (re-)register method
if (regMethodName != null) {
logger.debug("Replacing attributes for secure method ["
+ method + "]: current name [" + name
+ "] is more specific than [" + regMethodName + "]");
}

this.nameMap.put(method, name);
addSecureMethod(method, attr);
} else {
logger.debug("Keeping attributes for secure method [" + method
+ "]: current name [" + name
+ "] is not more specific than [" + regMethodName + "]");
}
}
}

private void checkMethodMap(){

if(this.methodMapCache.getMethodMapFromCache(METHO D_MAP) == null){
buildMethodMapFromDB();
}
}

protected ConfigAttributeDefinition lookupAttributes(Method method) {

ConfigAttributeDefinition definition = new ConfigAttributeDefinition();

checkMethodMap();

// Add attributes explictly defined for this method invocation
ConfigAttributeDefinition directlyAssigned =
(ConfigAttributeDefinition) this.methodMapCache.getMethodMapFromCache(METHOD_M AP).get(method);
merge(definition, directlyAssigned);

// Add attributes explicitly defined for this method invocation's interfaces
Class[] interfaces = method.getDeclaringClass().getInterfaces();

for (int i = 0; i < interfaces.length; i++) {
Class clazz = interfaces[i];

try {
// Look for the method on the current interface
Method interfaceMethod = clazz.getDeclaredMethod(method.getName(),
(Class[]) method.getParameterTypes());
ConfigAttributeDefinition interfaceAssigned =
(ConfigAttributeDefinition) this.methodMapCache.getMethodMapFromCache(METHOD_M AP).get(interfaceMethod);
merge(definition, interfaceAssigned);
} catch (Exception e) {
// skip this interface
}
}

// Return null if empty, as per abstract superclass contract
if (definition.size() == 0) {
return null;
} else {
return definition;
}
}

private boolean isMatch(String methodName, String mappedName) {
return (mappedName.endsWith("*")
&& methodName.startsWith(mappedName.substring(0, mappedName.length()
- 1)))
|| (mappedName.startsWith("*")
&& methodName.endsWith(mappedName.substring(1, mappedName.length())));
}

private void merge(ConfigAttributeDefinition definition,
ConfigAttributeDefinition toMerge) {
if (toMerge == null) {
return;
}

Iterator attribs = toMerge.getConfigAttributes();

while (attribs.hasNext()) {
definition.addConfigAttribute((ConfigAttribute) attribs.next());
}
}

}
DBDrivenMethodDefinitionSource (most of it's methods are copy from MethodDefinitionMap with little modification) is serving the criteria 1 & 2 , with a little change of criteria 1 -- the authorization table is not make from the DB at the beginning , not until the first invocation of a method . l changed it because l meet an NPE if l do it this way ,

public class DBDrivenMethodDefinitionSource extends AbstractMethodDefinitionSource {
public DBDrivenMethodDefinitionSource () {
buildMethodMapFromDB();
}
...
}

this happened while loading applicationContext-common-authorization.xml (l don't know how to do this with spring ioc , :))

MethodMapCache.java

package org.acegi.providers.dao;

import java.util.Map;

public interface MethodMapCache {

public Map getMethodMapFromCache(String mapName);

public void putMethodMapInCache(String mapName,Map methodMap);

public void removeMethodMapFromCache(String mapName);
}


to be continue ..

yfmoan
Dec 15th, 2005, 12:10 AM
EhCacheBasedMethodMapCache.java

package org.acegi.providers.dao.cache;

import java.util.HashMap;
import java.util.Map;

import org.acegi.providers.dao.MethodMapCache;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheException;
import net.sf.ehcache.Element;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.factory.InitializingBean ;

import org.springframework.dao.DataRetrievalFailureExcept ion;
import org.springframework.util.Assert;

public class EhCacheBasedMethodMapCache implements MethodMapCache, InitializingBean {

private static final Log logger = LogFactory.getLog(EhCacheBasedMethodMapCache.class );
private Cache cache;

public void setCache(Cache cache) {
this.cache = cache;
}

public Cache getCache() {
return cache;
}

public Map getMethodMapFromCache(String mapName) {
Element element = null;

try {
element = cache.get(mapName);
} catch (CacheException cacheException) {
throw new DataRetrievalFailureException("Cache failure: "
+ cacheException.getMessage());
}

if (logger.isDebugEnabled()) {
logger.debug("Cache hit: " + (element != null) + "; mapName: "
+ mapName);
}

if (element == null) {
return null;
} else {
return (Map) element.getValue();
}
}

public void afterPropertiesSet() throws Exception {
Assert.notNull(cache, "cache mandatory");
}

public void putMethodMapInCache(String mapName,Map methodMap) {
Element element = new Element(mapName, new HashMap(methodMap));

if (logger.isDebugEnabled()) {
logger.debug("Cache put: " + element.getKey());
}

cache.put(element);
}

public void removeMethodMapFromCache(String mapName) {
cache.remove(mapName);
}
}

NullMethodMapCache.java

package org.acegi.providers.dao.cache;

import java.util.Map;

import org.acegi.providers.dao.MethodMapCache;

public class NullMethodMapCache implements MethodMapCache {

public Map getMethodMapFromCache(String mapName){
return null;
}

public void putMethodMapInCache(String mapName,Map methodMap) {}

public void removeMethodMapFromCache(String mapName) {}

}


Remark:

1. This is first part of the complete code , second part will be using an interceptor to detect the change of the modification of the authorization table . l am using hibernate , so it is nature to use hibernate interceptor to do the job , althought l not yet implement , but l think the idea can be work theoretically . May be the code above is garbage , but what l am trying to prove is that dynamic authorization is useful , and is worth more attention for a complete security system like acegisecurity.

2. Because l avoid study all the codes , it may be not "save" , just to prove whether it can be implement easily into the current status of acegi (1.0 RC1).

Is this a working idea or there is other way to do it ?

moon

yfmoan
Dec 15th, 2005, 10:19 AM
detect the changes of authorization table by using hibernate event , and then caching the table again , this serve criteria 3.

applicationContext.xml

...
<bean id="authorizationSaveOrUpdateEventListener" class="org.acegi.providers.dao.hibernate.AuthorizationSav eOrUpdateEventListener">
<property name="methodDefSource"><ref bean="dbDrivenMethodDefinitionSource"/></property>
</bean>

<bean id="authorizationDeleteEventListener" class="org.acegi.providers.dao.hibernate.AuthorizationDel eteEventListener">
<property name="methodDefSource"><ref bean="dbDrivenMethodDefinitionSource"/></property>
</bean>

<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFac toryBean">
<property name="dataSource"><ref local="dataSource"/></property>
<property name="eventListeners">
<map>
<entry key="save-update"><ref local="authorizationSaveOrUpdateEventListener" /></entry>
<entry key="delete"><ref local="authorizationDeleteEventListener" /></entry>
</map>
</property>
<property name="mappingResources">
<list>
.....
</list>
</property>
<property name="hibernateProperties">
<props>
.....
</props>
</property>

</bean>
...

AuthorizationSaveOrUpdateEventListener.java

package org.acegi.providers.dao.hibernate;

import org.hibernate.HibernateException;
import org.hibernate.event.SaveOrUpdateEvent;
import org.hibernate.event.def.DefaultSaveOrUpdateEventLi stener;
import org.yourschool.library.domain.Role;

import org.acegi.intercept.method.DBDrivenMethodDefinitio nSource;

public class AuthorizationSaveOrUpdateEventListener extends DefaultSaveOrUpdateEventListener {

private DBDrivenMethodDefinitionSource methodDefSource;

public void setMethodDefSource(DBDrivenMethodDefinitionSource methodDefSource) {
this.methodDefSource = methodDefSource;
}

public void onSaveOrUpdate(SaveOrUpdateEvent event) throws HibernateException {

super.onSaveOrUpdate(event);

if(event.getEntity() instanceof Role)
methodDefSource.buildMethodMapFromDB();

}

}
similar for AuthorizationDeleteEventListener.java.

applicationContext-common-authorization.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>

<!-- An access decision manager used by the business objects -->
<bean id="businessAccessDecisionManager" class="org.acegisecurity.vote.AffirmativeBased">
<property name="allowIfAllAbstainDecisions"><value>false</value></property>
<property name="decisionVoters">
<list>
<ref local="roleVoter"/>
</list>
</property>
</bean>

<bean id="roleVoter" class="org.acegisecurity.vote.RoleVoter"/>

<!-- ================= METHOD INVOCATION AUTHORIZATION ==================== -->

<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFa ctoryBean"/>

<bean id="methodMapCacheBackend" class="org.springframework.cache.ehcache.EhCacheFactoryBe an">
<property name="cacheManager">
<ref local="cacheManager"/>
</property>
<property name="cacheName">
<value>methodMapCache</value>
</property>
</bean>

<bean id="methodMapCache" class="org.acegi.providers.dao.cache.EhCacheBasedMethodMa pCache">
<property name="cache"><ref local="methodMapCacheBackend"/></property>
</bean>

<bean id="dbDrivenMethodDefinitionSource" class="org.acegi.intercept.method.DBDrivenMethodDefinitio nSource">
<property name="securityManager"><ref bean="security"/></property>
<property name="methodMapCache"><ref local="methodMapCache"/></property>
</bean>

<bean id="librarySecurity" class="org.acegisecurity.intercept.method.aopalliance.Met hodSecurityInterceptor">
<property name="authenticationManager"><ref bean="authenticationManager"/></property>
<property name="accessDecisionManager"><ref local="businessAccessDecisionManager"/></property>
<property name="objectDefinitionSource"><ref local="dbDrivenMethodDefinitionSource"/></property>
</bean>

</beans>


moon

yfmoan
Dec 22nd, 2005, 10:57 AM
Sorry , the AuthorizationSaveOrUpdateEventListener.java above is not working correctly when l first save a entity (whatever object that intercepted by the hibernate event), it cause an endless loop . l guess l used the wrong listeners , and l make it work again by using another 3 listeners , AuthorizationInsertEventListener , AuthorizationUpdateEventListener and AuthorizationDeleteEventListener .

AuthorizationPostInsertEventListener.java

package org.acegi.providers.dao.hibernate;

import org.hibernate.event.PostInsertEvent;
import org.hibernate.event.PostInsertEventListener;
import org.yourschool.library.domain.Role;
import org.acegi.intercept.method.DBDrivenMethodDefinitio nSource;

public class AuthorizationPostInsertEventListener implements PostInsertEventListener {

private DBDrivenMethodDefinitionSource methodDefSource;

public void setMethodDefSource(
DBDrivenMethodDefinitionSource methodDefSource) {
this.methodDefSource = methodDefSource;
}

public void onPostInsert(PostInsertEvent arg0) {

if (arg0.getEntity() instanceof Role){
methodDefSource.buildMethodMapFromDB();
}

}

}

similar for AuthorizationPostUpdateEventListener.java and AuthorizationPostDeleteEventListener.java

applicationContext.xml

<bean id="authorizationPostInsertEventListener" class="org.acegi.providers.dao.hibernate.AuthorizationPos tInsertEventListener">
<property name="methodDefSource"><ref bean="dbDrivenMethodDefinitionSource"/></property>
</bean>

<bean id="authorizationPostUpdateEventListener" class="org.acegi.providers.dao.hibernate.AuthorizationPos tUpdateEventListener">
<property name="methodDefSource"><ref bean="dbDrivenMethodDefinitionSource"/></property>
</bean>

<bean id="authorizationPostDeleteEventListener" class="org.acegi.providers.dao.hibernate.AuthorizationPos tDeleteEventListener">
<property name="methodDefSource"><ref bean="dbDrivenMethodDefinitionSource"/></property>
</bean>

<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFac toryBean">
<property name="dataSource"><ref local="dataSource"/></property>
<property name="eventListeners">
<map>
<entry key="post-commit-insert"><ref local="authorizationPostInsertEventListener" /></entry>
<entry key="post-commit-update"><ref local="authorizationPostUpdateEventListener" /></entry>
<entry key="post-commit-delete"><ref local="authorizationPostDeleteEventListener" /></entry>
</map>
</property>
.....


l am not confirm all this implementations correct or not , but working for me now.

moon

russpitre
Feb 13th, 2006, 02:52 PM
To answer question 1, this works because Spring automatically applies PropertyEditors. Because MethodSecurityInterceptor.objectDefinitionSource is a MethodDefinitionSource, Spring uses the MethodDefinitionSourceEditor. The MethodDefinitionSourceEditor gets passed the entire multi-line String that comprises the entire object definition. It then tokenizes it as if it were a standard Java Properties file. This results in the property name of each line (ie anything to the left of the equals sign) being treated as the method name and passed to the newly created MethodDefinitionMap, with the property value of each line (ie anything to the right of the equals sign) being passed to a ConfigAttributeEditor. ConfigAttributeEditor uses the comma delimiter to build a SecurityConfig instance of each token, putting them all into a ConfigAttributeDefinition that is then returned. The returned ConfigAttributeDefinition is passed to the MethodDefinitionMap.

To answer question 2, I'd normalise the database so you have a SECURE_OBJECT table, a ROLES table, and a SECURE_OBJECT-ROLES table. Thus your DatabaseObjectDefinitionSource can be wired to either a MethodSecurityInterceptor or a FilterSecurityInterceptor. The SECURE_OBJECT table should have a TYPE column to denote whether it's for a MethodInvocation or a FilterInvocation declaration. Once you've got the schema sorted out, it's a simple matter to build a ConfigAttributeDefinition in response to a request. I'd consider returning null to the getConfigAttributeDefinitions() method, as it's only used by AbstractSecurityInterceptor to help with inital configuration checking.

I would also suggest adding a "sort_order" column to the SECURE_OBJECT table to determine sort order if you plan on using this table to FilterInvocation definitions. With FilterInvocation defintions, the more specific URLs will need to be evaluted first.

http://acegisecurity.org/docbook/acegi.html#security-interception-filterinvocation

Ben Alex
Mar 6th, 2006, 02:29 AM
I would also suggest adding a "sort_order" column to the SECURE_OBJECT table to determine sort order if you plan on using this table to FilterInvocation definitions. With FilterInvocation defintions, the more specific URLs will need to be evaluted first.

This is a good observation.

raulraja
Sep 12th, 2006, 10:43 AM
Hi,

I'm interested on implementing this approach in our app as well. Our admin user needs to be able to change property level access permissions based on the ROLE at runtime and save these permission in the database.

Is there any document or tutorial that I could use to guide me through this besides this thread?

Than you

Raul

nneuberger1
Sep 12th, 2006, 11:10 AM
I would suggest looking in the following:

http://www.springframework.org/articles

As far I could see this is a custom rolled solution.....it would be nice to see this in the acegi api, but I think since many people need different types of method level auth...that making a generic api for it my be difficult....although....I have been known to be wrong. LOL.

I would like to implement this....but will wait a while before I attempt it....

If someone does implement this....please added a tutorial in the articles or ask the senior members where to put it.....

miclaro
Mar 13th, 2007, 06:15 PM
Hi,

I'm interested on implementing this approach in our app as well. Our admin user needs to be able to change property level access permissions based on the ROLE at runtime and save these permission in the database.

Is there any document or tutorial that I could use to guide me through this besides this thread?

Than you

Raul

I am also very interested in this. I see that org.acegisecurity.userdetails.jdbc.JdbcDaoImpl allready implements this for the Users, it would be nice to have a
org.acegisecurity.intercept.web.jdbc.JdbcFilterInv ocationDefinitionSource
with cache, and properties for overriding the query.

I have searched in a lot of places and this doesn't seem to exist, anyone has some tips to share?

russpitre
Mar 13th, 2007, 08:45 PM
Here's a bit of code you may be able to adjust to your environment.


DatabasePathBasedFilterInvocationDefinitionMap.jav a


package com.mycompany.security.acegisecurity.intercept.web ;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Vector;
import org.acegisecurity.ConfigAttributeDefinition;
import org.acegisecurity.SecurityConfig;
import org.acegisecurity.intercept.web.AbstractFilterInvo cationDefinitionSource;
import org.acegisecurity.intercept.web.FilterInvocationDe finitionMap;
import org.apache.log4j.Logger;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import com.mycompany.model.security.Role;
import com.mycompany.model.security.SecureObject;
import com.mycompany.service.security.SecurityService;

public class DatabasePathBasedFilterInvocationDefinitionMap extends AbstractFilterInvocationDefinitionSource implements FilterInvocationDefinitionMap {

protected class EntryHolder {
private String antPath;
private ConfigAttributeDefinition configAttributeDefinition;
protected EntryHolder() {
throw new IllegalArgumentException("Cannot use default constructor");
}
public EntryHolder(String antPath, ConfigAttributeDefinition attr) {
this.antPath = antPath;
this.configAttributeDefinition = attr;
}
public String getAntPath() {
return antPath;
}
public ConfigAttributeDefinition getConfigAttributeDefinition() {
return configAttributeDefinition;
}
}

private static final Logger logger = Logger.getLogger(DatabasePathBasedFilterInvocation DefinitionMap.class);

private boolean convertUrlToLowercaseBeforeComparison = true;

private PathMatcher pathMatcher = new AntPathMatcher();

private List requestMap = new Vector();

private SecurityService securityService;

@SuppressWarnings("unchecked")
public void addSecureUrl(String antPath, ConfigAttributeDefinition attr) {
requestMap.add(new EntryHolder(antPath, attr));
if (logger.isDebugEnabled()) {
logger.debug("Added Ant path: " + antPath + "; attributes: " + attr);
}
}
@SuppressWarnings("unchecked")
public Iterator getConfigAttributeDefinitions() {
initRequestMap();
Set set = new HashSet();
Iterator iter = requestMap.iterator();
while (iter.hasNext()) {
EntryHolder entryHolder = (EntryHolder) iter.next();
set.add(entryHolder.getConfigAttributeDefinition() );
}
return set.iterator();
}
public int getMapSize() {
return this.requestMap.size();
}

@SuppressWarnings({"unused","unchecked"})
private void initRequestMap() {
List<SecureObject> secureUrlObjects = securityService.getSecureFilterInvocationObjects() ;
ConfigAttributeDefinition def = new ConfigAttributeDefinition();
for(SecureObject filterInvocation: secureUrlObjects) {
if(logger.isDebugEnabled()) {
logger.debug("<Setting Secure Object Filter Definition: " + filterInvocation.getSecureObject() );
}
def = new ConfigAttributeDefinition();
for(Role role : filterInvocation.getRoles()){
def.addConfigAttribute(new SecurityConfig(role.getName()));
requestMap.add(filterInvocation.getSortOrder() , new EntryHolder(filterInvocation.getSecureObject(), def) );
}
}
}

public boolean isConvertUrlToLowercaseBeforeComparison() {
return convertUrlToLowercaseBeforeComparison;
}
@SuppressWarnings("unchecked")
@Override
public ConfigAttributeDefinition lookupAttributes(String url) {
//initRequestMap();

// Strip anything after a question mark symbol, as per SEC-161.
int firstQuestionMarkIndex = url.lastIndexOf("?");
if (firstQuestionMarkIndex != -1) {
url = url.substring(0, firstQuestionMarkIndex);
}
if (convertUrlToLowercaseBeforeComparison) {
url = url.toLowerCase();
if (logger.isDebugEnabled()) {
logger.debug("Converted URL to lowercase, from: '" + url
+ "'; to: '" + url + "'");
}
}
Iterator iter = requestMap.iterator();
while (iter.hasNext()) {
EntryHolder entryHolder = (EntryHolder) iter.next();
boolean matched = pathMatcher.match(entryHolder.getAntPath(), url);
if (logger.isDebugEnabled()) {
logger.debug("Candidate is: '" + url + "'; pattern is "
+ entryHolder.getAntPath() + "; matched=" + matched);
}
if (matched) {
return entryHolder.getConfigAttributeDefinition();
}
}
return null;
}

public void setConvertUrlToLowercaseBeforeComparison(boolean convertUrlToLowercaseBeforeComparison) {
this.convertUrlToLowercaseBeforeComparison = convertUrlToLowercaseBeforeComparison;
}

public SecurityService getSecurityService() {
return securityService;
}

public void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}
}




SecureObject domain object....


package com.mycompany.model.security;
import java.util.Set;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import com.mycompany.model.BaseObject;

public class SecureObject extends BaseObject {

private static final long serialVersionUID = 7093257566823171405L;

private Integer secureObjectId;
private Integer secureObjectTypeId;
private String secureObject;
private Integer sortOrder;

private Set<Role> roles;

public Set<Role> getRoles() {
return roles;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
public String getSecureObject() {
return secureObject;
}
public void setSecureObject(String secureObject) {
this.secureObject = secureObject;
}
public Integer getSecureObjectId() {
return secureObjectId;
}
public void setSecureObjectId(Integer secureObjectId) {
this.secureObjectId = secureObjectId;
}
public Integer getSortOrder() {
return sortOrder;
}
public void setSortOrder(Integer sortOrder) {
this.sortOrder = sortOrder;
}
public Integer getSecureObjectTypeId() {
return secureObjectTypeId;
}
public void setSecureObjectTypeId(Integer secureObjectTypeId) {
this.secureObjectTypeId = secureObjectTypeId;
}

public boolean equals(Object object) {
if (!(object instanceof SecureObject)) {
return false;
}
SecureObject rhs = (SecureObject) object;
return new EqualsBuilder().
append(this.secureObjectTypeId,rhs.getSecureObject TypeId() ).
append(this.sortOrder, rhs.getSortOrder() ).
append(this.secureObject,rhs.getSecureObject() ).
isEquals();
}
public int hashCode() {
return new HashCodeBuilder(-1639360101, 1081503241).
append(this.secureObjectTypeId).
append(this.sortOrder).
append(this.roles).
append(this.secureObject).
toHashCode();
}

}

russpitre
Mar 13th, 2007, 08:54 PM
cont'd...

Role domain object.


package com.mycompany.model.security;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;
import com.mycompany.model.BaseObject;
import com.mycompany.model.activedirectory.ADGroup;
import com.mycompany.model.user.PrologUserGroup;

public class Role extends BaseObject {

private static final long serialVersionUID = 5977582917018176333L;

private static String ROLE_PREFIX_ACTIVE_DIRECTORY = "ROLE_";
private static String ROLE_PREFIX_PROLOG = "ROLE_PROLOG_";

private Integer roleId;
private String description;
private String originalName;
private String name;
private String sourcePrimaryKey;
private Integer typeId;
private RoleType roleType;

private List<String> problemsHolder;
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getOriginalName() {
return originalName;
}
public void setOriginalName(String originalName) {
this.originalName = originalName;
}
public Integer getRoleId() {
return roleId;
}
public void setRoleId(Integer roleId) {
this.roleId = roleId;
}
public RoleType getRoleType() {
return roleType;
}
public void setRoleType(RoleType roleType) {
this.roleType = roleType;
}
public String getSourcePrimaryKey() {
return sourcePrimaryKey;
}
public void setSourcePrimaryKey(String sourcePrimaryKey) {
this.sourcePrimaryKey = sourcePrimaryKey;
}
public Integer getTypeId() {
return typeId;
}
public void setTypeId(Integer typeId) {
this.typeId = typeId;
}
/**
* name is the business key. Do not change.
*
* @see java.lang.Object#equals(Object)
*/
public boolean equals(Object object) {
if (!(object instanceof Role)) {
return false;
}
Role rhs = (Role) object;
return new EqualsBuilder().
append(this.name, rhs.getName()).isEquals();
}

/**
* name is the business key. Do not change.
*
* @see java.lang.Object#hashCode()
*/
public int hashCode() {
return new HashCodeBuilder(-2067913629, -1342727107).
append(this.name).toHashCode();
}

/**
* @see java.lang.Object#toString()
*/
public String toString() {
return new ToStringBuilder(this) .append("name", this.name)
.append("description", this.description).append("typeId",
this.typeId).append("roleId", this.roleId).append(
"originalName", this.originalName).append(
"sourcePrimaryKey", this.sourcePrimaryKey).toString();
}

public static String formatActiveDirectoryGroupNameToRoleName(String groupName) {
try {
String retVal = groupName.toUpperCase();
retVal = StringUtils.replace(retVal, " ", "_");
retVal = StringUtils.replace(retVal, ",", "");
return ROLE_PREFIX_ACTIVE_DIRECTORY + retVal;
} catch (Exception e ) {
return null;
}
}

public static String formatPrologGroupNameToRoleName(String groupName) {
StringBuffer strBuffer = new StringBuffer( groupName.trim() );
return ROLE_PREFIX_PROLOG + strBuffer.toString().toUpperCase().replaceAll(" ", "_").replaceAll("/","_");
}

public static Role getRole(PrologUserGroup group) {
Role role = new Role();
role.setDescription(group.getDescription());
role.setName(Role.formatPrologGroupNameToRoleName( group.getName()));
role.setOriginalName(group.getName());
role.setSourcePrimaryKey(group.getGroupId() + "");
role.setTypeId(RoleType.PROLOG);
return role;
}
public static Role getRole(ADGroup group) {
Role role = new Role();
role.setDescription(group.getDescription());
role.setName(Role.formatActiveDirectoryGroupNameTo RoleName(group.getCn()));
role.setOriginalName(group.getCn());
role.setSourcePrimaryKey(group.getDn());
role.setTypeId(RoleType.ACTIVE_DIRECTORY);
return role;
}

public List<String> getProblems() {
return problemsHolder;
}
public void setProblems(List<String> problemsHolder) {
this.problemsHolder = problemsHolder;
}

public boolean isProblemsExists(){
if(this.problemsHolder.size() > 0){
return true;
}else{
return false;
}
}
}



...spring bean definition...


<bean id="filterSecurityInterceptor" class="org.acegisecurity.intercept.web.FilterSecurityInte rceptor">
<property name="validateConfigAttributes" value="true"/>
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="objectDefinitionSource" ref="databasePathBasedFilterInvocationDefinitionMap"/>
</bean>

<bean id="databasePathBasedFilterInvocationDefinitionMap" class="com.shawmut.security.acegisecurity.intercept.web.D atabasePathBasedFilterInvocationDefinitionMap">
<property name="securityService" ref="securityService"/>
</bean>

miclaro
Mar 14th, 2007, 10:37 AM
thanks! I'll give it a try.

xbit
May 25th, 2007, 06:18 AM
Sorry , the AuthorizationSaveOrUpdateEventListener.java above is not working correctly when l first save a entity (whatever object that intercepted by the hibernate event), it cause an endless loop . l guess l used the wrong listeners , and l make it work again by using another 3 listeners , AuthorizationInsertEventListener , AuthorizationUpdateEventListener and AuthorizationDeleteEventListener .

l am not confirm all this implementations correct or not , but working for me now.



Instead of using a seperate class I guess it should work if implementing the 3 interfaces in one class too?

It still causes an endless loop and ultimately a Stack Overflow here...

litterat
May 28th, 2007, 12:54 AM
I am new to Ageci so I might missing the all point but I think you went the wrong way
I think it would be better give each method it own role and let the adminstrator decide which users or groups can have that role
You then effectivly can load the list of role for certain user when the session start.
If permission change during an open session you will need to refresh the list.
Something like hot deploy. This can be done easliy by being aware to all open sessions.
Is this sound right?

xbit
May 28th, 2007, 04:16 AM
@litterat: I'm not sure who your reply was directed at but creating one role for each method would be pretty meaningless as one quickly would end up with hundreds of roles and a completely unmaintanable application...

Anyway, I solved my stack overflow problem by using a Hibernate interceptor instead of event listeners. The interceptor checks the domain object type on save/delete/insert and adds it to a set. If the Set is non-empty on flush the method definition map is reloaded from the database. See the Java Persistence with Hibernate book for a full example of extending EmptyInterceptor. :)

berserkpi
Oct 17th, 2007, 06:29 PM
When I started to think on implementing a DB based ObjectDefinitionSource... I just was looking for an extension point for achieve this...

I mean, you did a great work but the match-method-logic isn't easy. I wonder if acegi must achieve this logic for us...

Why did you implement your own matching-logic?

mswkk
Oct 24th, 2007, 12:01 AM
I pretty feel the same that Acegi does not support method(function) level dynamic authorization.
Anyone got good recommendation on that?:confused: