Sunday, January 30, 2011

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

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