Simple web application with Spring Security: Part 11

In this part we are going to add the the Access Control List (ACL) part of spring security so we can add security down to the domain object level.

Add dependencies to lib folder

  1. spring-security-acl-2.0.4.jar
  2. echcache-1.4.1.jar
  3. backport-util-concurrent-3.1.jar
  4. jsr107cache-1.0.jar

All of these are available in the lib folder of the deploy spring security example for contacts.

Create ACL Schema and Data Populator

The first step in doing this is to update our HsqldbSchemaAndDataPopulator.java file to create the required ACL schema as specified in spring security documentation

HsqldbSchemaAndDataPopulator.java

package com.heraclitus.service;

import javax.sql.DataSource;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.Authentication;
import org.springframework.security.GrantedAuthority;
import org.springframework.security.GrantedAuthorityImpl;
import org.springframework.security.acls.MutableAcl;
import org.springframework.security.acls.MutableAclService;
import org.springframework.security.acls.Permission;
import org.springframework.security.acls.domain.AclImpl;
import org.springframework.security.acls.domain.BasePermission;
import org.springframework.security.acls.objectidentity.ObjectIdentity;
import org.springframework.security.acls.objectidentity.ObjectIdentityImpl;
import org.springframework.security.acls.sid.PrincipalSid;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.providers.UsernamePasswordAuthenticationToken;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.Assert;

import com.heraclitus.domain.ProjectImpl;

/**
 * I am responsible for populating the configured datasource
 */
public class HsqldbSchemaAndDataPopulator implements InitializingBean {

    private MutableAclService mutableAclService;
    private JdbcTemplate template;
    private TransactionTemplate tt;

    /**
     *
     */
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(mutableAclService, "mutableAclService required");
        Assert.notNull(template, "dataSource required");
        Assert.notNull(tt, "platformTransactionManager required");

        // create schema to allow Spring Security ACL approach.
        template
                .execute("CREATE TABLE ACL_SID("
                        + "ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 100) NOT NULL PRIMARY KEY,"
                        + "PRINCIPAL BOOLEAN NOT NULL,"
                        + "SID VARCHAR_IGNORECASE(100) NOT NULL,"
                        + "CONSTRAINT UNIQUE_UK_1 UNIQUE(SID,PRINCIPAL));");

        template
                .execute("CREATE TABLE ACL_CLASS("
                        + "ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 100) NOT NULL PRIMARY KEY,"
                        + "CLASS VARCHAR_IGNORECASE(100) NOT NULL,"
                        + "CONSTRAINT UNIQUE_UK_2 UNIQUE(CLASS));");

        template
                .execute("CREATE TABLE ACL_OBJECT_IDENTITY("
                        + "ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 100) NOT NULL PRIMARY KEY,"
                        + "OBJECT_ID_CLASS BIGINT NOT NULL,"
                        + "OBJECT_ID_IDENTITY BIGINT NOT NULL,"
                        + "PARENT_OBJECT BIGINT,"
                        + "OWNER_SID BIGINT,"
                        + "ENTRIES_INHERITING BOOLEAN NOT NULL,"
                        + "CONSTRAINT UNIQUE_UK_3 UNIQUE(OBJECT_ID_CLASS,OBJECT_ID_IDENTITY),"
                        + "CONSTRAINT FOREIGN_FK_1 FOREIGN KEY(PARENT_OBJECT)REFERENCES ACL_OBJECT_IDENTITY(ID),"
                        + "CONSTRAINT FOREIGN_FK_2 FOREIGN KEY(OBJECT_ID_CLASS)REFERENCES ACL_CLASS(ID),"
                        + "CONSTRAINT FOREIGN_FK_3 FOREIGN KEY(OWNER_SID)REFERENCES ACL_SID(ID));");

        template
                .execute("CREATE TABLE ACL_ENTRY("
                        + "ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 100) NOT NULL PRIMARY KEY,"
                        + "ACL_OBJECT_IDENTITY BIGINT NOT NULL,"
                        + "ACE_ORDER INT NOT NULL,"
                        + "SID BIGINT NOT NULL,"
                        + "MASK INTEGER NOT NULL,"
                        + "GRANTING BOOLEAN NOT NULL,"
                        + "AUDIT_SUCCESS BOOLEAN NOT NULL,"
                        + "AUDIT_FAILURE BOOLEAN NOT NULL,"
                        + "CONSTRAINT UNIQUE_UK_4 UNIQUE(ACL_OBJECT_IDENTITY,ACE_ORDER),"
                        + "CONSTRAINT FOREIGN_FK_4 FOREIGN KEY(ACL_OBJECT_IDENTITY) REFERENCES ACL_OBJECT_IDENTITY(ID),"
                        + "CONSTRAINT FOREIGN_FK_5 FOREIGN KEY(SID) REFERENCES ACL_SID(ID));");

        // add tables to represent admin core-domain instances.
        template
                .execute("CREATE TABLE USERS(USERNAME VARCHAR_IGNORECASE(50) NOT NULL PRIMARY KEY,"
                        + "PASSWORD VARCHAR_IGNORECASE(50) NOT NULL,"
                        + "ENABLED BOOLEAN NOT NULL);");
        template
                .execute("CREATE TABLE AUTHORITIES(USERNAME VARCHAR_IGNORECASE(50) NOT NULL,AUTHORITY VARCHAR_IGNORECASE(50) NOT NULL,CONSTRAINT FK_AUTHORITIES_USERS FOREIGN KEY(USERNAME) REFERENCES USERS(USERNAME));");
        template
                .execute("CREATE UNIQUE INDEX IX_AUTH_USERNAME ON AUTHORITIES(USERNAME,AUTHORITY);");

        // add tables to represent bug tracking domain instances.
        // TODO - add project start and end date
        template
                .execute("CREATE TABLE PROJECTS(ID BIGINT NOT NULL PRIMARY KEY, NAME VARCHAR_IGNORECASE(50) NOT NULL, DESCRIPTION VARCHAR_IGNORECASE(200) NOT NULL);");

        // insert data here
        template
                .execute("INSERT INTO USERS VALUES('disabled','disabled',FALSE);");
        template.execute("INSERT INTO USERS VALUES('admin','admin',TRUE);");
        template
                .execute("INSERT INTO USERS VALUES('username','password',TRUE);");
        template.execute("INSERT INTO USERS VALUES('test','test',TRUE);");

        template
                .execute("INSERT INTO AUTHORITIES VALUES('admin','ROLE_USER');");
        template
                .execute("INSERT INTO AUTHORITIES VALUES('admin','ROLE_ADMIN');");

        template
                .execute("INSERT INTO AUTHORITIES VALUES('username','ROLE_USER');");

        template.execute("INSERT INTO AUTHORITIES VALUES('test','ROLE_USER');");

        template
                .execute("INSERT INTO projects VALUES (1, 'Test Project', 'A description not longer than 200 chars of what project is.');");
        template
                .execute("INSERT INTO projects VALUES (2, 'Test Project 2', 'Smaller description of project here.');");

        // Set a user account that will initially own all the created data
        final Authentication authRequest = new UsernamePasswordAuthenticationToken(
                "admin", "admin",
                new GrantedAuthority[] { new GrantedAuthorityImpl(
                        "ROLE_IGNORED") });
        SecurityContextHolder.getContext().setAuthentication(authRequest);

        // Now for the ACL stuff
        createObjectIdentityEntryFor(ProjectImpl.class, Long.valueOf(1));
        createObjectIdentityEntryFor(ProjectImpl.class, Long.valueOf(2));

        grantPermissions(ProjectImpl.class, Long.valueOf(1), "admin",
                BasePermission.ADMINISTRATION);
        grantPermissions(ProjectImpl.class, Long.valueOf(1), "username",
                BasePermission.READ);

        grantPermissions(ProjectImpl.class, Long.valueOf(2), "admin",
                BasePermission.ADMINISTRATION);
        grantPermissions(ProjectImpl.class, Long.valueOf(2), "username",
                BasePermission.READ);

        SecurityContextHolder.clearContext();
    }

    public void setDataSource(final DataSource dataSource) {
        this.template = new JdbcTemplate(dataSource);
    }

    public void setMutableAclService(final MutableAclService mutableAclService) {
        this.mutableAclService = mutableAclService;
    }

    public void setPlatformTransactionManager(
            final PlatformTransactionManager platformTransactionManager) {
        this.tt = new TransactionTemplate(platformTransactionManager);
    }

    /*
     * Create acl_object_identity rows (and also acl_class rows as needed)
     */
    private void createObjectIdentityEntryFor(final Class<?> c,
            final Long entityId) {
        final ObjectIdentity objectIdentity = new ObjectIdentityImpl(c,
                entityId);
        tt.execute(new TransactionCallback() {
            public Object doInTransaction(final TransactionStatus arg0) {
                mutableAclService.createAcl(objectIdentity);

                return null;
            }
        });
    }

    private void grantPermissions(final Class<?> entityType,
            final Long entityId, final String recipientUsername,
            final Permission permission) {
        final AclImpl acl = (AclImpl) mutableAclService
                .readAclById(new ObjectIdentityImpl(entityType, entityId));
        acl.insertAce(acl.getEntries().length, permission, new PrincipalSid(
                recipientUsername), true);
        updateAclInTransaction(acl);
    }

    private void updateAclInTransaction(final MutableAcl acl) {
        tt.execute(new TransactionCallback() {
            public Object doInTransaction(final TransactionStatus arg0) {
                mutableAclService.updateAcl(acl);

                return null;
            }
        });
    }
}

Next we need update the dataAccessContext.xml file to register the collaborators used within HsqldbSchemaAndDataPopulator.java:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

	<!-- business stuff below -->
	<bean id="dataSource"
		class="org.springframework.jdbc.datasource.DriverManagerDataSource">
		<property name="driverClassName" value="org.hsqldb.jdbcDriver" />
		<property name="url" value="jdbc:hsqldb:mem:test" />
		<property name="username" value="sa" />
		<property name="password" value="" />
	</bean>

	<bean id="transactionManager"
		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource" />
	</bean>

	<!--
		========= ACCESS CONTROL LIST LOOKUP MANAGER DEFINITIONS =========
	-->

	<bean id="aclCache"
		class="org.springframework.security.acls.jdbc.EhCacheBasedAclCache">
		<constructor-arg>
			<bean class="org.springframework.cache.ehcache.EhCacheFactoryBean">
				<property name="cacheManager">
					<bean class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" />
				</property>
				<property name="cacheName" value="aclCache" />
			</bean>
		</constructor-arg>
	</bean>

	<bean id="lookupStrategy"
		class="org.springframework.security.acls.jdbc.BasicLookupStrategy">
		<constructor-arg ref="dataSource" />
		<constructor-arg ref="aclCache" />
		<constructor-arg>
			<bean
				class="org.springframework.security.acls.domain.AclAuthorizationStrategyImpl">
				<constructor-arg>
					<list>
						<!-- authority for taking ownership -->
						<bean class="org.springframework.security.GrantedAuthorityImpl">
							<constructor-arg value="ROLE_ADMIN" />
						</bean>
						<!-- authority to modify auditing -->
						<bean class="org.springframework.security.GrantedAuthorityImpl">
							<constructor-arg value="ROLE_ADMIN" />
						</bean>
						<!-- authority to make general changes -->
						<bean class="org.springframework.security.GrantedAuthorityImpl">
							<constructor-arg value="ROLE_ADMIN" />
						</bean>
					</list>
				</constructor-arg>
			</bean>
		</constructor-arg>
		<constructor-arg>
			<bean class="org.springframework.security.acls.domain.ConsoleAuditLogger" />
		</constructor-arg>
	</bean>

	<bean id="aclService"
		class="org.springframework.security.acls.jdbc.JdbcMutableAclService">
		<constructor-arg ref="dataSource" />
		<constructor-arg ref="lookupStrategy" />
		<constructor-arg ref="aclCache" />
	</bean>

	<bean id="dataSourcePopulator" class="com.heraclitus.service.HsqldbSchemaAndDataPopulator">
		<property name="dataSource" ref="dataSource" />
		<property name="mutableAclService" ref="aclService" />
        <property name="platformTransactionManager" ref="transactionManager" />
	</bean>
</beans>

Build, Deploy, Test

Using this alone, you should build, and deploy to tomcat. Verify no functionality is broken by these changes.

We now have the basics for setting up our domain level security. In the next part we will add components to allow you to see the data within the ACL schema tables and explain how they work.

Getting the code

The code for this part is tagged and available for viewing online at: http://code.google.com/p/spring-security-series/source/browse/#svn/tags/SpringSecuritySeriesWAR-Part11

SVN Url: https://spring-security-series.googlecode.com/svn/tags/SpringSecuritySeriesWAR-Part11

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


%d bloggers like this: