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 Type | View | Add | Edit | Delete |
Admin | x | x | x | x |
Personal | x | | | |
Public | x | x | x | x |
A regular user has READ and WRITE access to Personal Posts and Public Posts but only READ access to
Admin Posts.
User
Post Type | View | Add | Edit | Delete |
Admin |
|
|
|
|
Personal | x | x | x | x |
Public | x | x | x | x |
A visitor can only read Admin and Public Posts but no access of whatsoever in the Personal Posts section.
Visitor
Post Type | View | Add | Edit | Delete |
Admin |
|
|
|
|
Personal |
| | | |
Public | x |
|
|
|
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:
Field | Description |
id | The primary key |
class | The 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:
Field | Description |
id | The primary key |
principal | A flag to indicate if the sid field is a username or a role |
sid | The 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.
Field | Description |
id | The primary key |
object_id_class | Refers to the id field in the acl_class. This is a reference to the fully qualified name of the class |
object_id_identity | Refers 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_object | Refers to the id of the parent object if existing |
owner_sid | Refers to the id field in the acl_sid. This is a reference to the username or role |
entries_inheriting | A 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.
Field | Description |
id | The primary key |
acl_object_identity | Refers to the id field in the acl_object_identity table |
ace_order | Refers to the ordering of the access control entries |
sid | Refers to the id field in the acl_sid table |
mask | A bitwise mask to indicate the permissions. A value of 1 is equivalent to READ permission, 2 for WRITE, and so forth. |
granting | A flag to indicate whether the mask should be interpreted as granting access or deny access |
audit_success | A flag to indicate whether to audit a successful permission |
audit_failure | A 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
Subscribe by reader
Subscribe by email
Share