Sunday, January 30, 2011

Spring Security 3: Full ACL Tutorial (Part 4)

In Part 1 of this tutorial we've completed setting up the ACL and Bulletin databases. In Part 2 we've completed the Spring Security configuration. In Part 3 we've developed the Spring MVC module of the application. In Part 4 we'll be testing and running the application. We'll also cover some of the unexpected issues we've encountered while developing the system.

Part 1: Functional Specs and the Application Database
Part 2: Spring Security Configuration
Part 3: Spring MVC Module
Part 4: Running the Application

Run the Application

We've completed all the necessary elements of the application. It's time to run it and check the results.

The Admin

We'll login first as an admin and check which actions are allowed to us.

Here are the steps:
1. Visit the login page at http://localhost:8080/spring-security-full-acl/krams/auth/login

2. Enter the following username and password:
username: john
password: admin

3. After a successful login, we should see the following:

Notice the current user is john with the following roles: ROLE_ADMIN, ROLE_USER, and ROLE_VISITOR. As an admin, we're allowed to see all posts in the Bulletin application.

4. Now let's try editing a post. Select post #2 and click Edit. We should see the following page:

Notice we're allowed to edit the post. That's because in the ACL database we've declared a permission for AdminPost object with object_identity_id of 2 that corresponds to the primary id of the admin_post table in the Bulletin database.

5. Edit the message then click Save.

We should see a success message.

6. Go back to the main page either by pressing the back button twice in your browser or typing the following url:
http://localhost:8080/spring-security-full-acl/krams/bulletin/view

7. Again on the Admin Posts, click on any of the Add links. We should see the following page:

8. Type a new message. Then click Save.

Notice the new posts has been added successfully.

9. Go back to the main page.

10. Again on the Admin Posts, select the post #1 and click on Delete. The post will be deleted automatically with the following confirmation page:

So far everything works as expected. Now try viewing the main page. You'll notice that we've managed to delete the first post. Edit the second post. But the fourth post we added is nowhere to be found!

Let's check the Bulletin database if the new posts has been added really.

The database shows that the fourth post has been added. But how come it doesn't show on the Bulletin application? That's because we haven't declared it yet in the ACL database. We also haven't configured the permissions for this record. We'll discuss this again in the Unexpected Problems section

11. Now let's check the Personal Post. Try adding, editing, or deleting any of the posts. Notice in all actions you will be denied with the following message:

It works as expected.

12. Now let's check the Public Post. Try adding, editing, or deleting any of the posts. Notice it works exactly the same as with the Admin Post, and also shows the same behavior when adding a new posts.

The User

After logging-in as an admin, we'll login next as a regular user and check which actions are allowed to us.

Here are the steps:
1. Visit the login page at http://localhost:8080/spring-security-full-acl/krams/auth/login

2. Enter the following username and password:
username: jane
password: user

3. After a successful login, we should see the following:

Notice the current user is jane with the following roles: ROLE_USER and ROLE_VISITOR. As a regular user, we're allowed to see all posts, except the Admin Posts, in the Bulletin application.

The Visitor

Lastly we'll login as a visitor and check which actions are allowed to us.

Here are the steps:
1. Visit the login page at http://localhost:8080/spring-security-full-acl/krams/auth/login

2. Enter the following username and password:
username: mike
password: visitor

3. After a successful login, we should see the following:

Notice the current user is mike with the following roles: ROLE_VISITOR. As a visitor, we're allowed to see only the visitor posts in the Bulletin application.

Unexpected Problems

If you'd been following the whole implementations closely, you'll discover the following issues:

Issue #1: Bit mask permission doesn't work as expected
When declaring permissions in the database, we're required to declare permissions as bit masks.
- Bit mask "1" (0001 in binary) is interpreted as "READ" access
- Bit mask "2" (0010 in binary) is interpreted as "WRITE" access.

Naturally declaring bit mask "3" (0011) should be interpreted as READ and WRITE access. But it's not. Why? Because AclImpl compares the values by normal equality comparison instead of performing an actual bitwise comparison!

To solve this issue, we can implement our own AclImpl. We'll cover this in future tutorials.

Issue #2: We're stuck with the default READ and WRITE permissions
When declaring Expression-based access control we're forced with the default READ and WRITE access. What if we would like to declare our own permission like READWRITE?

The reason for this behavior is that the by default the AclPermissionEvaluator relies on the DefaultPermissionFactory which by default uses the BasePermission implementation.

Let's examine the BasePermission class:
public class BasePermission extends AbstractPermission {
    public static final Permission READ = new BasePermission(1 << 0, 'R'); // 1
    public static final Permission WRITE = new BasePermission(1 << 1, 'W'); // 2
    public static final Permission CREATE = new BasePermission(1 << 2, 'C'); // 4
    public static final Permission DELETE = new BasePermission(1 << 3, 'D'); // 8
    public static final Permission ADMINISTRATION = new BasePermission(1 << 4, 'A'); // 16

...
}
Notice by default it declares a READ, WRITE, CREATE, DELETE, ADMINISTRATION permissions.

To solve this issue, we can extend the BasePermission. Again, we'll cover this in future tutorials.

Issue #3: New posts are not shown on the View all page
When we create a new post, the system will only allow us if we have the correct permission. However, we don't see our new posts in the View page. That's because the new post objects weren't recorded in the ACL database. If the objects do not exist, we don't get any permission.

To solve this issue, we need to incorporate the ACL service within the posts services. And again, we'll cover this in future tutorials.

Conclusion

That's it. We've completed our Bulletin application. We've successfully implemented an ACL system using Spring Security 3. We've explored how to apply expression-based access controls and explain in detail all the important elements in the system.

The best way to learn further is to try the actual application.

Download the project
You can access the project site at Google's Project Hosting at http://code.google.com/p/spring-security-acl-expression/

You can download the project as a Maven build. Look for the spring-security-full-acl.zip in the Download sections.

You can run the project directly using an embedded server via Maven.
For Tomcat: mvn tomcat:run
For Jetty: mvn jetty:run
StumpleUpon DiggIt! Del.icio.us Blinklist Yahoo Furl Technorati Simpy Spurl Reddit Google I'm reading: Spring Security 3: Full ACL Tutorial (Part 4) ~ Twitter FaceBook

Subscribe by reader Subscribe by email Share

Spring Security 3: Full ACL Tutorial (Part 3)

In Part 1 of this tutorial we've completed setting up the ACL and Bulletin databases. In Part 2 we've completed the Spring Security configuration. In Part 3 we'll be developing the Spring MVC module of the application.

Part 1: Functional Specs and the Application Database
Part 2: Spring Security Configuration
Part 3: Spring MVC Module
Part 4: Running the Application

Part 3: Spring MVC

The Spring MVC module development is quite straightforward. We'll be creating the following:
1. domain objects
2. services
3. controllers
4. configuration files

The Domain Objects

In Part 1 we discussed in the Functional Specs section that our Bulletin application has three types of posts:
AdminPost - contains an id, date, and message
PersonalPost - contains an id, date, and message
PublicPost - contains an id, date, and message
These posts correspond to the domain objects of the system. Let's start by creating a common interface:

Post.java
package org.krams.tutorial.domain;

import java.util.Date;

/**
 * A simple interface for Post objects
 */
public interface Post {

 public Long getId();

 public void setId(Long id);

 public Date getDate();

 public void setDate(Date date);

 public String getMessage();

 public void setMessage(String message);

}
The method signatures correspond to the properties we've discussed in the Functional Specs section.

Let's now create three concrete classes:

AdminPost.java
package org.krams.tutorial.domain;

import java.util.Date;

/**
 * A simple POJO representing admin posts
 */
public class AdminPost implements Post {
 private Long id;
 private Date date;
 private String message;
 
 public Long getId() {
  return id;
 }

 public void setId(Long id) {
  this.id = id;
 }

 public Date getDate() {
  return date;
 }

 public void setDate(Date date) {
  this.date = date;
 }

 public String getMessage() {
  return message;
 }

 public void setMessage(String message) {
  this.message = message;
 }
}

PersonalPost.java
package org.krams.tutorial.domain;

import java.util.Date;

/**
 * A simple POJO representing personal posts
 */
public class PersonalPost implements Post {
 private Long id;
 private Date date;
 private String message;
 
 public Long getId() {
  return id;
 }
 
 public void setId(Long id) {
  this.id = id;
 }
 
 public Date getDate() {
  return date;
 }
 
 public void setDate(Date date) {
  this.date = date;
 }
 
 public String getMessage() {
  return message;
 }
 
 public void setMessage(String message) {
  this.message = message;
 }
}

PublicPost.java
package org.krams.tutorial.domain;

import java.util.Date;

/**
 * A simple POJO representing public posts
 */
public class PublicPost implements Post {
 private Long id;
 private Date date;
 private String message;
 
 public Long getId() {
  return id;
 }
 
 public void setId(Long id) {
  this.id = id;
 }
 
 public Date getDate() {
  return date;
 }
 
 public void setDate(Date date) {
  this.date = date;
 }
 
 public String getMessage() {
  return message;
 }
 
 public void setMessage(String message) {
  this.message = message;
 }
}
Our concrete classes are just simple POJOs.

The Services

We'll be declaring three services where each one will handle a specific domain object:
AdminService - handles AdminPost
PersonalService - handles PersonalPost
PublicService - handles PublicPost

Our services will implement a common interface. It's worth noting this interface is the crux of successfully applying ACL in the application. Therefore, we've heavily placed comments within this interface to point out important information.

Here's the interface:

GenericService.java
package org.krams.tutorial.service;

import java.util.List;

import javax.sql.DataSource;

import org.krams.tutorial.domain.Post;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;

/**
 * A generic service for handling CRUD operations.
 * <p>
 * The method access-control expressions are specified in this interface.
 */
public interface GenericService {

 /**
  * Inject the datasource for the bulletingapplication
  */
 public void setDataSource(DataSource dataSource);

 /**
  *  Retrieves a single post.
  *  <p>
  *  Access-control will be evaluated after this method is invoked.
  *  returnObject refers to the returned object.
  */
 @PostAuthorize("hasPermission(returnObject, 'WRITE')")
 public Post getSingle(Long id);

 /**
  *  Retrieves all posts.
  *  <p>
  *  Access-control will be evaluated after this method is invoked.
  *  filterObject refers to the returned object list.
  */
 @PostFilter("hasPermission(filterObject, 'READ')")
 public List<Post> getAll();

 /**
  * Adds a new post.
  * <p>
  * We don't provide any access control here because  
  * the new object doesn't have an id yet. 
  * <p>
  * Instead we place the access control on the URL-level because
  * the Add page shouldn't be visible in the first place.
  * <p>
  * There are two places where we can place this restriction:
  * <pre>
  * 1. At the controller method
  * 2. At the external spring-security.xml file</pre>
  * <p>
  * 
  */
 public Boolean add(Post post);

 /**
  * Edits a post.
  * <p>
  * Access-control will be evaluated before this method is invoked.
  * <b>#post</b> refers to the current object in the method argument. 
  */
 @PreAuthorize("hasPermission(#post, 'WRITE')")
 public Boolean edit(Post post);

 /**
  * Deletes a post.
  * <p>
  * Access-control will be evaluated before this method is invoked.
  * <b>#post</b> refers to the current object in the method argument. 
  */
 @PreAuthorize("hasPermission(#post, 'WRITE')")
 public Boolean delete(Post post);

}
Notice the methods had been annotated with different types of Expression-based access controls, and because this is an interface all concrete classes will be secured with ACL.

Method Access-Control Expressions

Let's take an in-depth look at all the important annotations:

@PostAuthorize
@PostAuthorize("hasPermission(returnObject, 'WRITE')")
public Post getSingle(Long id);
Annotation for specifying a method access-control expression which will be evaluated after a method has been invoked - Source: Spring Security 3 API
This annotation will be triggered after the getSingle() method has completed its execution. The rule that will be applied is inside the annotation:
"hasPermission(returnObject, 'WRITE')"
This means whatever is the returned object (in our case, a Post object) make sure the current user has a WRITE access.

@PostFilter
@PostFilter("hasPermission(filterObject, 'READ')")
public List getAll();
Annotation for specifying a method filtering expression which will be evaluated after a method has been invoked - Source: Spring Security 3 API
This annotation will be triggered after the getAll() method has completed its execution. The rule that will be applied is inside the annotation:
"hasPermission(filterObject, 'READ')"
This means whatever is the returned list of objects (in our case, Post objects) make sure to filter the list. Return only objects where the current user has READ access.


@PreAuthorize("hasPermission(#post, 'WRITE')")
public Boolean edit(Post post);
Annotation for specifying a method access-control expression which will be evaluated to decide whether a method invocation is allowed or not - Source: Spring Security 3 API
This annotation will be triggered before the edit() method is executed. The rule that will be applied is inside the annotation:
"hasPermission(#post, 'WRITE')"
This means whatever is the object argument (in our case, a Post object) make sure the current user has WRITE access.

Extra Pointers

Notice all of the methods are annotated with method access-control expressions, except for two:
1. public void setDataSource(DataSource dataSource);
2. public Boolean add(Post post);
The method setDataSource() has nothing to do with Spring Security. It's just a reference to the application's datasource, the bulletinDataSource, which we'll discuss later.

The method add() is what's interesting. Remember the Functional Specs in Part 1:
1. Only users with ROLE_ADMIN can create AdminPost
2. Only users with ROLE_USER can create PersonalPost
3. Only users with ROLE_ADMIN or ROLE_USER can create PublicPost
4. Users with ROLE_VISITOR cannot create any post
Note: When we use the word 'create', we mean adding a new post.

Post creation is one of the crucial methods available in the application, but it's left unsecured! Here are the reasons why:

The object reference inside the hasPermission expression needs to have an existing id. But when creating a new Post, it doesn't have an id yet from the bulletin database!

Solution #1 (Bad)
We could use the @PostAuthorize expression to allow the user to add. Then check the returned object's id for valid permission. But this expression defeats the main purpose. The user has already created a record regardless of permission!

Solution #2 (Bad)
We could use the @PreAuthorize expression to verify if the current user has valid permissions before executing the add() method. But wait. That's wrong! The Post object doesn't have an existing id yet because it hasn't been created in the database!

There's another question: why does the object need an existing id? The simple answer is because the default PermissionEvaluator implementation and the default Acl implementation requires one. If we need to bypass this limitation, we can create our own implementations.

Solution #3 (Good)
Apply access control at the URL-level. In the first place, we don't really want to show the add new post page. Then it's better if we limit control at the URL-level. There are two ways where we can implement this:
a. add an intercept-url in the spring-security.xml 
b. apply method security control expression in the controller 
We'll choose option b because it's an interesting solution and allows us to explore method security control expressions at the controller level.

The Controllers

Our application controllers share the same implementation where the main difference only are the @RequestMapping values and the services.

We'll be declaring four controllers:
AdminController - handles admin related requests
PersonalController - handles user related requests
PublicController - handles visitor related requests
BulletinController - handles the general view all requests

AdminController.java
package org.krams.tutorial.controller;

import java.util.Date;

import org.apache.log4j.Logger;
import org.krams.tutorial.domain.AdminPost;
import org.krams.tutorial.domain.Post;
import org.krams.tutorial.service.GenericService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import javax.annotation.Resource;

/**
 * Handles Admin-related requests
 */
@Controller
@RequestMapping("/admin")
public class AdminController {

 protected static Logger logger = Logger.getLogger("controller");
 
 @Resource(name="adminService")
 private GenericService adminService;

 /**
     * Retrieves the Edit page
     */
    @RequestMapping(value = "/edit", method = RequestMethod.GET)
    public String getEdit(@RequestParam(value="id", required=true) Long id,  
              Model model) {
     logger.debug("Received request to show edit page");
    
     // Retrieve existing post and add to model
     // This is the formBackingOBject
     model.addAttribute("postAttribute", adminService.getSingle(id));
     
     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Admin");
     
     // This will resolve to /WEB-INF/jsp/crud-admin/editpage.jsp
     return "crud-admin/editpage";
 }
    
    /**
     * Saves the edited post from the Edit page and returns a result page.
     */
    @RequestMapping(value = "/edit", method = RequestMethod.POST)
    public String getEditPage(@ModelAttribute("postAttribute") AdminPost post, 
              @RequestParam(value="id", required=true) Long id,
              Model model) {
     logger.debug("Received request to view edit page");
    
     // Re-assign id
     post.setId(id);
     // Assign new date
     post.setDate(new Date());
     
     // Delegate to service
     if (adminService.edit(post) == true) {
         // Add result to model
         model.addAttribute("result", "Entry has been edited successfully!");
     } else {
         // Add result to model
         model.addAttribute("result", "You're not allowed to perform that action!");
     }

     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Admin");
     
     // Add our current role and username
     model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
     model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());
     
     // This will resolve to /WEB-INF/jsp/crud-admin/resultpage.jsp
     return "crud-admin/resultpage";
 }
    
    /**
     * Retrieves the Add page
     * <p>
     * Access-control is placed here (instead in the service) because we don't want 
     * to show this page if the client is unauthorized and because the new 
     * object doesn't have an id. The hasPermission requires an existing id!
     */
 @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    @RequestMapping(value = "/add", method = RequestMethod.GET)
    public String getAdd(Model model) {
     logger.debug("Received request to show add page");
    
     // Create new post and add to model
     // This is the formBackingOBject
     model.addAttribute("postAttribute", new AdminPost());

     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Admin");
     
     // This will resolve to /WEB-INF/jsp/crud-admin/addpage.jsp
     return "crud-admin/addpage";
 }
    
    /**
     * Saves a new post from the Add page and returns a result page.
     */
    @RequestMapping(value = "/add", method = RequestMethod.POST)
    public String getAddPage(@ModelAttribute("postAttribute") AdminPost post, Model model) {
     logger.debug("Received request to view add page");
    
     // Add date today
     post.setDate(new Date());
     
     // Delegate to service
     if (adminService.add(post)) {
         // Success. Add result to model
         model.addAttribute("result", "Entry has been added successfully!");
     } else {
         // Failure. Add result to model
         model.addAttribute("result", "You're not allowed to perform that action!");
     }
     
     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Admin");
     
     // Add our current role and username
     model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
     model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());
     
     // This will resolve to /WEB-INF/jsp/crud-admin/resultpage.jsp
     return "crud-admin/resultpage";
 }
    
    /**
     * Deletes an existing post and returns a result page.
     */
    @RequestMapping(value = "/delete", method = RequestMethod.GET)
    public String getDeletePage(@RequestParam(value="id", required=true) Long id,
              Model model) {
     logger.debug("Received request to view delete page");
    
     // Create new post
     Post post = new AdminPost();
     // Assign id
     post.setId(id);
     
     // Delegate to service
     if (adminService.delete(post)) {
         // Add result to model
         model.addAttribute("result", "Entry has been deleted successfully!");
     } else {
         // Add result to model
         model.addAttribute("result", "You're not allowed to perform that action!");
     }
     
     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Admin");
     
     // Add our current role and username
     model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
     model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());
     
     // This will resolve to /WEB-INF/jsp/crud-admin/resultpage.jsp
     return "crud-admin/resultpage";
 }
}

PersonalController.java
package org.krams.tutorial.controller;

import java.util.Date;

import org.apache.log4j.Logger;
import org.krams.tutorial.domain.PersonalPost;
import org.krams.tutorial.domain.Post;
import org.krams.tutorial.service.GenericService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import javax.annotation.Resource;

/**
 * Handles Personal-related requests
 */
@Controller
@RequestMapping("/personal")
public class PersonalController {

 protected static Logger logger = Logger.getLogger("controller");
 
 @Resource(name="personalService")
 private GenericService personalService;
    
 /**
     * Retrieves the Edit page
     */
    @RequestMapping(value = "/edit", method = RequestMethod.GET)
    public String getEdit(@RequestParam(value="id", required=true) Long id,  
              Model model) {
     logger.debug("Received request to show edit page");
    
     // Retrieve existing post and add to model
     // This is the formBackingOBject
     model.addAttribute("postAttribute", personalService.getSingle(id));
     
     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Personal");
     
     // This will resolve to /WEB-INF/jsp/crud-personal/editpage.jsp
     return "crud-personal/editpage";
 }
    
    /**
     * Saves the edited post from the Edit page and returns a result page.
     */
    @RequestMapping(value = "/edit", method = RequestMethod.POST)
    public String getEditPage(@ModelAttribute("postAttribute") PersonalPost post, 
              @RequestParam(value="id", required=true) Long id,
              Model model) {
     logger.debug("Received request to view edit page");
    
     // Re-assign id
     post.setId(id);
     // Assign new date
     post.setDate(new Date());
     
     // Delegate to service
     if (personalService.edit(post) == true) {
         // Add result to model
         model.addAttribute("result", "Entry has been edited successfully!");
     } else {
         // Add result to model
         model.addAttribute("result", "You're not allowed to perform that action!");
     }

     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Personal");
     
     // Add our current role and username
     model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
     model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());
     
     // This will resolve to /WEB-INF/jsp/crud-personal/resultpage.jsp
     return "crud-personal/resultpage";
 }
    
    /**
     * Retrieves the Add page
     * <p>
     * Access-control is placed here (instead in the service) because we don't want 
     * to show this page if the client is unauthorized and because the new 
     * object doesn't have an id. The hasPermission requires an existing id!
     */
 @PreAuthorize("hasAuthority('ROLE_USER') and !hasAuthority('ROLE_ADMIN')")
    @RequestMapping(value = "/add", method = RequestMethod.GET)
    public String getAdd(Model model) {
     logger.debug("Received request to show add page");
    
     // Create new post and add to model
     // This is the formBackingOBject
     model.addAttribute("postAttribute", new PersonalPost());

     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Personal");
     
     // This will resolve to /WEB-INF/jsp/crud-personal/addpage.jsp
     return "crud-personal/addpage";
 }
    
    /**
     * Saves a new post from the Add page and returns a result page.
     */
    @RequestMapping(value = "/add", method = RequestMethod.POST)
    public String getAddPage(@ModelAttribute("postAttribute") PersonalPost post, Model model) {
     logger.debug("Received request to view add page");
    
     // Add date today
     post.setDate(new Date());
     
     // Delegate to service
     if (personalService.add(post)) {
         // Success. Add result to model
         model.addAttribute("result", "Entry has been added successfully!");
     } else {
         // Failure. Add result to model
         model.addAttribute("result", "You're not allowed to perform that action!");
     }
     
     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Personal");
     
     // Add our current role and username
     model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
     model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());
     
     // This will resolve to /WEB-INF/jsp/crud-personal/resultpage.jsp
     return "crud-personal/resultpage";
 }
    
    /**
     * Deletes an existing post and returns a result page.
     */
    @RequestMapping(value = "/delete", method = RequestMethod.GET)
    public String getDeletePage(@RequestParam(value="id", required=true) Long id,
              Model model) {
     logger.debug("Received request to view delete page");
    
     // Create new post
     Post post = new PersonalPost();
     // Assign id
     post.setId(id);
     
     // Delegate to service
     if (personalService.delete(post)) {
         // Add result to model
         model.addAttribute("result", "Entry has been deleted successfully!");
     } else {
         // Add result to model
         model.addAttribute("result", "You're not allowed to perform that action!");
     }
     
     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Personal");
     
     // Add our current role and username
     model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
     model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());
     
     // This will resolve to /WEB-INF/jsp/crud-personal/resultpage.jsp
     return "crud-personal/resultpage";
 }
}

PublicController.java
package org.krams.tutorial.controller;

import java.util.Date;

import org.apache.log4j.Logger;
import org.krams.tutorial.domain.PublicPost;
import org.krams.tutorial.domain.Post;
import org.krams.tutorial.service.GenericService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import javax.annotation.Resource;

/**
 * Handles Public-related requests
 */
@Controller
@RequestMapping("/public")
public class PublicController {

 protected static Logger logger = Logger.getLogger("controller");
 
 @Resource(name="publicService")
 private GenericService publicService;
    
 /**
     * Retrieves the Edit page
     */
    @RequestMapping(value = "/edit", method = RequestMethod.GET)
    public String getEdit(@RequestParam(value="id", required=true) Long id,  
              Model model) {
     logger.debug("Received request to show edit page");
    
     // Retrieve existing post and add to model
     // This is the formBackingOBject
     model.addAttribute("postAttribute", publicService.getSingle(id));
     
     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Public");
     
     // This will resolve to /WEB-INF/jsp/crud-public/editpage.jsp
     return "crud-public/editpage";
 }
    
    /**
     * Saves the edited post from the Edit page and returns a result page.
     */
    @RequestMapping(value = "/edit", method = RequestMethod.POST)
    public String getEditPage(@ModelAttribute("postAttribute") PublicPost post, 
              @RequestParam(value="id", required=true) Long id,
              Model model) {
     logger.debug("Received request to view edit page");
    
     // Re-assign id
     post.setId(id);
     // Assign new date
     post.setDate(new Date());
     
     // Delegate to service
     if (publicService.edit(post) == true) {
         // Add result to model
         model.addAttribute("result", "Entry has been edited successfully!");
     } else {
         // Add result to model
         model.addAttribute("result", "You're not allowed to perform that action!");
     }

     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Public");
     
     // Add our current role and username
     model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
     model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());
     
     // This will resolve to /WEB-INF/jsp/crud-public/resultpage.jsp
     return "crud-public/resultpage";
 }
    
    /**
     * Retrieves the Add page
     * <p>
     * Access-control is placed here (instead in the service) because we don't want 
     * to show this page if the client is unauthorized and because the new 
     * object doesn't have an id. The hasPermission requires an existing id!
     */
 @PreAuthorize("hasAuthority('ROLE_USER') or hasAuthority('ROLE_ADMIN')")
    @RequestMapping(value = "/add", method = RequestMethod.GET)
    public String getAdd(Model model) {
     logger.debug("Received request to show add page");
    
     // Create new post and add to model
     // This is the formBackingOBject
     model.addAttribute("postAttribute", new PublicPost());

     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Public");
     
     // This will resolve to /WEB-INF/jsp/crud-public/addpage.jsp
     return "crud-public/addpage";
 }
    
    /**
     * Saves a new post from the Add page and returns a result page.
     */
    @RequestMapping(value = "/add", method = RequestMethod.POST)
    public String getAddPage(@ModelAttribute("postAttribute") PublicPost post, Model model) {
     logger.debug("Received request to view add page");
    
     // Add date today
     post.setDate(new Date());
     
     // Delegate to service
     if (publicService.add(post)) {
         // Success. Add result to model
         model.addAttribute("result", "Entry has been added successfully!");
     } else {
         // Failure. Add result to model
         model.addAttribute("result", "You're not allowed to perform that action!");
     }
     
     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Public");
     
     // Add our current role and username
     model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
     model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());
     
     // This will resolve to /WEB-INF/jsp/crud-public/resultpage.jsp
     return "crud-public/resultpage";
 }
    
    /**
     * Deletes an existing post and returns a result page.
     */
    @RequestMapping(value = "/delete", method = RequestMethod.GET)
    public String getDeletePage(@RequestParam(value="id", required=true) Long id,
              Model model) {
     logger.debug("Received request to view delete page");
    
     // Create new post
     Post post = new PublicPost();
     // Assign id
     post.setId(id);
     
     // Delegate to service
     if (publicService.delete(post)) {
         // Add result to model
         model.addAttribute("result", "Entry has been deleted successfully!");
     } else {
         // Add result to model
         model.addAttribute("result", "You're not allowed to perform that action!");
     }
     
     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Public");
     
     // Add our current role and username
     model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
     model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());
     
     // This will resolve to /WEB-INF/jsp/crud-public/resultpage.jsp
     return "crud-public/resultpage";
 }
}

BulletinController.java
/**
 * 
 */
package org.krams.tutorial.controller;

import javax.annotation.Resource;

import org.apache.log4j.Logger;
import org.krams.tutorial.service.GenericService;
import org.krams.tutorial.service.PersonalService;
import org.krams.tutorial.service.PublicService;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 * Handles Bulletin related requests
 */
@Controller
@RequestMapping("/bulletin")
public class BulletinController{
        
 protected static Logger logger = Logger.getLogger("controller");

 @Resource(name="adminService")
 private GenericService adminService;
 
 @Resource(name="personalService")
 private GenericService personalService;
 
 @Resource(name="publicService")
 private GenericService publicService;
 
 /**
  * Retrieves the View page. 
  * <p>
  * This loads all authorized posts.
  */
    @RequestMapping(value = "/view", method = RequestMethod.GET)
    public String getViewAllPage(Model model) {
     logger.debug("Received request to view all page");
    
     // Retrieve items from service and add to model
     model.addAttribute("adminposts", adminService.getAll());
     model.addAttribute("personalposts", personalService.getAll());
     model.addAttribute("publicposts", publicService.getAll());
     
     // Add our current role and username
     model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
     model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());
     
     // This will resolve to /WEB-INF/jsp/bulletinpage.jsp
     return "bulletinpage";
 }
}

Spring MVC Configuration

We've completed the Spring MVC classes of our application. Our next task is to declare the required configuration.

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

 <filter>
         <filter-name>springSecurityFilterChain</filter-name>
         <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
 </filter>
 
 <filter-mapping>
         <filter-name>springSecurityFilterChain</filter-name>
         <url-pattern>/*</url-pattern>
 </filter-mapping>

 <context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>
  /WEB-INF/spring-security.xml
  /WEB-INF/applicationContext.xml
  </param-value>
 </context-param>
 
 <servlet>
  <servlet-name>spring</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <load-on-startup>1</load-on-startup>
 </servlet>
 
 <servlet-mapping>
  <servlet-name>spring</servlet-name>
  <url-pattern>/krams/*</url-pattern>
 </servlet-mapping>

 <listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
 </listener>
 
</web-app>

Take note of the URL pattern. When accessing any pages in our MVC application, the host name must be appended with
/krams
In the web.xml we declared a servlet-name spring. By convention, we must declare a spring-servlet.xml as well.

spring-servlet.xml
<?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:p="http://www.springframework.org/schema/p" 
 xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
 
 <!-- Declare a view resolver -->
 <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver" 
      p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" />

</beans>
This XML config declares a view resolver. All references to a JSP name in the controllers will map to a corresponding JSP in the /WEB-INF/jsp location.

By convention, we must declare an applicationContext.xml

applicationContext.xml
<?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:context="http://www.springframework.org/schema/context"
 xmlns:mvc="http://www.springframework.org/schema/mvc"
 xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context-3.0.xsd
   http://www.springframework.org/schema/mvc 
   http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
 
 <!-- Activates various annotations to be detected in bean classes -->
 <context:annotation-config />
 
 <!-- Scans the classpath for annotated components that will be auto-registered as Spring beans.
  For example @Controller and @Service. Make sure to set the correct base-package-->
 <context:component-scan base-package="org.krams.tutorial" />
 
 <!-- Configures the annotation-driven Spring MVC Controller programming model.
 Note that, with Spring 3.0, this tag works in Servlet MVC only!  -->
 <mvc:annotation-driven /> 
 
 <!-- Loads bulletin related configuration-->
 <import resource="bulletin-context.xml" />
</beans>

Conclusion

We've completed the Spring MVC module of the Bulletin application. We've declared the required domain objects, services, and controllers. Our final task is to test and run the application. We'll also cover some of the unexpected problems within the application.

Proceed to Part 4: Running the Application
StumpleUpon DiggIt! Del.icio.us Blinklist Yahoo Furl Technorati Simpy Spurl Reddit Google I'm reading: Spring Security 3: Full ACL Tutorial (Part 3) ~ Twitter FaceBook

Subscribe by reader Subscribe by email Share

Spring Security 3: Full ACL Tutorial (Part 2)

In Part 1 of this tutorial we've completed setting up the ACL and Bulletin databases. In Part 2 we'll be building the Bulletin application with Spring Security and Spring MVC.

Part 1: Functional Specs and the Application Database
Part 2: Spring Security Configuration
Part 3: Spring MVC Module
Part 4: Running the Application

Part 2: Spring Security

Most of the development with Spring Security are composed of configuration files. We'll be declaring two configuration files:
1. spring-security.xml
2. acl-context.xml

spring-security.xml
This contains standard Spring Security configuration. It declares the following:
1. A set of intercept-url patterns.
2. An authentication manager
3. An Md5 password encoder
4. An in-memory user service

For an in-depth description of this file, please see the Spring Security 3 - MVC: Using a Simple User-Service Tutorial

acl-context.xml
This contains ACL-related configuration. It declares the following:
1. A global-method-security tag which enables method security expressions
2. An expression handler
3. A permission evaluator
4. An ACL service
5. A lookup strategy
6. A datasource
7. An ACL cache
8. An ACL authorization strategy
9. A role hierarchy

Here are the configuration files:

spring-security.xml
<?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"
 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.0.xsd">
 
 <!-- Loads ACL related configurations -->
 <import resource="acl-context.xml" />
 
 <!-- This is where we configure Spring-Security  -->
 <security:http auto-config="true" use-expressions="true" access-denied-page="/krams/auth/denied" >
 
  <security:intercept-url pattern="/krams/auth/login" access="permitAll"/>
  <security:intercept-url pattern="/krams/bulletin/view" access="hasRole('ROLE_VISITOR')"/>
  <security:intercept-url pattern="/krams/role/admin" access="hasRole('ROLE_ADMIN')"/>
  <security:intercept-url pattern="/krams/role/user" access="hasRole('ROLE_USER')"/>
  <security:intercept-url pattern="/krams/role/visitor" access="hasRole('ROLE_VISITOR')"/>
  
  <security:form-login
    login-page="/krams/auth/login" 
    authentication-failure-url="/krams/auth/login?error=true" 
    default-target-url="/krams/bulletin/view"/>
   
  <security:logout 
    invalidate-session="true" 
    logout-success-url="/krams/auth/login" 
    logout-url="/krams/auth/logout"/>
 
 </security:http>
 
 <!-- Declare an authentication-manager to use a custom userDetailsService -->
 <security:authentication-manager>
         <security:authentication-provider user-service-ref="userDetailsService">
           <security:password-encoder ref="passwordEncoder"/>
         </security:authentication-provider>
 </security:authentication-manager>
 
 <!-- Use a Md5 encoder since the user's passwords are stored as Md5 in the database -->
 <bean class="org.springframework.security.authentication.encoding.Md5PasswordEncoder" id="passwordEncoder"/>

  <!-- An in-memory list of users. No need to access an external database layer.
      See Spring Security 3.1 Reference 5.2.1 In-Memory Authentication -->
  <!-- john's password: admin
    jane's password: user
    mike's password: visitor  -->
  <security:user-service id="userDetailsService">
     <security:user name="john" password="21232f297a57a5a743894a0e4a801fc3" authorities="ROLE_ADMIN, ROLE_USER, ROLE_VISITOR" />
     <security:user name="jane" password="ee11cbb19052e40b07aac0ca060c23ee" authorities="ROLE_USER, ROLE_VISITOR" />
     <security:user name="mike" password="127870930d65c57ee65fcc47f2170d38" authorities="ROLE_VISITOR" />
   </security:user-service>
 
</beans>

acl-context.xml
<?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.0.xsd
   http://www.springframework.org/schema/jdbc 
   http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd">

 <!-- Enables Method Security and Expression-based access control -->
 <security:global-method-security pre-post-annotations="enabled">
  <!-- Enables custom expression handler -->
  <security:expression-handler ref="expressionHandler" />
 </security:global-method-security>

 <!-- 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="dataSource"/>
        <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="dataSource"/>
        <constructor-arg ref="aclCache"/>
        <constructor-arg ref="aclAuthorizationStrategy"/>
        <constructor-arg ref="auditLogger"/>
    </bean>
    
    <!-- Declare a datasource -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
   destroy-method="close"
   p:driverClass="com.mysql.jdbc.Driver"
   p:jdbcUrl="jdbc:mysql://localhost/acl"
   p:user="root"
   p:password=""
   p:acquireIncrement="5"
   p:idleConnectionTestPeriod="60"
   p:maxPoolSize="100"
   p:maxStatements="50"
   p:minPoolSize="10" />

    <!-- 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"/>
                </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_ADMIN"/>
                </bean>
                <bean class="org.springframework.security.core.authority.GrantedAuthorityImpl">
                    <constructor-arg value="ROLE_ADMIN"/>
                </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
             ROLE_USER > ROLE_VISITOR
         </value>
     </property>
 </bean>
</beans>

In-Depth Look of acl-context.xml

Let's take an in-depth look of the acl-context.xml file.

The Method Security

<security:global-method-security pre-post-annotations="enabled">
  <security:expression-handler ref="expressionHandler" />
 </security:global-method-security>
The global-method-security enables method security annotations. There are three types of method security annotations available in Spring Security (See Spring Security Reference 2.4 Method Security)
1. @Secured annotation
2. JSR-250 annotation
3. Expression-based access control

By adding the pre-post-annotations attribute, we've activated the Expression-based access control. If you need to learn how to setup a simple application using the Expression-based access control, please see Spring Security 3 - MVC: Using Native Expression-Based Annotation Tutorial

The Expression Handler

The expression-handler property defines a custom expression handler instance. Without this property Spring Security will declare a default expression handler with no ACL support. We need to declare a custom handler because we need ACL support. It turns out Spring provides a default implementation that we can customize so that we don't have to create one from scratch.

<bean id="expressionHandler" class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
  <property name="permissionEvaluator" ref="permissionEvaluator" />
  <property name = "roleHierarchy" ref="roleHierarchy"/>
 </bean>
Here we declared a reference to a customized expression handler: DefaultMethodSecurityExpressionHandler. This is actually the default expression handler but it needs to be declared manually so that we can provide a customized permission evaluator.

The permissionEvaluator property defines a reference to a custom permission evaluator, while the roleHierarchy allows us to define the hierarchy of our roles.

The Role Hierarchy

<bean id="roleHierarchy"  class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
     <property name="hierarchy">
         <value>
             ROLE_ADMIN > ROLE_USER
             ROLE_USER > ROLE_VISITOR
         </value>
     </property>
 </bean>
</beans>
Role hierarchy is a way of declaring which role is the boss of other roles. In our sample configuration ROLE_ADMIN > ROLE USER, means whenever a user has a ROLE_ADMIN, he also gets
the ROLE_USER. And because we declared ROLE_USER > ROLE_VISITOR, he also gets the ROLE_VISITOR.

The ACL Permission Evaluator

<bean class="org.springframework.security.acls.AclPermissionEvaluator" id="permissionEvaluator">
  <constructor-arg ref="aclService"/>
 </bean>
What is AclPermissionEvaluator?
Used by Spring Security's expression-based access control implementation to evaluate permissions for a particular object using the ACL module. Similar in behaviour to AclEntryVoter.

Source: Spring Security 3 API AclPermissionEvaluator
AclPermissionEvaluator is the default implementation for evaluating ACLs with expression-based access control but it's not enabled by default. It needs to be declared manually and it needs to be customized.

The <constructor-arg ref="aclService"/> is a reference to a custom ACL service. Basically this is the service that will access the ACL database.

The ACL Service

<bean class="org.springframework.security.acls.jdbc.JdbcMutableAclService" id="aclService">
  <constructor-arg ref="dataSource"/>
        <constructor-arg ref="lookupStrategy"/>
        <constructor-arg ref="aclCache"/>
 </bean>
What is JdbcMutableAclService?
Provides a base JDBC implementation of MutableAclService.

The default settings are for HSQLDB. If you are using a different database you will probably need to set the sidIdentityQuery and classIdentityQuery properties appropriately. The other queries, SQL inserts and updates can also be customized to accomodate schema variations, but must produce results consistent with those expected by the defaults.

Source: Spring Security 3 API JdbcMutableAclService
The JdbcMutableAclService is a JDBC-based ACL service. It uses JdbcTemplate to simplify JDBC access. Remember in Part 1 of this tutorial we declared a custom MySQL ACL schema. That's because by default the JdbcMutableAclService uses HSQLDB. In fact, if you examine the Spring Security ACL package, you'll find that there are two schemas available: an HSQLDB schema and PostgreSQL schema. See the screenshot below:


The <constructor-arg ref="dataSource"/> is a reference to a datasource, in our case, a MySQL datasource.

The <constructor-arg ref="lookupStrategy"/> is a reference to a lookup strategy. Its purpose is to provide an optimized lookup when querying the database.

The <constructor-arg ref="aclCache"/> is a reference to an ACL cache. Its purpose is to lessen database access by checking first if the ACL entry is already available in the cache.

The Datasource

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
   destroy-method="close"
   p:driverClass="com.mysql.jdbc.Driver"
   p:jdbcUrl="jdbc:mysql://localhost/acl"
   p:user="root"
   p:password=""
   p:acquireIncrement="5"
   p:idleConnectionTestPeriod="60"
   p:maxPoolSize="100"
   p:maxStatements="50"
   p:minPoolSize="10" />
This is a standard MySQL datasource that uses a C3P0 connection pool. The jdbcUrl property points to the acl database.

What is Pooling?
In software engineering, a connection pool is a cache of database connections maintained so that the connections can be reused when future requests to the database are required. Connection pools are used to enhance the performance of executing commands on a database. Opening and maintaining a database connection for each user, especially requests made to a dynamic database-driven website application, is costly and wastes resources. In connection pooling, after a connection is created, it is placed in the pool and it is used over again so that a new connection does not have to be established.

Source: http://en.wikipedia.org/wiki/Connection_pool

For more info on configuring C3P0, you can check this reference from JBoss: HowTo configure the C3P0 connection pool.

The Lookup Strategy

<bean id="lookupStrategy" class="org.springframework.security.acls.jdbc.BasicLookupStrategy">
        <constructor-arg ref="dataSource"/>
        <constructor-arg ref="aclCache"/>
        <constructor-arg ref="aclAuthorizationStrategy"/>
        <constructor-arg ref="auditLogger"/>
    </bean>
Here we declare Spring Security's default implementation of a lookup strategy BasicLookupStrategy. As mentioned earlier, the purpose of a lookup strategy is to provide an optimized lookup when querying the database.

Here's an in-depth description of BasicLookupStrategy:
Performs lookups in a manner that is compatible with ANSI SQL.

This implementation does attempt to provide reasonably optimised lookups - within the constraints of a normalised database and standard ANSI SQL features. If you are willing to sacrifice either of these constraints (e.g. use a particular database feature such as hierarchical queries or materalized views, or reduce normalisation) you are likely to achieve better performance. In such situations you will need to provide your own custom LookupStrategy. This class does not support subclassing, as it is likely to change in future releases and therefore subclassing is unsupported.

Source: Spring Security 3 API - BasicLookupStrategy

The <constructor-arg ref="dataSource"/> is a reference to the same MySQL datasource we described earlier.

The <constructor-arg ref="aclCache"/> is a reference to the same ACL cache we described earlier.

The <constructor-arg ref="aclAuthorizationStrategy"/> is a reference to an AclAuthorizationStrategy implementation.

What is AclAuthorizationStrategy?
Strategy used by AclImpl to determine whether a principal is permitted to call adminstrative methods on the AclImpl.

Source: Spring Security 3 API - AclAuthorizationStrategy

The <constructor-arg ref="auditLogger"/> is a reference to an AuditLogger implementation.

<bean id="auditLogger" class="org.springframework.security.acls.domain.ConsoleAuditLogger"/>
What is AuditLogger?
Used by AclImpl to log audit events.

Source: Spring Security 3 API - AuditLogger

The 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"/>
                </property>
                <property name="cacheName" value="aclCache"/>
            </bean>
        </constructor-arg>
    </bean>
Here we declare Spring Security's default implementation of AclCache interface. It's purpose is to lessen database lookups.

What is EhCacheBasedAclCache?
Simple implementation of AclCache that delegates to EH-CACHE.

Designed to handle the transient fields in AclImpl. Note that this implementation assumes all AclImpl instances share the same AuditLogger and AclAuthorizationStrategy instance.

Source: Spring Security 3 API - EhCacheBasedAclCache

What is EhCache?
Ehcache is an open source, standards-based cache used to boost performance, offload the database and simplify scalability. Ehcache is robust, proven and full-featured, which has made it the most popular Java-based cache with 100,000’s of production deployments.

Source: EhCache.org

The EhCacheBasedAclCache constructor accepts an instance of an EHCache instance. The EhCacheFactoryBean is responsible for crating this instance.

The 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_ADMIN"/>
                </bean>
                <bean class="org.springframework.security.core.authority.GrantedAuthorityImpl">
                    <constructor-arg value="ROLE_ADMIN"/>
                </bean>
            </list>
        </constructor-arg>
    </bean>
AclAuthorizationStrategyImpl is the default implementation of AclAuthorizationStrategy. Notice the constructor accepts three arguments. Based on the Spring Security API, constructor signature is as follows:

public AclAuthorizationStrategyImpl(GrantedAuthority[] auths)
And here's what auths represent:
auths - an array of GrantedAuthoritys that have special permissions (index 0 is the authority needed to change ownership, index 1 is the authority needed to modify auditing details, index 2 is the authority needed to change other ACL and ACE details)

Source: Spring Security 3 API - AclAuthorizationStrategyImpl

Conclusion

We have completed the standard Spring Security configuration, including the ACL-related beans. We've also described in detail the purpose and meaning behind the configuration. Our next task is to setup the Spring MVC module.

Proceed to Part 3: Spring MVC Module
StumpleUpon DiggIt! Del.icio.us Blinklist Yahoo Furl Technorati Simpy Spurl Reddit Google I'm reading: Spring Security 3: Full ACL Tutorial (Part 2) ~ Twitter FaceBook

Subscribe by reader Subscribe by email Share

Spring Security 3: Full ACL Tutorial (Part 1)

In this tutorial we'll develop a simple Bulletin application where various users can create, add, edit, and delete posts depending on their access levels. Our application is a simple CRUD system that utilizes an Access Control List (ACL) to secure domain objects. The corresponding permissions will be retrieved from an external MySQL database. There's a separate database for the Bulletin posts and the ACL itself.

Here's what we'll be doing:
1. Setup a MySQL database containing ACL data
2. Setup a separate database containg the application's data
3. Secure domain objects using Expression-Based Access Control
4. Secure URLs using the intercept-url and Expression-Based Access Control
5. Tackle unexpected issues

We'll be dividing the tutorial in four parts:
Part 1: Functional Specs and the Application Database
Part 2: Spring Security Configuration
Part 3: Spring MVC Module
Part 4: Running the Application

Our system will be built on Spring MVC 3 and Spring Security 3 for the security layer. The primary goal of this tutorial is to help us setup a quick ACL-based application. To achieve that, we'll be relying on standard implementations.

Part 1: Functional Specs

Let's describe the application's requirements first, so that we know our purpose.

In our system we have three roles:
ROLE_ADMIN - provides administrative access
ROLE_USER - provides regular access
ROLE_VISITOR - provides visitor access

We also have three concrete users along with their roles:
john - ROLE_ADMIN 
jane - ROLE_USER 
mike - ROLE_VISITOR 

When john logs-in, he is given the ROLE_ADMIN. When jane logs-in, she is given the ROLE_USER. And when mike logs-in, he gets the ROLE_VISITOR.

Our Bulletin application has three types of posts:
AdminPost - contains an id, date, and message
PersonalPost - contains an id, date, and message
PublicPost - contains an id, date, and message

Here are the simple rules:
1. Only users with ROLE_ADMIN can create AdminPost
2. Only users with ROLE_USER can create PersonalPost
3. Only users with ROLE_ADMIN or ROLE_USER can create PublicPost
4. Users with ROLE_VISITOR cannot create any post
Note: When we use the word 'create', we mean adding a new post.

Here are the complex rules:
1. A user can edit and delete posts that belongs only to them regardless of the role.
2. A user with ROLE_ADMIN or ROLE_USER can edit and delete PublicPosts.
3. We are required to show all posts in the main Bulletin page
a. ROLE_ADMIN can see all posts
b. ROLE_USER can see Personal and Public posts
c. ROLE_VISITOR can only see Public posts

Let's visualize the rules using tables:

An admin has READ and WRITE access to everything, but only READ access to the Personal Posts.

Admin
Post TypeViewAddEditDelete
Adminxxxx
Personalx
Publicxxxx

A regular user has READ and WRITE access to Personal Posts and Public Posts but only READ access to Admin Posts.

User
Post TypeViewAddEditDelete
Admin



Personalxxxx
Publicxxxx

A visitor can only read Admin and Public Posts but no access of whatsoever in the Personal Posts section.

Visitor
Post TypeViewAddEditDelete
Admin



Personal
Publicx



The main problem:
If we focus on the simple rules, the solution looks easy. Just configure a simple http tag with a couple of intercept-url declarations. Here's how we may tackle this problem:

Admin Posts
<security:intercept-url pattern="/krams/admin/view" access="hasRole('ROLE_ADMIN')"/>
<security:intercept-url pattern="/krams/admin/add" access="hasRole('ROLE_ADMIN')"/>
<security:intercept-url pattern="/krams/admin/edit" access="hasRole('ROLE_ADMIN')"/>
<security:intercept-url pattern="/krams/admin/delete" access="hasRole('ROLE_ADMIN')"/>

Personal Posts
<security:intercept-url pattern="/krams/personal/view" access="hasRole('ROLE_ADMIN') or hasRole('ROLE_USER')"/>
<security:intercept-url pattern="/krams/personal/add" access="hasRole('ROLE_USER')"/>
<security:intercept-url pattern="/krams/personal/edit" access="hasRole('ROLE_USER')"/>
<security:intercept-url pattern="/krams/personal/delete" access="hasRole('ROLE_USER')"/>

Public Posts
<security:intercept-url pattern="/krams/public/view" access="hasRole('ROLE_ADMIN') or hasRole('ROLE_USER') or hasRole('ROLE_VISITOR')"/>
<security:intercept-url pattern="/krams/public/add" access="hasRole('ROLE_ADMIN') or hasRole('ROLE_USER')"/>
<security:intercept-url pattern="/krams/public/edit" access="hasRole('ROLE_ADMIN') or hasRole('ROLE_USER')"/>
<security:intercept-url pattern="/krams/public/delete" access="hasRole('ROLE_ADMIN') or hasRole('ROLE_USER')"/>

However if we consider the complex rules, the intercept-url is unable to cope with the complex rules. Why? Because intercept-url is meant to secure at the URL-level. The complex rules are operating at the domain level.

The solution is to use ACL at the object level and intercept-url at the URL-level.

The ACL Database

We'll start our multi-part tutorial by creating a new MySQL database named acl. This database will contain our access control list. It's composed of four tables:
acl_class
acl_sid
acl_object_identity
acl_entry


Let's create our database. Here are the steps:

1. Run MySQL.
Note: I'm using phpmyadmin to manage my MySQL database.

2. Create a new database named acl

3. Import the following SQL script to create the tables:

acl_structure_mysql.sql
-- phpMyAdmin SQL Dump
-- version 3.2.4
-- http://www.phpmyadmin.net
--
-- Host: localhost
-- Generation Time: Jan 26, 2011 at 04:34 PM
-- Server version: 5.1.41
-- PHP Version: 5.3.1

SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";

--
-- Database: `acl`
--

-- --------------------------------------------------------

--
-- Table structure for table `acl_sid`
--

CREATE TABLE IF NOT EXISTS `acl_sid` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `principal` tinyint(1) NOT NULL,
  `sid` varchar(100) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_uk_1` (`sid`,`principal`)
) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=4 ;

-- --------------------------------------------------------

--
-- Table structure for table `acl_class`
--

CREATE TABLE IF NOT EXISTS `acl_class` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `class` varchar(255) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_uk_2` (`class`)
) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=4 ;

-- --------------------------------------------------------

--
-- Table structure for table `acl_entry`
--

CREATE TABLE IF NOT EXISTS `acl_entry` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `acl_object_identity` bigint(20) NOT NULL,
  `ace_order` int(11) NOT NULL,
  `sid` bigint(20) NOT NULL,
  `mask` int(11) NOT NULL,
  `granting` tinyint(1) NOT NULL,
  `audit_success` tinyint(1) NOT NULL,
  `audit_failure` tinyint(1) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_uk_4` (`acl_object_identity`,`ace_order`),
  KEY `foreign_fk_5` (`sid`)
) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=43 ;

-- --------------------------------------------------------

--
-- Table structure for table `acl_object_identity`
--

CREATE TABLE IF NOT EXISTS `acl_object_identity` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `object_id_class` bigint(20) NOT NULL,
  `object_id_identity` bigint(20) NOT NULL,
  `parent_object` bigint(20) DEFAULT NULL,
  `owner_sid` bigint(20) DEFAULT NULL,
  `entries_inheriting` tinyint(1) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_uk_3` (`object_id_class`,`object_id_identity`),
  KEY `foreign_fk_1` (`parent_object`),
  KEY `foreign_fk_3` (`owner_sid`)
) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=10 ;

-- --------------------------------------------------------

--
-- Constraints for dumped tables
--

--
-- Constraints for table `acl_entry`
--
ALTER TABLE `acl_entry`
  ADD CONSTRAINT `foreign_fk_4` FOREIGN KEY (`acl_object_identity`) REFERENCES `acl_object_identity` (`id`),
  ADD CONSTRAINT `foreign_fk_5` FOREIGN KEY (`sid`) REFERENCES `acl_sid` (`id`);

--
-- Constraints for table `acl_object_identity`
--
ALTER TABLE `acl_object_identity`
  ADD CONSTRAINT `foreign_fk_1` FOREIGN KEY (`parent_object`) REFERENCES `acl_object_identity` (`id`),
  ADD CONSTRAINT `foreign_fk_2` FOREIGN KEY (`object_id_class`) REFERENCES `acl_class` (`id`),
  ADD CONSTRAINT `foreign_fk_3` FOREIGN KEY (`owner_sid`) REFERENCES `acl_sid` (`id`);

After importing the SQL script, you should have the following tables:

4. Import the following SQL script to populate the tables with data:

acl_data_mysql.sql
-- phpMyAdmin SQL Dump
-- version 3.2.4
-- http://www.phpmyadmin.net
--
-- Host: localhost
-- Generation Time: Jan 24, 2011 at 01:28 AM
-- Server version: 5.1.41
-- PHP Version: 5.3.1

SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";

--
-- Database: `acl`
--

--
-- Dumping data for table `acl_sid`
--

INSERT INTO `acl_sid` (`id`, `principal`, `sid`) VALUES
(1, 1, 'john'),
(2, 1, 'jane'),
(3, 1, 'mike');

--
-- Dumping data for table `acl_class`
--

INSERT INTO `acl_class` (`id`, `class`) VALUES
(1, 'org.krams.tutorial.domain.AdminPost'),
(2, 'org.krams.tutorial.domain.PersonalPost'),
(3, 'org.krams.tutorial.domain.PublicPost');

--
-- Dumping data for table `acl_object_identity`
--

INSERT INTO `acl_object_identity` (`id`, `object_id_class`, `object_id_identity`, `parent_object`, `owner_sid`, `entries_inheriting`) VALUES
(1, 1, 1, NULL, 1, 0),
(2, 1, 2, NULL, 1, 0),
(3, 1, 3, NULL, 1, 0),
(4, 2, 1, NULL, 1, 0),
(5, 2, 2, NULL, 1, 0),
(6, 2, 3, NULL, 1, 0),
(7, 3, 1, NULL, 1, 0),
(8, 3, 2, NULL, 1, 0),
(9, 3, 3, NULL, 1, 0);

--
-- Dumping data for table `acl_entry`
--

INSERT INTO `acl_entry` (`id`, `acl_object_identity`, `ace_order`, `sid`, `mask`, `granting`, `audit_success`, `audit_failure`) VALUES
(1, 1, 1, 1, 1, 1, 1, 1),
(2, 2, 1, 1, 1, 1, 1, 1),
(3, 3, 1, 1, 1, 1, 1, 1),
(4, 1, 2, 1, 2, 1, 1, 1),
(5, 2, 2, 1, 2, 1, 1, 1),
(6, 3, 2, 1, 2, 1, 1, 1),
(7, 4, 1, 1, 1, 1, 1, 1),
(8, 5, 1, 1, 1, 1, 1, 1),
(9, 6, 1, 1, 1, 1, 1, 1),
(10, 7, 1, 1, 1, 1, 1, 1),
(11, 8, 1, 1, 1, 1, 1, 1),
(12, 9, 1, 1, 1, 1, 1, 1),
(13, 7, 2, 1, 2, 1, 1, 1),
(14, 8, 2, 1, 2, 1, 1, 1),
(15, 9, 2, 1, 2, 1, 1, 1),
(28, 4, 3, 2, 1, 1, 1, 1),
(29, 5, 3, 2, 1, 1, 1, 1),
(30, 6, 3, 2, 1, 1, 1, 1),
(31, 4, 4, 2, 2, 1, 1, 1),
(32, 5, 4, 2, 2, 1, 1, 1),
(33, 6, 4, 2, 2, 1, 1, 1),
(34, 7, 3, 2, 1, 1, 1, 1),
(35, 8, 3, 2, 1, 1, 1, 1),
(36, 9, 3, 2, 1, 1, 1, 1),
(37, 7, 4, 2, 2, 1, 1, 1),
(38, 8, 4, 2, 2, 1, 1, 1),
(39, 9, 4, 2, 2, 1, 1, 1),
(40, 7, 5, 3, 1, 1, 1, 1),
(41, 8, 5, 3, 1, 1, 1, 1),
(42, 9, 5, 3, 1, 1, 1, 1);

Verify that the tables had been populated with data:
- acl_class should contain 3 records.
- acl_sid should contain 3 records.
- acl_object_identity should contain 9 records.
- acl_entry should contain 30 records.

Table Definitions

So far what we've done is create a new database named acl and add four tables:
acl_class
acl_sid
acl_object_identity
acl_entry
But what are these tables exacly?

acl_class
The table acl_class stores the fully qualified name of domain objects. It is made up of the package name and class name of the object.

In the table below we have declared three fully qualified names that pertain to our three domain objects:

FieldDescription
idThe primary key
classThe fully qualified name of the domain object

acl_sid
The table acl_sid stores the name of the users which can be a principal (like usernames john, james, mark) or an authority (like roles ROLE_ADMIN, ROLE USER, ROLE_ANYONE).

In the table below we have declared three sid objects:

FieldDescription
idThe primary key
principalA flag to indicate if the sid field is a username or a role
sidThe actual username (ie. john) or role (ie. ROLE_ADMIN)

acl_object_identity
The table acl_object_identity stores the actual identities of the domain objects. The identities are referenced via a unique id which is retrieved from another database: the Bulletin database.


FieldDescription
idThe primary key
object_id_classRefers to the id field in the acl_class. This is a reference to the fully qualified name of the class
object_id_identityRefers to the primary id of the domain object. The id is assigned from another database: the Bulletin database (See the Bulletin Database below). Every domain object in the application needs to have a unique id.
parent_objectRefers to the id of the parent object if existing
owner_sidRefers to the id field in the acl_sid. This is a reference to the username or role
entries_inheritingA flag to indicate whether the object has inherited entries

acl_entry
The table acl_entry stores the actual permissions assigned for each user and domain object.


FieldDescription
idThe primary key
acl_object_identityRefers to the id field in the acl_object_identity table
ace_orderRefers to the ordering of the access control entries
sidRefers to the id field in the acl_sid table
maskA bitwise mask to indicate the permissions. A value of 1 is equivalent to READ permission, 2 for WRITE, and so forth.
grantingA flag to indicate whether the mask should be interpreted as granting access or deny access
audit_successA flag to indicate whether to audit a successful permission
audit_failureA flag to indicate whether to audit a failed permission

The Bulletin Database

We've finished setting up the ACL database. Now it's time to setup the application's database: the bulletin database.

The bulletin database contains the actual posts from various users. It contains three tables:

Let's create this database. Here are the steps:

1. Run MySQL
Note: I'm using phpmyadmin to manage my MySQL database

2. Create a new database named bulletin

3. Import the following SQL script to create the tables and populate them with data automatically:

bulletin_mysql.sql
-- phpMyAdmin SQL Dump
-- version 3.2.4
-- http://www.phpmyadmin.net
--
-- Host: localhost
-- Generation Time: Jan 23, 2011 at 02:41 PM
-- Server version: 5.1.41
-- PHP Version: 5.3.1

SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;

--
-- Database: `bulletin`
--

-- --------------------------------------------------------

--
-- Table structure for table `admin_post`
--

CREATE TABLE IF NOT EXISTS `admin_post` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `date` datetime NOT NULL,
  `message` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=4 ;

--
-- Dumping data for table `admin_post`
--

INSERT INTO `admin_post` (`id`, `date`, `message`) VALUES
(1, '2011-01-03 21:37:58', 'Custom post #1 from admin'),
(2, '2011-01-04 21:38:39', 'Custom post #2 from admin'),
(3, '2011-01-05 21:39:37', 'Custom post #3 from admin');

-- --------------------------------------------------------

--
-- Table structure for table `personal_post`
--

CREATE TABLE IF NOT EXISTS `personal_post` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `date` datetime NOT NULL,
  `message` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=4 ;

--
-- Dumping data for table `personal_post`
--

INSERT INTO `personal_post` (`id`, `date`, `message`) VALUES
(1, '2011-01-06 21:40:02', 'Custom post #1 from user'),
(2, '2011-01-07 21:40:13', 'Custom post #2 from user'),
(3, '2011-01-08 21:40:34', 'Custom post #3 from user');

-- --------------------------------------------------------

--
-- Table structure for table `public_post`
--

CREATE TABLE IF NOT EXISTS `public_post` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `date` datetime NOT NULL,
  `message` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=4 ;

--
-- Dumping data for table `public_post`
--

INSERT INTO `public_post` (`id`, `date`, `message`) VALUES
(1, '2011-01-10 21:40:44', 'Custom post #1 from public'),
(2, '2011-01-11 21:40:48', 'Custom post #2 from public'),
(3, '2011-01-12 21:41:08', 'Custom post #3 from public');

4. After importing the SQL script, verify that you have the following tables and data:

AdminPost

PersonalPost

PublicPost

Reminder

Remember the object_id_identity field from the acl_object_identity table? The value of object_id_identity field is derived from the actual value of the id field in the bulletin database.

Conclusion

We have completed the database setup both for the ACL and the Bulletin database. We've also explained the meaning behind the tables and the corresponding fields. Note we haven't touch anything specific to Spring Security, Spring MVC, or even Java yet. Our next task is to setup the Spring Security configuration.

Proceed to Part 2: Spring Security Configuration
StumpleUpon DiggIt! Del.icio.us Blinklist Yahoo Furl Technorati Simpy Spurl Reddit Google I'm reading: Spring Security 3: Full ACL Tutorial (Part 1) ~ Twitter FaceBook

Subscribe by reader Subscribe by email Share