Hello again!
In the last
blog entry, I described on how to get Spring MVC flavor of AppFuse 3.0 Web Application Stack into your Eclipse IDE ready for development. In this tutorial, I will describe how to enable Spring ACL to control access to your web resources as fine-grained as to method level.
First of, I owed a lot of contents and references to Krams Tutorial on Spring Security to get up and running almost immediately. Thanks Krams. I highly recommend anyone who are starting out using Spring Security ACL to review Krams tutorial. To learn more,
click on.
Why Spring Security ACL? Spring Security ACL provides a mechanism to protect your web resources way down to micro level such as your methods. This definitely is very nice feature if you have or wanted to have multi-tenanted web application such as gmail where multiple organisations may use single application without "knowing" they're sharing the same software platform with the rest of the world or in other term, SaaS (Software As A Service). Futhermore, our "myproject" is already using Spring Security but with ACL disabled by default. Therefore, protected resources are ONLY currently secured by their url patterns. Please take a look at src/main/webapp/WEB-INF/security.xml to learn more.
And being said that, let's get back to our last "myproject" web app to continue on. In brief, the starter kit already has user management module with two roles ROLE_ADMIN and ROLE_USER. Users with role ROLE_ADMIN can perform all administrative tasks like creating, editing, deleting existing users. While the users with ROLE_USER limited to editing their own profile.
Now, we will enable our "myproject" web application is the following
5 steps:
Step 1: Creating ACL tables
Firstly, we need to create four (4) acl related tables:
i. acl_class
ii. acl_sid
iii. acl_object_identity
iv. acl_entry
In brief:
-
acl_class table stores fully qualified Class name to be secured and referenced as unique numeric id.
-
acl_sid stores table user id or role (actors) and referenced as numeric id.
-
acl_object_identity table stores fully qualified class to its existing instance/object/record mapping.
-
acl_entry table stores permissions mapping (READ/WRITE etc) between actors and existing instance/object/record.
Now that we know a little bit about the tables, we need to create them. In order to do so, we need a DDL. Luckily, Spring supplies them within the jar itself as shown in the image below:
Go ahead copy and paste "createAclSchemaMySQL.sql" into your project main resource folder (src/main/resources)
Now, we have choices either to run them in MySQL admin or console but I chose to use maven plugin called SQL Maven Plugin instead. You may learn more
here.
Add the following snippet to your pom.xml after "
maven-compiler-
plugin" </plugin> entry within
<plugins>...</plugins>
<!-- Maven SQL Plugin -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>sql-maven-plugin</artifactId>
<version>1.5</version>
<dependencies>
<dependency>
<groupId>${jdbc.groupId}</groupId>
<artifactId>${jdbc.artifactId}</artifactId>
<version>${jdbc.version}</version>
</dependency>
</dependencies>
<configuration>
<driver>${jdbc.driverClassName}</driver>
<url>${jdbc.url}</url>
<username>${jdbc.username}</username>
<password>${jdbc.password}</password>
<settingsKey>sensibleKey</settingsKey>
<skip>${maven.test.skip}</skip>
</configuration>
<executions>
<execution>
<id>default-cli</id>
<phase>process-test-resources</phase>
<goals>
<goal>execute</goal>
</goals>
<configuration>
<url>${jdbc.url}</url>
<autocommit>true</autocommit>
<sqlCommand>
USE myproject;
DROP TABLE IF EXISTS acl_entry;
DROP TABLE IF EXISTS acl_object_identity;
DROP TABLE IF EXISTS acl_sid;
DROP TABLE IF EXISTS acl_class;
</sqlCommand>
<srcFiles>
<srcFile>src/main/resources/createAclSchemaMySQL.sql</srcFile>
</srcFiles>
<onError>abort</onError>
</configuration>
</execution>
</executions>
</plugin>
Next, in command line window, execute the following:
mvn sql:execute
If you have properly followed this step, you should be getting the same result shown below:
[INFO] ------------------------------------------------------------------------
[INFO] Building AppFuse Spring MVC Application 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- sql-maven-plugin:1.5:execute (default-cli) @ myproject ---
[INFO] Executing commands
[INFO] Executing file: /var/folders/l3/1vl9ht9j2wg56dzvv_69lk6m0000gp/T/createAclSchemaMySQL.194031581sql
[INFO] 9 of 9 SQL statements executed successfully
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.525s
[INFO] Finished at: Fri Jun 13 17:56:55 MYT 2014
[INFO] Final Memory: 16M/230M
[INFO] ------------------------------------------------------------------------
To verify, while in command line windows, issue the following:
mysql -u root -e 'use myproject; show tables;'
You should be seeing additional four (4) tables listed out in the output.
Step 2: Add ACL Sample-data
Currently this stack has a set of sample data populated from a file called sample-data.xml in src/test/resources folder. Primarily, it contains seed data for users (admin, user, two_roles_user) and role (ROLE_ADMIN, ROLE_USER) tables. This seed data is populated using the following command line:
mvn dbunit:operation
Now, we need to seed acl tables the same way. For simplicity (the better way is create separate file but require more steps), we will use the same file to seed with the following entries by copy and paste them right before </dataset> element in src/test/resources/sampe-data.xml file:
<table name="acl_class">
<column>id</column>
<column>class</column>
<row>
<value description="id">1</value>
<value description="class">com.mycompany.model.User</value>
</row>
<row>
<value description="id">2</value>
<value description="class">com.mycompany.model.Role</value>
</row>
</table>
<table name="acl_sid">
<column>id</column>
<column>principal</column>
<column>sid</column>
<row>
<value description="id">1</value>
<value description="principal">false</value>
<value description="sid">ROLE_ADMIN</value>
</row>
<row>
<value description="id">2</value>
<value description="principal">true</value>
<value description="sid">user</value>
</row>
<row>
<value description="id">3</value>
<value description="principal">true</value>
<value description="sid">two_roles_user</value>
</row>
</table>
<table name="acl_object_identity">;
<column>id</column>
<column>object_id_class</column>
<column>object_id_identity</column>
<column>parent_object</column>
<column>owner_sid</column>
<column>entries_inheriting</column>
<!-- com.mycompany.model.User -->
<row>
<value description="id">1</value>
<value description="object_id_class">1</value><!-- com.mycompany.model.User -->
<value description="object_id_identity">-1</value><!-- user -->
<null/>
<value description="owner_sid">1</value>
<value description="entries_inheriting">false</value>
</row>
<row>
<value description="id">2</value>
<value description="object_id_class">1</value><!-- com.mycompany.model.User -->
<value description="object_id_identity">-2</value><!-- admin -->
<null/>
<value description="owner_sid">1</value>
<value description="entries_inheriting">false</value>
</row>
<row>
<value description="id">3</value>
<value description="object_id_class">1</value><!-- com.mycompany.model.User -->
<value description="object_id_identity">-3</value><!-- two_roles_user -->
<null/>
<value description="owner_sid">1</value>
<value description="entries_inheriting">false</value>
</row>
<!-- com.mycompany.model.Role -->
<row>
<value description="id">4</value>
<value description="object_id_class">2</value><!-- com.mycompany.model.Role -->
<value description="object_id_identity">-1</value><!-- ROLE_ADMIN -->
<null/>
<value description="owner_sid">1</value>
<value description="entries_inheriting">false</value>
</row>
<row>
<value description="id">5</value>
<value description="object_id_class">2</value><!-- Role Model -->
<value description="object_id_identity">-2</value><!-- ROLE_USER -->
<null/>
<value description="owner_sid">1</value>
<value description="entries_inheriting">false</value>
</row>
</table>
<table name="acl_entry">;
<column>id</column>
<column>acl_object_identity</column>
<column>ace_order</column>
<column>sid</column>
<column>mask</column>
<column>granting</column>
<column>audit_success</column>
<column>audit_failure</column>
<!-- admin ACL ENTRIES -->
<row>
<value description="id">1</value>
<value description="acl_object_identity">1</value><!-- user -->
<value description="ace_order">1</value>
<value description="sid">1</value><!-- admin -->
<value description="mask">1</value><!-- READ -->
<value description="granting">true</value><!-- Granting Mask -->
<value description="audit_success">true</value><!-- Audit every success permission -->
<value description="audit_failure">true</value><!-- Audit every failure permission -->
</row>
<row>
<value description="id">2</value>
<value description="acl_object_identity">1</value><!-- user -->
<value description="ace_order">2</value>
<value description="sid">1</value><!-- admin -->
<value description="mask">2</value><!-- WRITE -->
<value description="granting">true</value><!-- Granting Mask -->
<value description="audit_success">true</value><!-- Audit every success permission -->
<value description="audit_failure">true</value><!-- Audit every failure permission -->
</row>
<row>
<value description="id">3</value>
<value description="acl_object_identity">2</value><!-- admin -->
<value description="ace_order">1</value>
<value description="sid">1</value><!-- admin -->
<value description="mask">1</value><!-- READ -->
<value description="granting">true</value><!-- Granting Mask -->
<value description="audit_success">true</value><!-- Audit every success permission -->
<value description="audit_failure">true</value><!-- Audit every failure permission -->
</row>
<row>
<value description="id">4</value>
<value description="acl_object_identity">2</value><!-- admin -->
<value description="ace_order">2</value>
<value description="sid">1</value><!-- admin -->
<value description="mask">2</value><!-- WRITE -->
<value description="granting">true</value><!-- Granting Mask -->
<value description="audit_success">true</value><!-- Audit every success permission -->
<value description="audit_failure">true</value><!-- Audit every failure permission -->
</row>
<row>
<value description="id">5</value>
<value description="acl_object_identity">3</value><!-- two_roles_user -->
<value description="ace_order">1</value>
<value description="sid">1</value><!-- admin -->
<value description="mask">1</value><!-- READ -->
<value description="granting">true</value><!-- Granting Mask -->
<value description="audit_success">true</value><!-- Audit every success permission -->
<value description="audit_failure">true</value><!-- Audit every failure permission -->
</row>
<row>
<value description="id">6</value>
<value description="acl_object_identity">3</value><!-- two_roles_user -->
<value description="ace_order">2</value>
<value description="sid">1</value><!-- admin -->
<value description="mask">2</value><!-- WRITE -->
<value description="granting">true</value><!-- Granting Mask -->
<value description="audit_success">true</value><!-- Audit every success permission -->
<value description="audit_failure">true</value><!-- Audit every failure permission -->
</row>
<!-- user ACL ENTRIES -->
<row>
<value description="id">7</value>
<value description="acl_object_identity">1</value><!-- user -->
<value description="ace_order">3</value>
<value description="sid">2</value><!-- user -->
<value description="mask">1</value><!-- READ -->
<value description="granting">true</value><!-- Granting Mask -->
<value description="audit_success">true</value><!-- Audit every success permission -->
<value description="audit_failure">true</value><!-- Audit every failure permission -->
</row>
<row>
<value description="id">8</value>
<value description="acl_object_identity">1</value><!-- user -->
<value description="ace_order">4</value>
<value description="sid">2</value><!-- user -->
<value description="mask">2</value><!-- WRITE -->
<value description="granting">true</value><!-- Granting Mask -->
<value description="audit_success">true</value><!-- Audit every success permission -->
<value description="audit_failure">true</value><!-- Audit every failure permission -->
</row>
<!-- two_roles_user ACL ENTRIES -->
<row>
<value description="id">9</value>
<value description="acl_object_identity">3</value><!-- two_roles_user -->
<value description="ace_order">3</value>
<value description="sid">3</value><!-- two_roles_user -->
<value description="mask">1</value><!-- READ -->
<value description="granting">true</value><!-- Granting Mask -->
<value description="audit_success">true</value><!-- Audit every success permission -->
<value description="audit_failure">true</value><!-- Audit every failure permission -->
</row>
<row>
<value description="id">10</value>
<value description="acl_object_identity">3</value><!-- two_roles_user -->
<value description="ace_order">4</value>
<value description="sid">3</value><!-- two_roles_user -->
<value description="mask">2</value><!-- WRITE -->
<value description="granting">true</value><!-- Granting Mask -->
<value description="audit_success">true</value><!-- Audit every success permission -->
<value description="audit_failure">true</value><!-- Audit every failure permission -->
</row>
</table>
Once copied and saved. Apply them to database by issuing the following in the command line window:
mvn dbunit:operation
To verify, while in command line windows, issue the following:
mysql -u root -e "use myproject; select count(*) from acl_entry"
You should be seeing ten (10) records count listed out in the output.
Step 3: Set ACL data source and add Spring ACL Configuration and import them into applicationContext.xml
Let's set ACL data source by riding on the existing data source in all the way at the bottom file of src/main/resources/applicationContext-resources.xml (just before </beans> element):
<alias name="dataSource" alias="aclDataSource"/>
To avoid test failures, please add the same in src/test/resources/applicationContext.-resources.xml as well.
Next, in order Spring Security ACL to work properly in a basic configuration must be in place.
Go ahead copy and paste the following file and named them as acl-context.xml in
src/main/webapp/WEB-INF now.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.2.xsd
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd">
<!-- See 15.3.2 Built-In Expression @http://static.springsource.org/spring-security/site/docs/3.0.x/reference/el-access.html#el-permission-evaluator -->
<bean id="expressionHandler"
class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
<!-- To use hasPermission() in expressions, configure a PermissionEvaluator -->
<property name="permissionEvaluator" ref="permissionEvaluator" />
<property name="roleHierarchy" ref="roleHierarchy" />
</bean>
<!-- Declare a custom PermissionEvaluator We'll rely on the standard AclPermissionEvaluator
implementation -->
<bean class="org.springframework.security.acls.AclPermissionEvaluator"
id="permissionEvaluator">
<constructor-arg ref="aclService" />
</bean>
<!-- Declare an acl service -->
<bean class="org.springframework.security.acls.jdbc.JdbcMutableAclService"
id="aclService">
<constructor-arg ref="aclDataSource" />
<constructor-arg ref="lookupStrategy" />
<constructor-arg ref="aclCache" />
</bean>
<!-- Declare a lookup strategy -->
<bean id="lookupStrategy"
class="org.springframework.security.acls.jdbc.BasicLookupStrategy">
<constructor-arg ref="aclDataSource" />
<constructor-arg ref="aclCache" />
<constructor-arg ref="aclAuthorizationStrategy" />
<constructor-arg ref="auditLogger" />
</bean>
<!-- Declare an acl cache -->
<bean id="aclCache"
class="org.springframework.security.acls.domain.EhCacheBasedAclCache">
<constructor-arg>
<bean class="org.springframework.cache.ehcache.EhCacheFactoryBean">
<property name="cacheManager">
<bean class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:shared="true"/>
</property>
<property name="cacheName" value="aclCache" />
</bean>
</constructor-arg>
</bean>
<!-- Declare an acl authorization strategy -->
<bean id="aclAuthorizationStrategy"
class="org.springframework.security.acls.domain.AclAuthorizationStrategyImpl">
<constructor-arg>
<list>
<bean
class="org.springframework.security.core.authority.GrantedAuthorityImpl">
<constructor-arg value="ROLE_ADMIN" />
</bean>
<bean
class="org.springframework.security.core.authority.GrantedAuthorityImpl">
<constructor-arg value="ROLE_USER" />
</bean>
<bean
class="org.springframework.security.core.authority.GrantedAuthorityImpl">
<constructor-arg value="ROLE_USER" />
</bean>
</list>
</constructor-arg>
</bean>
<!-- Declare an audit logger -->
<bean id="auditLogger"
class="org.springframework.security.acls.domain.ConsoleAuditLogger" />
<!-- http://static.springsource.org/spring-security/site/docs/3.0.x/apidocs/org/springframework/security/access/hierarchicalroles/RoleHierarchyImpl.html -->
<bean id="roleHierarchy"
class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
<property name="hierarchy">
<value>
ROLE_ADMIN > ROLE_USER
</value>
</property>
</bean>
</beans>
Then import it as resource in all the way at the bottom file of src/main/webapp/applicationContext.xml as follows (just before </beans> element):
<import resource="acl-context.xml"/>
Don't forget to save the file.
Step 4: Enable Spring ACL expression and set filtering annotation
Up to this point, the ACL setup won't take effect and our application "myproject" would run as of nothing have been added or changed since. In order to enable, edit src/main/webapp/security.xml as follows:
Look for the following entry in the file:
<global-method-security>
<protect-pointcut expression="execution(* *..service.UserManager.getUsers(..))" access="ROLE_ADMIN"/>
<protect-pointcut expression="execution(* *..service.UserManager.removeUser(..))" access="ROLE_ADMIN"/>
</global-method-security>
and replace them with the following:
<global-method-security pre-post-annotations="enabled">
<expression-handler ref="expressionHandler" />
</global-method-security>
then, don't for get to save the file.
Next, we need to set filtering annotation in our service interface file, src/main/java/com/mycompany/service/UserManager.java in two method signatures:
From
List<User> search(String searchTerm);
...
List<User> getUsers();
to
@PostFilter("hasPermission(filterObject, 'READ')")
List<User> search(String searchTerm);
....
@PostFilter("hasPermission(filterObject, 'READ')")
List<User> getUsers();
The above annotation simply reads as right after returning from execution of the annotated method, filter out records in the result list the current user does NOT have READ access to, hence "POST" and "FILTER" in @PostFilter annotation.
Step 5: Start up our "myproject" web application and see them in action!
As precaution, please start your app by skipping on the test, as it will probably throw test failures because we have NOT yet refactor our test code to adapt to these changes. In the command line, issue the following:
mvn -Dmaven.test.skip=true jetty:run
(but as tested, it did not produce any test failures thus you may not skip the test if you wish so)
So, in this tutorial we have filtered out user listing in Administration -> Manage Users.
By default, URL to this page has been restricted to users with ROLE_ADMIN only, thus only accessible by 'admin' and 'two_roles_user'. If you add ROLE_USER next to ROLE_ADMIN separated by a comma in security.xml file and manually navigate to http://localhost:8080/admin/users, all users will be listed and seen by user with ROLE_USER as well. Go ahead and play around with it.
src/main/webapp/security.xml
You should be seeing the following (BEFORE the ACL Expression is enabled)
Now, filtered users are gone (AFTER the ACL Expression is enabled)
There you have it.
Next Step: Play around with the application and apply more filtering options to the controllers and services on your own! You may learn more
here.
That's it. Now that you have successfully secured in fine-grained'ly manner, it is time to do something even more exciting like having a multi-tenanted web application. Until next time....