diff --git a/servlet/xml/java/dms/build.gradle b/servlet/xml/java/dms/build.gradle new file mode 100644 index 0000000..d697c81 --- /dev/null +++ b/servlet/xml/java/dms/build.gradle @@ -0,0 +1,41 @@ +plugins { + id "java" + id "war" +} + +repositories { + jcenter() + maven { url "https://repo.spring.io/snapshot" } +} + +dependencies { + implementation platform("org.springframework.security:spring-security-bom:5.6.0-SNAPSHOT") + implementation platform("org.springframework:spring-framework-bom:5.3.9") + implementation platform("org.junit:junit-bom:5.7.0") + + implementation 'org.springframework:spring-beans' + implementation 'org.springframework:spring-jdbc' + implementation 'org.springframework:spring-tx' + implementation "org.springframework.security:spring-security-acl" + implementation "org.springframework.security:spring-security-core" + implementation "org.springframework.security:spring-security-config" + implementation "org.thymeleaf:thymeleaf-spring5:3.0.11.RELEASE" + implementation 'javax.servlet:jstl:1.2' + implementation 'org.slf4j:slf4j-api:1.7.30' + implementation 'org.slf4j:slf4j-simple:1.7.30' + + runtime 'net.sf.ehcache:ehcache:2.10.5' + runtime 'org.hsqldb:hsqldb:2.5.0' + runtime 'org.springframework:spring-context-support' + + testImplementation "org.springframework:spring-test" + testImplementation "org.springframework.security:spring-security-test" + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation "org.assertj:assertj-core:3.18.0" + + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") +} + +tasks.withType(Test).configureEach { + useJUnitPlatform() +} diff --git a/servlet/xml/java/dms/gradle.properties b/servlet/xml/java/dms/gradle.properties new file mode 100644 index 0000000..f309933 --- /dev/null +++ b/servlet/xml/java/dms/gradle.properties @@ -0,0 +1 @@ +version=5.6.0-SNAPSHOT diff --git a/servlet/xml/java/dms/gradle/wrapper/gradle-wrapper.jar b/servlet/xml/java/dms/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..62d4c05 Binary files /dev/null and b/servlet/xml/java/dms/gradle/wrapper/gradle-wrapper.jar differ diff --git a/servlet/xml/java/dms/gradle/wrapper/gradle-wrapper.properties b/servlet/xml/java/dms/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..549d844 --- /dev/null +++ b/servlet/xml/java/dms/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/servlet/xml/java/dms/gradlew b/servlet/xml/java/dms/gradlew new file mode 100755 index 0000000..fbd7c51 --- /dev/null +++ b/servlet/xml/java/dms/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/servlet/xml/java/dms/gradlew.bat b/servlet/xml/java/dms/gradlew.bat new file mode 100644 index 0000000..a9f778a --- /dev/null +++ b/servlet/xml/java/dms/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/servlet/xml/java/dms/src/main/java/sample/dms/AbstractElement.java b/servlet/xml/java/dms/src/main/java/sample/dms/AbstractElement.java new file mode 100755 index 0000000..28ec863 --- /dev/null +++ b/servlet/xml/java/dms/src/main/java/sample/dms/AbstractElement.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.dms; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * Base implementation for a element. + * + * @author Ben Alex + */ +public abstract class AbstractElement { + + /** The name of this token (a filename or directory segment name). */ + private final String name; + + /** The parent of this token (a directory, or null if referring to root). */ + private final 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 = -1L; + } + + /** + * 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 this.id; + } + + /** + * Gets the name of this element. + * @return the name of this token (never null, although will be "/" if root, otherwise + * it won't include separators) + */ + public String getName() { + return this.name; + } + + public AbstractElement getParent() { + return this.parent; + } + + /** + * Gets the fully-qualified name of this element, including any parents. + * @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(); + } + + StringBuilder sb = new StringBuilder(); + String lastCharacter = null; + for (String token : strings) { + if (!"/".equals(lastCharacter) && lastCharacter != null) { + sb.append("/"); + } + sb.append(token); + lastCharacter = token.substring(token.length() - 1); + } + return sb.toString(); + } + +} diff --git a/servlet/xml/java/dms/src/main/java/sample/dms/DataSourcePopulator.java b/servlet/xml/java/dms/src/main/java/sample/dms/DataSourcePopulator.java new file mode 100755 index 0000000..0d30d3b --- /dev/null +++ b/servlet/xml/java/dms/src/main/java/sample/dms/DataSourcePopulator.java @@ -0,0 +1,186 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.dms; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.Assert; + +/** + * Populates the DMS in-memory database with document and ACL information. + * + * @author Ben Alex + */ +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; + + public DataSourcePopulator(DataSource dataSource, DocumentDao documentDao) { + Assert.notNull(dataSource, "DataSource required"); + Assert.notNull(documentDao, "DocumentDao required"); + this.template = new JdbcTemplate(dataSource); + this.documentDao = documentDao; + } + + public void afterPropertiesSet() { + // ACL tables + this.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));"); + this.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,CLASS_ID_TYPE VARCHAR_IGNORECASE(100),CONSTRAINT UNIQUE_UK_2 UNIQUE(CLASS));"); + this.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 VARCHAR_IGNORECASE(36) 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));"); + this.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 + this.template.execute( + "CREATE TABLE USERS(USERNAME VARCHAR_IGNORECASE(50) NOT NULL PRIMARY KEY,PASSWORD VARCHAR_IGNORECASE(500) NOT NULL,ENABLED BOOLEAN NOT NULL);"); + this.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));"); + this.template.execute("CREATE UNIQUE INDEX IX_AUTH_USERNAME ON AUTHORITIES(USERNAME,AUTHORITY);"); + + // Document management system business tables + this.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)"); + this.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 + this.template.execute( + "INSERT INTO USERS VALUES('rod','$2a$10$75pBjapg4Nl8Pzd.3JRnUe7PDJmk9qBGwNEJDAlA3V.dEJxcDKn5O',TRUE);"); + this.template.execute( + "INSERT INTO USERS VALUES('dianne','$2a$04$bCMEyxrdF/7sgfUiUJ6Ose2vh9DAMaVBldS1Bw2fhi1jgutZrr9zm',TRUE);"); + this.template.execute( + "INSERT INTO USERS VALUES('scott','$2a$06$eChwvzAu3TSexnC3ynw4LOSw1qiEbtNItNeYv5uI40w1i3paoSfLu',TRUE);"); + this.template.execute( + "INSERT INTO USERS VALUES('peter','$2a$04$8.H8bCMROLF4CIgd7IpeQ.tcBXLP5w8iplO0n.kCIkISwrIgX28Ii',FALSE);"); + this.template.execute( + "INSERT INTO USERS VALUES('bill','$2a$04$8.H8bCMROLF4CIgd7IpeQ.3khQlPVNWbp8kzSQqidQHGFurim7P8O',TRUE);"); + this.template.execute( + "INSERT INTO USERS VALUES('bob','$2a$06$zMgxlMf01SfYNcdx7n4NpeFlAGU8apCETz/i2C7VlYWu6IcNyn4Ay',TRUE);"); + this.template.execute( + "INSERT INTO USERS VALUES('jane','$2a$05$ZrdS7yMhCZ1J.AAidXZhCOxdjD8LO/dhlv4FJzkXA6xh9gdEbBT/u',TRUE);"); + this.template.execute("INSERT INTO AUTHORITIES VALUES('rod','ROLE_USER');"); + this.template.execute("INSERT INTO AUTHORITIES VALUES('rod','ROLE_SUPERVISOR');"); + this.template.execute("INSERT INTO AUTHORITIES VALUES('dianne','ROLE_USER');"); + this.template.execute("INSERT INTO AUTHORITIES VALUES('scott','ROLE_USER');"); + this.template.execute("INSERT INTO AUTHORITIES VALUES('peter','ROLE_USER');"); + this.template.execute("INSERT INTO AUTHORITIES VALUES('bill','ROLE_USER');"); + this.template.execute("INSERT INTO AUTHORITIES VALUES('bob','ROLE_USER');"); + this.template.execute("INSERT INTO AUTHORITIES VALUES('jane','ROLE_USER');"); + + // Now create an ACL entry for the root directory + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("rod", "ignored", + AuthorityUtils.createAuthorityList(("ROLE_IGNORED")))); + + addPermission(this.documentDao, Directory.ROOT_DIRECTORY, "ROLE_USER", LEVEL_GRANT_WRITE); + + // Now go off and create some directories and files for our users + createSampleData("rod", "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". + * @param username the user's username + * @param password the user's password + */ + private void createSampleData(String username, String password) { + Assert.notNull(this.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); + this.documentDao.create(home); + addPermission(this.documentDao, home, username, LEVEL_GRANT_ADMIN); + addPermission(this.documentDao, home, "ROLE_USER", LEVEL_GRANT_READ); + createFiles(this.documentDao, home); + + // Now create the confidential directory + Directory confid = new Directory("confidential", home); + this.documentDao.create(confid); + addPermission(this.documentDao, confid, "ROLE_USER", LEVEL_NEGATE_READ); + createFiles(this.documentDao, confid); + + // Now create the shared directory + Directory shared = new Directory("shared", home); + this.documentDao.create(shared); + addPermission(this.documentDao, shared, "ROLE_USER", LEVEL_GRANT_READ); + addPermission(this.documentDao, shared, "ROLE_USER", LEVEL_GRANT_WRITE); + createFiles(this.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) { + } + +} diff --git a/servlet/xml/java/dms/src/main/java/sample/dms/Directory.java b/servlet/xml/java/dms/src/main/java/sample/dms/Directory.java new file mode 100755 index 0000000..4d7634c --- /dev/null +++ b/servlet/xml/java/dms/src/main/java/sample/dms/Directory.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.dms; + +/** + * Represents a directory. + * + * @author Ben Alex + */ +public class Directory extends AbstractElement { + + /** The root directory. */ + public static final Directory ROOT_DIRECTORY = new Directory(); + + private Directory() { + } + + public Directory(String name, Directory parent) { + super(name, parent); + } + + @Override + public String toString() { + return "Directory[fullName='" + getFullName() + "'; name='" + getName() + "'; id='" + getId() + "'; parent='" + + getParent() + "']"; + } + +} diff --git a/servlet/xml/java/dms/src/main/java/sample/dms/DocumentDao.java b/servlet/xml/java/dms/src/main/java/sample/dms/DocumentDao.java new file mode 100755 index 0000000..84faaf4 --- /dev/null +++ b/servlet/xml/java/dms/src/main/java/sample/dms/DocumentDao.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.dms; + +/** + * Interface to define Document data access operations. + * + * @author Ben Alex + */ +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) + */ + void create(AbstractElement element); + + /** + * Removes a file from the database for the specified element. + * @param file the file to remove (cannot be null) + */ + void delete(File file); + + /** + * Modifies a file in the database. + * @param file the file to update (cannot be null) + */ + 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) + */ + AbstractElement[] findElements(Directory directory); + +} diff --git a/servlet/xml/java/dms/src/main/java/sample/dms/DocumentDaoImpl.java b/servlet/xml/java/dms/src/main/java/sample/dms/DocumentDaoImpl.java new file mode 100755 index 0000000..904e71a --- /dev/null +++ b/servlet/xml/java/dms/src/main/java/sample/dms/DocumentDaoImpl.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.dms; + +import java.util.List; + +import org.springframework.jdbc.core.support.JdbcDaoSupport; +import org.springframework.security.util.FieldUtils; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; + +/** + * Basic JDBC implementation of {@link DocumentDao}. + * + * @author Ben Alex + */ +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 getJdbcTemplate().queryForObject(SELECT_IDENTITY, Long.class); + } + + 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() }); + } + + private Directory getDirectoryWithImmediateParentPopulated(final Long id) { + return getJdbcTemplate().queryForObject(SELECT_FROM_DIRECTORY_SINGLE, new Object[] { id }, (rs, rowNumber) -> { + Long parentDirectoryId = rs.getLong("parent_directory_id"); + Directory parentDirectory = Directory.ROOT_DIRECTORY; + if (parentDirectoryId != null && !parentDirectoryId.equals(-1L)) { + // 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, 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, + (rs, rowNumber) -> getDirectoryWithImmediateParentPopulated(rs.getLong("id"))); + return directories.toArray(new AbstractElement[] {}); + } + List directories = getJdbcTemplate().query(SELECT_FROM_DIRECTORY, + new Object[] { directory.getId() }, + (rs, rowNumber) -> getDirectoryWithImmediateParentPopulated(rs.getLong("id"))); + List files = getJdbcTemplate().query(SELECT_FROM_FILE, new Object[] { directory.getId() }, + (rs, rowNumber) -> { + Long parentDirectoryId = 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, rs.getLong("id")); + return file; + }); + // Add the File elements after the Directory elements + directories.addAll(files); + return 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/servlet/xml/java/dms/src/main/java/sample/dms/File.java b/servlet/xml/java/dms/src/main/java/sample/dms/File.java new file mode 100755 index 0000000..33b23fa --- /dev/null +++ b/servlet/xml/java/dms/src/main/java/sample/dms/File.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.dms; + +import org.springframework.util.Assert; + +/** + * Represents a File. + * + * @author Ben Alex + */ +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 this.content; + } + + public void setContent(String content) { + this.content = content; + } + + @Override + public String toString() { + return "File[fullName='" + getFullName() + "'; name='" + getName() + "'; id='" + getId() + "'; content=" + + getContent() + "'; parent='" + getParent() + "']"; + } + +} diff --git a/servlet/xml/java/dms/src/main/java/sample/dms/secured/SecureDataSourcePopulator.java b/servlet/xml/java/dms/src/main/java/sample/dms/secured/SecureDataSourcePopulator.java new file mode 100755 index 0000000..8c4d63f --- /dev/null +++ b/servlet/xml/java/dms/src/main/java/sample/dms/secured/SecureDataSourcePopulator.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.dms.secured; + +import javax.sql.DataSource; + +import sample.dms.AbstractElement; +import sample.dms.DataSourcePopulator; +import sample.dms.DocumentDao; + +import org.springframework.security.acls.domain.BasePermission; +import org.springframework.security.acls.domain.GrantedAuthoritySid; +import org.springframework.security.acls.domain.ObjectIdentityImpl; +import org.springframework.security.acls.domain.PrincipalSid; +import org.springframework.security.acls.model.MutableAcl; +import org.springframework.security.acls.model.MutableAclService; +import org.springframework.security.acls.model.NotFoundException; +import org.springframework.security.acls.model.ObjectIdentity; +import org.springframework.security.acls.model.Permission; +import org.springframework.security.acls.model.Sid; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.Assert; + +public class SecureDataSourcePopulator extends DataSourcePopulator { + + private final MutableAclService aclService; + + public SecureDataSourcePopulator(DataSource dataSource, SecureDocumentDao documentDao, + MutableAclService aclService) { + super(dataSource, documentDao); + 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) this.aclService.readAclById(identity); + } + catch (NotFoundException nfe) { + acl = this.aclService.createAcl(identity); + Assert.notNull(acl, "Acl could not be retrieved or created"); + } + + // Now we have an ACL, add another ACE to it + // granting + // granting + acl.insertAce(acl.getEntries().size(), permission, sid, level != LEVEL_NEGATE_READ); // not + + // Finally, persist the modified ACL + this.aclService.updateAcl(acl); + } + +} diff --git a/servlet/xml/java/dms/src/main/java/sample/dms/secured/SecureDocumentDao.java b/servlet/xml/java/dms/src/main/java/sample/dms/secured/SecureDocumentDao.java new file mode 100755 index 0000000..95839d8 --- /dev/null +++ b/servlet/xml/java/dms/src/main/java/sample/dms/secured/SecureDocumentDao.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.dms.secured; + +import sample.dms.DocumentDao; + +/** + * Extends the {@link DocumentDao} and introduces ACL-related methods. + * + * @author Ben Alex + * + */ +public interface SecureDocumentDao extends DocumentDao { + + /** + * Gets all the users existing in the system. + * @return all the usernames existing in the system. + */ + String[] getUsers(); + +} diff --git a/servlet/xml/java/dms/src/main/java/sample/dms/secured/SecureDocumentDaoImpl.java b/servlet/xml/java/dms/src/main/java/sample/dms/secured/SecureDocumentDaoImpl.java new file mode 100755 index 0000000..cff5a6a --- /dev/null +++ b/servlet/xml/java/dms/src/main/java/sample/dms/secured/SecureDocumentDaoImpl.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.dms.secured; + +import sample.dms.AbstractElement; +import sample.dms.DocumentDaoImpl; + +import org.springframework.security.acls.domain.BasePermission; +import org.springframework.security.acls.domain.ObjectIdentityImpl; +import org.springframework.security.acls.domain.PrincipalSid; +import org.springframework.security.acls.model.MutableAcl; +import org.springframework.security.acls.model.MutableAclService; +import org.springframework.security.acls.model.ObjectIdentity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.Assert; + +/** + * Adds extra {@link SecureDocumentDao} methods. + * + * @author Ben Alex + * + */ +public class SecureDocumentDaoImpl extends DocumentDaoImpl implements SecureDocumentDao { + + private static final String SELECT_FROM_USERS = "SELECT USERNAME FROM USERS ORDER BY USERNAME"; + + private final MutableAclService mutableAclService; + + public SecureDocumentDaoImpl(MutableAclService mutableAclService) { + Assert.notNull(mutableAclService, "MutableAclService required"); + this.mutableAclService = mutableAclService; + } + + public String[] getUsers() { + return getJdbcTemplate().query(SELECT_FROM_USERS, (rs, rowNumber) -> 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 = this.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) this.mutableAclService.readAclById(parentIdentity); + acl.setParent(aclParent); + } + acl.insertAce(acl.getEntries().size(), BasePermission.ADMINISTRATION, + new PrincipalSid(SecurityContextHolder.getContext().getAuthentication()), true); + + this.mutableAclService.updateAcl(acl); + } + +} diff --git a/servlet/xml/java/dms/src/main/resources/applicationContext-dms-insecure.xml b/servlet/xml/java/dms/src/main/resources/applicationContext-dms-insecure.xml new file mode 100755 index 0000000..4ad7faf --- /dev/null +++ b/servlet/xml/java/dms/src/main/resources/applicationContext-dms-insecure.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + sample.dms.DocumentDao.*=PROPAGATION_REQUIRED + + + + + + + + + + + + + + + diff --git a/servlet/xml/java/dms/src/main/resources/applicationContext-dms-secure.xml b/servlet/xml/java/dms/src/main/resources/applicationContext-dms-secure.xml new file mode 100755 index 0000000..5a291d3 --- /dev/null +++ b/servlet/xml/java/dms/src/main/resources/applicationContext-dms-secure.xml @@ -0,0 +1,245 @@ + + + + + + + + + + + + + + + + + + + + + sample.dms.secured.SecureDocumentDao.*=PROPAGATION_REQUIRED + sample.dms.DocumentDao.*=PROPAGATION_REQUIRED + org.springframework.security.acls.model.AclService.*=PROPAGATION_REQUIRED + org.springframework.security.acls.model.MutableAclService.*=PROPAGATION_REQUIRED + org.springframework.security.acls.jdbc.JdbcMutableAclService.*=PROPAGATION_REQUIRED + org.springframework.security.acls.jdbc.JdbcAclService.*=PROPAGATION_REQUIRED + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/servlet/xml/java/dms/src/main/resources/applicationContext-dms-shared.xml b/servlet/xml/java/dms/src/main/resources/applicationContext-dms-shared.xml new file mode 100755 index 0000000..e65da9b --- /dev/null +++ b/servlet/xml/java/dms/src/main/resources/applicationContext-dms-shared.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/servlet/xml/java/dms/src/test/java/sample/DmsIntegrationTests.java b/servlet/xml/java/dms/src/test/java/sample/DmsIntegrationTests.java new file mode 100644 index 0000000..f8588c9 --- /dev/null +++ b/servlet/xml/java/dms/src/test/java/sample/DmsIntegrationTests.java @@ -0,0 +1,156 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample; + +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import sample.dms.AbstractElement; +import sample.dms.Directory; +import sample.dms.DocumentDao; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Basic integration test for DMS sample. + * + * @author Ben Alex + * + */ +@ContextConfiguration( + locations = { "classpath:applicationContext-dms-shared.xml", "classpath:applicationContext-dms-insecure.xml" }) +@ExtendWith(SpringExtension.class) +@Transactional +public class DmsIntegrationTests { + + @Autowired + protected JdbcTemplate jdbcTemplate; + + @Autowired + protected DocumentDao documentDao; + + @AfterEach + void clearContext() { + SecurityContextHolder.clearContext(); + } + + public void setDocumentDao(DocumentDao documentDao) { + this.documentDao = documentDao; + } + + @Test + void testBasePopulation() { + assertThat(this.jdbcTemplate.queryForObject("select count(id) from DIRECTORY", Integer.class)).isEqualTo(9); + assertThat((int) this.jdbcTemplate.queryForObject("select count(id) from FILE", Integer.class)).isEqualTo(90); + assertThat(this.documentDao.findElements(Directory.ROOT_DIRECTORY).length).isEqualTo(3); + } + + @Test + void testMarissaRetrieval() { + process("rod", "koala", false); + } + + @Test + void testScottRetrieval() { + process("scott", "wombat", false); + } + + @Test + 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 = this.documentDao.findElements(Directory.ROOT_DIRECTORY); + assertThat(rootElements).hasSize(3); + Directory homeDir = null; + Directory nonHomeDir = null; + for (AbstractElement rootElement : rootElements) { + if (rootElement.getName().equals(username)) { + homeDir = (Directory) rootElement; + } + else { + nonHomeDir = (Directory) rootElement; + } + } + System.out.println("Home directory......: " + homeDir.getFullName()); + System.out.println("Non-home directory..: " + nonHomeDir.getFullName()); + + AbstractElement[] homeElements = this.documentDao.findElements(homeDir); + assertThat(homeElements).hasSize(12); // confidential and shared + // directories, + // plus 10 files + + AbstractElement[] nonHomeElements = this.documentDao.findElements(nonHomeDir); + assertThat(nonHomeElements).hasSize(shouldBeFiltered ? 11 : 12); + + // 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 (AbstractElement nonHomeElement : nonHomeElements) { + if (nonHomeElement.getName().equals("confidential")) { + nonHomeConfidentialDir = (Directory) nonHomeElement; + } + } + + if (shouldBeFiltered) { + assertThat(nonHomeConfidentialDir).withFailMessage("Found confidential directory when we should not have") + .isNull(); + } + else { + System.out.println("Inaccessible dir....: " + nonHomeConfidentialDir.getFullName()); + assertThat(this.documentDao.findElements(nonHomeConfidentialDir).length).isEqualTo(10); // 10 + // files + // (no + // sub-directories) + } + + SecurityContextHolder.clearContext(); + } + +} diff --git a/servlet/xml/java/dms/src/test/java/sample/SecureDmsIntegrationTests.java b/servlet/xml/java/dms/src/test/java/sample/SecureDmsIntegrationTests.java new file mode 100644 index 0000000..e5eacb0 --- /dev/null +++ b/servlet/xml/java/dms/src/test/java/sample/SecureDmsIntegrationTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample; + +import org.junit.jupiter.api.Test; + +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Basic integration test for DMS sample when security has been added. + * + * @author Ben Alex + * + */ +@ContextConfiguration( + locations = { "classpath:applicationContext-dms-shared.xml", "classpath:applicationContext-dms-secure.xml" }) +public class SecureDmsIntegrationTests extends DmsIntegrationTests { + + @Override + @Test + void testBasePopulation() { + assertThat(this.jdbcTemplate.queryForObject("select count(id) from DIRECTORY", Integer.class)).isEqualTo(9); + assertThat(this.jdbcTemplate.queryForObject("select count(id) from FILE", Integer.class)).isEqualTo(90); + assertThat(this.jdbcTemplate.queryForObject("select count(id) from ACL_SID", Integer.class)).isEqualTo(4); // 3 + // users + // + + // 1 + // role + assertThat(this.jdbcTemplate.queryForObject("select count(id) from ACL_CLASS", Integer.class)).isEqualTo(2); // Directory + // and + // File + assertThat(this.jdbcTemplate.queryForObject("select count(id) from ACL_OBJECT_IDENTITY", Integer.class)) + .isEqualTo(100); + assertThat(this.jdbcTemplate.queryForObject("select count(id) from ACL_ENTRY", Integer.class)).isEqualTo(115); + } + + @Override + @Test + void testMarissaRetrieval() { + process("rod", "koala", true); + } + + @Override + @Test + void testScottRetrieval() { + process("scott", "wombat", true); + } + + @Override + @Test + void testDianneRetrieval() { + process("dianne", "emu", true); + } + +} diff --git a/servlet/xml/java/dms/src/test/resources/logback-test.xml b/servlet/xml/java/dms/src/test/resources/logback-test.xml new file mode 100644 index 0000000..2d51ba4 --- /dev/null +++ b/servlet/xml/java/dms/src/test/resources/logback-test.xml @@ -0,0 +1,15 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + diff --git a/settings.gradle b/settings.gradle index 413c29a..4d2c9b3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -59,3 +59,4 @@ include ":servlet:spring-boot:kotlin:hello-security" include ":servlet:xml:java:helloworld" include ":servlet:xml:java:preauth" include ":servlet:xml:java:contacts" +include ":servlet:xml:java:dms"