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

10 comments:

  1. But when you try to add a message it doesn't show up on the view page and I can see the message been added to the respective table

    ReplyDelete
    Replies
    1. Use a sample class for post method and, use predefined existing object

      Delete
    2. Can please explain further?

      Delete
  2. @Anonymous, if you check part 4 of the tutorial under the Unexpected Problems section, you'll see that issue has been discussed.

    ReplyDelete
  3. Dear
    Do all java classes and domain object tables need to have id field so as to implement acl using above method.cant we have our own primary key?

    ReplyDelete
  4. CREATE TABLE acl.public.acl_class (
    id BIGINT NOT NULL,
    class VARCHAR(255) NOT NULL,
    CONSTRAINT acl_class_pk PRIMARY KEY (id)
    );


    CREATE TABLE acl.public.acl_entry (
    id BIGINT NOT NULL,
    acl_object_identity BIGINT NOT NULL,
    ace_order INTEGER NOT NULL,
    sid BIGINT NOT NULL,
    mask INTEGER NOT NULL,
    granting BIT NOT NULL,
    audit_success BIT NOT NULL,
    audit_failure BIT NOT NULL,
    CONSTRAINT acl_entry_pk PRIMARY KEY (id)
    );


    CREATE TABLE acl.public.acl_object_identity (
    id BIGINT NOT NULL,
    object_id_class BIGINT NOT NULL,
    object_id_identity BIGINT NOT NULL,
    parent_object BIGINT,
    owner_sid BIGINT NOT NULL,
    entries_inheriting BIT NOT NULL,
    CONSTRAINT acl_object_identity_pk PRIMARY KEY (id)
    );


    CREATE TABLE acl.public.acl_sid (
    id BIGINT NOT NULL,
    principal BIT NOT NULL,
    sid VARCHAR(100) NOT NULL,
    CONSTRAINT acl_sid_pk PRIMARY KEY (id)
    );


    ALTER TABLE acl.public.acl_object_identity ADD CONSTRAINT foreign_fk_2
    FOREIGN KEY (object_id_class)
    REFERENCES acl.public.acl_class (id)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION
    NOT DEFERRABLE;

    ALTER TABLE acl.public.acl_entry ADD CONSTRAINT foreign_fk_4
    FOREIGN KEY (acl_object_identity)
    REFERENCES acl.public.acl_object_identity (id)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION
    NOT DEFERRABLE;

    ALTER TABLE acl.public.acl_entry ADD CONSTRAINT foreign_fk_5
    FOREIGN KEY (sid)
    REFERENCES acl.public.acl_sid (id)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION
    NOT DEFERRABLE;

    ALTER TABLE acl.public.acl_object_identity ADD CONSTRAINT foreign_fk_3
    FOREIGN KEY (owner_sid)
    REFERENCES acl.public.acl_sid (id)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION
    NOT DEFERRABLE;


    for postgres

    ReplyDelete
    Replies
    1. 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');

      Delete
    2. I think if you look at the Spring Security jars, you will find the schema for Postgres as well (including schemas for other databases). Anyway, thank you for sharing this one. I'm sure it will help others reading this guide.

      Delete
  5. There are lots of great choices now when it comes on home security system. You can ask for a professional guide when selecting one that would fit to your needs.
    security consulting

    ReplyDelete