Wednesday, January 19, 2011

Spring Security: Simple ACL using Expression-Based Access Control (Part 2)

In this tutorial we will continue what we had left off from the Spring Security: Simple ACL using Expression-Based Access Control (Part 1). Last time we set-up a simple ACL configuration. We also implemented the PermissionsEvaluator interface and provided our own custom Permissions list. In this part we will start building the MVC section of the application.

Spring MVC Section

We now move to the MVC section of the application.

Domain Objects

First, let's declare our domain objects:
AdminPost.java
PersonalPost.java
PublicPost.java
Here are the class declarations:

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

import java.util.Date;

public class AdminPost {
 private String id;
 private Date date;
 private String message;
 
 public String getId() {
  return id;
 }
 public void setId(String 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;

public class PersonalPost {
 private String id;
 private Date date;
 private String message;
 
 public String getId() {
  return id;
 }
 public void setId(String 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;

public class PublicPost {
 private String id;
 private Date date;
 private String message;
 
 public String getId() {
  return id;
 }
 public void setId(String 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;
 }
}

The Controllers

Next, we declare the controllers:
AdminController.java
PersonalController.java
PublicController.java

Here are the class declarations:

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

import org.apache.log4j.Logger;
import org.krams.tutorial.domain.AdminPost;
import org.krams.tutorial.service.AdminService;
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;
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 AdminService adminService;

    /**
     * Retrieves the Edit page
     */
    @RequestMapping(value = "/edit", method = RequestMethod.GET)
    public String getEditPage(Model model) {
     logger.debug("Received request to view edit page");
    
     // Call service. If true, we have appropriate authority
     if (adminService.edit(new AdminPost()) == 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 >> Edit");
     
     // 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/resultpage.jsp
     return "resultpage";
 }
    
    /**
     * Retrieves the Add page
     */
    @RequestMapping(value = "/add", method = RequestMethod.GET)
    public String getAddPage(Model model) {
     logger.debug("Received request to view add page");
    
     // Call service. If true, we have appropriate authority
     if (adminService.add(new AdminPost()) == true) {
         // Add result to model
         model.addAttribute("result", "Entry has been added 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");
     
     // 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/resultpage.jsp
     return "resultpage";
 }
    
    /**
     * Retrieves the Delete page
     */
    @RequestMapping(value = "/delete", method = RequestMethod.GET)
    public String getDeletePage(Model model) {
     logger.debug("Received request to view delete page");
    
     // Call service. If true, we have appropriate authority
     if (adminService.delete(new AdminPost()) == true) {
         // 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 >> Delete");
     
     // 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/resultpage.jsp
     return "resultpage";
 }
}

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

import org.apache.log4j.Logger;
import org.krams.tutorial.domain.PersonalPost;
import org.krams.tutorial.service.PersonalService;
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;
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 PersonalService personalService;
    
    /**
     * Retrieves the Edit page
     */
    @RequestMapping(value = "/edit", method = RequestMethod.GET)
    public String getEditPage(Model model) {
     logger.debug("Received request to view edit page");
    
     // Call service. If true, we have appropriate authority
     if (personalService.edit(new PersonalPost()) == 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 >> Edit");
     
     // 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/resultpage.jsp
     return "resultpage";
 }
    
    /**
     * Retrieves the Add page
     */
    @RequestMapping(value = "/add", method = RequestMethod.GET)
    public String getAddPage(Model model) {
     logger.debug("Received request to view add page");
    
     // Call service. If true, we have appropriate authority
     if (personalService.add(new PersonalPost()) == true) {
         // Add result to model
         model.addAttribute("result", "Entry has been added 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");
     
     // 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/resultpage.jsp
     return "resultpage";
 }
    
    /**
     * Retrieves the Delete page
     */
    @RequestMapping(value = "/delete", method = RequestMethod.GET)
    public String getDeletePage(Model model) {
     logger.debug("Received request to view delete page");
    
     // Call service. If true, we have appropriate authority
     if (personalService.delete(new PersonalPost()) == true) {
         // 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 >> Delete");
     
     // 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/resultpage.jsp
     return "resultpage";
 }
}

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

import org.apache.log4j.Logger;
import org.krams.tutorial.domain.PublicPost;
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;
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 PublicService publicService;
    
    /**
     * Retrieves the Edit page
     */
    @RequestMapping(value = "/edit", method = RequestMethod.GET)
    public String getEditPage(Model model) {
     logger.debug("Received request to view edit page");
    
     // Call service. If true, we have appropriate authority
     if (publicService.edit(new PublicPost()) == 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 >> Edit");
     
     // 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/resultpage.jsp
     return "resultpage";
 }
    
    /**
     * Retrieves the Add page
     */
    @RequestMapping(value = "/add", method = RequestMethod.GET)
    public String getAddPage(Model model) {
     logger.debug("Received request to view add page");
    
     // Call service. If true, we have appropriate authority
     if (publicService.add(new PublicPost()) == true) {
         // Add result to model
         model.addAttribute("result", "Entry has been added 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");
     
     // 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/resultpage.jsp
     return "resultpage";
 }
    
    /**
     * Retrieves the Delete page
     */
    @RequestMapping(value = "/delete", method = RequestMethod.GET)
    public String getDeletePage(Model model) {
     logger.debug("Received request to view delete page");
    
     // Call service. If true, we have appropriate authority
     if (publicService.delete(new PublicPost()) == true) {
         // 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 >> Delete");
     
     // 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/resultpage.jsp
     return "resultpage";
 }
}

We also declare a fourth controller that displays all records:

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

import javax.annotation.Resource;

import org.apache.log4j.Logger;
import org.krams.tutorial.service.AdminService;
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 authentication related requests
 */
@Controller
@RequestMapping("/all")
public class AllController {
        
 protected static Logger logger = Logger.getLogger("controller");

 @Resource(name="adminService")
 private AdminService adminService;
 
 @Resource(name="personalService")
 private PersonalService personalService;
 
 @Resource(name="publicService")
 private PublicService 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";
 }
}

The Services

Next, we declare the corresponding services:
AdminService.java
PersonService.java
PublicService.java
Here are the class declarations:

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

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.krams.tutorial.domain.AdminPost;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service("adminService")
public class AdminService {

 private Map<String, AdminPost> adminPosts = new HashMap<String, AdminPost>();
 
 public AdminService() {
  // Initiliaze our in-memory HashMap list
  init();
 }

 // filterObject refers to the current object in the collection
 @PostFilter("hasPermission(filterObject, 'READ')")
 public List<adminpost> getAll() {
  // Iterate our HashMap list and convert it to an ArrayList
  List<adminpost> adminList = new ArrayList<adminpost>();
  for (String key: adminPosts.keySet()) {
   adminList.add(adminPosts.get(key));
  }
  // Return our new list
  return adminList;
 }
 
 @PreAuthorize("hasPermission(#post, 'WRITE')")
 public Boolean add(AdminPost post)  {
  // This will return true if it's accessible
  return true;
 }
 
 @PreAuthorize("hasPermission(#post, 'WRITE')")
 public Boolean edit(AdminPost post)  {
  // This will return true if it's accessible
  return true;
 }

 @PreAuthorize("hasPermission(#post, 'WRITE')")
 public Boolean delete(AdminPost post)  {
  // This will return true if it's accessible
  return true;
 }
 
 // Initiliazes an in-memory HashMap list
 private void init() {
  // Create new post
  AdminPost post1 = new AdminPost();
  post1.setId(UUID.randomUUID().toString());
  post1.setDate(new Date());
  post1.setMessage("This is admin's post #1");
  
  // Create new post
  AdminPost post2 = new AdminPost();
  post2.setId(UUID.randomUUID().toString());
  post2.setDate(new Date());
  post2.setMessage("This is admin's post #2");
  
  // Create new post
  AdminPost post3 = new AdminPost();
  post3.setId(UUID.randomUUID().toString());
  post3.setDate(new Date());
  post3.setMessage("This is admin's post #3");
  
  // Add to adminPosts
  adminPosts.put(post1.getId(), post1);
  adminPosts.put(post2.getId(), post2);
  adminPosts.put(post3.getId(), post3);
 }
}

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

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.krams.tutorial.domain.PersonalPost;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service("personalService")
public class PersonalService {

 private Map<String, PersonalPost> personalPosts = new HashMap<String, PersonalPost>();
 
 public PersonalService() {
  // Initiliaze our in-memory HashMap list
  init();
 }

 // filterObject refers to the current object in the collection
 @PostFilter("hasPermission(filterObject, 'READ')")
 public List<personalpost> getAll() {
  // Iterate our HashMap list and convert it to an ArrayList
  List<personalpost> personalList = new ArrayList<personalpost>();
  for (String key: personalPosts.keySet()) {
   personalList.add(personalPosts.get(key));
  }
  // Return our new list
  return personalList;
 }
 
 @PreAuthorize("hasPermission(#post, 'WRITE')")
 public Boolean add(PersonalPost post)  {
  // This will return true if it's accessible
  return true;
 }
 
 @PreAuthorize("hasPermission(#post, 'WRITE')")
 public Boolean edit(PersonalPost post)  {
  // This will return true if it's accessible
  return true;
 }

 @PreAuthorize("hasPermission(#post, 'WRITE')")
 public Boolean delete(PersonalPost post)  {
  // This will return true if it's accessible
  return true;
 }
 
 // Initiliazes an in-memory HashMap list
 private void init() {
  // Create new post
  PersonalPost post1 = new PersonalPost();
  post1.setId(UUID.randomUUID().toString());
  post1.setDate(new Date());
  post1.setMessage("This is personal's post #1");
  
  // Create new post
  PersonalPost post2 = new PersonalPost();
  post2.setId(UUID.randomUUID().toString());
  post2.setDate(new Date());
  post2.setMessage("This is personal's post #2");
  
  // Create new post
  PersonalPost post3 = new PersonalPost();
  post3.setId(UUID.randomUUID().toString());
  post3.setDate(new Date());
  post3.setMessage("This is personal's post #3");
  
  // Add to personalPosts
  personalPosts.put(post1.getId(), post1);
  personalPosts.put(post2.getId(), post2);
  personalPosts.put(post3.getId(), post3);
 }
}

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

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.krams.tutorial.domain.PublicPost;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service("publicService")
public class PublicService {

 private Map<String, PublicPost> publicPosts = new HashMap<String, PublicPost>();
 
 public PublicService() {
  // Initiliaze our in-memory HashMap list
  init();
 }

 // filterObject refers to the current object in the collection
 @PostFilter("hasPermission(filterObject, 'READ')")
 public List<publicpost> getAll() {
  // Iterate our HashMap list and convert it to an ArrayList
  List<publicpost> publicList = new ArrayList<publicpost>();
  for (String key: publicPosts.keySet()) {
   publicList.add(publicPosts.get(key));
  }
  // Return our new list
  return publicList;
 }
 
 @PreAuthorize("hasPermission(#post, 'WRITE')")
 public Boolean add(PublicPost post)  {
  // This will return true if it's accessible
  return true;
 }
 
 @PreAuthorize("hasPermission(#post, 'WRITE')")
 public Boolean edit(PublicPost post)  {
  // This will return true if it's accessible
  return true;
 }

 @PreAuthorize("hasPermission(#post, 'WRITE')")
 public Boolean delete(PublicPost post)  {
  // This will return true if it's accessible
  return true;
 }
 
 // Initiliazes an in-memory HashMap list
 private void init() {
  // Create new post
  PublicPost post1 = new PublicPost();
  post1.setId(UUID.randomUUID().toString());
  post1.setDate(new Date());
  post1.setMessage("This is public's post #1");
  
  // Create new post
  PublicPost post2 = new PublicPost();
  post2.setId(UUID.randomUUID().toString());
  post2.setDate(new Date());
  post2.setMessage("This is public's post #2");
  
  // Create new post
  PublicPost post3 = new PublicPost();
  post3.setId(UUID.randomUUID().toString());
  post3.setDate(new Date());
  post3.setMessage("This is public's post #3");
  
  // Add to publicPosts
  publicPosts.put(post1.getId(), post1);
  publicPosts.put(post2.getId(), post2);
  publicPosts.put(post3.getId(), post3);
 }
}

Observations

Notice the service methods have been annotated with @PreAuthorize and @PostFilter. Also, we're not actually performing any add, edit, or delete actions. Instead we're just returning a Boolean value to test whether the method is accessible.

The following declaration means check the domain object post and see if the current user has WRITE access to this object:
@PreAuthorize("hasPermission(#post, 'WRITE')")
 public Boolean add(PublicPost post)  {
  // This will return true if it's accessible
  return true;
 }

Whereas the following declaration means after returning, check the domain object post and see if the current user has READ access to this object:
@PostFilter("hasPermission(filterObject, 'READ')")
 public List<publicpost> getAll() {
  // Iterate our HashMap list and convert it to an ArrayList
  List<publicpost> publicList = new ArrayList<publicpost>();
  for (String key: publicPosts.keySet()) {
   publicList.add(publicPosts.get(key));
  }
  // Return our new list
  return publicList;
 }

Run the Application

Let's run the application to see the results (To see the remaining XML configuration, please download the source code below):

We'll need to login first. Enter the following URL to login:
http://localhost:8080/spring-security-acl-expression/krams/auth/login

To access the Bulletin Page, enter the following URL:
http://localhost:8080/spring-security-acl-expression/krams/all/view

We will log-in first as an admin using john/admin as the username/password pair.


Next, we'll login as a user using jane/user as the username/password pair.


Then, we'll login as a visitor using mike/visitor as the username/password pair.


Notice the admin can see all posts while the user can see only the Personal and Public posts; whereas the visitor can only see the Public posts. We've also indicated at the top the name of the current user and the associated role.

If any of the three tries to access an unauthorized resource for their role, they will get the following:


This is the benefit of ACL. We're restricting access on the domain level, not just on the URL level. However if we use the normal intercept-url setup, we won't be able to display all three types of posts. It's either we see everything or we get denied. Try clicking on the remaining links, and verify if they are really protected. I'll bet you they are :)

Conclusion

That's it. We've finished our simple ACL application that leverages Spring Security's Expression-Based Access Control. It may look complicated at first but the concepts are really simple. Also, instead of going the heavyweight solution, we opted to use the lightweight solution using the hasPermission() expression and a custom PermissionsEvaluator implementation

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-acl-expression.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

If you want to learn more about Spring MVC and integration with other technologies, feel free to read my other tutorials in the Tutorials section.
StumpleUpon DiggIt! Del.icio.us Blinklist Yahoo Furl Technorati Simpy Spurl Reddit Google I'm reading: Spring Security: Simple ACL using Expression-Based Access Control (Part 2) ~ Twitter FaceBook

Subscribe by reader Subscribe by email Share

Spring Security: Simple ACL using Expression-Based Access Control (Part 1)

In this tutorial we will study how to setup a simple Spring MVC 3 application with ACL-based security using Spring Security 3's Expression-Based Access Control. Our application is a simple Bulletin site where different types of users can read and post messages. It is composed of three sections: Admin Posts, Personal Posts, and Public Posts and three types of users: admin, user, and visitor.

Here's a sample screenshot:


This tutorial assumes you're familiar with setting-up a basic intercept-url based Spring Security application, and Spring MVC application. If you need a review, please read my other tutorials first regarding these topics.

What is Expression-Based Access Control?
Spring Security 3.0 introduced the ability to use Spring EL expressions as an authorization mechanism in addition to the simple use of configuration attributes and access-decision voters which have seen before. Expression-based access control is built on the same architecture but allows complicated boolean logic to be encapsulated in a single expression

Source: http://static.springsource.org/spring-security/site/docs/3.0.x/reference/el-access.html

What is ACL?
Complex applications often will find the need to define access permissions not simply at a web request or method invocation level. Instead, security decisions need to comprise both who (Authentication), where (MethodInvocation) and what (SomeDomainObject). In other words, authorization decisions also need to consider the actual domain object instance subject of a method invocation.

Source: http://static.springsource.org/spring-security/site/docs/3.0.x/reference/domain-acls.html

An access control list (ACL), with respect to a computer file system, is a list of permissions attached to an object. An ACL specifies which users or system processes are granted access to objects, as well as what operations are allowed on given objects. Each entry in a typical ACL specifies a subject and an operation. For instance, if a file has an ACL that contains (Alice, delete), this would give Alice permission to delete the file.

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

Functional Specs

To understand more about ACL, let's compare it against a simple Spring Security intercept-url based configuration. If we have three sections, Admin Posts, Personal Posts, Visitors Posts, we'll probably pattern our configuration similar to the following:

<security:intercept-url pattern="/krams/admin" access="hasRole('ROLE_ADMIN')"/>
<security:intercept-url pattern="/krams/personal" access="hasRole('ROLE_USER')"/>
<security:intercept-url pattern="/krams/public" access="hasRole('ROLE_VISITOR')"/>
In this setup, here's what we can observe:
  • Only admins can access Admin Posts
  • Only users can access Personal Posts
  • Only visitors can access Visitors Posts
That's good if that's the client's requirements. What if we have the following requirements instead:

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

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. See the table below:

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. See the table below:

Visitor
Post TypeViewAddEditDelete
Admin



Personal
Publicx



The Difficulty

To do this using intercept-url you will have to do the following:

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')"/>
This setup works. However there are some problems that we observe:

1. It only works at the Controller level--that is at the URL level.
2. What happens if we have a domain object that doesn't correspond to a particular URL?
3. What if we need to display the same URL that contains our three domain objects together: Admin, Personal, and Public Posts? It's either we get an Access Denied or we see everything.

The Solution

We use ACL. The idea is we put the restriction on the domain object itself. It's similar to the way we access files in the computer. Various users have different READ and WRITE access to certain files. Some have READ access, but some have both READ and WRITE access.

The Application

We'll begin our application by configuring the required Spring Security configuration and the required custom classes. Then in Part 2, we'll build the Spring MVC section.

In Spring Security 3, there's the heavyweight solution of implementing ACL (See Spring Security 3 Reference Chapter 16). And there's also the lightweight solution of implementing ACL through the use of Expression-Based Access Control

To use it, we need to mark our methods with Method Security Expressions (see Chapter 15.3). They are @PreAuthorize, @PreFilter, @PostAuthorize and @PostFilter.

Then we need to use the hasPermission() expression inside these Method Security Expressions. For example:
@PreAuthorize("hasPermission(#post, 'WRITE')")
public Boolean add(PublicPost post) {
...
}

How do we enable hasPermission() and where does it get its permissions?
To use hasPermission() expressions, you have to explicitly configure a PermissionEvaluator

Source: Spring Security 3 Reference 15.3.2 Built-In Expressions

The PermissionEvaluator can be a custom implementation or a default Spring Security implementation. For this tutorial, we will do a custom implementation.

Why custom implementation?

Because we want to stay away from the complexity of setting up an ACL database and to make this tutorial simple to learn. Also, the strength of the PermissionEvaluator interface and Expression-Based Access Control is it allows us to construct our own implementation. And since it's our own implementation, we know how it works.

The Custom Map

We'll declare a simple Map that contains a list of ROLES and OBJECTS. Whenever a user tries to access a protected object, it will be checked against this Map. For each ROLE, we assigned what objects it can owned and what permissions it can have.

We have three domain objects that correspond to an actual object in the application.
org.krams.tutorial.domain.AdminPost
org.krams.tutorial.domain.PersonalPost
org.krams.tutorial.domain.PublicPost
We also have three roles that correspond to the roles declared in the authentication-manager
ROLE_ADMIN
ROLE_USER
ROLE_VISITOR
Here's the configuration. Notice we created a new XML configuration to isolate ACL-related configurations.

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:p="http://www.springframework.org/schema/p" 
 xmlns:util="http://www.springframework.org/schema/util"
 xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
   http://www.springframework.org/schema/util 
   http://www.springframework.org/schema/util/spring-util-3.0.xsd">
    
    <!-- Declare a simple map containing all our roles --> 
    <util:map id="permissionsMap">
     <entry key="ROLE_ADMIN" value-ref="admin"/>
     <entry key="ROLE_USER" value-ref="user"/>
     <entry key="ROLE_VISITOR" value-ref="visitor"/>
 </util:map>
 
 <!-- Declare permissions for Admin
  Contains a map of objects and their associated allowed actions -->
 <bean id="admin" class="org.krams.tutorial.security.Permission" >
  <property name="objects">
   <map>
    <entry key="org.krams.tutorial.domain.AdminPost">
     <list>
      <value>READ</value>
      <value>WRITE</value>
     </list>
    </entry>
    <entry key="org.krams.tutorial.domain.PersonalPost">
     <list>
      <value>READ</value>
     </list>
    </entry>
    <entry key="org.krams.tutorial.domain.PublicPost">
     <list>
      <value>READ</value>
      <value>WRITE</value>
     </list>
    </entry>
   </map>
  </property>
 </bean>
 
 <!-- Declare permissions for User 
  Contains a map of objects and their associated allowed actions -->
 <bean id="user" class="org.krams.tutorial.security.Permission" > 
  <property name="objects">
   <map>
    <entry key="org.krams.tutorial.domain.PersonalPost">
     <list>
      <value>READ</value>
      <value>WRITE</value>
     </list>
    </entry>
    <entry key="org.krams.tutorial.domain.PublicPost">
     <list>
      <value>READ</value>
      <value>WRITE</value>
     </list>
    </entry>
   </map>
  </property> 
 </bean>
 
 <!-- Declare permissions for Visitor 
  Contains a map of objects and their associated allowed actions -->
 <bean id="visitor" class="org.krams.tutorial.security.Permission" > 
  <property name="objects">
   <map>
    <entry key="org.krams.tutorial.domain.PublicPost">
     <list>
      <value>READ</value>
     </list>
    </entry>
   </map>
  </property> 
 </bean>
</beans>
This configuration matches the requirements we laid in the tables earlier. Here are the tables again:

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

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. See the table below:

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. See the table below:

Visitor
Post TypeViewAddEditDelete
Admin



Personal
Publicx



The Custom Permission

To store our objects and allowed actions, we used a custom org.krams.tutorial.security.Permission class.

Permission.java
package org.krams.tutorial.security;

import java.util.List;
import java.util.Map;

/**
 * Contains a map of objects and their associated allowed actions
 */
public class Permission {
 
 /**
  *  A Map containing a list of objects and their corresponding actions
  *  <p>
  *  String: key name of the object
  *  List<String>: a list of permissions
  */
 private Map<String, List<String>> objects;
 
 public Map<String, List<String>> getObjects() {
  return objects;
 }
 public void setObjects(Map<String, List<String>> objects) {
  this.objects = objects;
 }

}
This is a simple class containing a Map and a List.

The Custom PermissionEvaluator

Next, we implement the PermissionEvaluator interface by creating a custom class:

CustomPermissionEvaluator.java
package org.krams.tutorial.security;

import java.io.Serializable;
import java.util.Collection;
import java.util.Map;

import org.apache.log4j.Logger;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;

import javax.annotation.Resource;

/**
 * A custom PermissionEvaluator implementation that uses a Map to
 * check whether a domain Object and access level exists for a particular user. 
 * This also uses RoleHiearchy to retrieve the highest role possible for the user.
 */
public class CustomPermissionEvaluator implements PermissionEvaluator {
        
 protected static Logger logger = Logger.getLogger("security");

 @Resource(name="permissionsMap")
 private Map permissionsMap;
 
 @Resource(name="roleHierarchy")
 private RoleHierarchy roleHierarchy;
 
 /**
  * Evaluates whether the user has permission by delegating to 
  * hasPermission(String role, Object permission, Object domain)
  */
 public boolean hasPermission(Authentication authentication,
   Object targetDomainObject, Object permission) {
  logger.debug("Evaluating expression using hasPermission signature #1");
  
  logger.debug("Retrieving user's highest role");
  String role = getRole(authentication);

  logger.debug("****************");
  logger.debug("role: " + role);
  logger.debug("targetDomainObject: " + targetDomainObject);
  logger.debug("permission: " + permission);
  logger.debug("****************");
 
  // Check the type of object
  logger.debug("User is trying to access the object: " + targetDomainObject);

  logger.debug("Check if user has permission");
  // Delegate to another hasPermission signature
  return hasPermission(role, permission, targetDomainObject);
 }

 /**
  * Another hasPermission signature. We will not implement this.
  */
 public boolean hasPermission(Authentication authentication,
   Serializable targetId, String targetType, Object permission) {
  logger.debug("Evaluating expression using hasPermission signature #2");

  return false;
 }

 /**
  * Retrieves the user's highest role
  */
 private String getRole(Authentication authentication) {
  String highestRole = null;
  
  try {
   Collection auths = roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities());
   for (GrantedAuthority auth: auths) {
    highestRole = auth.getAuthority();
    break;
   }
   logger.debug("Highest role hiearchy: " + roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities()));
   
  } catch (Exception e) {
   logger.debug("No authorities assigned");
  }
  
  return highestRole;
 }
 
 /**
  * Evaluates whether the user has permission
  */
 private Boolean hasPermission(String role, Object permission, Object domain) {
  logger.debug("Check if role exists: " + role);
  if ( permissionsMap.containsKey(role) ) {
   logger.debug("Role exists: " + role);
   
   // Retrieve userPermission object
   Permission userPermission = (Permission) permissionsMap.get(role);
   
   // Check if domain exists in Map
   logger.debug("Check if domain exists: " + domain.getClass().getName());
   if ( userPermission.getObjects().containsKey(domain.getClass().getName())){
    logger.debug("Domain exists: " + domain.getClass().getName());

    // Loop the internal list and see if the class' full name matches
    logger.debug("Check if permission exists: " + permission);
    for (String action: userPermission.getObjects().get(domain.getClass().getName()) ) {
     if (action.equals(permission)) {
      logger.debug("Permission exists: " + action);
      logger.debug("Permission Granted!");
      return true;
     }
    }
   }
  }
  
  // By default, do not give permission
  logger.debug("Permission Denied!");
  return false;
 }
}

Configure Spring Security

Let's use our new classes and enable Spring Security at the same time.

Here's the configuration:

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">

 <!-- To enable Method Security Expressions and custom PermissionEvaluator
  we need to add the following -->
 <security:global-method-security pre-post-annotations="enabled">
  <security:expression-handler ref="expressionHandler" />
 </security:global-method-security>

 <!-- To use hasPermission() expressions, we have to configure a PermissionEvaluator -->
 <!-- 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">
  <property name="permissionEvaluator" ref="customPermissionEvaluator" />
  <property name = "roleHierarchy" ref="roleHierarchy"/>
 </bean>
 
 <!-- Declare a custom PermissionEvaluator interface -->
 <bean class="org.krams.tutorial.security.CustomPermissionEvaluator" id="customPermissionEvaluator"/>
  
 <!-- 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:form-login
    login-page="/krams/auth/login" 
    authentication-failure-url="/krams/auth/login?error=true" 
    default-target-url="/krams/all/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" />
     <security:user name="jane" password="ee11cbb19052e40b07aac0ca060c23ee" authorities="ROLE_USER" />
     <security:user name="mike" password="127870930d65c57ee65fcc47f2170d38" authorities="ROLE_VISITOR" />
   </security:user-service>
 
 <!-- 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>

The key configuration here are the following (the rest are standard Spring Security 3 setup):
<!-- To enable Method Security Expressions and custom PermissionEvaluator
  we need to add the following -->
 <security:global-method-security pre-post-annotations="enabled">
  <security:expression-handler ref="expressionHandler" />
 </security:global-method-security>

 <!-- To use hasPermission() expressions, we have to configure a PermissionEvaluator -->
 <!-- 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">
  <property name="permissionEvaluator" ref="customPermissionEvaluator" />
  <property name = "roleHierarchy" ref="roleHierarchy"/>
 </bean>
 
 <!-- Declare a custom PermissionEvaluator interface -->
 <bean class="org.krams.tutorial.security.CustomPermissionEvaluator" id="customPermissionEvaluator"/>

<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>

Conclusion

We're done with setting-up the Spring Security section. Our next task is to integrate this application with Spring MVC. Visit the following link for the continuation: Spring Security: Simple ACL using Expression-Based Access Control (Part 2)
StumpleUpon DiggIt! Del.icio.us Blinklist Yahoo Furl Technorati Simpy Spurl Reddit Google I'm reading: Spring Security: Simple ACL using Expression-Based Access Control (Part 1) ~ Twitter FaceBook

Subscribe by reader Subscribe by email Share