From 17cc70a3cd5f475d3a2e983f89a285f265f3328a Mon Sep 17 00:00:00 2001 From: Ben Alex Date: Sun, 17 Dec 2006 00:54:13 +0000 Subject: [PATCH] SEC-415: Add document management system ACL sample. --- .classpath | 9 +- .../main/java/sample/dms/AbstractElement.java | 85 +++++++ .../java/sample/dms/DataSourcePopulator.java | 150 +++++++++++ .../src/main/java/sample/dms/Directory.java | 24 ++ .../src/main/java/sample/dms/DocumentDao.java | 39 +++ .../main/java/sample/dms/DocumentDaoImpl.java | 115 +++++++++ .../dms/src/main/java/sample/dms/File.java | 32 +++ .../secured/SecureDataSourcePopulator.java | 88 +++++++ .../sample/dms/secured/SecureDocumentDao.java | 17 ++ .../dms/secured/SecureDocumentDaoImpl.java | 61 +++++ .../applicationContext-dms-insecure.xml | 31 +++ .../applicationContext-dms-secure.xml | 233 ++++++++++++++++++ .../applicationContext-dms-shared.xml | 27 ++ .../src/test/java/DmsIntegrationTests.java | 87 +++++++ .../test/java/SecureDmsIntegrationTests.java | 67 +++++ 15 files changed, 1063 insertions(+), 2 deletions(-) create mode 100755 samples/dms/src/main/java/sample/dms/AbstractElement.java create mode 100755 samples/dms/src/main/java/sample/dms/DataSourcePopulator.java create mode 100755 samples/dms/src/main/java/sample/dms/Directory.java create mode 100755 samples/dms/src/main/java/sample/dms/DocumentDao.java create mode 100755 samples/dms/src/main/java/sample/dms/DocumentDaoImpl.java create mode 100755 samples/dms/src/main/java/sample/dms/File.java create mode 100755 samples/dms/src/main/java/sample/dms/secured/SecureDataSourcePopulator.java create mode 100755 samples/dms/src/main/java/sample/dms/secured/SecureDocumentDao.java create mode 100755 samples/dms/src/main/java/sample/dms/secured/SecureDocumentDaoImpl.java create mode 100755 samples/dms/src/main/resources/applicationContext-dms-insecure.xml create mode 100755 samples/dms/src/main/resources/applicationContext-dms-secure.xml create mode 100755 samples/dms/src/main/resources/applicationContext-dms-shared.xml create mode 100755 samples/dms/src/test/java/DmsIntegrationTests.java create mode 100755 samples/dms/src/test/java/SecureDmsIntegrationTests.java diff --git a/.classpath b/.classpath index 771c31dc30..6787456140 100644 --- a/.classpath +++ b/.classpath @@ -4,6 +4,10 @@ + + + + @@ -52,7 +56,7 @@ - + @@ -64,7 +68,7 @@ - + @@ -79,5 +83,6 @@ + diff --git a/samples/dms/src/main/java/sample/dms/AbstractElement.java b/samples/dms/src/main/java/sample/dms/AbstractElement.java new file mode 100755 index 0000000000..614256134d --- /dev/null +++ b/samples/dms/src/main/java/sample/dms/AbstractElement.java @@ -0,0 +1,85 @@ +package sample.dms; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * @author Ben Alex + * @version $Id$ + * + */ +public abstract class AbstractElement { + /** The name of this token (ie filename or directory segment name */ + private String name; + + /** The parent of this token (ie directory, or null if referring to root) */ + private AbstractElement parent; + + /** The database identifier for this object (null if not persisted) */ + private Long id; + + /** + * Constructor to use to represent a root element. A root element has an id of -1. + */ + protected AbstractElement() { + this.name = "/"; + this.parent = null; + this.id = new Long(-1); + } + + /** + * Constructor to use to represent a non-root element. + * + * @param name name for this element (required, cannot be "/") + * @param parent for this element (required, cannot be null) + */ + protected AbstractElement(String name, AbstractElement parent) { + Assert.hasText(name, "Name required"); + Assert.notNull(parent, "Parent required"); + Assert.notNull(parent.getId(), "The parent must have been saved in order to create a child"); + this.name = name; + this.parent = parent; + } + + public Long getId() { + return id; + } + + /** + * @return the name of this token (never null, although will be "/" if root, otherwise it won't include separators) + */ + public String getName() { + return name; + } + + public AbstractElement getParent() { + return parent; + } + + /** + * @return the fully-qualified name of this element, including any parents + */ + public String getFullName() { + List strings = new ArrayList(); + AbstractElement currentElement = this; + while (currentElement != null) { + strings.add(0, currentElement.getName()); + currentElement = currentElement.getParent(); + } + + StringBuffer sb = new StringBuffer(); + String lastCharacter = null; + for (Iterator i = strings.iterator(); i.hasNext();) { + String token = (String) i.next(); + if (!"/".equals(lastCharacter) && lastCharacter != null) { + sb.append("/"); + } + sb.append(token); + lastCharacter = token.substring(token.length()-1); + } + return sb.toString(); + } +} diff --git a/samples/dms/src/main/java/sample/dms/DataSourcePopulator.java b/samples/dms/src/main/java/sample/dms/DataSourcePopulator.java new file mode 100755 index 0000000000..9f14442c11 --- /dev/null +++ b/samples/dms/src/main/java/sample/dms/DataSourcePopulator.java @@ -0,0 +1,150 @@ +package sample.dms; + +import javax.sql.DataSource; + +import org.acegisecurity.Authentication; +import org.acegisecurity.GrantedAuthority; +import org.acegisecurity.GrantedAuthorityImpl; +import org.acegisecurity.context.SecurityContextHolder; +import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jdbc.core.JdbcTemplate; +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; + + +/** +* Populates the DMS in-memory database with document and ACL information. +* +* @author Ben Alex +* @version $Id$ +*/ +public class DataSourcePopulator implements InitializingBean { + protected static final int LEVEL_NEGATE_READ = 0; + protected static final int LEVEL_GRANT_READ = 1; + protected static final int LEVEL_GRANT_WRITE = 2; + protected static final int LEVEL_GRANT_ADMIN = 3; + protected JdbcTemplate template; + protected DocumentDao documentDao; + protected TransactionTemplate tt; + + public DataSourcePopulator(DataSource dataSource, DocumentDao documentDao, PlatformTransactionManager platformTransactionManager) { + Assert.notNull(dataSource, "DataSource required"); + Assert.notNull(documentDao, "DocumentDao required"); + Assert.notNull(platformTransactionManager, "PlatformTransactionManager required"); + this.template = new JdbcTemplate(dataSource); + this.documentDao = documentDao; + this.tt = new TransactionTemplate(platformTransactionManager); + } + + public void afterPropertiesSet() throws Exception { + // ACL tables + 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));"); + + // Normal authentication tables + 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);"); + + // Document management system business tables + template.execute("CREATE TABLE DIRECTORY(ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 100) NOT NULL PRIMARY KEY, DIRECTORY_NAME VARCHAR_IGNORECASE(50) NOT NULL, PARENT_DIRECTORY_ID BIGINT)"); + template.execute("CREATE TABLE FILE(ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 100) NOT NULL PRIMARY KEY, FILE_NAME VARCHAR_IGNORECASE(50) NOT NULL, CONTENT VARCHAR_IGNORECASE(1024), PARENT_DIRECTORY_ID BIGINT)"); + + // Populate the authentication and role tables + template.execute("INSERT INTO USERS VALUES('marissa','a564de63c2d0da68cf47586ee05984d7',TRUE);"); + template.execute("INSERT INTO USERS VALUES('dianne','65d15fe9156f9c4bbffd98085992a44e',TRUE);"); + template.execute("INSERT INTO USERS VALUES('scott','2b58af6dddbd072ed27ffc86725d7d3a',TRUE);"); + template.execute("INSERT INTO USERS VALUES('peter','22b5c9accc6e1ba628cedc63a72d57f8',FALSE);"); + template.execute("INSERT INTO USERS VALUES('bill','2b58af6dddbd072ed27ffc86725d7d3a',TRUE);"); + template.execute("INSERT INTO USERS VALUES('bob','2b58af6dddbd072ed27ffc86725d7d3a',TRUE);"); + template.execute("INSERT INTO USERS VALUES('jane','2b58af6dddbd072ed27ffc86725d7d3a',TRUE);"); + template.execute("INSERT INTO AUTHORITIES VALUES('marissa','ROLE_USER');"); + template.execute("INSERT INTO AUTHORITIES VALUES('marissa','ROLE_SUPERVISOR');"); + template.execute("INSERT INTO AUTHORITIES VALUES('dianne','ROLE_USER');"); + template.execute("INSERT INTO AUTHORITIES VALUES('scott','ROLE_USER');"); + template.execute("INSERT INTO AUTHORITIES VALUES('peter','ROLE_USER');"); + template.execute("INSERT INTO AUTHORITIES VALUES('bill','ROLE_USER');"); + template.execute("INSERT INTO AUTHORITIES VALUES('bob','ROLE_USER');"); + template.execute("INSERT INTO AUTHORITIES VALUES('jane','ROLE_USER');"); + + // Now create an ACL entry for the root directory + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("marissa", "ignored", new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_IGNORED")})); + tt.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus arg0) { + addPermission(documentDao, Directory.ROOT_DIRECTORY, "ROLE_USER", LEVEL_GRANT_WRITE); + return null; + } + }); + + // Now go off and create some directories and files for our users + createSampleData("marissa", "koala"); + createSampleData("dianne", "emu"); + createSampleData("scott", "wombat"); + } + + /** + * Creates a directory for the user, and a series of sub-directories. The root directory is the parent for the user directory. The sub-directories + * are "confidential" and "shared". The ROLE_USER will be given read and write access to "shared". + */ + private void createSampleData(String username, String password) { + Assert.notNull(documentDao, "DocumentDao required"); + Assert.hasText(username, "Username required"); + + Authentication auth = new UsernamePasswordAuthenticationToken(username, password); + + try { + // Set the SecurityContextHolder ThreadLocal so any subclasses automatically know which user is operating + SecurityContextHolder.getContext().setAuthentication(auth); + + // Create the home directory first + Directory home = new Directory(username, Directory.ROOT_DIRECTORY); + documentDao.create(home); + addPermission(documentDao, home, username, LEVEL_GRANT_ADMIN); + addPermission(documentDao, home, "ROLE_USER", LEVEL_GRANT_READ); + createFiles(documentDao, home); + + // Now create the confidential directory + Directory confid = new Directory("confidential", home); + documentDao.create(confid); + addPermission(documentDao, confid, "ROLE_USER", LEVEL_NEGATE_READ); + createFiles(documentDao, confid); + + // Now create the shared directory + Directory shared = new Directory("shared", home); + documentDao.create(shared); + addPermission(documentDao, shared, "ROLE_USER", LEVEL_GRANT_READ); + addPermission(documentDao, shared, "ROLE_USER", LEVEL_GRANT_WRITE); + createFiles(documentDao, shared); + } finally { + // Clear the SecurityContextHolder ThreadLocal so future calls are guaranteed to be clean + SecurityContextHolder.clearContext(); + } + } + + private void createFiles(DocumentDao documentDao, Directory parent) { + Assert.notNull(documentDao, "DocumentDao required"); + Assert.notNull(parent, "Parent required"); + int countBeforeInsert = documentDao.findElements(parent).length; + for (int i = 0; i < 10; i++) { + File file = new File("file_" + i + ".txt", parent); + documentDao.create(file); + } + Assert.isTrue(countBeforeInsert + 10 == documentDao.findElements(parent).length, "Failed to increase count by 10"); + } + + /** + * Allows subclass to add permissions. + * + * @param documentDao that will presumably offer methods to enable the operation to be completed + * @param element to the subject of the new permissions + * @param recipient to receive permission (if it starts with ROLE_ it is assumed to be a GrantedAuthority, else it is a username) + * @param level based on the static final integer fields on this class + */ + protected void addPermission(DocumentDao documentDao, AbstractElement element, String recipient, int level) {} +} \ No newline at end of file diff --git a/samples/dms/src/main/java/sample/dms/Directory.java b/samples/dms/src/main/java/sample/dms/Directory.java new file mode 100755 index 0000000000..e82c412f61 --- /dev/null +++ b/samples/dms/src/main/java/sample/dms/Directory.java @@ -0,0 +1,24 @@ +package sample.dms; + +/** + * + * @author Ben Alex + * @version $Id$ + * + */ +public class Directory extends AbstractElement { + public static final Directory ROOT_DIRECTORY = new Directory(); + + private Directory() { + super(); + } + + public Directory(String name, Directory parent) { + super(name, parent); + } + + public String toString() { + return "Directory[fullName='" + getFullName() + "'; name='" + getName() + "'; id='" + getId() + "'; parent='" + getParent() + "']"; + } + +} diff --git a/samples/dms/src/main/java/sample/dms/DocumentDao.java b/samples/dms/src/main/java/sample/dms/DocumentDao.java new file mode 100755 index 0000000000..03b1c00eb3 --- /dev/null +++ b/samples/dms/src/main/java/sample/dms/DocumentDao.java @@ -0,0 +1,39 @@ +package sample.dms; + + +/** + * + * @author Ben Alex + * @version $Id$ + * + */ +public interface DocumentDao { + /** + * Creates an entry in the database for the element. + * + * @param element an unsaved element (the "id" will be updated after method is invoked) + */ + public void create(AbstractElement element); + + /** + * Removes a file from the database for the specified element. + * + * @param file the file to remove (cannot be null) + */ + public void delete(File file); + + /** + * Modifies a file in the database. + * + * @param file the file to update (cannot be null) + */ + public void update(File file); + + /** + * Locates elements in the database which appear under the presented directory + * + * @param directory the directory (cannot be null - use {@link Directory#ROOT_DIRECTORY} for root) + * @return zero or more elements in the directory (an empty array may be returned - never null) + */ + public AbstractElement[] findElements(Directory directory); +} diff --git a/samples/dms/src/main/java/sample/dms/DocumentDaoImpl.java b/samples/dms/src/main/java/sample/dms/DocumentDaoImpl.java new file mode 100755 index 0000000000..eece667263 --- /dev/null +++ b/samples/dms/src/main/java/sample/dms/DocumentDaoImpl.java @@ -0,0 +1,115 @@ +package sample.dms; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import org.acegisecurity.util.FieldUtils; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.support.JdbcDaoSupport; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; + +/** + * Basic JDBC implementation of {@link DocumentDao}. + * + * @author Ben Alex + * @version $Id$ + */ +public class DocumentDaoImpl extends JdbcDaoSupport implements DocumentDao { + + private static final String INSERT_INTO_DIRECTORY = "insert into directory(directory_name, parent_directory_id) values (?,?)"; + private static final String INSERT_INTO_FILE = "insert into file(file_name, content, parent_directory_id) values (?,?,?)"; + private static final String SELECT_FROM_DIRECTORY = "select id from directory where parent_directory_id = ?"; + private static final String SELECT_FROM_DIRECTORY_NULL = "select id from directory where parent_directory_id is null"; + private static final String SELECT_FROM_FILE = "select id, file_name, content, parent_directory_id from file where parent_directory_id = ?"; + private static final String SELECT_FROM_DIRECTORY_SINGLE = "select id, directory_name, parent_directory_id from directory where id = ?"; + private static final String DELETE_FROM_FILE = "delete from file where id = ?"; + private static final String UPDATE_FILE = "update file set content = ? where id = ?"; + private static final String SELECT_IDENTITY = "call identity()"; + + private Long obtainPrimaryKey() { + Assert.isTrue(TransactionSynchronizationManager.isSynchronizationActive(), "Transaction must be running"); + return new Long(getJdbcTemplate().queryForLong(SELECT_IDENTITY)); + } + + public void create(AbstractElement element) { + Assert.notNull(element, "Element required"); + Assert.isNull(element.getId(), "Element has previously been saved"); + if (element instanceof Directory) { + Directory directory = (Directory) element; + Long parentId = directory.getParent() == null ? null : directory.getParent().getId(); + getJdbcTemplate().update(INSERT_INTO_DIRECTORY, new Object[] {directory.getName(), parentId}); + FieldUtils.setProtectedFieldValue("id", directory, obtainPrimaryKey()); + } else if (element instanceof File) { + File file = (File) element; + Long parentId = file.getParent() == null ? null : file.getParent().getId(); + getJdbcTemplate().update(INSERT_INTO_FILE, new Object[] {file.getName(), file.getContent(), parentId}); + FieldUtils.setProtectedFieldValue("id", file, obtainPrimaryKey()); + } else { + throw new IllegalArgumentException("Unsupported AbstractElement"); + } + } + + public void delete(File file) { + Assert.notNull(file, "File required"); + Assert.notNull(file.getId(), "File ID required"); + getJdbcTemplate().update(DELETE_FROM_FILE, new Object[] {file.getId()}); + } + + /** Executes recursive SQL as needed to build a full Directory hierarchy of objects */ + private Directory getDirectoryWithImmediateParentPopulated(final Long id) { + return (Directory) getJdbcTemplate().queryForObject(SELECT_FROM_DIRECTORY_SINGLE, new Object[] {id}, new RowMapper() { + public Object mapRow(ResultSet rs, int rowNumber) throws SQLException { + Long parentDirectoryId = new Long(rs.getLong("parent_directory_id")); + Directory parentDirectory = Directory.ROOT_DIRECTORY; + if (parentDirectoryId != null && !parentDirectoryId.equals(new Long(-1))) { + // Need to go and lookup the parent, so do that first + parentDirectory = getDirectoryWithImmediateParentPopulated(parentDirectoryId); + } + Directory directory = new Directory(rs.getString("directory_name"), parentDirectory); + FieldUtils.setProtectedFieldValue("id", directory, new Long(rs.getLong("id"))); + return directory; + } + }); + } + + public AbstractElement[] findElements(Directory directory) { + Assert.notNull(directory, "Directory required (the ID can be null to refer to root)"); + if (directory.getId() == null) { + List directories = getJdbcTemplate().query(SELECT_FROM_DIRECTORY_NULL, new RowMapper() { + public Object mapRow(ResultSet rs, int rowNumber) throws SQLException { + return getDirectoryWithImmediateParentPopulated(new Long(rs.getLong("id"))); + } + }); + return (AbstractElement[]) directories.toArray(new AbstractElement[] {}); + } + List directories = getJdbcTemplate().query(SELECT_FROM_DIRECTORY, new Object[] {directory.getId()}, new RowMapper() { + public Object mapRow(ResultSet rs, int rowNumber) throws SQLException { + return getDirectoryWithImmediateParentPopulated(new Long(rs.getLong("id"))); + } + }); + List files = getJdbcTemplate().query(SELECT_FROM_FILE, new Object[] {directory.getId()}, new RowMapper() { + public Object mapRow(ResultSet rs, int rowNumber) throws SQLException { + Long parentDirectoryId = new Long(rs.getLong("parent_directory_id")); + Directory parentDirectory = null; + if (parentDirectoryId != null) { + parentDirectory = getDirectoryWithImmediateParentPopulated(parentDirectoryId); + } + File file = new File(rs.getString("file_name"), parentDirectory); + FieldUtils.setProtectedFieldValue("id", file, new Long(rs.getLong("id"))); + return file; + } + }); + // Add the File elements after the Directory elements + directories.addAll(files); + return (AbstractElement[]) directories.toArray(new AbstractElement[] {}); + } + + public void update(File file) { + Assert.notNull(file, "File required"); + Assert.notNull(file.getId(), "File ID required"); + getJdbcTemplate().update(UPDATE_FILE, new Object[] {file.getContent(), file.getId()}); + } + +} diff --git a/samples/dms/src/main/java/sample/dms/File.java b/samples/dms/src/main/java/sample/dms/File.java new file mode 100755 index 0000000000..f5e664a50e --- /dev/null +++ b/samples/dms/src/main/java/sample/dms/File.java @@ -0,0 +1,32 @@ +package sample.dms; + +import org.springframework.util.Assert; + + +/** + * + * @author Ben Alex + * @version $Id$ + */ +public class File extends AbstractElement { + /** Content of the file, which can be null */ + private String content; + + public File(String name, Directory parent) { + super(name, parent); + Assert.isTrue(!parent.equals(Directory.ROOT_DIRECTORY), "Cannot insert File into root directory"); + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String toString() { + return "File[fullName='" + getFullName() + "'; name='" + getName() + "'; id='" + getId() + "'; content=" + getContent() + "'; parent='" + getParent() + "']"; + } + +} diff --git a/samples/dms/src/main/java/sample/dms/secured/SecureDataSourcePopulator.java b/samples/dms/src/main/java/sample/dms/secured/SecureDataSourcePopulator.java new file mode 100755 index 0000000000..0187892d6d --- /dev/null +++ b/samples/dms/src/main/java/sample/dms/secured/SecureDataSourcePopulator.java @@ -0,0 +1,88 @@ +package sample.dms.secured; + +import javax.sql.DataSource; + +import org.acegisecurity.acls.MutableAcl; +import org.acegisecurity.acls.MutableAclService; +import org.acegisecurity.acls.NotFoundException; +import org.acegisecurity.acls.Permission; +import org.acegisecurity.acls.domain.BasePermission; +import org.acegisecurity.acls.objectidentity.ObjectIdentity; +import org.acegisecurity.acls.objectidentity.ObjectIdentityImpl; +import org.acegisecurity.acls.sid.GrantedAuthoritySid; +import org.acegisecurity.acls.sid.PrincipalSid; +import org.acegisecurity.acls.sid.Sid; +import org.acegisecurity.context.SecurityContextHolder; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.util.Assert; + +import sample.dms.AbstractElement; +import sample.dms.DataSourcePopulator; +import sample.dms.DocumentDao; + +public class SecureDataSourcePopulator extends DataSourcePopulator { + + private MutableAclService aclService; + + public SecureDataSourcePopulator(DataSource dataSource, SecureDocumentDao documentDao, PlatformTransactionManager platformTransactionManager, MutableAclService aclService) { + super(dataSource, documentDao, platformTransactionManager); + Assert.notNull(aclService, "MutableAclService required"); + this.aclService = aclService; + } + + protected void addPermission(DocumentDao documentDao, AbstractElement element, String recipient, int level) { + Assert.notNull(documentDao, "DocumentDao required"); + Assert.isInstanceOf(SecureDocumentDao.class, documentDao, "DocumentDao should have been a SecureDocumentDao"); + Assert.notNull(element, "Element required"); + Assert.hasText(recipient, "Recipient required"); + Assert.notNull(SecurityContextHolder.getContext().getAuthentication(), "SecurityContextHolder must contain an Authentication"); + + // We need SecureDocumentDao to assign different permissions + SecureDocumentDao dao = (SecureDocumentDao) documentDao; + + // We need to construct an ACL-specific Sid. Note the prefix contract is defined on the superclass method's JavaDocs + Sid sid = null; + if (recipient.startsWith("ROLE_")) { + sid = new GrantedAuthoritySid(recipient); + } else { + sid = new PrincipalSid(recipient); + } + + // We need to identify the target domain object and create an ObjectIdentity for it + // This works because AbstractElement has a "getId()" method + ObjectIdentity identity = new ObjectIdentityImpl(element); + // ObjectIdentity identity = new ObjectIdentityImpl(element.getClass(), element.getId()); // equivalent + + // Next we need to create a Permission + Permission permission = null; + if (level == LEVEL_NEGATE_READ || level == LEVEL_GRANT_READ) { + permission = BasePermission.READ; + } else if (level == LEVEL_GRANT_WRITE) { + permission = BasePermission.WRITE; + } else if (level == LEVEL_GRANT_ADMIN) { + permission = BasePermission.ADMINISTRATION; + } else { + throw new IllegalArgumentException("Unsupported LEVEL_"); + } + + // Attempt to retrieve the existing ACL, creating an ACL if it doesn't already exist for this ObjectIdentity + MutableAcl acl = null; + try { + acl = (MutableAcl) aclService.readAclById(identity); + } catch (NotFoundException nfe) { + acl = aclService.createAcl(identity); + Assert.notNull(acl, "Acl could not be retrieved or created"); + } + + // Now we have an ACL, add another ACE to it + if (level == LEVEL_NEGATE_READ) { + acl.insertAce(null, permission, sid, false); // not granting + } else { + acl.insertAce(null, permission, sid, true); // granting + } + + // Finally, persist the modified ACL + aclService.updateAcl(acl); + } + +} diff --git a/samples/dms/src/main/java/sample/dms/secured/SecureDocumentDao.java b/samples/dms/src/main/java/sample/dms/secured/SecureDocumentDao.java new file mode 100755 index 0000000000..04bc466dd7 --- /dev/null +++ b/samples/dms/src/main/java/sample/dms/secured/SecureDocumentDao.java @@ -0,0 +1,17 @@ +package sample.dms.secured; + +import sample.dms.DocumentDao; + +/** + * Extends the {@link DocumentDao} and introduces ACL-related methods. + * + * @author Ben Alex + * @version $Id$ + * + */ +public interface SecureDocumentDao extends DocumentDao { + /** + * @return all the usernames existing in the system. + */ + public String[] getUsers(); +} diff --git a/samples/dms/src/main/java/sample/dms/secured/SecureDocumentDaoImpl.java b/samples/dms/src/main/java/sample/dms/secured/SecureDocumentDaoImpl.java new file mode 100755 index 0000000000..5a06d0d436 --- /dev/null +++ b/samples/dms/src/main/java/sample/dms/secured/SecureDocumentDaoImpl.java @@ -0,0 +1,61 @@ +package sample.dms.secured; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.acegisecurity.acls.MutableAcl; +import org.acegisecurity.acls.MutableAclService; +import org.acegisecurity.acls.domain.BasePermission; +import org.acegisecurity.acls.objectidentity.ObjectIdentity; +import org.acegisecurity.acls.objectidentity.ObjectIdentityImpl; +import org.acegisecurity.acls.sid.PrincipalSid; +import org.acegisecurity.context.SecurityContextHolder; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.util.Assert; + +import sample.dms.AbstractElement; +import sample.dms.DocumentDaoImpl; + +/** + * Adds extra {@link SecureDocumentDao} methods. + * + * @author Ben Alex + * @version $Id$ + * + */ +public class SecureDocumentDaoImpl extends DocumentDaoImpl implements SecureDocumentDao { + + private static final String SELECT_FROM_USERS = "SELECT USERNAME FROM USERS ORDER BY USERNAME"; + private MutableAclService mutableAclService; + + public SecureDocumentDaoImpl(MutableAclService mutableAclService) { + Assert.notNull(mutableAclService, "MutableAclService required"); + this.mutableAclService = mutableAclService; + } + + public String[] getUsers() { + return (String[]) getJdbcTemplate().query(SELECT_FROM_USERS, new RowMapper() { + public Object mapRow(ResultSet rs, int rowNumber) throws SQLException { + return rs.getString("USERNAME"); + } + }).toArray(new String[] {}); + } + + public void create(AbstractElement element) { + super.create(element); + + // Create an ACL identity for this element + ObjectIdentity identity = new ObjectIdentityImpl(element); + MutableAcl acl = mutableAclService.createAcl(identity); + + // If the AbstractElement has a parent, go and retrieve its identity (it should already exist) + if (element.getParent() != null) { + ObjectIdentity parentIdentity = new ObjectIdentityImpl(element.getParent()); + MutableAcl aclParent = (MutableAcl) mutableAclService.readAclById(parentIdentity); + acl.setParent(aclParent); + } + acl.insertAce(null, BasePermission.ADMINISTRATION, new PrincipalSid(SecurityContextHolder.getContext().getAuthentication()), true); + + mutableAclService.updateAcl(acl); + } +} diff --git a/samples/dms/src/main/resources/applicationContext-dms-insecure.xml b/samples/dms/src/main/resources/applicationContext-dms-insecure.xml new file mode 100755 index 0000000000..a2c866b6e8 --- /dev/null +++ b/samples/dms/src/main/resources/applicationContext-dms-insecure.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + sample.dms.DocumentDao.*=PROPAGATION_REQUIRED + + + + + + + + + + + + + + + + diff --git a/samples/dms/src/main/resources/applicationContext-dms-secure.xml b/samples/dms/src/main/resources/applicationContext-dms-secure.xml new file mode 100755 index 0000000000..608b57b3fd --- /dev/null +++ b/samples/dms/src/main/resources/applicationContext-dms-secure.xml @@ -0,0 +1,233 @@ + + + + + + + + + + + sample.dms.secured.SecureDocumentDao.*=PROPAGATION_REQUIRED + sample.dms.DocumentDao.*=PROPAGATION_REQUIRED + org.acegisecurity.acls.AclService.*=PROPAGATION_REQUIRED + org.acegisecurity.acls.MutableAclService.*=PROPAGATION_REQUIRED + org.acegisecurity.acls.jdbc.JdbcMutableAclService.*=PROPAGATION_REQUIRED + org.acegisecurity.acls.jdbc.JdbcAclService.*=PROPAGATION_REQUIRED + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.acegisecurity.acls.domain.BasePermission.ADMINISTRATION + + + org.acegisecurity.acls.domain.BasePermission.READ + + + org.acegisecurity.acls.domain.BasePermission.WRITE + + + + + + + + + + + + + + + + + sample.dms.AbstractElement + + + + + + + + + + + + + + sample.dms.AbstractElement + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + sample.dms.DocumentDao.create=ACL_ABSTRACT_ELEMENT_WRITE_PARENT + sample.dms.DocumentDao.delete=ACL_ABSTRACT_ELEMENT_WRITE + sample.dms.DocumentDao.update=ACL_ABSTRACT_ELEMENT_WRITE + sample.dms.DocumentDao.findElements=AFTER_ACL_COLLECTION_READ + sample.dms.secured.SecureDocumentDao.getUsers=ROLE_USER + + + + + diff --git a/samples/dms/src/main/resources/applicationContext-dms-shared.xml b/samples/dms/src/main/resources/applicationContext-dms-shared.xml new file mode 100755 index 0000000000..27a14e910f --- /dev/null +++ b/samples/dms/src/main/resources/applicationContext-dms-shared.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/dms/src/test/java/DmsIntegrationTests.java b/samples/dms/src/test/java/DmsIntegrationTests.java new file mode 100755 index 0000000000..8e65d312be --- /dev/null +++ b/samples/dms/src/test/java/DmsIntegrationTests.java @@ -0,0 +1,87 @@ +import org.acegisecurity.context.SecurityContextHolder; +import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; +import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests; + +import sample.dms.AbstractElement; +import sample.dms.Directory; +import sample.dms.DocumentDao; + +/** + * Basic integration test for DMS sample. + * + * @author Ben Alex + * @version $Id$ + * + */ +public class DmsIntegrationTests extends AbstractTransactionalDataSourceSpringContextTests { + protected DocumentDao documentDao; + + protected String[] getConfigLocations() { + return new String[] {"classpath:applicationContext-dms-shared.xml", "classpath:applicationContext-dms-insecure.xml"}; + } + + public void setDocumentDao(DocumentDao documentDao) { + this.documentDao = documentDao; + } + + public void testBasePopulation() { + assertEquals(9, jdbcTemplate.queryForInt("select count(id) from DIRECTORY")); + assertEquals(90, jdbcTemplate.queryForInt("select count(id) from FILE")); + assertEquals(3, documentDao.findElements(Directory.ROOT_DIRECTORY).length); + } + + public void testMarissaRetrieval() { + process("marissa", "koala", false); + } + + public void testScottRetrieval() { + process("scott", "wombat", false); + } + + public void testDianneRetrieval() { + process("dianne", "emu", false); + } + + protected void process(String username, String password, boolean shouldBeFiltered) { + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(username, password)); + System.out.println("------ Test for username: " + username + " ------"); + AbstractElement[] rootElements = documentDao.findElements(Directory.ROOT_DIRECTORY); + assertEquals(3, rootElements.length); + Directory homeDir = null; + Directory nonHomeDir = null; + for (int i = 0; i < rootElements.length; i++) { + if (rootElements[i].getName().equals(username)) { + homeDir = (Directory) rootElements[i]; + } else { + nonHomeDir = (Directory) rootElements[i]; + } + } + System.out.println("Home directory......: " + homeDir.getFullName()); + System.out.println("Non-home directory..: " + nonHomeDir.getFullName()); + + AbstractElement[] homeElements = documentDao.findElements(homeDir); + assertEquals(12, homeElements.length); // confidential and shared directories, plus 10 files + + AbstractElement[] nonHomeElements = documentDao.findElements(nonHomeDir); + assertEquals(shouldBeFiltered ? 11 : 12, nonHomeElements.length); // cannot see the user's "confidential" sub-directory when filtering + + // Attempt to read the other user's confidential directory from the returned results + // Of course, we shouldn't find a "confidential" directory in the results if we're filtering + Directory nonHomeConfidentialDir = null; + for (int i = 0; i < nonHomeElements.length; i++) { + if (nonHomeElements[i].getName().equals("confidential")) { + nonHomeConfidentialDir = (Directory) nonHomeElements[i]; + } + } + + if (shouldBeFiltered) { + assertNull("Found confidential directory when we should not have", nonHomeConfidentialDir); + } else { + System.out.println("Inaccessible dir....: " + nonHomeConfidentialDir.getFullName()); + assertEquals(10, documentDao.findElements(nonHomeConfidentialDir).length); // 10 files (no sub-directories) + } + + SecurityContextHolder.clearContext(); + } + +} diff --git a/samples/dms/src/test/java/SecureDmsIntegrationTests.java b/samples/dms/src/test/java/SecureDmsIntegrationTests.java new file mode 100755 index 0000000000..e49fa349ac --- /dev/null +++ b/samples/dms/src/test/java/SecureDmsIntegrationTests.java @@ -0,0 +1,67 @@ +import org.acegisecurity.GrantedAuthority; +import org.acegisecurity.GrantedAuthorityImpl; +import org.acegisecurity.acls.Acl; +import org.acegisecurity.acls.AclService; +import org.acegisecurity.acls.objectidentity.ObjectIdentity; +import org.acegisecurity.acls.objectidentity.ObjectIdentityImpl; +import org.acegisecurity.context.SecurityContextHolder; +import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; + +import sample.dms.AbstractElement; +import sample.dms.Directory; + + + +/** + * Basic integration test for DMS sample when security has been added. + * + * @author Ben Alex + * @version $Id$ + * + */ +public class SecureDmsIntegrationTests extends DmsIntegrationTests { + + private AclService aclService; + + public void setAclService(AclService aclService) { + this.aclService = aclService; + } + + protected String[] getConfigLocations() { + return new String[] {"classpath:applicationContext-dms-shared.xml", "classpath:applicationContext-dms-secure.xml"}; + } + + public void testBasePopulation() { + assertEquals(9, jdbcTemplate.queryForInt("select count(id) from DIRECTORY")); + assertEquals(90, jdbcTemplate.queryForInt("select count(id) from FILE")); + assertEquals(4, jdbcTemplate.queryForInt("select count(id) from ACL_SID")); // 3 users + 1 role + assertEquals(2, jdbcTemplate.queryForInt("select count(id) from ACL_CLASS")); // Directory and File + assertEquals(100, jdbcTemplate.queryForInt("select count(id) from ACL_OBJECT_IDENTITY")); + assertEquals(115, jdbcTemplate.queryForInt("select count(id) from ACL_ENTRY")); + } + /* + public void testItOut() { + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("marissa", "password", new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_SUPERVISOR")})); + + + AbstractElement[] elements = documentDao.findElements(Directory.ROOT_DIRECTORY); + ObjectIdentity oid = new ObjectIdentityImpl(elements[0]); + //ObjectIdentity oid = new ObjectIdentityImpl(Directory.class, new Long(3)); + Acl acl = aclService.readAclById(oid); + System.out.println(acl); + + }*/ + + public void testMarissaRetrieval() { + process("marissa", "koala", true); + } + + + public void testScottRetrieval() { + process("scott", "wombat", true); + } + + public void testDianneRetrieval() { + process("dianne", "emu", true); + } +}