diff --git a/servlet/xml/java/contacts/build.gradle b/servlet/xml/java/contacts/build.gradle new file mode 100644 index 0000000..8c91dec --- /dev/null +++ b/servlet/xml/java/contacts/build.gradle @@ -0,0 +1,53 @@ +plugins { + id "java" + id "war" + id "nebula.integtest" version "7.0.9" + id "org.gretty" version "3.0.3" +} + +apply from: "gradle/gretty.gradle" + +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.security:spring-security-config" + implementation "org.springframework.security:spring-security-web" + implementation "org.springframework.security:spring-security-acl" + implementation "org.springframework.security:spring-security-taglibs" + implementation 'org.springframework:spring-web' + implementation "org.springframework:spring-webmvc" + implementation 'org.springframework:spring-aop' + implementation 'org.springframework:spring-beans' + implementation 'org.springframework:spring-context' + implementation 'org.springframework:spring-jdbc' + implementation 'org.springframework:spring-tx' + implementation 'org.slf4j:slf4j-api:1.7.30' + implementation 'org.slf4j:slf4j-simple:1.7.30' + implementation 'javax.servlet:jstl:1.2' + + runtime 'net.sf.ehcache:ehcache:2.10.5' + runtime 'org.hsqldb:hsqldb:2.5.0' + runtime 'org.springframework:spring-context-support' + + providedCompile 'javax.servlet:javax.servlet-api:4.0.0' + + 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") + + integTestImplementation "org.seleniumhq.selenium:htmlunit-driver:2.44.0" +} + +tasks.withType(Test).configureEach { + useJUnitPlatform() +} diff --git a/servlet/xml/java/contacts/client/client.properties b/servlet/xml/java/contacts/client/client.properties new file mode 100644 index 0000000..9f3feee --- /dev/null +++ b/servlet/xml/java/contacts/client/client.properties @@ -0,0 +1,8 @@ +# Properties file with server URL settings for remote access. +# Applied by PropertyPlaceholderConfigurer from "clientContext.xml". +# + +serverName=localhost +httpPort=8080 +contextPath=/spring-security-sample-contacts-filter +rmiPort=1099 diff --git a/servlet/xml/java/contacts/client/clientContext.xml b/servlet/xml/java/contacts/client/clientContext.xml new file mode 100644 index 0000000..f30d172 --- /dev/null +++ b/servlet/xml/java/contacts/client/clientContext.xml @@ -0,0 +1,73 @@ + + + + + + + + + + client.properties + + + + + + + + + + sample.contact.ContactManager + + + http://${serverName}:${httpPort}${contextPath}/remoting/ContactManager-httpinvoker + + + + + + + + + + + + + + diff --git a/servlet/xml/java/contacts/gradle.properties b/servlet/xml/java/contacts/gradle.properties new file mode 100644 index 0000000..99e4019 --- /dev/null +++ b/servlet/xml/java/contacts/gradle.properties @@ -0,0 +1 @@ +version=5.6.0-SNAPSHOT \ No newline at end of file diff --git a/servlet/xml/java/contacts/gradle/gretty.gradle b/servlet/xml/java/contacts/gradle/gretty.gradle new file mode 100644 index 0000000..6949bdf --- /dev/null +++ b/servlet/xml/java/contacts/gradle/gretty.gradle @@ -0,0 +1,41 @@ +gretty { + servletContainer = "tomcat9" + contextPath = "/" + fileLogEnabled = false + integrationTestTask = 'integrationTest' +} + +Task prepareAppServerForIntegrationTests = project.tasks.create('prepareAppServerForIntegrationTests') { + group = 'Verification' + description = 'Prepares the app server for integration tests' + doFirst { + project.gretty { + httpPort = -1 + } + } +} + +project.tasks.matching { it.name == "appBeforeIntegrationTest" }.all { task -> + task.dependsOn prepareAppServerForIntegrationTests +} + +project.tasks.matching { it.name == "integrationTest" }.all { + task -> task.doFirst { + def gretty = project.gretty + String host = project.gretty.host ?: 'localhost' + boolean isHttps = gretty.httpsEnabled + Integer httpPort = integrationTest.systemProperties['gretty.httpPort'] + Integer httpsPort = integrationTest.systemProperties['gretty.httpsPort'] + int port = isHttps ? httpsPort : httpPort + String contextPath = project.gretty.contextPath + String httpBaseUrl = "http://${host}:${httpPort}${contextPath}" + String httpsBaseUrl = "https://${host}:${httpsPort}${contextPath}" + String baseUrl = isHttps ? httpsBaseUrl : httpBaseUrl + integrationTest.systemProperty 'app.port', port + integrationTest.systemProperty 'app.httpPort', httpPort + integrationTest.systemProperty 'app.httpsPort', httpsPort + integrationTest.systemProperty 'app.baseURI', baseUrl + integrationTest.systemProperty 'app.httpBaseURI', httpBaseUrl + integrationTest.systemProperty 'app.httpsBaseURI', httpsBaseUrl + } +} \ No newline at end of file diff --git a/servlet/xml/java/contacts/gradle/wrapper/gradle-wrapper.jar b/servlet/xml/java/contacts/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..62d4c05 Binary files /dev/null and b/servlet/xml/java/contacts/gradle/wrapper/gradle-wrapper.jar differ diff --git a/servlet/xml/java/contacts/gradle/wrapper/gradle-wrapper.properties b/servlet/xml/java/contacts/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..549d844 --- /dev/null +++ b/servlet/xml/java/contacts/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/contacts/gradlew b/servlet/xml/java/contacts/gradlew new file mode 100755 index 0000000..fbd7c51 --- /dev/null +++ b/servlet/xml/java/contacts/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/contacts/gradlew.bat b/servlet/xml/java/contacts/gradlew.bat new file mode 100644 index 0000000..a9f778a --- /dev/null +++ b/servlet/xml/java/contacts/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/contacts/src/integTest/java/org/springframework/security/samples/ContactsTests.java b/servlet/xml/java/contacts/src/integTest/java/org/springframework/security/samples/ContactsTests.java new file mode 100644 index 0000000..faa5583 --- /dev/null +++ b/servlet/xml/java/contacts/src/integTest/java/org/springframework/security/samples/ContactsTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2018 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 org.springframework.security.samples; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.htmlunit.HtmlUnitDriver; + +import org.springframework.security.samples.pages.ContactsPage; +import org.springframework.security.samples.pages.HomePage; + +/** + * Test for Contacts application. + * + * @author Michael Simons + */ +public class ContactsTests { + + private WebDriver driver; + + private int port; + + @BeforeEach + void setup() { + this.port = Integer.parseInt(System.getProperty("app.httpPort")); + this.driver = new HtmlUnitDriver(); + } + + @AfterEach + void tearDown() { + this.driver.quit(); + } + + @Test + void accessHomePageWithUnauthenticatedUserSuccess() { + final HomePage homePage = HomePage.to(this.driver, this.port); + homePage.assertAt(); + } + + @Test + void authenticatedUserCanAddContacts() { + final String name = "Rob Winch"; + final String email = "rob@example.com"; + + // @formatter:off + ContactsPage.accessManagePageWithUnauthenticatedUser(this.driver, this.port) + .sendsToLoginPage() + .username("rod") + .password("koala") + .submit() + .isAtContactsPage() + .addContact() + .name(name) + .email(email) + .submit() + .andHasContact(name, email) + .delete() + .andConfirmDeletion() + .isAtContactsPage() + .andContactHasBeenRemoved(name, email); + // @formatter:on + } + + @Test + void authenticatedUserLogsOut() { + // @formatter:off + final HomePage homePage = ContactsPage.accessManagePageWithUnauthenticatedUser(this.driver, this.port) + .sendsToLoginPage() + .username("rod") + .password("koala") + .submit() + .isAtContactsPage() + .logout(); + // @formatter:on + homePage.assertAt(); + + ContactsPage.accessManagePageWithUnauthenticatedUser(this.driver, this.port).sendsToLoginPage(); + } + +} diff --git a/servlet/xml/java/contacts/src/integTest/java/org/springframework/security/samples/pages/AddPage.java b/servlet/xml/java/contacts/src/integTest/java/org/springframework/security/samples/pages/AddPage.java new file mode 100644 index 0000000..812ee52 --- /dev/null +++ b/servlet/xml/java/contacts/src/integTest/java/org/springframework/security/samples/pages/AddPage.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2018 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 org.springframework.security.samples.pages; + +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; +import org.openqa.selenium.support.PageFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The add new contact page. + * + * @author Michael Simons + */ +public class AddPage { + + private final WebDriver webDriver; + + private final AddForm addForm; + + public AddPage(WebDriver webDriver) { + this.webDriver = webDriver; + this.addForm = PageFactory.initElements(this.webDriver, AddForm.class); + } + + AddForm addForm() { + assertThat(this.webDriver.getTitle()).isEqualTo("Add New Contact"); + return this.addForm; + } + + public static class AddForm { + + private WebDriver webDriver; + + private WebElement name; + + private WebElement email; + + @FindBy(css = "input[type=submit]") + private WebElement submit; + + public AddForm(WebDriver webDriver) { + this.webDriver = webDriver; + } + + public AddForm name(String name) { + this.name.sendKeys(name); + return this; + } + + public AddForm email(String email) { + this.email.sendKeys(email); + return this; + } + + public ContactsPage submit() { + this.submit.click(); + return PageFactory.initElements(this.webDriver, ContactsPage.class); + } + + } + +} diff --git a/servlet/xml/java/contacts/src/integTest/java/org/springframework/security/samples/pages/ContactsPage.java b/servlet/xml/java/contacts/src/integTest/java/org/springframework/security/samples/pages/ContactsPage.java new file mode 100644 index 0000000..3de827f --- /dev/null +++ b/servlet/xml/java/contacts/src/integTest/java/org/springframework/security/samples/pages/ContactsPage.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2018 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 org.springframework.security.samples.pages; + +import java.util.List; +import java.util.function.Predicate; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; +import org.openqa.selenium.support.PageFactory; + +import org.springframework.security.samples.pages.AddPage.AddForm; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The contacts / manage page. + * + * @author Michael Simons + */ +public class ContactsPage { + + public static LoginPage accessManagePageWithUnauthenticatedUser(WebDriver driver, int port) { + driver.get("http://localhost:" + port + "/secure/index.htm"); + return PageFactory.initElements(driver, LoginPage.class); + } + + private final WebDriver webDriver; + + @FindBy(linkText = "Add") + private WebElement a; + + @FindBy(css = "table tr") + private List contacts; + + @FindBy(xpath = "//input[@type='submit' and @value='Logoff']") + private WebElement logout; + + public ContactsPage(WebDriver webDriver) { + this.webDriver = webDriver; + } + + public ContactsPage isAtContactsPage() { + assertThat(this.webDriver.getTitle()).isEqualTo("Your Contacts"); + return this; + } + + public AddForm addContact() { + this.a.click(); + final AddPage addPage = PageFactory.initElements(this.webDriver, AddPage.class); + return addPage.addForm(); + } + + Predicate byEmail(final String val) { + return (e) -> e.findElements(By.xpath("td[position()=3 and normalize-space()='" + val + "']")).size() == 1; + } + + Predicate byName(final String val) { + return (e) -> e.findElements(By.xpath("td[position()=2 and normalize-space()='" + val + "']")).size() == 1; + } + + public DeleteContactLink andHasContact(final String name, final String email) { + return this.contacts.stream().filter(byEmail(email).and(byName(name))) + .map((e) -> e.findElement(By.cssSelector("td:nth-child(4) > a"))).findFirst() + .map((e) -> new DeleteContactLink(this.webDriver, e)).get(); + } + + public ContactsPage andContactHasBeenRemoved(final String name, final String email) { + assertThat(this.contacts.stream().filter(byEmail(email).and(byName(name))).findAny()).isEmpty(); + return this; + } + + public HomePage logout() { + this.logout.click(); + return PageFactory.initElements(this.webDriver, HomePage.class); + } + + public static class DeleteContactLink { + + private final WebDriver webDriver; + + private final WebElement a; + + public DeleteContactLink(WebDriver webDriver, WebElement a) { + this.webDriver = webDriver; + this.a = a; + } + + public DeleteConfirmationPage delete() { + this.a.click(); + return PageFactory.initElements(this.webDriver, DeleteConfirmationPage.class); + } + + } + + public static class DeleteConfirmationPage { + + private final WebDriver webDriver; + + @FindBy(linkText = "Manage") + private WebElement a; + + public DeleteConfirmationPage(WebDriver webDriver) { + this.webDriver = webDriver; + } + + public ContactsPage andConfirmDeletion() { + assertThat(this.webDriver.getTitle()).isEqualTo("Deletion completed"); + this.a.click(); + return PageFactory.initElements(this.webDriver, ContactsPage.class); + } + + } + +} diff --git a/servlet/xml/java/contacts/src/integTest/java/org/springframework/security/samples/pages/HomePage.java b/servlet/xml/java/contacts/src/integTest/java/org/springframework/security/samples/pages/HomePage.java new file mode 100644 index 0000000..41fae8f --- /dev/null +++ b/servlet/xml/java/contacts/src/integTest/java/org/springframework/security/samples/pages/HomePage.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2018 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 org.springframework.security.samples.pages; + +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; +import org.openqa.selenium.support.PageFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The home page. + * + * @author Michael Simons + */ +public class HomePage { + + public static HomePage to(WebDriver driver, int port) { + driver.get("http://localhost:" + port + "/"); + return PageFactory.initElements(driver, HomePage.class); + } + + private final WebDriver webDriver; + + @FindBy(css = "p") + private WebElement message; + + @FindBy(css = "input[type=submit]") + private WebElement logoutButton; + + public HomePage(WebDriver webDriver) { + this.webDriver = webDriver; + } + + public Content assertAt() { + assertThat(this.webDriver.getTitle()).isEqualTo("Contacts Security Demo"); + return PageFactory.initElements(this.webDriver, Content.class); + } + + public LoginPage logout() { + this.logoutButton.submit(); + return PageFactory.initElements(this.webDriver, LoginPage.class); + } + + public static class Content { + + @FindBy(css = "p") + private WebElement message; + + public Content andTheUserNameIsDisplayed() { + assertThat(this.message.getText()).isEqualTo("Hello user"); + return this; + } + + } + +} diff --git a/servlet/xml/java/contacts/src/integTest/java/org/springframework/security/samples/pages/LoginPage.java b/servlet/xml/java/contacts/src/integTest/java/org/springframework/security/samples/pages/LoginPage.java new file mode 100644 index 0000000..9ceff20 --- /dev/null +++ b/servlet/xml/java/contacts/src/integTest/java/org/springframework/security/samples/pages/LoginPage.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2018 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 org.springframework.security.samples.pages; + +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; +import org.openqa.selenium.support.PageFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The login page. + * + * @author Michael Simons + */ +public class LoginPage { + + private final WebDriver webDriver; + + private final LoginForm loginForm; + + public LoginPage(WebDriver webDriver) { + this.webDriver = webDriver; + this.loginForm = PageFactory.initElements(this.webDriver, LoginForm.class); + } + + public LoginForm sendsToLoginPage() { + assertThat(this.webDriver.getTitle()).isEqualTo("Login"); + return this.loginForm; + } + + public static class LoginForm { + + private WebDriver webDriver; + + private WebElement username; + + private WebElement password; + + @FindBy(css = "input[type=submit]") + private WebElement submit; + + public LoginForm(WebDriver webDriver) { + this.webDriver = webDriver; + } + + public LoginForm username(String username) { + this.username.sendKeys(username); + return this; + } + + public LoginForm password(String password) { + this.password.sendKeys(password); + return this; + } + + public ContactsPage submit() { + this.submit.click(); + return PageFactory.initElements(this.webDriver, ContactsPage.class); + } + + } + +} diff --git a/servlet/xml/java/contacts/src/main/java/sample/contact/AddDeleteContactController.java b/servlet/xml/java/contacts/src/main/java/sample/contact/AddDeleteContactController.java new file mode 100644 index 0000000..bf69b39 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/java/sample/contact/AddDeleteContactController.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2021 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.contact; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Validator; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.ModelAndView; + +/** + * AddDeleteContactController. + * + * @author Luke Taylor + * @since 3.0 + */ +@Controller +public class AddDeleteContactController { + + @Autowired + private ContactManager contactManager; + + private final Validator validator = new WebContactValidator(); + + @RequestMapping(value = "/secure/add.htm", method = RequestMethod.GET) + public ModelAndView addContactDisplay() { + return new ModelAndView("add", "webContact", new WebContact()); + } + + @InitBinder + public void initBinder(WebDataBinder binder) { + System.out.println("A binder for object: " + binder.getObjectName()); + } + + @RequestMapping(value = "/secure/add.htm", method = RequestMethod.POST) + public String addContact(WebContact form, BindingResult result) { + this.validator.validate(form, result); + + if (result.hasErrors()) { + return "add"; + } + + Contact contact = new Contact(form.getName(), form.getEmail()); + this.contactManager.create(contact); + + return "redirect:/secure/index.htm"; + } + + @RequestMapping(value = "/secure/del.htm", method = RequestMethod.GET) + public ModelAndView delContact(@RequestParam("contactId") int contactId) { + Contact contact = this.contactManager.getById((long) contactId); + this.contactManager.delete(contact); + + return new ModelAndView("deleted", "contact", contact); + } + +} diff --git a/servlet/xml/java/contacts/src/main/java/sample/contact/AddPermission.java b/servlet/xml/java/contacts/src/main/java/sample/contact/AddPermission.java new file mode 100644 index 0000000..93bc6c7 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/java/sample/contact/AddPermission.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2021 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.contact; + +import org.springframework.security.acls.domain.BasePermission; + +/** + * Model object for add permission use case. + * + * @author Ben Alex + */ +public class AddPermission { + + private Contact contact; + + private Integer permission = BasePermission.READ.getMask(); + + private String recipient; + + public Contact getContact() { + return this.contact; + } + + public Integer getPermission() { + return this.permission; + } + + public String getRecipient() { + return this.recipient; + } + + public void setContact(Contact contact) { + this.contact = contact; + } + + public void setPermission(Integer permission) { + this.permission = permission; + } + + public void setRecipient(String recipient) { + this.recipient = recipient; + } + +} diff --git a/servlet/xml/java/contacts/src/main/java/sample/contact/AddPermissionValidator.java b/servlet/xml/java/contacts/src/main/java/sample/contact/AddPermissionValidator.java new file mode 100644 index 0000000..9a277e3 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/java/sample/contact/AddPermissionValidator.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2021 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.contact; + +import org.springframework.security.acls.domain.BasePermission; +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; + +/** + * Validates {@link AddPermission}. + * + * @author Ben Alex + */ +public class AddPermissionValidator implements Validator { + + @SuppressWarnings("unchecked") + public boolean supports(Class clazz) { + return clazz.equals(AddPermission.class); + } + + public void validate(Object obj, Errors errors) { + AddPermission addPermission = (AddPermission) obj; + + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "permission", "err.permission", "Permission is required. *"); + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "recipient", "err.recipient", "Recipient is required. *"); + + if (addPermission.getPermission() != null) { + int permission = addPermission.getPermission(); + + if ((permission != BasePermission.ADMINISTRATION.getMask()) && (permission != BasePermission.READ.getMask()) + && (permission != BasePermission.DELETE.getMask())) { + errors.rejectValue("permission", "err.permission.invalid", "The indicated permission is invalid. *"); + } + } + + if (addPermission.getRecipient() != null) { + if (addPermission.getRecipient().length() > 100) { + errors.rejectValue("recipient", "err.recipient.length", + "The recipient is too long (maximum 100 characters). *"); + } + } + } + +} diff --git a/servlet/xml/java/contacts/src/main/java/sample/contact/AdminPermissionController.java b/servlet/xml/java/contacts/src/main/java/sample/contact/AdminPermissionController.java new file mode 100644 index 0000000..88830bf --- /dev/null +++ b/servlet/xml/java/contacts/src/main/java/sample/contact/AdminPermissionController.java @@ -0,0 +1,176 @@ +/* + * Copyright 2002-2021 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.contact; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceAware; +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.dao.DataAccessException; +import org.springframework.security.acls.domain.BasePermission; +import org.springframework.security.acls.domain.DefaultPermissionFactory; +import org.springframework.security.acls.domain.ObjectIdentityImpl; +import org.springframework.security.acls.domain.PermissionFactory; +import org.springframework.security.acls.domain.PrincipalSid; +import org.springframework.security.acls.model.Acl; +import org.springframework.security.acls.model.AclService; +import org.springframework.security.acls.model.Permission; +import org.springframework.security.acls.model.Sid; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Validator; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.SessionAttributes; +import org.springframework.web.servlet.ModelAndView; + +/** + * Web controller to handle Permission administration functions - adding and + * deleting permissions for contacts. + * + * @author Luke Taylor + * @since 3.0 + */ +@Controller +@SessionAttributes("addPermission") +public final class AdminPermissionController implements MessageSourceAware { + + @Autowired + private AclService aclService; + + @Autowired + private ContactManager contactManager; + + private MessageSourceAccessor messages; + + private final Validator addPermissionValidator = new AddPermissionValidator(); + + private final PermissionFactory permissionFactory = new DefaultPermissionFactory(); + + @RequestMapping(value = "/secure/adminPermission.htm", method = RequestMethod.GET) + public ModelAndView displayAdminPage(@RequestParam("contactId") int contactId) { + Contact contact = this.contactManager.getById((long) contactId); + Acl acl = this.aclService.readAclById(new ObjectIdentityImpl(contact)); + + Map model = new HashMap<>(); + model.put("contact", contact); + model.put("acl", acl); + + return new ModelAndView("adminPermission", "model", model); + } + + @RequestMapping(value = "/secure/addPermission.htm", method = RequestMethod.GET) + public ModelAndView displayAddPermissionPageForContact(@RequestParam("contactId") long contactId) { + Contact contact = this.contactManager.getById(contactId); + + AddPermission addPermission = new AddPermission(); + addPermission.setContact(contact); + + Map model = new HashMap<>(); + model.put("addPermission", addPermission); + model.put("recipients", listRecipients()); + model.put("permissions", listPermissions()); + + return new ModelAndView("addPermission", model); + } + + @InitBinder("addPermission") + public void initBinder(WebDataBinder binder) { + binder.setAllowedFields("recipient", "permission"); + } + + @RequestMapping(value = "/secure/addPermission.htm", method = RequestMethod.POST) + public String addPermission(AddPermission addPermission, BindingResult result, ModelMap model) { + this.addPermissionValidator.validate(addPermission, result); + + if (result.hasErrors()) { + model.put("recipients", listRecipients()); + model.put("permissions", listPermissions()); + + return "addPermission"; + } + + PrincipalSid sid = new PrincipalSid(addPermission.getRecipient()); + Permission permission = this.permissionFactory.buildFromMask(addPermission.getPermission()); + + try { + this.contactManager.addPermission(addPermission.getContact(), sid, permission); + } + catch (DataAccessException existingPermission) { + existingPermission.printStackTrace(); + result.rejectValue("recipient", "err.recipientExistsForContact", "Addition failure."); + + model.put("recipients", listRecipients()); + model.put("permissions", listPermissions()); + return "addPermission"; + } + + return "redirect:/secure/index.htm"; + } + + @RequestMapping("/secure/deletePermission.htm") + public ModelAndView deletePermission(@RequestParam("contactId") long contactId, @RequestParam("sid") String sid, + @RequestParam("permission") int mask) { + + Contact contact = this.contactManager.getById(contactId); + + Sid sidObject = new PrincipalSid(sid); + Permission permission = this.permissionFactory.buildFromMask(mask); + + this.contactManager.deletePermission(contact, sidObject, permission); + + Map model = new HashMap<>(); + model.put("contact", contact); + model.put("sid", sidObject); + model.put("permission", permission); + + return new ModelAndView("deletePermission", "model", model); + } + + private Map listPermissions() { + Map map = new LinkedHashMap<>(); + map.put(BasePermission.ADMINISTRATION.getMask(), this.messages.getMessage("select.administer", "Administer")); + map.put(BasePermission.READ.getMask(), this.messages.getMessage("select.read", "Read")); + map.put(BasePermission.DELETE.getMask(), this.messages.getMessage("select.delete", "Delete")); + + return map; + } + + private Map listRecipients() { + Map map = new LinkedHashMap<>(); + map.put("", this.messages.getMessage("select.pleaseSelect", "-- please select --")); + + for (String recipient : this.contactManager.getAllRecipients()) { + map.put(recipient, recipient); + } + + return map; + } + + public void setMessageSource(MessageSource messageSource) { + this.messages = new MessageSourceAccessor(messageSource); + } + +} diff --git a/servlet/xml/java/contacts/src/main/java/sample/contact/ClientApplication.java b/servlet/xml/java/contacts/src/main/java/sample/contact/ClientApplication.java new file mode 100644 index 0000000..a404666 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/java/sample/contact/ClientApplication.java @@ -0,0 +1,140 @@ +/* + * Copyright 2002-2021 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.contact; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.context.support.FileSystemXmlApplicationContext; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StopWatch; + +/** + * Demonstrates accessing the {@link ContactManager} via remoting protocols. + *

+ * Based on Spring's JPetStore sample, written by Juergen Hoeller. + * + * @author Ben Alex + */ +public class ClientApplication { + + private final ListableBeanFactory beanFactory; + + public ClientApplication(ListableBeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + public void invokeContactManager(Authentication authentication, int nrOfCalls) { + StopWatch stopWatch = new StopWatch(nrOfCalls + " ContactManager call(s)"); + Map contactServices = this.beanFactory.getBeansOfType(ContactManager.class, true, true); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + for (Map.Entry entry : contactServices.entrySet()) { + String beanName = entry.getKey(); + ContactManager remoteContactManager = entry.getValue(); + Object object = this.beanFactory.getBean("&" + beanName); + + try { + System.out.println("Trying to find setUsername(String) method on: " + object.getClass().getName()); + + Method method = object.getClass().getMethod("setUsername", new Class[] { String.class }); + System.out.println("Found; Trying to setUsername(String) to " + authentication.getPrincipal()); + method.invoke(object, authentication.getPrincipal()); + } + catch (NoSuchMethodException ignored) { + System.out.println("This client proxy factory does not have a setUsername(String) method"); + } + catch (IllegalAccessException | InvocationTargetException ignored) { + ignored.printStackTrace(); + } + + try { + System.out.println("Trying to find setPassword(String) method on: " + object.getClass().getName()); + + Method method = object.getClass().getMethod("setPassword", new Class[] { String.class }); + method.invoke(object, authentication.getCredentials()); + System.out.println("Found; Trying to setPassword(String) to " + authentication.getCredentials()); + } + catch (NoSuchMethodException ignored) { + System.out.println("This client proxy factory does not have a setPassword(String) method"); + } + catch (IllegalAccessException | InvocationTargetException ignored) { + } + + System.out.println("Calling ContactManager '" + beanName + "'"); + + stopWatch.start(beanName); + + List contacts = null; + + for (int i = 0; i < nrOfCalls; i++) { + contacts = remoteContactManager.getAll(); + } + + stopWatch.stop(); + + if (contacts.size() != 0) { + for (Contact contact : contacts) { + System.out.println("Contact: " + contact); + } + } + else { + System.out.println("No contacts found which this user has permission to"); + } + + System.out.println(); + System.out.println(stopWatch.prettyPrint()); + } + + SecurityContextHolder.clearContext(); + } + + public static void main(String[] args) { + String username = System.getProperty("username", ""); + String password = System.getProperty("password", ""); + String nrOfCallsString = System.getProperty("nrOfCalls", ""); + + if ("".equals(username) || "".equals(password)) { + System.out.println( + "You need to specify the user ID to use, the password to use, and optionally a number of calls " + + "using the username, password, and nrOfCalls system properties respectively. eg for user rod, " + + "use: -Dusername=rod -Dpassword=koala' for a single call per service and " + + "use: -Dusername=rod -Dpassword=koala -DnrOfCalls=10 for ten calls per service."); + System.exit(-1); + } + else { + int nrOfCalls = 1; + + if (!"".equals(nrOfCallsString)) { + nrOfCalls = Integer.parseInt(nrOfCallsString); + } + + ListableBeanFactory beanFactory = new FileSystemXmlApplicationContext("clientContext.xml"); + ClientApplication client = new ClientApplication(beanFactory); + + client.invokeContactManager(new UsernamePasswordAuthenticationToken(username, password), nrOfCalls); + System.exit(0); + } + } + +} diff --git a/servlet/xml/java/contacts/src/main/java/sample/contact/Contact.java b/servlet/xml/java/contacts/src/main/java/sample/contact/Contact.java new file mode 100644 index 0000000..fa5361a --- /dev/null +++ b/servlet/xml/java/contacts/src/main/java/sample/contact/Contact.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2021 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.contact; + +import java.io.Serializable; + +/** + * Represents a contact. + * + * @author Ben Alex + */ +public class Contact implements Serializable { + + private Long id; + + private String email; + + private String name; + + public Contact(String name, String email) { + this.name = name; + this.email = email; + } + + public Contact() { + } + + public String getEmail() { + return this.email; + } + + public Long getId() { + return this.id; + } + + public String getName() { + return this.name; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setId(Long id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(super.toString() + ": "); + sb.append("Id: " + this.getId() + "; "); + sb.append("Name: " + this.getName() + "; "); + sb.append("Email: " + this.getEmail()); + + return sb.toString(); + } + +} diff --git a/servlet/xml/java/contacts/src/main/java/sample/contact/ContactDao.java b/servlet/xml/java/contacts/src/main/java/sample/contact/ContactDao.java new file mode 100644 index 0000000..495e30f --- /dev/null +++ b/servlet/xml/java/contacts/src/main/java/sample/contact/ContactDao.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2021 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.contact; + +import java.util.List; + +/** + * Provides access to the application's persistence layer. + * + * @author Ben Alex + */ +public interface ContactDao { + + void create(Contact contact); + + void delete(Long contactId); + + List findAll(); + + List findAllPrincipals(); + + List findAllRoles(); + + Contact getById(Long id); + + void update(Contact contact); + +} diff --git a/servlet/xml/java/contacts/src/main/java/sample/contact/ContactDaoSpring.java b/servlet/xml/java/contacts/src/main/java/sample/contact/ContactDaoSpring.java new file mode 100644 index 0000000..8b86206 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/java/sample/contact/ContactDaoSpring.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2021 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.contact; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import org.springframework.jdbc.core.support.JdbcDaoSupport; + +/** + * Base implementation of {@link ContactDao} that uses Spring's JdbcTemplate. + * + * @author Ben Alex + * @author Luke Taylor + */ +public class ContactDaoSpring extends JdbcDaoSupport implements ContactDao { + + public void create(final Contact contact) { + getJdbcTemplate().update("insert into contacts values (?, ?, ?)", (ps) -> { + ps.setLong(1, contact.getId()); + ps.setString(2, contact.getName()); + ps.setString(3, contact.getEmail()); + }); + } + + public void delete(final Long contactId) { + getJdbcTemplate().update("delete from contacts where id = ?", (ps) -> ps.setLong(1, contactId)); + } + + public void update(final Contact contact) { + getJdbcTemplate().update("update contacts set contact_name = ?, address = ? where id = ?", (ps) -> { + ps.setString(1, contact.getName()); + ps.setString(2, contact.getEmail()); + ps.setLong(3, contact.getId()); + }); + } + + public List findAll() { + return getJdbcTemplate().query("select id, contact_name, email from contacts order by id", + (rs, rowNum) -> mapContact(rs)); + } + + public List findAllPrincipals() { + return getJdbcTemplate().queryForList("select username from users order by username", String.class); + } + + public List findAllRoles() { + return getJdbcTemplate().queryForList("select distinct authority from authorities order by authority", + String.class); + } + + public Contact getById(Long id) { + List list = getJdbcTemplate().query( + "select id, contact_name, email from contacts where id = ? order by id", (rs, rowNum) -> mapContact(rs), + id); + + if (list.size() == 0) { + return null; + } + else { + return list.get(0); + } + } + + private Contact mapContact(ResultSet rs) throws SQLException { + Contact contact = new Contact(); + contact.setId(rs.getLong("id")); + contact.setName(rs.getString("contact_name")); + contact.setEmail(rs.getString("email")); + + return contact; + } + +} diff --git a/servlet/xml/java/contacts/src/main/java/sample/contact/ContactManager.java b/servlet/xml/java/contacts/src/main/java/sample/contact/ContactManager.java new file mode 100644 index 0000000..c090d9a --- /dev/null +++ b/servlet/xml/java/contacts/src/main/java/sample/contact/ContactManager.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2021 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.contact; + +import java.util.List; + +import org.springframework.security.access.prepost.PostFilter; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.acls.model.Permission; +import org.springframework.security.acls.model.Sid; + +/** + * Interface for the application's services layer. + * + * @author Ben Alex + */ +public interface ContactManager { + + @PreAuthorize("hasPermission(#contact, admin)") + void addPermission(Contact contact, Sid recipient, Permission permission); + + @PreAuthorize("hasPermission(#contact, admin)") + void deletePermission(Contact contact, Sid recipient, Permission permission); + + @PreAuthorize("hasRole('ROLE_USER')") + void create(Contact contact); + + @PreAuthorize("hasPermission(#contact, 'delete') or hasPermission(#contact, admin)") + void delete(Contact contact); + + @PreAuthorize("hasRole('ROLE_USER')") + @PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, admin)") + List getAll(); + + @PreAuthorize("hasRole('ROLE_USER')") + List getAllRecipients(); + + @PreAuthorize("hasPermission(#id, 'sample.contact.Contact', read) or " + + "hasPermission(#id, 'sample.contact.Contact', admin)") + Contact getById(Long id); + + Contact getRandomContact(); + +} diff --git a/servlet/xml/java/contacts/src/main/java/sample/contact/ContactManagerBackend.java b/servlet/xml/java/contacts/src/main/java/sample/contact/ContactManagerBackend.java new file mode 100644 index 0000000..98efea5 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/java/sample/contact/ContactManagerBackend.java @@ -0,0 +1,181 @@ +/* + * Copyright 2002-2021 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.contact; + +import java.util.List; +import java.util.Random; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.support.ApplicationObjectSupport; +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.AccessControlEntry; +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.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +/** + * Concrete implementation of {@link ContactManager}. + * + * @author Ben Alex + */ +@Transactional +public class ContactManagerBackend extends ApplicationObjectSupport implements ContactManager, InitializingBean { + + private ContactDao contactDao; + + private MutableAclService mutableAclService; + + private int counter = 1000; + + public void afterPropertiesSet() { + Assert.notNull(this.contactDao, "contactDao required"); + Assert.notNull(this.mutableAclService, "mutableAclService required"); + } + + public void addPermission(Contact contact, Sid recipient, Permission permission) { + MutableAcl acl; + ObjectIdentity oid = new ObjectIdentityImpl(Contact.class, contact.getId()); + + try { + acl = (MutableAcl) this.mutableAclService.readAclById(oid); + } + catch (NotFoundException nfe) { + acl = this.mutableAclService.createAcl(oid); + } + + acl.insertAce(acl.getEntries().size(), permission, recipient, true); + this.mutableAclService.updateAcl(acl); + + logger.debug("Added permission " + permission + " for Sid " + recipient + " contact " + contact); + } + + public void create(Contact contact) { + // Create the Contact itself + contact.setId((long) this.counter++); + this.contactDao.create(contact); + + // Grant the current principal administrative permission to the contact + addPermission(contact, new PrincipalSid(getUsername()), BasePermission.ADMINISTRATION); + + if (logger.isDebugEnabled()) { + logger.debug("Created contact " + contact + " and granted admin permission to recipient " + getUsername()); + } + } + + public void delete(Contact contact) { + this.contactDao.delete(contact.getId()); + + // Delete the ACL information as well + ObjectIdentity oid = new ObjectIdentityImpl(Contact.class, contact.getId()); + this.mutableAclService.deleteAcl(oid, false); + + if (logger.isDebugEnabled()) { + logger.debug("Deleted contact " + contact + " including ACL permissions"); + } + } + + public void deletePermission(Contact contact, Sid recipient, Permission permission) { + ObjectIdentity oid = new ObjectIdentityImpl(Contact.class, contact.getId()); + MutableAcl acl = (MutableAcl) this.mutableAclService.readAclById(oid); + + // Remove all permissions associated with this particular recipient (string + // equality to KISS) + List entries = acl.getEntries(); + + for (int i = 0; i < entries.size(); i++) { + if (entries.get(i).getSid().equals(recipient) && entries.get(i).getPermission().equals(permission)) { + acl.deleteAce(i); + } + } + + this.mutableAclService.updateAcl(acl); + + if (logger.isDebugEnabled()) { + logger.debug("Deleted contact " + contact + " ACL permissions for recipient " + recipient); + } + } + + @Transactional(readOnly = true) + public List getAll() { + logger.debug("Returning all contacts"); + + return this.contactDao.findAll(); + } + + @Transactional(readOnly = true) + public List getAllRecipients() { + logger.debug("Returning all recipients"); + + return this.contactDao.findAllPrincipals(); + } + + @Transactional(readOnly = true) + public Contact getById(Long id) { + if (logger.isDebugEnabled()) { + logger.debug("Returning contact with id: " + id); + } + + return this.contactDao.getById(id); + } + + @Transactional(readOnly = true) + public Contact getRandomContact() { + logger.debug("Returning random contact"); + + Random rnd = new Random(); + List contacts = this.contactDao.findAll(); + int getNumber = rnd.nextInt(contacts.size()); + + return contacts.get(getNumber); + } + + protected String getUsername() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + + if (auth.getPrincipal() instanceof UserDetails) { + return ((UserDetails) auth.getPrincipal()).getUsername(); + } + else { + return auth.getPrincipal().toString(); + } + } + + public void setContactDao(ContactDao contactDao) { + this.contactDao = contactDao; + } + + public void setMutableAclService(MutableAclService mutableAclService) { + this.mutableAclService = mutableAclService; + } + + public void update(Contact contact) { + this.contactDao.update(contact); + + logger.debug("Updated contact " + contact); + } + +} diff --git a/servlet/xml/java/contacts/src/main/java/sample/contact/DataSourcePopulator.java b/servlet/xml/java/contacts/src/main/java/sample/contact/DataSourcePopulator.java new file mode 100644 index 0000000..d064d95 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/java/sample/contact/DataSourcePopulator.java @@ -0,0 +1,279 @@ +/* + * Copyright 2002-2021 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.contact; + +import java.util.Random; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.acls.domain.AclImpl; +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.acls.model.Permission; +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.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.util.Assert; + +/** + * Populates the Contacts in-memory database with contact and ACL information. + * + * @author Ben Alex + */ +public class DataSourcePopulator implements InitializingBean { + + JdbcTemplate template; + + private MutableAclService mutableAclService; + + final Random rnd = new Random(); + + TransactionTemplate tt; + + final String[] firstNames = { "Bob", "Mary", "James", "Jane", "Kristy", "Kirsty", "Kate", "Jeni", "Angela", + "Melanie", "Kent", "William", "Geoff", "Jeff", "Adrian", "Amanda", "Lisa", "Elizabeth", "Prue", "Richard", + "Darin", "Phillip", "Michael", "Belinda", "Samantha", "Brian", "Greg", "Matthew" }; + + final String[] lastNames = { "Smith", "Williams", "Jackson", "Rictor", "Nelson", "Fitzgerald", "McAlpine", + "Sutherland", "Abbott", "Hall", "Edwards", "Gates", "Black", "Brown", "Gray", "Marwell", "Booch", "Johnson", + "McTaggart", "Parklin", "Findlay", "Robinson", "Giugni", "Lang", "Chi", "Carmichael" }; + + private int createEntities = 50; + + public void afterPropertiesSet() { + Assert.notNull(this.mutableAclService, "mutableAclService required"); + Assert.notNull(this.template, "dataSource required"); + Assert.notNull(this.tt, "platformTransactionManager required"); + + // Set a user account that will initially own all the created data + Authentication authRequest = new UsernamePasswordAuthenticationToken("rod", "koala", + AuthorityUtils.createAuthorityList("ROLE_IGNORED")); + SecurityContextHolder.getContext().setAuthentication(authRequest); + + try { + this.template.execute("DROP TABLE CONTACTS"); + this.template.execute("DROP TABLE AUTHORITIES"); + this.template.execute("DROP TABLE USERS"); + this.template.execute("DROP TABLE ACL_ENTRY"); + this.template.execute("DROP TABLE ACL_OBJECT_IDENTITY"); + this.template.execute("DROP TABLE ACL_CLASS"); + this.template.execute("DROP TABLE ACL_SID"); + } + catch (Exception ex) { + System.out.println("Failed to drop tables: " + ex.getMessage()); + } + + 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));"); + + 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);"); + + this.template.execute( + "CREATE TABLE CONTACTS(ID BIGINT NOT NULL PRIMARY KEY, CONTACT_NAME VARCHAR_IGNORECASE(50) NOT NULL, EMAIL VARCHAR_IGNORECASE(50) NOT NULL)"); + + /* + * Passwords encoded using MD5, NOT in Base64 format, with null as salt Encoded + * password for rod is "koala" Encoded password for dianne is "emu" Encoded + * password for scott is "wombat" Encoded password for peter is "opal" (but user + * is disabled) Encoded password for bill is "wombat" Encoded password for bob is + * "wombat" Encoded password for jane is "wombat" + */ + 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');"); + + this.template.execute("INSERT INTO contacts VALUES (1, 'John Smith', 'john@somewhere.com');"); + this.template.execute("INSERT INTO contacts VALUES (2, 'Michael Citizen', 'michael@xyz.com');"); + this.template.execute("INSERT INTO contacts VALUES (3, 'Joe Bloggs', 'joe@demo.com');"); + this.template.execute("INSERT INTO contacts VALUES (4, 'Karen Sutherland', 'karen@sutherland.com');"); + this.template.execute("INSERT INTO contacts VALUES (5, 'Mitchell Howard', 'mitchell@abcdef.com');"); + this.template.execute("INSERT INTO contacts VALUES (6, 'Rose Costas', 'rose@xyz.com');"); + this.template.execute("INSERT INTO contacts VALUES (7, 'Amanda Smith', 'amanda@abcdef.com');"); + this.template.execute("INSERT INTO contacts VALUES (8, 'Cindy Smith', 'cindy@smith.com');"); + this.template.execute("INSERT INTO contacts VALUES (9, 'Jonathan Citizen', 'jonathan@xyz.com');"); + + for (int i = 10; i < this.createEntities; i++) { + String[] person = selectPerson(); + this.template.execute("INSERT INTO contacts VALUES (" + i + ", '" + person[2] + "', '" + + person[0].toLowerCase() + "@" + person[1].toLowerCase() + ".com');"); + } + + // Create acl_object_identity rows (and also acl_class rows as needed + for (int i = 1; i < this.createEntities; i++) { + final ObjectIdentity objectIdentity = new ObjectIdentityImpl(Contact.class, (long) i); + this.tt.execute((arg0) -> { + this.mutableAclService.createAcl(objectIdentity); + + return null; + }); + } + + // Now grant some permissions + grantPermissions(1, "rod", BasePermission.ADMINISTRATION); + grantPermissions(2, "rod", BasePermission.READ); + grantPermissions(3, "rod", BasePermission.READ); + grantPermissions(3, "rod", BasePermission.WRITE); + grantPermissions(3, "rod", BasePermission.DELETE); + grantPermissions(4, "rod", BasePermission.ADMINISTRATION); + grantPermissions(4, "dianne", BasePermission.ADMINISTRATION); + grantPermissions(4, "scott", BasePermission.READ); + grantPermissions(5, "dianne", BasePermission.ADMINISTRATION); + grantPermissions(5, "dianne", BasePermission.READ); + grantPermissions(6, "dianne", BasePermission.READ); + grantPermissions(6, "dianne", BasePermission.WRITE); + grantPermissions(6, "dianne", BasePermission.DELETE); + grantPermissions(6, "scott", BasePermission.READ); + grantPermissions(7, "scott", BasePermission.ADMINISTRATION); + grantPermissions(8, "dianne", BasePermission.ADMINISTRATION); + grantPermissions(8, "dianne", BasePermission.READ); + grantPermissions(8, "scott", BasePermission.READ); + grantPermissions(9, "scott", BasePermission.ADMINISTRATION); + grantPermissions(9, "scott", BasePermission.READ); + grantPermissions(9, "scott", BasePermission.WRITE); + grantPermissions(9, "scott", BasePermission.DELETE); + + // Now expressly change the owner of the first ten contacts + // We have to do this last, because "rod" owns all of them (doing it sooner would + // prevent ACL updates) + // Note that ownership has no impact on permissions - they're separate (ownership + // only allows ACl editing) + changeOwner(5, "dianne"); + changeOwner(6, "dianne"); + changeOwner(7, "scott"); + changeOwner(8, "dianne"); + changeOwner(9, "scott"); + + String[] users = { "bill", "bob", "jane" }; // don't want to mess around with + // consistent sample data + Permission[] permissions = { BasePermission.ADMINISTRATION, BasePermission.READ, BasePermission.DELETE }; + + for (int i = 10; i < this.createEntities; i++) { + String user = users[this.rnd.nextInt(users.length)]; + Permission permission = permissions[this.rnd.nextInt(permissions.length)]; + grantPermissions(i, user, permission); + + String user2 = users[this.rnd.nextInt(users.length)]; + Permission permission2 = permissions[this.rnd.nextInt(permissions.length)]; + grantPermissions(i, user2, permission2); + } + + SecurityContextHolder.clearContext(); + } + + private void changeOwner(int contactNumber, String newOwnerUsername) { + AclImpl acl = (AclImpl) this.mutableAclService + .readAclById(new ObjectIdentityImpl(Contact.class, (long) contactNumber)); + acl.setOwner(new PrincipalSid(newOwnerUsername)); + updateAclInTransaction(acl); + } + + public int getCreateEntities() { + return this.createEntities; + } + + private void grantPermissions(int contactNumber, String recipientUsername, Permission permission) { + AclImpl acl = (AclImpl) this.mutableAclService + .readAclById(new ObjectIdentityImpl(Contact.class, (long) contactNumber)); + acl.insertAce(acl.getEntries().size(), permission, new PrincipalSid(recipientUsername), true); + updateAclInTransaction(acl); + } + + private String[] selectPerson() { + String firstName = this.firstNames[this.rnd.nextInt(this.firstNames.length)]; + String lastName = this.lastNames[this.rnd.nextInt(this.lastNames.length)]; + + return new String[] { firstName, lastName, firstName + " " + lastName }; + } + + public void setCreateEntities(int createEntities) { + this.createEntities = createEntities; + } + + public void setDataSource(DataSource dataSource) { + this.template = new JdbcTemplate(dataSource); + } + + public void setMutableAclService(MutableAclService mutableAclService) { + this.mutableAclService = mutableAclService; + } + + public void setPlatformTransactionManager(PlatformTransactionManager platformTransactionManager) { + this.tt = new TransactionTemplate(platformTransactionManager); + } + + private void updateAclInTransaction(final MutableAcl acl) { + this.tt.execute((arg0) -> { + this.mutableAclService.updateAcl(acl); + + return null; + }); + } + +} diff --git a/servlet/xml/java/contacts/src/main/java/sample/contact/IndexController.java b/servlet/xml/java/contacts/src/main/java/sample/contact/IndexController.java new file mode 100644 index 0000000..9dffd12 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/java/sample/contact/IndexController.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2021 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.contact; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.PermissionEvaluator; +import org.springframework.security.acls.AclPermissionEvaluator; +import org.springframework.security.acls.domain.BasePermission; +import org.springframework.security.acls.model.Permission; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.servlet.ModelAndView; + +/** + * Controller which handles simple, single request use cases such as index pages and + * contact deletion. + * + * @author Luke Taylor + * @since 3.0 + */ +@Controller +public class IndexController { + + private static final Permission[] HAS_DELETE = new Permission[] { BasePermission.DELETE, + BasePermission.ADMINISTRATION }; + + private static final Permission[] HAS_ADMIN = new Permission[] { BasePermission.ADMINISTRATION }; + + @Autowired + private ContactManager contactManager; + + @Autowired + private PermissionEvaluator permissionEvaluator; + + /** + * The public index page, used for unauthenticated users. + * @return the public index page + */ + @RequestMapping(value = "/hello.htm", method = RequestMethod.GET) + public ModelAndView displayPublicIndex() { + Contact rnd = this.contactManager.getRandomContact(); + + return new ModelAndView("hello", "contact", rnd); + } + + /** + * The index page for an authenticated user. + *

+ * This controller displays a list of all the contacts for which the current user has + * read or admin permissions. It makes a call to {@link ContactManager#getAll()} which + * automatically filters the returned list using Spring Security's ACL mechanism (see + * the expression annotations on this interface for the details). + *

+ * In addition to rendering the list of contacts, the view will also include a "Del" + * or "Admin" link beside the contact, depending on whether the user has the + * corresponding permissions (admin permission is assumed to imply delete here). This + * information is stored in the model using the injected {@link PermissionEvaluator} + * instance. The implementation should be an instance of + * {@link AclPermissionEvaluator} or one which is compatible with Spring Security's + * ACL module. + * @return index page + */ + @RequestMapping(value = "/secure/index.htm", method = RequestMethod.GET) + public ModelAndView displayUserContacts() { + List myContactsList = this.contactManager.getAll(); + Map hasDelete = new HashMap<>(myContactsList.size()); + Map hasAdmin = new HashMap<>(myContactsList.size()); + + Authentication user = SecurityContextHolder.getContext().getAuthentication(); + + for (Contact contact : myContactsList) { + hasDelete.put(contact, this.permissionEvaluator.hasPermission(user, contact, HAS_DELETE)); + hasAdmin.put(contact, this.permissionEvaluator.hasPermission(user, contact, HAS_ADMIN)); + } + + Map model = new HashMap<>(); + model.put("contacts", myContactsList); + model.put("hasDeletePermission", hasDelete); + model.put("hasAdminPermission", hasAdmin); + + return new ModelAndView("index", "model", model); + } + +} diff --git a/servlet/xml/java/contacts/src/main/java/sample/contact/WebContact.java b/servlet/xml/java/contacts/src/main/java/sample/contact/WebContact.java new file mode 100644 index 0000000..d2496f7 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/java/sample/contact/WebContact.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2021 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.contact; + +/** + * An object that represents user-editable sections of a {@link Contact}. + * + * @author Ben Alex + */ +public class WebContact { + + private String email; + + private String name; + + public String getEmail() { + return this.email; + } + + public String getName() { + return this.name; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/servlet/xml/java/contacts/src/main/java/sample/contact/WebContactValidator.java b/servlet/xml/java/contacts/src/main/java/sample/contact/WebContactValidator.java new file mode 100644 index 0000000..4033ec1 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/java/sample/contact/WebContactValidator.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2021 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.contact; + +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +/** + * Validates {@link WebContact}. + * + * @author Ben Alex + */ +public class WebContactValidator implements Validator { + + @SuppressWarnings("unchecked") + public boolean supports(Class clazz) { + return clazz.equals(WebContact.class); + } + + public void validate(Object obj, Errors errors) { + WebContact wc = (WebContact) obj; + + if ((wc.getName() == null) || (wc.getName().length() < 3) || (wc.getName().length() > 50)) { + errors.rejectValue("name", "err.name", "Name 3-50 characters is required. *"); + } + + if ((wc.getEmail() == null) || (wc.getEmail().length() < 3) || (wc.getEmail().length() > 50)) { + errors.rejectValue("email", "err.email", "Email 3-50 characters is required. *"); + } + } + +} diff --git a/servlet/xml/java/contacts/src/main/resources/applicationContext-common-authorization.xml b/servlet/xml/java/contacts/src/main/resources/applicationContext-common-authorization.xml new file mode 100644 index 0000000..397b39e --- /dev/null +++ b/servlet/xml/java/contacts/src/main/resources/applicationContext-common-authorization.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/servlet/xml/java/contacts/src/main/resources/applicationContext-common-business.xml b/servlet/xml/java/contacts/src/main/resources/applicationContext-common-business.xml new file mode 100644 index 0000000..28ebc0a --- /dev/null +++ b/servlet/xml/java/contacts/src/main/resources/applicationContext-common-business.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/servlet/xml/java/contacts/src/main/resources/applicationContext-security.xml b/servlet/xml/java/contacts/src/main/resources/applicationContext-security.xml new file mode 100644 index 0000000..40ff543 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/resources/applicationContext-security.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/servlet/xml/java/contacts/src/main/resources/logback.xml b/servlet/xml/java/contacts/src/main/resources/logback.xml new file mode 100644 index 0000000..993a0bf --- /dev/null +++ b/servlet/xml/java/contacts/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + diff --git a/servlet/xml/java/contacts/src/main/resources/messages.properties b/servlet/xml/java/contacts/src/main/resources/messages.properties new file mode 100644 index 0000000..058905c --- /dev/null +++ b/servlet/xml/java/contacts/src/main/resources/messages.properties @@ -0,0 +1,6 @@ +err.name=Name 3-50 characters is required. +err.email=Email 3-50 characters is required. +err.permission=Permission is required. +err.recipient=Recipient is required. +err.permission.invalid=The indicated permission is invalid. +err.recipient.length=The recipient is too long (maximum 100 characters). \ No newline at end of file diff --git a/servlet/xml/java/contacts/src/main/webapp/WEB-INF/contacts-servlet.xml b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/contacts-servlet.xml new file mode 100644 index 0000000..1ec14d6 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/contacts-servlet.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/add.jsp b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/add.jsp new file mode 100644 index 0000000..02630f9 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/add.jsp @@ -0,0 +1,42 @@ +<%@ include file="/WEB-INF/jsp/include.jsp" %> + +Add New Contact + +

Add Contact

+
+ + + + + + + + + + + + + + + +
Name: + "> + + +
Email: + "> + + +
+
+ + Please fix all errors! + +

+ + " value=""/> + + +">Home + + diff --git a/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/addPermission.jsp b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/addPermission.jsp new file mode 100644 index 0000000..dfd2476 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/addPermission.jsp @@ -0,0 +1,56 @@ +<%@ include file="/WEB-INF/jsp/include.jsp" %> + +Add Permission + +

Add Permission

+
+ + + + + + + + + + + + + + + + + + + +
Contact:
Recipient: + + + +
Permission: + + + +
+
+ + Please fix all errors! + +

+ " value=""/> + + +

+">Admin Permission ">Manage + + diff --git a/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/adminPermission.jsp b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/adminPermission.jsp new file mode 100644 index 0000000..2e0d43f --- /dev/null +++ b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/adminPermission.jsp @@ -0,0 +1,30 @@ +<%@ include file="/WEB-INF/jsp/include.jsp" %> + + +Administer Permissions + +

Administer Permissions

+

+ + + +

+ + + + + + + +
+ + + + + ">Del +
+

+">Add Permission ">Manage +

+ + diff --git a/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/deletePermission.jsp b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/deletePermission.jsp new file mode 100644 index 0000000..85a71a2 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/deletePermission.jsp @@ -0,0 +1,20 @@ +<%@ include file="/WEB-INF/jsp/include.jsp" %> + + +Permission Deleted + +

Permission Deleted

+

+ + + +

+ + + + + + +

">Manage + + diff --git a/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/deleted.jsp b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/deleted.jsp new file mode 100644 index 0000000..8fed87c --- /dev/null +++ b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/deleted.jsp @@ -0,0 +1,13 @@ +<%@ include file="/WEB-INF/jsp/include.jsp" %> + + +Deletion completed + +

Deleted

+

+ + + +

">Manage + + diff --git a/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/frames.jsp b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/frames.jsp new file mode 100644 index 0000000..cf3ad20 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/frames.jsp @@ -0,0 +1,10 @@ + + +Frames + + +

This contains frames, but the frames will not be loaded due to the X-Frame-Options +being specified as denied. This protects against clickjacking attacks

+ + + \ No newline at end of file diff --git a/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/hello.jsp b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/hello.jsp new file mode 100644 index 0000000..8713e0a --- /dev/null +++ b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/hello.jsp @@ -0,0 +1,52 @@ +<%@ include file="/WEB-INF/jsp/include.jsp" %> + + +Contacts Security Demo + +

Contacts Security Demo

+

Contacts demonstrates the following central Spring Security capabilities: +

    +
  • Role-based security. Each principal is a member of certain roles, + which are used to restrict access to certain secure objects.
  • +
  • Domain object instance security. The Contact, the + main domain object in the application, has an access control list (ACL) + that indicates who is allowed read, administer and delete the object.
  • +
  • Method invocation security. The ContactManager service + layer bean has a number of secured (protected) and public (unprotected) + methods.
  • +
  • Web request security. The /secure URI path is protected + by Spring Security from principals not holding the + ROLE_USER granted authority.
  • +
  • Security unaware application objects. None of the objects + are aware of the security being implemented by Spring Security. *
  • +
  • Security taglib usage. All of the JSPs use Spring Security's + taglib to evaluate security information. *
  • +
  • Fully declarative security. Every capability is configured in + the application context using standard Spring Security classes. *
  • +
  • Database-sourced security data. All of the user, role and ACL + information is obtained from an in-memory JDBC-compliant database.
  • +
  • Integrated form-based and BASIC authentication. Any BASIC + authentication header is detected and used for authentication. Normal + interactive form-based authentication is used by default.
  • +
  • Remember-me services. Spring Security's pluggable remember-me + strategy is demonstrated, with a corresponding checkbox on the login form.
  • +
+ +* As the application provides an "ACL Administration" use case, those +classes are necessarily aware of security. But no business use cases are. + +

Please excuse the lack of look 'n' feel polish in this application. +It is about security, after all! :-) + +

To demonstrate a public method on ContactManager, +here's a random Contact: +

+ + + +

Get started by clicking "Manage"... +

">Manage +">Debug +">Frames + + diff --git a/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/include.jsp b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/include.jsp new file mode 100644 index 0000000..3b31878 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/include.jsp @@ -0,0 +1,6 @@ +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %> + +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ page pageEncoding="UTF-8" %> diff --git a/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/index.jsp b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/index.jsp new file mode 100644 index 0000000..10dc023 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/jsp/index.jsp @@ -0,0 +1,37 @@ +<%@ include file="/WEB-INF/jsp/include.jsp" %> + + +Your Contacts + +

's Contacts

+

+ + + + + + + + + + + + + + + +
idNameEmail
+ + + + + + ">Del">Admin Permission
+

">Add

+ +
" method="post"> + (also clears any remember-me cookie) + + + + diff --git a/servlet/xml/java/contacts/src/main/webapp/WEB-INF/remoting-servlet.xml b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/remoting-servlet.xml new file mode 100644 index 0000000..f3f25bd --- /dev/null +++ b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/remoting-servlet.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/servlet/xml/java/contacts/src/main/webapp/WEB-INF/spring.tld b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/spring.tld new file mode 100644 index 0000000..895f80a --- /dev/null +++ b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/spring.tld @@ -0,0 +1,311 @@ + + + + + + 1.1.1 + + 1.2 + + Spring + + http://www.springframework.org/tags + + Spring Framework JSP Tag Library. Authors: Rod Johnson, Juergen Hoeller + + + + + htmlEscape + org.springframework.web.servlet.tags.HtmlEscapeTag + JSP + + + Sets default HTML escape value for the current page. + Overrides a "defaultHtmlEscape" context-param in web.xml, if any. + + + + defaultHtmlEscape + true + true + + + + + + + + escapeBody + org.springframework.web.servlet.tags.EscapeBodyTag + JSP + + + Escapes its enclosed body content, applying HTML escaping and/or JavaScript escaping. + The HTML escaping flag participates in a page-wide or application-wide setting + (i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml). + + + + htmlEscape + false + true + + + + javaScriptEscape + false + true + + + + + + + + message + org.springframework.web.servlet.tags.MessageTag + JSP + + + Retrieves the message with the given code, or text if code isn't resolvable. + The HTML escaping flag participates in a page-wide or application-wide setting + (i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml). + + + + code + false + true + + + + arguments + false + true + + + + text + false + true + + + + var + false + true + + + + scope + false + true + + + + htmlEscape + false + true + + + + javaScriptEscape + false + true + + + + + + + + theme + org.springframework.web.servlet.tags.ThemeTag + JSP + + + Retrieves the theme message with the given code, or text if code isn't resolvable. + The HTML escaping flag participates in a page-wide or application-wide setting + (i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml). + + + + code + false + true + + + + arguments + false + true + + + + text + false + true + + + + var + false + true + + + + scope + false + true + + + + htmlEscape + false + true + + + + javaScriptEscape + false + true + + + + + + + + hasBindErrors + org.springframework.web.servlet.tags.BindErrorsTag + JSP + + + Provides Errors instance in case of bind errors. + The HTML escaping flag participates in a page-wide or application-wide setting + (i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml). + + + + errors + org.springframework.validation.Errors + + + + name + true + true + + + + htmlEscape + false + true + + + + + + + + nestedPath + org.springframework.web.servlet.tags.NestedPathTag + JSP + + + Sets a nested path to be used by the bind tag's path. + + + + nestedPath + java.lang.String + + + + path + true + true + + + + + + + + bind + org.springframework.web.servlet.tags.BindTag + JSP + + + Provides BindStatus object for the given bind path. + The HTML escaping flag participates in a page-wide or application-wide setting + (i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml). + + + + status + org.springframework.web.servlet.support.BindStatus + + + + path + true + true + + + + ignoreNestedPath + false + true + + + + htmlEscape + false + true + + + + + + + + transform + org.springframework.web.servlet.tags.TransformTag + JSP + + + Provides transformation of variables to Strings, using an appropriate + custom PropertyEditor from BindTag (can only be used inside BindTag). + The HTML escaping flag participates in a page-wide or application-wide setting + (i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml). + + + + value + true + true + + + + var + false + true + + + + scope + false + true + + + + htmlEscape + false + true + + + + + diff --git a/servlet/xml/java/contacts/src/main/webapp/WEB-INF/web.xml b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..7b4f0d9 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,99 @@ + + + + + + Contacts Sample Application + + + + contextConfigLocation + + classpath:applicationContext-common-business.xml + classpath:applicationContext-common-authorization.xml + classpath:applicationContext-security.xml + + + + + + + webAppRootKey + contacts.root + + + + localizationFilter + org.springframework.web.filter.RequestContextFilter + + + + springSecurityFilterChain + org.springframework.web.filter.DelegatingFilterProxy + + + + localizationFilter + /* + + + + springSecurityFilterChain + /* + + + + + org.springframework.web.context.ContextLoaderListener + + + + + contacts + org.springframework.web.servlet.DispatcherServlet + 1 + + + + + remoting + org.springframework.web.servlet.DispatcherServlet + 2 + + + + contacts + *.htm + + + + remoting + /remoting/* + + + + index.jsp + + + + 403 + /error.html + + + diff --git a/servlet/xml/java/contacts/src/main/webapp/accessDenied.jsp b/servlet/xml/java/contacts/src/main/webapp/accessDenied.jsp new file mode 100644 index 0000000..c94ae99 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/webapp/accessDenied.jsp @@ -0,0 +1,22 @@ +<%@ page import="org.springframework.security.core.context.SecurityContextHolder" %> +<%@ page import="org.springframework.security.core.Authentication" %> + + + + Access Denied + + + +

Sorry, access is denied

+ +

+<%= request.getAttribute("SPRING_SECURITY_403_EXCEPTION")%> +

+

+<% Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null) { %> + Authentication object as a String: <%= auth.toString() %>

+<% } %> +

+ + diff --git a/servlet/xml/java/contacts/src/main/webapp/error.html b/servlet/xml/java/contacts/src/main/webapp/error.html new file mode 100644 index 0000000..3c58108 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/webapp/error.html @@ -0,0 +1,5 @@ + + Access denied! +

Access Denied

+

We're sorry, but you are not authorized to perform the requested operation.

+ \ No newline at end of file diff --git a/servlet/xml/java/contacts/src/main/webapp/exitUser.jsp b/servlet/xml/java/contacts/src/main/webapp/exitUser.jsp new file mode 100644 index 0000000..5f9e33b --- /dev/null +++ b/servlet/xml/java/contacts/src/main/webapp/exitUser.jsp @@ -0,0 +1,39 @@ +<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core' %> + +<%@ page import="org.springframework.security.core.Authentication" %> +<%@ page import="org.springframework.security.core.context.SecurityContextHolder" %> +<%@ page pageEncoding="UTF-8" %> + + + + Exit User + + + +

Exit User

+ + + + Your 'Exit User' attempt was not successful, try again.

+ Reason: +
+
+ +
+ + + +
Current User: + +<% + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null) { %> + + <%= auth.getPrincipal().toString() %> + + <% } %> +
+ " value=""/> + + + diff --git a/servlet/xml/java/contacts/src/main/webapp/index.jsp b/servlet/xml/java/contacts/src/main/webapp/index.jsp new file mode 100644 index 0000000..4c86e33 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/webapp/index.jsp @@ -0,0 +1,4 @@ +<%@ include file="/WEB-INF/jsp/include.jsp" %> + +<%-- Redirected because we can't set the welcome page to a virtual URL. --%> + diff --git a/servlet/xml/java/contacts/src/main/webapp/login.jsp b/servlet/xml/java/contacts/src/main/webapp/login.jsp new file mode 100644 index 0000000..ed07288 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/webapp/login.jsp @@ -0,0 +1,47 @@ +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ page pageEncoding="UTF-8" %> + + + + Login + + + +

Login

+ +

Valid users: +

+

username rod, password koala +

username dianne, password emu +

username scott, password wombat +

username peter, password opal (user disabled) +

username bill, password wombat +

username bob, password wombat +

username jane, password wombat +

+ +

Locale is: <%= request.getLocale() %>

+ <%-- this form-login-page form is also used as the + form-error-page to ask for a login again. + --%> + + + Your login attempt was not successful, try again.

+ Reason: . +
+
+ +
+ + + + + + + +
User:
Password:
Don't ask for my password for two weeks
+ " value=""/> + + + + diff --git a/servlet/xml/java/contacts/src/main/webapp/secure/debug.jsp b/servlet/xml/java/contacts/src/main/webapp/secure/debug.jsp new file mode 100644 index 0000000..553bc3a --- /dev/null +++ b/servlet/xml/java/contacts/src/main/webapp/secure/debug.jsp @@ -0,0 +1,40 @@ +<%@ page import="org.springframework.security.core.context.SecurityContextHolder" %> +<%@ page import="org.springframework.security.core.Authentication" %> +<%@ page import="org.springframework.security.core.GrantedAuthority" %> + + + +Security Debug Information + + + +

Security Debug Information

+ +<% + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null) { %> +

+ Authentication object is of type: <%= auth.getClass().getName() %> +

+

+ Authentication object as a String:

<%= auth.toString() %> +

+ + Authentication object holds the following granted authorities:

+<% + for (GrantedAuthority authority : auth.getAuthorities()) { %> + <%= authority %> (getAuthority(): <%= authority.getAuthority() %>)
+<% } +%> + +

Success! Your web filters appear to be properly configured!

+<% + } else { +%> + Authentication object is null.
+ This is an error and your Spring Security application will not operate properly until corrected.

+<% } +%> + + + diff --git a/servlet/xml/java/contacts/src/main/webapp/switchUser.jsp b/servlet/xml/java/contacts/src/main/webapp/switchUser.jsp new file mode 100644 index 0000000..a8d3e15 --- /dev/null +++ b/servlet/xml/java/contacts/src/main/webapp/switchUser.jsp @@ -0,0 +1,42 @@ +<%@ taglib prefix='c' uri='http://java.sun.com/jstl/core' %> +<%@ page import="org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter" %> +<%@ page import="org.springframework.security.core.AuthenticationException" %> +<%@ page pageEncoding="UTF-8" %> + + + + Switch User + + + +

Switch to User

+ +

Valid users:

+ +

username rod, password koala

+

username dianne, password emu

+

username scott, password wombat

+

username bill, password wombat

+

username bob, password wombat

+

username jane, password wombat

+ <%-- this form-login-page form is also used as the + form-error-page to ask for a login again. + --%> + +

+ + Your 'su' attempt was not successful, try again.
+
+

+
+ +
+ + + +
User:
+ " value=""/> + + + + diff --git a/servlet/xml/java/contacts/src/site/resources/logback-test.xml b/servlet/xml/java/contacts/src/site/resources/logback-test.xml new file mode 100644 index 0000000..2d51ba4 --- /dev/null +++ b/servlet/xml/java/contacts/src/site/resources/logback-test.xml @@ -0,0 +1,15 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + diff --git a/servlet/xml/java/contacts/src/site/resources/sslhowto.txt b/servlet/xml/java/contacts/src/site/resources/sslhowto.txt new file mode 100644 index 0000000..9e073a6 --- /dev/null +++ b/servlet/xml/java/contacts/src/site/resources/sslhowto.txt @@ -0,0 +1,99 @@ +$Id$ + +CAS requires HTTPS be used for all operations, with the certificate used +having been signed by a certificate in the cacerts files shipped with Java. + +If you're using a HTTPS certificate signed by a well known authority +(like Verisign), you can safely ignore the procedure below (although you +might find the troubleshooting section at the end helpful). + +The following demonstrates how to create a self-signed certificate and add +it to the cacerts file. If you just want to use the certificate we have +already created and shipped with Spring Security, you +can skip directly to step 3. + + +1. keytool -keystore keystore -alias acegisecurity -genkey -keyalg RSA -validity 9999 -storepass password -keypass password + +What is your first and last name? + [Unknown]: localhost +What is the name of your organizational unit? + [Unknown]: Spring Security +What is the name of your organization? + [Unknown]: TEST CERTIFICATE ONLY. DO NOT USE IN PRODUCTION. +What is the name of your City or Locality? + [Unknown]: +What is the name of your State or Province? + [Unknown]: +What is the two-letter country code for this unit? + [Unknown]: +Is CN=localhost, OU=Spring Security, O=TEST CERTIFICATE ONLY. D +O NOT USE IN PRODUCTION., L=Unknown, ST=Unknown, C=Unknown correct? + [no]: yes + + +2. keytool -export -v -rfc -alias acegisecurity -file acegisecurity.txt -keystore keystore -storepass password + +3. copy acegisecurity.txt %JAVA_HOME%\lib\security + +4. copy keystore %YOUR_WEB_CONTAINER_LOCATION% + + NOTE: You will need to configure your web container as appropriate. + We recommend you test the certificate works by visiting + https://localhost:8443. When prompted by your browser, select to + install the certificate. + +5. cd %JAVA_HOME%\lib\security + +6. keytool -import -v -file acegisecurity.txt -keypass password -keystore cacerts -storepass changeit -alias acegisecurity + +Owner: CN=localhost, OU=Spring Security, O=TEST CERTIFICATE ONL +Y. DO NOT USE IN PRODUCTION., L=Unknown, ST=Unknown, C=Unknown +Issuer: CN=localhost, OU=Spring Security, O=TEST CERTIFICATE ON +LY. DO NOT USE IN PRODUCTION., L=Unknown, ST=Unknown, C=Unknown +Serial number: 4080daf4 +Valid from: Sat Apr 17 07:21:24 GMT 2004 until: Tue Sep 02 07:21:24 GMT 2031 +Certificate fingerprints: + MD5: B4:AC:A8:24:34:99:F1:A9:F8:1D:A5:6C:BF:0A:34:FA + SHA1: F1:E6:B1:3A:01:39:2D:CF:06:FA:82:AB:86:0D:77:9D:06:93:D6:B0 +Trust this certificate? [no]: yes +Certificate was added to keystore +[Saving cacerts] + + +7. Finished. You can now run the sample application as if you purchased a + properly signed certificate. For production applications, of course you should + use an appropriately signed certificate so your web visitors will trust it + (such as issued by Thawte, Verisign etc). + +TROUBLESHOOTING + +* First of all, most CAS-Acegi Security problems are because of untrusted + SSL certificates. So it's important to understand why. Most people can + load the Acegi Security webapp, get redirected to the CAS server, then + after login they get redirected back to the Acegi Security webapp and + receive a failure. This is because the CAS server redirects to something + like https://server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ + which causes the "service ticket" (the "ticket" parameter) to be validated. + net.sf.acegisecurity.providers.cas.ticketvalidator.CasProxyTicketValidator + performs service ticket validation by delegation to CAS' + ProxyTicketValidator class. The ProxyTicketValidator class will perform a + HTTPS connection from the web server running the Acegi Security webapp + (server3.company.com) above to the CAS server. If for some reason the + web server keystore does not trust the HTTPS certificate presented by the + CAS server, you will receive various failures as discussed below. NB: This + has NOTHING to do with client-side (browser) certificates. You need to + correct the trust between the two webserver keystores alone. + +* A "sun.security.validator.ValidatorException: No trusted certificate + found" indicates the cacerts is not being used or it did not correctly + import the certificate. To rule out your web container replacing or in + some way modifying the trust manager, set the + CasProxyTicketValidator.trustStore property to the full file system + location to your cacerts file. + +* If your web container is ignoring your cacerts file, double-check it + is stored in $JAVA_HOME\lib\security\cacerts. $JAVA_HOME might be + pointing to the SDK, not JRE. In that case, copy + $JAVA_HOME\jre\lib\security\cacerts to $JAVA_HOME\lib\security\cacerts + diff --git a/servlet/xml/java/contacts/src/test/java/sample/contact/ContactManagerTests.java b/servlet/xml/java/contacts/src/test/java/sample/contact/ContactManagerTests.java new file mode 100644 index 0000000..47999e2 --- /dev/null +++ b/servlet/xml/java/contacts/src/test/java/sample/contact/ContactManagerTests.java @@ -0,0 +1,166 @@ +/* + * Copyright 2002-2021 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.contact; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.acls.domain.BasePermission; +import org.springframework.security.acls.domain.PrincipalSid; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * Tests {@link ContactManager}. + * + * @author David Leal + * @author Ben Alex + * @author Luke Taylor + */ +@ContextConfiguration(locations = { "/applicationContext-security.xml", "/applicationContext-common-authorization.xml", + "/applicationContext-common-business.xml" }) +@SpringJUnitWebConfig +public class ContactManagerTests { + + @Autowired + protected ContactManager contactManager; + + void assertContainsContact(long id, List contacts) { + for (Contact contact : contacts) { + if (contact.getId().equals(id)) { + return; + } + } + + fail("List of contacts should have contained: " + id); + } + + void assertDoestNotContainContact(long id, List contacts) { + for (Contact contact : contacts) { + if (contact.getId().equals(id)) { + fail("List of contact should NOT (but did) contain: " + id); + } + } + } + + /** + * Locates the first Contact of the exact name specified. + *

+ * Uses the {@link ContactManager#getAll()} method. + * @param id Identify of the contact to locate (must be an exact match) + * @return the domain or null if not found + */ + Contact getContact(String id) { + for (Contact contact : this.contactManager.getAll()) { + if (contact.getId().equals(id)) { + return contact; + } + } + + return null; + } + + private void makeActiveUser(String username) { + String password = ""; + + if ("rod".equals(username)) { + password = "koala"; + } + else if ("dianne".equals(username)) { + password = "emu"; + } + else if ("scott".equals(username)) { + password = "wombat"; + } + else if ("peter".equals(username)) { + password = "opal"; + } + + Authentication authRequest = new UsernamePasswordAuthenticationToken(username, password); + SecurityContextHolder.getContext().setAuthentication(authRequest); + } + + @AfterEach + void clearContext() { + SecurityContextHolder.clearContext(); + } + + @Test + void testDianne() { + makeActiveUser("dianne"); // has ROLE_USER + + List contacts = this.contactManager.getAll(); + assertThat(contacts).hasSize(4); + + assertContainsContact(4, contacts); + assertContainsContact(5, contacts); + assertContainsContact(6, contacts); + assertContainsContact(8, contacts); + + assertDoestNotContainContact(1, contacts); + assertDoestNotContainContact(2, contacts); + assertDoestNotContainContact(3, contacts); + } + + @Test + void testrod() { + makeActiveUser("rod"); // has ROLE_SUPERVISOR + + List contacts = this.contactManager.getAll(); + + assertThat(contacts).hasSize(4); + + assertContainsContact(1, contacts); + assertContainsContact(2, contacts); + assertContainsContact(3, contacts); + assertContainsContact(4, contacts); + + assertDoestNotContainContact(5, contacts); + + Contact c1 = this.contactManager.getById(4L); + + this.contactManager.deletePermission(c1, new PrincipalSid("bob"), BasePermission.ADMINISTRATION); + this.contactManager.addPermission(c1, new PrincipalSid("bob"), BasePermission.ADMINISTRATION); + } + + @Test + void testScott() { + makeActiveUser("scott"); // has ROLE_USER + + List contacts = this.contactManager.getAll(); + + assertThat(contacts).hasSize(5); + + assertContainsContact(4, contacts); + assertContainsContact(6, contacts); + assertContainsContact(7, contacts); + assertContainsContact(8, contacts); + assertContainsContact(9, contacts); + + assertDoestNotContainContact(1, contacts); + } + +} diff --git a/servlet/xml/java/contacts/src/test/resources/logback-test.xml b/servlet/xml/java/contacts/src/test/resources/logback-test.xml new file mode 100644 index 0000000..aa69aab --- /dev/null +++ b/servlet/xml/java/contacts/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 90fc343..45fc616 100644 --- a/settings.gradle +++ b/settings.gradle @@ -56,3 +56,4 @@ include ":servlet:spring-boot:java:saml2-login" include ":servlet:spring-boot:kotlin:hello-security" include ":servlet:xml:java:helloworld" include ":servlet:xml:java:preauth" +include ":servlet:xml:java:contacts"