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);
+ }
+}