diff --git a/README.adoc b/README.adoc index 3ee66f6..d479d83 100644 --- a/README.adoc +++ b/README.adoc @@ -2,3 +2,6 @@ This repository contains the source code for the InfoQ mini-book 'Practical Guide to Building an API Back End with Spring Boot'. See https://www.infoq.com/minibooks/spring-boot-building-api-backend for the free download of the book. +This branch contains the sources for the latest version of the book, targeting Spring Boot 3. + +If you want to view the sources for the previous edition of the book targeting Spring Boot 2, see the https://github.com/wimdeblauwe/spring-boot-building-api-backend/tree/release/1.x[release/1.x] branch. diff --git a/chapter02/01 - Generated project/.gitignore b/chapter02/01 - Generated project/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter02/01 - Generated project/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter02/01 - Generated project/.mvn/wrapper/maven-wrapper.jar b/chapter02/01 - Generated project/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter02/01 - Generated project/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter02/01 - Generated project/.mvn/wrapper/maven-wrapper.properties b/chapter02/01 - Generated project/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter02/01 - Generated project/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter02/01 - Generated project/mvnw b/chapter02/01 - Generated project/mvnw index 5bf251c..66df285 100755 --- a/chapter02/01 - Generated project/mvnw +++ b/chapter02/01 - Generated project/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter02/01 - Generated project/mvnw.cmd b/chapter02/01 - Generated project/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter02/01 - Generated project/mvnw.cmd +++ b/chapter02/01 - Generated project/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter02/01 - Generated project/pom.xml b/chapter02/01 - Generated project/pom.xml index d3f0867..747977a 100644 --- a/chapter02/01 - Generated project/pom.xml +++ b/chapter02/01 - Generated project/pom.xml @@ -1,29 +1,21 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - - com.springbook - application - 0.0.1-SNAPSHOT - jar - - application - Demo project for Spring Boot - org.springframework.boot spring-boot-starter-parent - 2.1.4.RELEASE + 3.2.2 - + com.springbook + application + 0.0.1-SNAPSHOT + application + Demo project for Spring Boot - UTF-8 - UTF-8 - 1.8 + 17 - @@ -32,11 +24,6 @@ - - org.projectlombok - lombok - true - org.springframework.boot spring-boot-starter-test @@ -53,5 +40,4 @@ - diff --git a/chapter02/01 - Generated project/src/main/resources/application.properties b/chapter02/01 - Generated project/src/main/resources/application.properties index e69de29..8b13789 100644 --- a/chapter02/01 - Generated project/src/main/resources/application.properties +++ b/chapter02/01 - Generated project/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/chapter02/01 - Generated project/src/test/java/com/springbook/application/ApplicationTests.java b/chapter02/01 - Generated project/src/test/java/com/springbook/application/ApplicationTests.java index 618c39d..ee8ff1a 100644 --- a/chapter02/01 - Generated project/src/test/java/com/springbook/application/ApplicationTests.java +++ b/chapter02/01 - Generated project/src/test/java/com/springbook/application/ApplicationTests.java @@ -1,16 +1,13 @@ package com.springbook.application; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -public class ApplicationTests { +class ApplicationTests { @Test - public void contextLoads() { + void contextLoads() { } } diff --git a/chapter02/02 - Logging config/.gitignore b/chapter02/02 - Logging config/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter02/02 - Logging config/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter02/02 - Logging config/.mvn/wrapper/maven-wrapper.jar b/chapter02/02 - Logging config/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter02/02 - Logging config/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter02/02 - Logging config/.mvn/wrapper/maven-wrapper.properties b/chapter02/02 - Logging config/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter02/02 - Logging config/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter02/02 - Logging config/mvnw b/chapter02/02 - Logging config/mvnw index 5bf251c..66df285 100755 --- a/chapter02/02 - Logging config/mvnw +++ b/chapter02/02 - Logging config/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter02/02 - Logging config/mvnw.cmd b/chapter02/02 - Logging config/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter02/02 - Logging config/mvnw.cmd +++ b/chapter02/02 - Logging config/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter02/02 - Logging config/pom.xml b/chapter02/02 - Logging config/pom.xml index 3524c80..fb6a34c 100644 --- a/chapter02/02 - Logging config/pom.xml +++ b/chapter02/02 - Logging config/pom.xml @@ -1,42 +1,29 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - - com.springbook - application - 0.0.1-SNAPSHOT - jar - - application - Demo project for Spring Boot - org.springframework.boot spring-boot-starter-parent - 2.1.4.RELEASE + 3.2.2 - + com.springbook + application + 0.0.1-SNAPSHOT + application + Demo project for Spring Boot - UTF-8 - UTF-8 - 1.8 + 17 - - + org.springframework.boot spring-boot-starter-web - + - - org.projectlombok - lombok - true - org.springframework.boot spring-boot-starter-test @@ -63,5 +50,4 @@ - diff --git a/chapter02/02 - Logging config/src/test/java/com/springbook/application/ApplicationTests.java b/chapter02/02 - Logging config/src/test/java/com/springbook/application/ApplicationTests.java index 618c39d..ee8ff1a 100644 --- a/chapter02/02 - Logging config/src/test/java/com/springbook/application/ApplicationTests.java +++ b/chapter02/02 - Logging config/src/test/java/com/springbook/application/ApplicationTests.java @@ -1,16 +1,13 @@ package com.springbook.application; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -public class ApplicationTests { +class ApplicationTests { @Test - public void contextLoads() { + void contextLoads() { } } diff --git a/chapter03/01 - Generated project/.gitignore b/chapter03/01 - Generated project/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter03/01 - Generated project/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter03/01 - Generated project/.mvn/wrapper/maven-wrapper.jar b/chapter03/01 - Generated project/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter03/01 - Generated project/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter03/01 - Generated project/.mvn/wrapper/maven-wrapper.properties b/chapter03/01 - Generated project/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter03/01 - Generated project/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter03/01 - Generated project/mvnw b/chapter03/01 - Generated project/mvnw index 5bf251c..66df285 100755 --- a/chapter03/01 - Generated project/mvnw +++ b/chapter03/01 - Generated project/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter03/01 - Generated project/mvnw.cmd b/chapter03/01 - Generated project/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter03/01 - Generated project/mvnw.cmd +++ b/chapter03/01 - Generated project/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter03/01 - Generated project/pom.xml b/chapter03/01 - Generated project/pom.xml index 611e34d..61e2c35 100644 --- a/chapter03/01 - Generated project/pom.xml +++ b/chapter03/01 - Generated project/pom.xml @@ -1,50 +1,46 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar - - copsboot - Demo project for Spring Boot - org.springframework.boot spring-boot-starter-parent - 2.1.4.RELEASE + 3.2.2 - + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot - UTF-8 - UTF-8 - 1.8 + 17 org.springframework.boot - spring-boot-starter-data-jpa + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security org.springframework.boot - spring-boot-starter-security + spring-boot-starter-validation org.springframework.boot - spring-boot-starter-web + spring-boot-starter-web com.h2database - h2 + h2 runtime - org.springframework.boot spring-boot-starter-test @@ -52,7 +48,7 @@ org.springframework.security - spring-security-test + spring-security-test test @@ -67,5 +63,4 @@ - diff --git a/chapter03/01 - Generated project/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter03/01 - Generated project/src/main/java/com/example/copsboot/CopsbootApplication.java index 5774a17..7b031d7 100644 --- a/chapter03/01 - Generated project/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter03/01 - Generated project/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -9,4 +9,5 @@ public class CopsbootApplication { public static void main(String[] args) { SpringApplication.run(CopsbootApplication.class, args); } + } diff --git a/chapter03/01 - Generated project/src/main/resources/application.properties b/chapter03/01 - Generated project/src/main/resources/application.properties index e69de29..8b13789 100644 --- a/chapter03/01 - Generated project/src/main/resources/application.properties +++ b/chapter03/01 - Generated project/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/chapter03/01 - Generated project/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter03/01 - Generated project/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index a9797c6..73e7b68 100644 --- a/chapter03/01 - Generated project/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter03/01 - Generated project/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,16 +1,13 @@ package com.example.copsboot; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -public class CopsbootApplicationTests { +class CopsbootApplicationTests { @Test - public void contextLoads() { + void contextLoads() { } } diff --git a/chapter04/01 - User domain/.gitignore b/chapter04/01 - User domain/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter04/01 - User domain/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter04/01 - User domain/.mvn/wrapper/maven-wrapper.jar b/chapter04/01 - User domain/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter04/01 - User domain/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter04/01 - User domain/.mvn/wrapper/maven-wrapper.properties b/chapter04/01 - User domain/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter04/01 - User domain/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter04/01 - User domain/mvnw b/chapter04/01 - User domain/mvnw index 5bf251c..66df285 100755 --- a/chapter04/01 - User domain/mvnw +++ b/chapter04/01 - User domain/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter04/01 - User domain/mvnw.cmd b/chapter04/01 - User domain/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter04/01 - User domain/mvnw.cmd +++ b/chapter04/01 - User domain/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter04/01 - User domain/pom.xml b/chapter04/01 - User domain/pom.xml index 7178a28..c3c6b3d 100644 --- a/chapter04/01 - User domain/pom.xml +++ b/chapter04/01 - User domain/pom.xml @@ -1,42 +1,39 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar - - copsboot - Demo project for Spring Boot - org.springframework.boot spring-boot-starter-parent - 2.1.4.RELEASE + 3.2.2 - + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot - UTF-8 - UTF-8 - 1.8 + 17 - + org.springframework.boot - spring-boot-starter-data-jpa + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security org.springframework.boot - spring-boot-starter-security + spring-boot-starter-validation org.springframework.boot - spring-boot-starter-web + spring-boot-starter-web @@ -44,7 +41,6 @@ h2 runtime - org.springframework.boot spring-boot-starter-test @@ -52,11 +48,11 @@ org.springframework.security - spring-security-test + spring-security-test test - + @@ -67,5 +63,4 @@ - diff --git a/chapter04/01 - User domain/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter04/01 - User domain/src/main/java/com/example/copsboot/CopsbootApplication.java index 5774a17..7b031d7 100644 --- a/chapter04/01 - User domain/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter04/01 - User domain/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -9,4 +9,5 @@ public class CopsbootApplication { public static void main(String[] args) { SpringApplication.run(CopsbootApplication.class, args); } + } diff --git a/chapter04/01 - User domain/src/main/resources/application.properties b/chapter04/01 - User domain/src/main/resources/application.properties index e69de29..8b13789 100644 --- a/chapter04/01 - User domain/src/main/resources/application.properties +++ b/chapter04/01 - User domain/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/chapter04/01 - User domain/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter04/01 - User domain/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index a9797c6..73e7b68 100644 --- a/chapter04/01 - User domain/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter04/01 - User domain/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,16 +1,13 @@ package com.example.copsboot; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -public class CopsbootApplicationTests { +class CopsbootApplicationTests { @Test - public void contextLoads() { + void contextLoads() { } } diff --git a/chapter04/02 - User with JPA/.gitignore b/chapter04/02 - User with JPA/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter04/02 - User with JPA/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter04/02 - User with JPA/.mvn/wrapper/maven-wrapper.jar b/chapter04/02 - User with JPA/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter04/02 - User with JPA/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter04/02 - User with JPA/.mvn/wrapper/maven-wrapper.properties b/chapter04/02 - User with JPA/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter04/02 - User with JPA/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter04/02 - User with JPA/mvnw b/chapter04/02 - User with JPA/mvnw index 5bf251c..66df285 100755 --- a/chapter04/02 - User with JPA/mvnw +++ b/chapter04/02 - User with JPA/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter04/02 - User with JPA/mvnw.cmd b/chapter04/02 - User with JPA/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter04/02 - User with JPA/mvnw.cmd +++ b/chapter04/02 - User with JPA/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter04/02 - User with JPA/pom.xml b/chapter04/02 - User with JPA/pom.xml index 5d116ae..9497e89 100644 --- a/chapter04/02 - User with JPA/pom.xml +++ b/chapter04/02 - User with JPA/pom.xml @@ -1,79 +1,68 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + 17 + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - - com.h2database - h2 - runtime - - - - - org.assertj - assertj-core - test - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + diff --git a/chapter04/02 - User with JPA/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter04/02 - User with JPA/src/main/java/com/example/copsboot/CopsbootApplication.java index 5774a17..7b031d7 100644 --- a/chapter04/02 - User with JPA/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter04/02 - User with JPA/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -9,4 +9,5 @@ public class CopsbootApplication { public static void main(String[] args) { SpringApplication.run(CopsbootApplication.class, args); } + } diff --git a/chapter04/02 - User with JPA/src/main/java/com/example/copsboot/user/User.java b/chapter04/02 - User with JPA/src/main/java/com/example/copsboot/user/User.java index ef1f991..21d4368 100644 --- a/chapter04/02 - User with JPA/src/main/java/com/example/copsboot/user/User.java +++ b/chapter04/02 - User with JPA/src/main/java/com/example/copsboot/user/User.java @@ -1,8 +1,14 @@ //tag::annotations-part[] package com.example.copsboot.user; -import javax.persistence.*; -import javax.validation.constraints.NotNull; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; import java.util.Set; import java.util.UUID; diff --git a/chapter04/02 - User with JPA/src/main/resources/application.properties b/chapter04/02 - User with JPA/src/main/resources/application.properties index e69de29..8b13789 100644 --- a/chapter04/02 - User with JPA/src/main/resources/application.properties +++ b/chapter04/02 - User with JPA/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/chapter04/02 - User with JPA/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter04/02 - User with JPA/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index a9797c6..73e7b68 100644 --- a/chapter04/02 - User with JPA/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter04/02 - User with JPA/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,16 +1,13 @@ package com.example.copsboot; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -public class CopsbootApplicationTests { +class CopsbootApplicationTests { @Test - public void contextLoads() { + void contextLoads() { } } diff --git a/chapter04/02 - User with JPA/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter04/02 - User with JPA/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index 65ccaad..f3d8176 100644 --- a/chapter04/02 - User with JPA/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter04/02 - User with JPA/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -1,7 +1,6 @@ package com.example.copsboot.user; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.test.context.junit4.SpringRunner; @@ -11,23 +10,22 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) //<1> -@DataJpaTest //<2> +@DataJpaTest // <.> public class UserRepositoryTest { @Autowired - private UserRepository repository; //<3> + private UserRepository repository; // <.> @Test - public void testStoreUser() { //<4> + public void testStoreUser() { // <.> HashSet roles = new HashSet<>(); roles.add(UserRole.OFFICER); - User user = repository.save(new User(UUID.randomUUID(), //<5> + User user = repository.save(new User(UUID.randomUUID(), // <.> "alex.foley@beverly-hills.com", "my-secret-pwd", roles)); - assertThat(user).isNotNull(); //<6> + assertThat(user).isNotNull(); // <.> - assertThat(repository.count()).isEqualTo(1L); //<7> + assertThat(repository.count()).isEqualTo(1L); // <.> } -} \ No newline at end of file +} diff --git a/chapter04/03 - User with JPA refactored/.gitignore b/chapter04/03 - User with JPA refactored/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter04/03 - User with JPA refactored/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter04/03 - User with JPA refactored/.mvn/wrapper/maven-wrapper.jar b/chapter04/03 - User with JPA refactored/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter04/03 - User with JPA refactored/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter04/03 - User with JPA refactored/.mvn/wrapper/maven-wrapper.properties b/chapter04/03 - User with JPA refactored/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter04/03 - User with JPA refactored/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter04/03 - User with JPA refactored/mvnw b/chapter04/03 - User with JPA refactored/mvnw index 5bf251c..66df285 100755 --- a/chapter04/03 - User with JPA refactored/mvnw +++ b/chapter04/03 - User with JPA refactored/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter04/03 - User with JPA refactored/mvnw.cmd b/chapter04/03 - User with JPA refactored/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter04/03 - User with JPA refactored/mvnw.cmd +++ b/chapter04/03 - User with JPA refactored/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter04/03 - User with JPA refactored/pom.xml b/chapter04/03 - User with JPA refactored/pom.xml index c584541..d13fbb7 100644 --- a/chapter04/03 - User with JPA refactored/pom.xml +++ b/chapter04/03 - User with JPA refactored/pom.xml @@ -1,87 +1,76 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + + + com.google.guava + guava + ${guava.version} + - copsboot - Demo project for Spring Boot + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - 29.0-jre - - - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-web - - - com.google.guava - guava - ${guava.version} - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - - com.h2database - h2 - runtime - - - - - org.assertj - assertj-core - test - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + diff --git a/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/CopsbootApplication.java index f360096..7b031d7 100644 --- a/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,12 +1,7 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; - -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { @@ -15,10 +10,4 @@ public static void main(String[] args) { SpringApplication.run(CopsbootApplication.class, args); } - //tag::unique-id-generator[] - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - //end::unique-id-generator[] } diff --git a/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..2db0e8c --- /dev/null +++ b/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,16 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import java.util.UUID; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } +} diff --git a/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/user/User.java b/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/user/User.java index af92199..68b1cbd 100644 --- a/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/user/User.java +++ b/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/user/User.java @@ -2,8 +2,14 @@ import com.example.orm.jpa.AbstractEntity; -import javax.persistence.*; -import javax.validation.constraints.NotNull; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; import java.util.Set; import java.util.UUID; diff --git a/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/user/UserRepository.java index e5b6d1f..5109326 100644 --- a/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter04/03 - User with JPA refactored/src/main/java/com/example/copsboot/user/UserRepository.java @@ -4,6 +4,6 @@ import java.util.UUID; //tag::class[] -public interface UserRepository extends CrudRepository, UserRepositoryCustom { +public interface UserRepository extends CrudRepository, UserRepositoryCustom { } //end::class[] diff --git a/chapter04/03 - User with JPA refactored/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter04/03 - User with JPA refactored/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter04/03 - User with JPA refactored/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter04/03 - User with JPA refactored/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter04/03 - User with JPA refactored/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter04/03 - User with JPA refactored/src/main/java/com/example/orm/jpa/AbstractEntityId.java index db748ae..4902f41 100755 --- a/chapter04/03 - User with JPA refactored/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter04/03 - User with JPA refactored/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter04/03 - User with JPA refactored/src/main/resources/application.properties b/chapter04/03 - User with JPA refactored/src/main/resources/application.properties index e69de29..8b13789 100644 --- a/chapter04/03 - User with JPA refactored/src/main/resources/application.properties +++ b/chapter04/03 - User with JPA refactored/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/chapter04/03 - User with JPA refactored/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter04/03 - User with JPA refactored/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index a9797c6..73e7b68 100644 --- a/chapter04/03 - User with JPA refactored/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter04/03 - User with JPA refactored/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,16 +1,13 @@ package com.example.copsboot; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -public class CopsbootApplicationTests { +class CopsbootApplicationTests { @Test - public void contextLoads() { + void contextLoads() { } } diff --git a/chapter04/03 - User with JPA refactored/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter04/03 - User with JPA refactored/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index c525776..6260f64 100644 --- a/chapter04/03 - User with JPA refactored/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter04/03 - User with JPA refactored/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -2,8 +2,7 @@ import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; @@ -15,7 +14,6 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest public class UserRepositoryTest { @@ -46,4 +44,4 @@ public UniqueIdGenerator generator() { } } //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter05/01 - Oauth2/.gitignore b/chapter05/01 - Oauth2/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter05/01 - Oauth2/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter05/01 - Oauth2/.mvn/wrapper/maven-wrapper.jar b/chapter05/01 - Oauth2/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter05/01 - Oauth2/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter05/01 - Oauth2/.mvn/wrapper/maven-wrapper.properties b/chapter05/01 - Oauth2/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter05/01 - Oauth2/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter05/01 - Oauth2/docker-compose.yaml b/chapter05/01 - Oauth2/docker-compose.yaml new file mode 100644 index 0000000..7f8428d --- /dev/null +++ b/chapter05/01 - Oauth2/docker-compose.yaml @@ -0,0 +1,14 @@ +version: '3' +services: + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter05/01 - Oauth2/mvnw b/chapter05/01 - Oauth2/mvnw index 5bf251c..66df285 100755 --- a/chapter05/01 - Oauth2/mvnw +++ b/chapter05/01 - Oauth2/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter05/01 - Oauth2/mvnw.cmd b/chapter05/01 - Oauth2/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter05/01 - Oauth2/mvnw.cmd +++ b/chapter05/01 - Oauth2/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter05/01 - Oauth2/pom.xml b/chapter05/01 - Oauth2/pom.xml index c293efc..912a5ae 100644 --- a/chapter05/01 - Oauth2/pom.xml +++ b/chapter05/01 - Oauth2/pom.xml @@ -1,94 +1,83 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + - - - UTF-8 - UTF-8 - 1.8 - - 29.0-jre - - - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - - org.springframework.boot - spring-boot-starter-web - - - com.google.guava - guava - ${guava.version} - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - - com.h2database - h2 - runtime - - - - - org.assertj - assertj-core - test - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 0313f8b..0000000 --- a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { //<1> - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index 7e03add..0000000 --- a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - //tag::resource-server[] - @Configuration - @EnableResourceServer //<1> - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() //<2> - .and() - .antMatcher("/api/**").authorizeRequests() - .anyRequest().authenticated(); //<3> - } - } - //end::resource-server[] - - //tag::authorization-server[] - @Configuration - @EnableAuthorizationServer //<1> - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; //<2> - - @Autowired - private PasswordEncoder passwordEncoder; //<3> - - @Autowired - private TokenStore tokenStore; //<4> - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); //<3> - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() // <5> - .withClient("copsboot-mobile-client") //<6> - .authorizedGrantTypes("password", "refresh_token") //<7> - .scopes("mobile_app") //<8> - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode("ccUyb6vS4S8nxfbKPCrN")); //<9> - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - //end::authorization-server[] - - //tag::web-security[] - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } - //end::web-security[] -} diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..3fd01ba --- /dev/null +++ b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,24 @@ +package com.example.copsboot.infrastructure.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class WebSecurityConfiguration { + @Bean + SecurityFilterChain configureSecurityFilterChain(HttpSecurity http) throws Exception { + + http + .authorizeHttpRequests(authorizeRequests -> authorizeRequests + .requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() //<.> + .requestMatchers("/api/**").authenticated() //<.> + .anyRequest().authenticated()) + .oauth2ResourceServer(it -> it.jwt(Customizer.withDefaults())); //<.> + + return http.build(); + } +} diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/User.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/User.java index 236cd6d..e6cf8d2 100644 --- a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/User.java +++ b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/User.java @@ -1,13 +1,11 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; -import javax.persistence.*; -import javax.validation.constraints.NotNull; import java.util.Set; - @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { @@ -32,11 +30,11 @@ public User(UserId id, String email, String password, Set roles) { } public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); + return new User(userId, email, encodedPassword, Set.of(UserRole.OFFICER)); } public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + return new User(userId, email, encodedPassword, Set.of(UserRole.CAPTAIN)); } public String getEmail() { diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserRepository.java index 37eda97..c72d5e4 100644 --- a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,8 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] -public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); +public interface UserRepository extends CrudRepository, UserRepositoryCustom { } //end::class[] diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserService.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserService.java deleted file mode 100644 index b87df7e..0000000 --- a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.copsboot.user; - -public interface UserService { - User createOfficer(String email, String password); -} diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 6d45ead..0000000 --- a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } -} diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/web/UserRestController.java new file mode 100644 index 0000000..4dd76a9 --- /dev/null +++ b/chapter05/01 - Oauth2/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user.web; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +@RequestMapping("/api/users") +public class UserRestController { + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + return Map.of("subject", jwt.getSubject(), //<.> + "claims", jwt.getClaims()); + } +} diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/Entity.java b/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/Entity.java index 96cadf0..3a45231 100644 --- a/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter05/01 - Oauth2/src/main/java/com/example/orm/jpa/Entity.java @@ -1,5 +1,6 @@ package com.example.orm.jpa; + /** * Interface for entity objects. * diff --git a/chapter05/01 - Oauth2/src/main/resources/application.properties b/chapter05/01 - Oauth2/src/main/resources/application.properties index e69de29..27301ca 100644 --- a/chapter05/01 - Oauth2/src/main/resources/application.properties +++ b/chapter05/01 - Oauth2/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/copsboot diff --git a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index a9797c6..73e7b68 100644 --- a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,16 +1,13 @@ package com.example.copsboot; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -public class CopsbootApplicationTests { +class CopsbootApplicationTests { @Test - public void contextLoads() { + void contextLoads() { } } diff --git a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 41f8990..0000000 --- a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - service.loadUserByUsername("i@donotexist.com"); - } -} \ No newline at end of file diff --git a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 1d79252..0000000 --- a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "copsboot-mobile-client"; - String clientSecret = "ccUyb6vS4S8nxfbKPCrN"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index ad7aa55..c349713 100644 --- a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -2,13 +2,11 @@ import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -17,7 +15,6 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest public class UserRepositoryTest { @@ -29,48 +26,16 @@ public class UserRepositoryTest { public void testStoreUser() { HashSet roles = new HashSet<>(); roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> + User user = repository.save(new User(repository.nextId(), "alex.foley@beverly-hills.com", "my-secret-pwd", roles)); - assertThat(user).isNotNull(); //<6> + assertThat(user).isNotNull(); - assertThat(repository.count()).isEqualTo(1L); //<7> + assertThat(repository.count()).isEqualTo(1L); } //end::testStoreUser[] - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); - - assertThat(optional).isEmpty(); - } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @@ -80,4 +45,4 @@ public UniqueIdGenerator generator() { } } //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/Users.java b/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 557cd04..0000000 --- a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } -} diff --git a/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java new file mode 100644 index 0000000..8f6b763 --- /dev/null +++ b/chapter05/01 - Oauth2/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -0,0 +1,35 @@ +package com.example.copsboot.user.web; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(UserRestController.class) //<.> +class UserRestControllerTest { + + @Autowired + private MockMvc mockMvc; //<.> + + @Test + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) //<.> + .andExpect(status().isUnauthorized()); //<.> + } + + @Test + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + mockMvc.perform(get("/api/users/me") + .with(jwt())) //<.> + .andExpect(status().isOk()) //<.> + .andExpect(jsonPath("subject").value("user")) //<.> + .andExpect(jsonPath("claims").isMap()) //<.> + .andDo(print()); //<.> + } +} diff --git a/chapter05/02 - Oauth configurable/mvnw b/chapter05/02 - Oauth configurable/mvnw deleted file mode 100755 index 5bf251c..0000000 --- a/chapter05/02 - Oauth configurable/mvnw +++ /dev/null @@ -1,225 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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 -# -# http://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. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Migwn, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - 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 - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter05/02 - Oauth configurable/pom.xml b/chapter05/02 - Oauth configurable/pom.xml deleted file mode 100644 index f1f923a..0000000 --- a/chapter05/02 - Oauth configurable/pom.xml +++ /dev/null @@ -1,95 +0,0 @@ - - - 4.0.0 - - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar - - copsboot - Demo project for Spring Boot - - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - 29.0-jre - - - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - com.google.guava - guava - ${guava.version} - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - - diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/CopsbootApplication.java deleted file mode 100644 index f4e3307..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/CopsbootApplication.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.copsboot; - -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; - -@SpringBootApplication -public class CopsbootApplication { - - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] -} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index 04353db..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - //tag::resource-server[] - @Configuration - @EnableResourceServer //<1> - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() //<2> - .and() - .antMatcher("/api/**").authorizeRequests() - .anyRequest().authenticated(); //<3> - } - } - //end::resource-server[] - - //tag::authorization-server[] - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; //<1> - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) //<2> - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); //<3> - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - //end::authorization-server[] - - //tag::web-security[] - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } - //end::web-security[] -} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/User.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/User.java deleted file mode 100644 index 236cd6d..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/User.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.copsboot.user; - -import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - - -@Entity -@Table(name = "copsboot_user") -public class User extends AbstractEntity { - - private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; - - protected User() { - - } - - public User(UserId id, String email, String password, Set roles) { - super(id); - this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); - } - - public String getEmail() { - return email; - } - - public String getPassword() { - return password; - } - - public Set getRoles() { - return roles; - } -} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserRepository.java deleted file mode 100644 index 37eda97..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.data.repository.CrudRepository; - -import java.util.Optional; -import java.util.UUID; -//tag::class[] -public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); -} -//end::class[] diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserService.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserService.java deleted file mode 100644 index b87df7e..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.copsboot.user; - -public interface UserService { - User createOfficer(String email, String password); -} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 6d45ead..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } -} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/Entity.java b/chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/Entity.java deleted file mode 100644 index 96cadf0..0000000 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/Entity.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.orm.jpa; - -/** - * Interface for entity objects. - * - * @param the type of {@link EntityId} that will be used in this entity - */ -public interface Entity { - - T getId(); -} diff --git a/chapter05/02 - Oauth configurable/src/main/resources/application-dev.properties b/chapter05/02 - Oauth configurable/src/main/resources/application-dev.properties deleted file mode 100644 index 819196a..0000000 --- a/chapter05/02 - Oauth configurable/src/main/resources/application-dev.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN \ No newline at end of file diff --git a/chapter05/02 - Oauth configurable/src/main/resources/application.properties b/chapter05/02 - Oauth configurable/src/main/resources/application.properties deleted file mode 100644 index e69de29..0000000 diff --git a/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/CopsbootApplicationTests.java deleted file mode 100644 index add5a9b..0000000 --- a/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -@RunWith(SpringRunner.class) -@SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { - - @Test - public void contextLoads() { - } - -} diff --git a/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/user/UserRepositoryTest.java deleted file mode 100644 index ad7aa55..0000000 --- a/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.example.copsboot.user; - -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.test.context.junit4.SpringRunner; - -import java.util.HashSet; -import java.util.Locale; -import java.util.Optional; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@DataJpaTest -public class UserRepositoryTest { - - @Autowired - private UserRepository repository; - - //tag::testStoreUser[] - @Test - public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> - - assertThat(repository.count()).isEqualTo(1L); //<7> - } - //end::testStoreUser[] - - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); - - assertThat(optional).isEmpty(); - } - //end::find-by-email-tests[] - - //tag::testconfig[] - @TestConfiguration - static class TestConfig { - @Bean - public UniqueIdGenerator generator() { - return new InMemoryUniqueIdGenerator(); - } - } - //end::testconfig[] -} \ No newline at end of file diff --git a/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/user/Users.java b/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 557cd04..0000000 --- a/chapter05/02 - Oauth configurable/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } -} diff --git a/chapter05/02 - Oauth configurable/src/test/resources/application-test.properties b/chapter05/02 - Oauth configurable/src/test/resources/application-test.properties deleted file mode 100644 index 78c3fdb..0000000 --- a/chapter05/02 - Oauth configurable/src/test/resources/application-test.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret \ No newline at end of file diff --git a/chapter06/01 - User rest controller/.gitignore b/chapter06/01 - User rest controller/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter06/01 - User rest controller/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter06/01 - User rest controller/.mvn/wrapper/maven-wrapper.jar b/chapter06/01 - User rest controller/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter06/01 - User rest controller/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter06/01 - User rest controller/.mvn/wrapper/maven-wrapper.properties b/chapter06/01 - User rest controller/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter06/01 - User rest controller/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter06/01 - User rest controller/docker-compose.yaml b/chapter06/01 - User rest controller/docker-compose.yaml new file mode 100644 index 0000000..7f8428d --- /dev/null +++ b/chapter06/01 - User rest controller/docker-compose.yaml @@ -0,0 +1,14 @@ +version: '3' +services: + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter06/01 - User rest controller/mvnw b/chapter06/01 - User rest controller/mvnw index 5bf251c..66df285 100755 --- a/chapter06/01 - User rest controller/mvnw +++ b/chapter06/01 - User rest controller/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter06/01 - User rest controller/mvnw.cmd b/chapter06/01 - User rest controller/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter06/01 - User rest controller/mvnw.cmd +++ b/chapter06/01 - User rest controller/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter06/01 - User rest controller/pom.xml b/chapter06/01 - User rest controller/pom.xml index 58be00e..912a5ae 100644 --- a/chapter06/01 - User rest controller/pom.xml +++ b/chapter06/01 - User rest controller/pom.xml @@ -1,101 +1,83 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - 29.0-jre - - - + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - com.google.guava - guava - ${guava.version} - - - - org.projectlombok - lombok - - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index 4ce4da8..0000000 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - //tag::resource-server[] - @Configuration - @EnableResourceServer //<1> - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() //<2> - .and() - .antMatcher("/api/**") - .authorizeRequests() - .anyRequest().authenticated(); //<3> - } - } - //end::resource-server[] - - //tag::authorization-server[] - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; //<1> - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) //<2> - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); //<3> - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - //end::authorization-server[] - - //tag::web-security[] - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } - //end::web-security[] -} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..3fd01ba --- /dev/null +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,24 @@ +package com.example.copsboot.infrastructure.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class WebSecurityConfiguration { + @Bean + SecurityFilterChain configureSecurityFilterChain(HttpSecurity http) throws Exception { + + http + .authorizeHttpRequests(authorizeRequests -> authorizeRequests + .requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() //<.> + .requestMatchers("/api/**").authenticated() //<.> + .anyRequest().authenticated()) + .oauth2ResourceServer(it -> it.jwt(Customizer.withDefaults())); //<.> + + return http.build(); + } +} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/User.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/User.java +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserNotFoundException.java deleted file mode 100644 index 1f65f04..0000000 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> -public class UserNotFoundException extends RuntimeException { - public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); - } -} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserService.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserService.java index 9e155a3..61846a5 100644 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserService.java @@ -1,9 +1,28 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } - Optional getUser(UserId userId); + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } + // end::createUser[] } diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..0d8f0ab --- /dev/null +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,16 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); //<.> + String email = jwt.getClaimAsString("email"); //<.> + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/UserRestController.java index e27f788..6000867 100644 --- a/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter06/01 - User rest controller/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,30 +1,50 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.*; -@RestController //<1> -@RequestMapping("/api/users") //<2> +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +@RestController +@RequestMapping("/api/users") public class UserRestController { + private final UserService userService; + + public UserRestController(UserService userService) { + this.userService = userService; + } + + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); - private final UserService service; + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); - @Autowired - public UserRestController(UserService service) { //<3> - this.service = service; + return result; } + // end::myself[] - @GetMapping("/me") //<4> - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { //<5> - User user = service.getUser(userDetails.getUserId()) //<6> - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); //<7> + // tag::createUser[] + @PostMapping + @ResponseStatus(HttpStatus.CREATED) //<.> + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @RequestBody CreateUserRequest request) { //<.> + CreateUserParameters parameters = request.toParameters(jwt); //<.> + User user = userService.createUser(parameters); + return UserDto.fromUser(user); //<.> } + // end::createUser[] } diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/Entity.java b/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/Entity.java index 96cadf0..3a45231 100644 --- a/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter06/01 - User rest controller/src/main/java/com/example/orm/jpa/Entity.java @@ -1,5 +1,6 @@ package com.example.orm.jpa; + /** * Interface for entity objects. * diff --git a/chapter06/01 - User rest controller/src/main/resources/application-dev.properties b/chapter06/01 - User rest controller/src/main/resources/application-dev.properties deleted file mode 100644 index 819196a..0000000 --- a/chapter06/01 - User rest controller/src/main/resources/application-dev.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN \ No newline at end of file diff --git a/chapter06/01 - User rest controller/src/main/resources/application.properties b/chapter06/01 - User rest controller/src/main/resources/application.properties index e69de29..27301ca 100644 --- a/chapter06/01 - User rest controller/src/main/resources/application.properties +++ b/chapter06/01 - User rest controller/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/copsboot diff --git a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..73e7b68 100644 --- a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,13 @@ package com.example.copsboot; -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index b01a4ed..0000000 --- a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index ad7aa55..b37e583 100644 --- a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -2,13 +2,11 @@ import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -17,7 +15,6 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest public class UserRepositoryTest { @@ -27,50 +24,16 @@ public class UserRepositoryTest { //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - assertThat(repository.count()).isEqualTo(1L); //<7> + assertThat(repository.count()).isEqualTo(1L); } //end::testStoreUser[] - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); - - assertThat(optional).isEmpty(); - } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @@ -80,4 +43,4 @@ public UniqueIdGenerator generator() { } } //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/Users.java b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 557cd04..0000000 --- a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } -} diff --git a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index b1fe1e0..8d3910e 100644 --- a/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter06/01 - User rest controller/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,104 +1,43 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::webmvctest[] -@RunWith(SpringRunner.class) //<1> -@WebMvcTest(UserRestController.class) //<2> -@ActiveProfiles(SpringProfiles.TEST) //<3> -public class UserRestControllerTest { +@WebMvcTest(UserRestController.class) //<.> +class UserRestControllerTest { @Autowired - private MockMvc mvc; //<4> + private MockMvc mockMvc; //<.> @MockBean - private UserService service; //<5> + private UserService userService; - //end::webmvctest[] - //tag::notauth[] @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) //<1> - .andExpect(status().isUnauthorized()); //<2> + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) //<.> + .andExpect(status().isUnauthorized()); //<.> } - //end::notauth[] - //tag::authofficer[] @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<1> - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); //<2> - - mvc.perform(get("/api/users/me") //<3> - .header(HEADER_AUTHORIZATION, bearer(accessToken))) //<4> - .andExpect(status().isOk()) //<5> - .andExpect(jsonPath("id").exists()) //<6> - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; - } - //end::authofficer[] - - //tag::authcaptain[] - @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); - } - //end::authcaptain[] - - //tag::testconfig[] - @TestConfiguration //<1> - @Import(OAuth2ServerConfiguration.class) //<2> - static class TestConfig { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); //<3> - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); //<5> - } - + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) //<.> + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()) //<.> + .andDo(print()); //<.> } - //end::testconfig[] } diff --git a/chapter06/01 - User rest controller/src/test/resources/application-test.properties b/chapter06/01 - User rest controller/src/test/resources/application-test.properties deleted file mode 100644 index 78c3fdb..0000000 --- a/chapter06/01 - User rest controller/src/test/resources/application-test.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret \ No newline at end of file diff --git a/chapter06/02 - Post mapping/mvnw b/chapter06/02 - Post mapping/mvnw deleted file mode 100755 index 5bf251c..0000000 --- a/chapter06/02 - Post mapping/mvnw +++ /dev/null @@ -1,225 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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 -# -# http://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. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Migwn, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - 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 - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter06/02 - Post mapping/mvnw.cmd b/chapter06/02 - Post mapping/mvnw.cmd deleted file mode 100644 index 019bd74..0000000 --- a/chapter06/02 - Post mapping/mvnw.cmd +++ /dev/null @@ -1,143 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% diff --git a/chapter06/02 - Post mapping/pom.xml b/chapter06/02 - Post mapping/pom.xml deleted file mode 100644 index d8177ae..0000000 --- a/chapter06/02 - Post mapping/pom.xml +++ /dev/null @@ -1,101 +0,0 @@ - - - 4.0.0 - - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar - - copsboot - Demo project for Spring Boot - - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - 29.0-jre - - - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - com.google.guava - guava - ${guava.version} - - - - org.projectlombok - lombok - - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - - diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/CopsbootApplication.java deleted file mode 100644 index f4e3307..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/CopsbootApplication.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.copsboot; - -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; - -@SpringBootApplication -public class CopsbootApplication { - - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java deleted file mode 100644 index 1361ab0..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.copsboot.infrastructure; - -public final class SpringProfiles { - public static final String DEV = "dev"; - public static final String LOCAL = "local"; - public static final String TEST = "test"; - public static final String STAGING = "staging"; - public static final String PROD = "prod"; - - private SpringProfiles() { - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/User.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/User.java deleted file mode 100644 index 236cd6d..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/User.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.copsboot.user; - -import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - - -@Entity -@Table(name = "copsboot_user") -public class User extends AbstractEntity { - - private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; - - protected User() { - - } - - public User(UserId id, String email, String password, Set roles) { - super(id); - this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); - } - - public String getEmail() { - return email; - } - - public String getPassword() { - return password; - } - - public Set getRoles() { - return roles; - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserId.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserId.java deleted file mode 100644 index a112f47..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserId.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.copsboot.user; - -import com.example.orm.jpa.AbstractEntityId; - -import java.util.UUID; - -public class UserId extends AbstractEntityId { - - protected UserId() { //<1> - - } - - public UserId(UUID id) { //<2> - super(id); - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserNotFoundException.java deleted file mode 100644 index 1f65f04..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> -public class UserNotFoundException extends RuntimeException { - public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRepositoryCustom.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRepositoryCustom.java deleted file mode 100644 index b848e3c..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRepositoryCustom.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.copsboot.user; - -public interface UserRepositoryCustom { - UserId nextId(); -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRepositoryImpl.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRepositoryImpl.java deleted file mode 100644 index 6ddfbe1..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRepositoryImpl.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user; - -import com.example.orm.jpa.UniqueIdGenerator; - -import java.util.UUID; - -public class UserRepositoryImpl implements UserRepositoryCustom { - private final UniqueIdGenerator generator; - - public UserRepositoryImpl(UniqueIdGenerator generator) { - this.generator = generator; - } - - @Override - public UserId nextId() { - return new UserId(generator.getNextUniqueId()); - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRole.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRole.java deleted file mode 100644 index d750719..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRole.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.copsboot.user; - -public enum UserRole { - OFFICER, - CAPTAIN, - ADMIN -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserService.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserService.java deleted file mode 100644 index 9e155a3..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserService.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.copsboot.user; - -import java.util.Optional; - -public interface UserService { - User createOfficer(String email, String password); - - Optional getUser(UserId userId); -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index 7ab85e9..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/UserDto.java deleted file mode 100644 index 3769d1a..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/UserDto.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.user.web; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; - -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; - - public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/UserRestController.java deleted file mode 100644 index c74ccd8..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.example.copsboot.user.web; - -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; - -import javax.validation.Valid; - -@RestController -@RequestMapping("/api/users") -public class UserRestController { - - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; - } - - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); - } - - //tag::post[] - @PostMapping //<1> - @ResponseStatus(HttpStatus.CREATED) //<2> - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { //<3> - User officer = service.createOfficer(parameters.getEmail(), //<4> - parameters.getPassword()); - return UserDto.fromUser(officer); //<5> - } - //end::post[] -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/AbstractEntity.java deleted file mode 100644 index dfa9f1e..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.orm.jpa; - -import com.example.util.ArtifactForFramework; - -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; -import java.util.Objects; - -import static com.google.common.base.MoreObjects.toStringHelper; -import static com.google.common.base.Preconditions.checkNotNull; - -/** - * Abstract super class for entities. We are assuming that early primary key - * generation will be used. - * - * @param the type of {@link EntityId} that will be used for this entity - */ -@MappedSuperclass -public abstract class AbstractEntity implements Entity { - - @EmbeddedId - private T id; - - - @ArtifactForFramework - protected AbstractEntity() { - } - - public AbstractEntity(T id) { - this.id = checkNotNull(id); - } - - - @Override - public T getId() { - return id; - } - - @Override - public boolean equals(Object obj) { - boolean result = false; - - if (this == obj) { - result = true; - } else if (obj instanceof AbstractEntity) { - AbstractEntity other = (AbstractEntity) obj; - result = Objects.equals(id, other.id); - } - - return result; - } - - @Override - public int hashCode() { - return Objects.hash(id); - } - - @Override - public String toString() { - return toStringHelper(this) - .add("id", id) - .toString(); - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/AbstractEntityId.java deleted file mode 100755 index b9ddc5b..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.example.orm.jpa; - -import com.example.util.ArtifactForFramework; - -import javax.persistence.MappedSuperclass; -import java.io.Serializable; -import java.util.Objects; - -import static com.google.common.base.MoreObjects.toStringHelper; -import static com.google.common.base.Preconditions.checkNotNull; - -@MappedSuperclass -public abstract class AbstractEntityId implements Serializable, EntityId { - private T id; - - @ArtifactForFramework - protected AbstractEntityId() { - } - - protected AbstractEntityId(T id) { - this.id = checkNotNull(id); - } - - @Override - public T getId() { - return id; - } - - @Override - public String asString() { - return id.toString(); - } - - @Override - public boolean equals(Object o) { - boolean result = false; - - if (this == o) { - result = true; - } else if (o instanceof AbstractEntityId) { - AbstractEntityId other = (AbstractEntityId) o; - result = Objects.equals(id, other.id); - } - - return result; - } - - @Override - public int hashCode() { - return Objects.hash(id); - } - - @Override - public String toString() { - return toStringHelper(this) - .add("id", id) - .toString(); - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/EntityId.java b/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/EntityId.java deleted file mode 100644 index 53da1e7..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/EntityId.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.orm.jpa; - -import java.io.Serializable; - -/** - * Interface for primary keys of entities. - * - * @param the underlying type of the entity id - */ -public interface EntityId extends Serializable { - - T getId(); - - String asString(); //<1> -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/InMemoryUniqueIdGenerator.java b/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/InMemoryUniqueIdGenerator.java deleted file mode 100755 index 06e9521..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/InMemoryUniqueIdGenerator.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.example.orm.jpa; - -import java.util.UUID; - -public class InMemoryUniqueIdGenerator implements UniqueIdGenerator { - @Override - public UUID getNextUniqueId() { - return UUID.randomUUID(); - } -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/UniqueIdGenerator.java b/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/UniqueIdGenerator.java deleted file mode 100755 index 1264905..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/UniqueIdGenerator.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.orm.jpa; - -public interface UniqueIdGenerator { - T getNextUniqueId(); -} diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/util/ArtifactForFramework.java b/chapter06/02 - Post mapping/src/main/java/com/example/util/ArtifactForFramework.java deleted file mode 100644 index 5d4ec38..0000000 --- a/chapter06/02 - Post mapping/src/main/java/com/example/util/ArtifactForFramework.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.example.util; - -public @interface ArtifactForFramework { -} diff --git a/chapter06/02 - Post mapping/src/main/resources/application-dev.properties b/chapter06/02 - Post mapping/src/main/resources/application-dev.properties deleted file mode 100644 index 819196a..0000000 --- a/chapter06/02 - Post mapping/src/main/resources/application-dev.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN \ No newline at end of file diff --git a/chapter06/02 - Post mapping/src/main/resources/application.properties b/chapter06/02 - Post mapping/src/main/resources/application.properties deleted file mode 100644 index e69de29..0000000 diff --git a/chapter06/02 - Post mapping/src/main/resources/logback-spring.xml b/chapter06/02 - Post mapping/src/main/resources/logback-spring.xml deleted file mode 100644 index 6aff652..0000000 --- a/chapter06/02 - Post mapping/src/main/resources/logback-spring.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/CopsbootApplicationTests.java deleted file mode 100644 index add5a9b..0000000 --- a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -@RunWith(SpringRunner.class) -@SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { - - @Test - public void contextLoads() { - } - -} diff --git a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index b01a4ed..0000000 --- a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/UserRepositoryTest.java deleted file mode 100644 index ad7aa55..0000000 --- a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.example.copsboot.user; - -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.test.context.junit4.SpringRunner; - -import java.util.HashSet; -import java.util.Locale; -import java.util.Optional; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@DataJpaTest -public class UserRepositoryTest { - - @Autowired - private UserRepository repository; - - //tag::testStoreUser[] - @Test - public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> - - assertThat(repository.count()).isEqualTo(1L); //<7> - } - //end::testStoreUser[] - - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); - - assertThat(optional).isEmpty(); - } - //end::find-by-email-tests[] - - //tag::testconfig[] - @TestConfiguration - static class TestConfig { - @Bean - public UniqueIdGenerator generator() { - return new InMemoryUniqueIdGenerator(); - } - } - //end::testconfig[] -} \ No newline at end of file diff --git a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/Users.java b/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java deleted file mode 100644 index 2a0e431..0000000 --- a/chapter06/02 - Post mapping/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.example.copsboot.user.web; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.AdditionalAnswers; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.http.MediaType; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; - -import java.util.Optional; - -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@RunWith(SpringRunner.class) -@WebMvcTest(UserRestController.class) -@ActiveProfiles(SpringProfiles.TEST) -public class UserRestControllerTest { - - @Autowired - private MockMvc mvc; - - //tag::extra-fields[] - @Autowired - private ObjectMapper objectMapper; //<1> - @MockBean - private UserService service; //<2> - //end::extra-fields[] - - @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); - } - - @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; - } - - @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); - } - - //tag::test-create-officer[] - @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<1> - - mvc.perform(post("/api/users") //<2> - .contentType(MediaType.APPLICATION_JSON_UTF8) //<3> - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andExpect(jsonPath("id").exists()) //<6> - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); //<7> - } - //end::test-create-officer[] - - @TestConfiguration - @Import(OAuth2ServerConfiguration.class) - static class TestConfig { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - - } -} diff --git a/chapter06/02 - Post mapping/src/test/resources/application-test.properties b/chapter06/02 - Post mapping/src/test/resources/application-test.properties deleted file mode 100644 index 78c3fdb..0000000 --- a/chapter06/02 - Post mapping/src/test/resources/application-test.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret \ No newline at end of file diff --git a/chapter06/02 - Post mapping/src/test/resources/logback-test.xml b/chapter06/02 - Post mapping/src/test/resources/logback-test.xml deleted file mode 100644 index f81fa4a..0000000 --- a/chapter06/02 - Post mapping/src/test/resources/logback-test.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - %date{YYYY-MM-dd HH:mm:ss} %level [%thread] %logger{0} - %msg%n%ex - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/chapter06/02 - user role/.gitignore b/chapter06/02 - user role/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter06/02 - user role/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter06/02 - user role/.mvn/wrapper/maven-wrapper.jar b/chapter06/02 - user role/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter06/02 - user role/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter06/02 - user role/.mvn/wrapper/maven-wrapper.properties b/chapter06/02 - user role/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter06/02 - user role/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter06/02 - user role/docker-compose.yaml b/chapter06/02 - user role/docker-compose.yaml new file mode 100644 index 0000000..7f8428d --- /dev/null +++ b/chapter06/02 - user role/docker-compose.yaml @@ -0,0 +1,14 @@ +version: '3' +services: + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter06/02 - user role/mvnw b/chapter06/02 - user role/mvnw new file mode 100755 index 0000000..66df285 --- /dev/null +++ b/chapter06/02 - user role/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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 + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter05/02 - Oauth configurable/mvnw.cmd b/chapter06/02 - user role/mvnw.cmd similarity index 54% rename from chapter05/02 - Oauth configurable/mvnw.cmd rename to chapter06/02 - user role/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter05/02 - Oauth configurable/mvnw.cmd +++ b/chapter06/02 - user role/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter06/02 - user role/pom.xml b/chapter06/02 - user role/pom.xml new file mode 100644 index 0000000..2b02dae --- /dev/null +++ b/chapter06/02 - user role/pom.xml @@ -0,0 +1,96 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + + + + com.google.guava + guava + ${guava.version} + + + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/CopsbootApplication.java new file mode 100644 index 0000000..7b031d7 --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -0,0 +1,13 @@ +package com.example.copsboot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CopsbootApplication { + + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } + +} diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java similarity index 100% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java rename to chapter06/02 - user role/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/user/User.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/User.java new file mode 100644 index 0000000..32d02a4 --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/User.java @@ -0,0 +1,37 @@ +package com.example.copsboot.user; + +import com.example.orm.jpa.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +@Entity +@Table(name = "copsboot_user") +public class User extends AbstractEntity { + + private String email; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> + + protected User() { + + } + + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> + super(id); + this.email = email; + this.authServerId = authServerId; + this.mobileToken = mobileToken; + } + + public String getEmail() { + return email; + } + + public AuthServerId getAuthServerId() { //<.> + return authServerId; + } + + public String getMobileToken() { + return mobileToken; + } +} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserId.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserId.java similarity index 100% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserId.java rename to chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserId.java diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserRepository.java similarity index 75% rename from chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRepository.java rename to chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter06/02 - Post mapping/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserRepositoryCustom.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserRepositoryCustom.java similarity index 100% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserRepositoryCustom.java rename to chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserRepositoryCustom.java diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserRepositoryImpl.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserRepositoryImpl.java similarity index 100% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserRepositoryImpl.java rename to chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserRepositoryImpl.java diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserRole.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserRole.java similarity index 100% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/copsboot/user/UserRole.java rename to chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserRole.java diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserService.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserService.java new file mode 100644 index 0000000..61846a5 --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/UserService.java @@ -0,0 +1,28 @@ +package com.example.copsboot.user; + +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } + + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } + // end::createUser[] +} diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..0d8f0ab --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,16 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); //<.> + String email = jwt.getClaimAsString("email"); //<.> + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/UserDto.java new file mode 100644 index 0000000..2fac96c --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -0,0 +1,14 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.User; + +import java.util.UUID; + +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { + public static UserDto fromUser(User user) { + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); + } +} diff --git a/chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/UserRestController.java new file mode 100644 index 0000000..cd2fe43 --- /dev/null +++ b/chapter06/02 - user role/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -0,0 +1,52 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserService; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +@RestController +@RequestMapping("/api/users") +public class UserRestController { + private final UserService userService; + + public UserRestController(UserService userService) { + this.userService = userService; + } + + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; + } + // end::myself[] + + // tag::createUser[] + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + @PreAuthorize("hasRole('OFFICER')") //<.> + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); + } + // end::createUser[] +} diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter06/02 - user role/src/main/java/com/example/orm/jpa/AbstractEntity.java similarity index 94% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/AbstractEntity.java rename to chapter06/02 - user role/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter06/02 - user role/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter06/02 - user role/src/main/java/com/example/orm/jpa/AbstractEntityId.java similarity index 96% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/AbstractEntityId.java rename to chapter06/02 - user role/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter06/02 - user role/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/Entity.java b/chapter06/02 - user role/src/main/java/com/example/orm/jpa/Entity.java similarity index 99% rename from chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/Entity.java rename to chapter06/02 - user role/src/main/java/com/example/orm/jpa/Entity.java index 96cadf0..3a45231 100644 --- a/chapter06/02 - Post mapping/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter06/02 - user role/src/main/java/com/example/orm/jpa/Entity.java @@ -1,5 +1,6 @@ package com.example.orm.jpa; + /** * Interface for entity objects. * diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/EntityId.java b/chapter06/02 - user role/src/main/java/com/example/orm/jpa/EntityId.java similarity index 100% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/EntityId.java rename to chapter06/02 - user role/src/main/java/com/example/orm/jpa/EntityId.java diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/InMemoryUniqueIdGenerator.java b/chapter06/02 - user role/src/main/java/com/example/orm/jpa/InMemoryUniqueIdGenerator.java similarity index 100% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/InMemoryUniqueIdGenerator.java rename to chapter06/02 - user role/src/main/java/com/example/orm/jpa/InMemoryUniqueIdGenerator.java diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/UniqueIdGenerator.java b/chapter06/02 - user role/src/main/java/com/example/orm/jpa/UniqueIdGenerator.java similarity index 100% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/orm/jpa/UniqueIdGenerator.java rename to chapter06/02 - user role/src/main/java/com/example/orm/jpa/UniqueIdGenerator.java diff --git a/chapter05/02 - Oauth configurable/src/main/java/com/example/util/ArtifactForFramework.java b/chapter06/02 - user role/src/main/java/com/example/util/ArtifactForFramework.java similarity index 100% rename from chapter05/02 - Oauth configurable/src/main/java/com/example/util/ArtifactForFramework.java rename to chapter06/02 - user role/src/main/java/com/example/util/ArtifactForFramework.java diff --git a/chapter06/02 - user role/src/main/resources/application.properties b/chapter06/02 - user role/src/main/resources/application.properties new file mode 100644 index 0000000..22c3363 --- /dev/null +++ b/chapter06/02 - user role/src/main/resources/application.properties @@ -0,0 +1,3 @@ +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter05/02 - Oauth configurable/src/main/resources/logback-spring.xml b/chapter06/02 - user role/src/main/resources/logback-spring.xml similarity index 100% rename from chapter05/02 - Oauth configurable/src/main/resources/logback-spring.xml rename to chapter06/02 - user role/src/main/resources/logback-spring.xml diff --git a/chapter06/02 - user role/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter06/02 - user role/src/test/java/com/example/copsboot/CopsbootApplicationTests.java new file mode 100644 index 0000000..73e7b68 --- /dev/null +++ b/chapter06/02 - user role/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.copsboot; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CopsbootApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/chapter06/02 - user role/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter06/02 - user role/src/test/java/com/example/copsboot/user/UserRepositoryTest.java new file mode 100644 index 0000000..b37e583 --- /dev/null +++ b/chapter06/02 - user role/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -0,0 +1,46 @@ +package com.example.copsboot.user; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +public class UserRepositoryTest { + + @Autowired + private UserRepository repository; + + //tag::testStoreUser[] + @Test + public void testStoreUser() { + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); + + assertThat(repository.count()).isEqualTo(1L); + } + //end::testStoreUser[] + + //tag::testconfig[] + @TestConfiguration + static class TestConfig { + @Bean + public UniqueIdGenerator generator() { + return new InMemoryUniqueIdGenerator(); + } + } + //end::testconfig[] +} diff --git a/chapter06/02 - user role/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter06/02 - user role/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java new file mode 100644 index 0000000..2654f2c --- /dev/null +++ b/chapter06/02 - user role/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -0,0 +1,87 @@ +package com.example.copsboot.user.web; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(UserRestController.class) +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> +class UserRestControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private UserService userService; //<.> + + @Test + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); + } + + @Test + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); + } + + @Test + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); + } + + @Test + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> + } +} diff --git a/chapter05/02 - Oauth configurable/src/test/resources/logback-test.xml b/chapter06/02 - user role/src/test/resources/logback-test.xml similarity index 100% rename from chapter05/02 - Oauth configurable/src/test/resources/logback-test.xml rename to chapter06/02 - user role/src/test/resources/logback-test.xml diff --git a/chapter06/03 - Writing API Documentation/.gitignore b/chapter06/03 - Writing API Documentation/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter06/03 - Writing API Documentation/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter06/03 - Writing API Documentation/.mvn/wrapper/maven-wrapper.jar b/chapter06/03 - Writing API Documentation/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter06/03 - Writing API Documentation/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter06/03 - Writing API Documentation/.mvn/wrapper/maven-wrapper.properties b/chapter06/03 - Writing API Documentation/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter06/03 - Writing API Documentation/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter06/03 - Writing API Documentation/docker-compose.yaml b/chapter06/03 - Writing API Documentation/docker-compose.yaml new file mode 100644 index 0000000..7f8428d --- /dev/null +++ b/chapter06/03 - Writing API Documentation/docker-compose.yaml @@ -0,0 +1,14 @@ +version: '3' +services: + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter06/03 - Writing API Documentation/mvnw b/chapter06/03 - Writing API Documentation/mvnw index 5bf251c..66df285 100755 --- a/chapter06/03 - Writing API Documentation/mvnw +++ b/chapter06/03 - Writing API Documentation/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter06/03 - Writing API Documentation/mvnw.cmd b/chapter06/03 - Writing API Documentation/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter06/03 - Writing API Documentation/mvnw.cmd +++ b/chapter06/03 - Writing API Documentation/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter06/03 - Writing API Documentation/pom.xml b/chapter06/03 - Writing API Documentation/pom.xml index 0715bbe..960db1b 100644 --- a/chapter06/03 - Writing API Documentation/pom.xml +++ b/chapter06/03 - Writing API Documentation/pom.xml @@ -1,214 +1,169 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 29.0-jre - - - 2.0.3.RELEASE - - + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - com.google.guava - guava - ${guava.version} - - - - org.projectlombok - lombok - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter06/03 - Writing API Documentation/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter06/03 - Writing API Documentation/src/docs/asciidoc/Copsboot REST API Guide.adoc similarity index 100% rename from chapter06/03 - Writing API Documentation/src/main/asciidoc/Copsboot REST API Guide.adoc rename to chapter06/03 - Writing API Documentation/src/docs/asciidoc/Copsboot REST API Guide.adoc diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/User.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/User.java +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserNotFoundException.java deleted file mode 100644 index 1f65f04..0000000 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> -public class UserNotFoundException extends RuntimeException { - public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); - } -} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserService.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserService.java index 9e155a3..61846a5 100644 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserService.java @@ -1,9 +1,28 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } - Optional getUser(UserId userId); + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } + // end::createUser[] } diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index 7ab85e9..0000000 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..0d8f0ab --- /dev/null +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,16 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); //<.> + String email = jwt.getClaimAsString("email"); //<.> + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/UserRestController.java index c74ccd8..cd2fe43 100644 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,41 +1,52 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] - @PostMapping //<1> - @ResponseStatus(HttpStatus.CREATED) //<2> - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { //<3> - User officer = service.createOfficer(parameters.getEmail(), //<4> - parameters.getPassword()); - return UserDto.fromUser(officer); //<5> + // tag::createUser[] + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + @PreAuthorize("hasRole('OFFICER')") //<.> + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/Entity.java b/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/Entity.java index 96cadf0..3a45231 100644 --- a/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter06/03 - Writing API Documentation/src/main/java/com/example/orm/jpa/Entity.java @@ -1,5 +1,6 @@ package com.example.orm.jpa; + /** * Interface for entity objects. * diff --git a/chapter06/03 - Writing API Documentation/src/main/resources/application-dev.properties b/chapter06/03 - Writing API Documentation/src/main/resources/application-dev.properties deleted file mode 100644 index 819196a..0000000 --- a/chapter06/03 - Writing API Documentation/src/main/resources/application-dev.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN \ No newline at end of file diff --git a/chapter06/03 - Writing API Documentation/src/main/resources/application.properties b/chapter06/03 - Writing API Documentation/src/main/resources/application.properties index e69de29..22c3363 100644 --- a/chapter06/03 - Writing API Documentation/src/main/resources/application.properties +++ b/chapter06/03 - Writing API Documentation/src/main/resources/application.properties @@ -0,0 +1,3 @@ +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..73e7b68 100644 --- a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,13 @@ package com.example.copsboot; -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index b01a4ed..0000000 --- a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index ad7aa55..b37e583 100644 --- a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -2,13 +2,11 @@ import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -17,7 +15,6 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest public class UserRepositoryTest { @@ -27,50 +24,16 @@ public class UserRepositoryTest { //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - assertThat(repository.count()).isEqualTo(1L); //<7> + assertThat(repository.count()).isEqualTo(1L); } //end::testStoreUser[] - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); - - assertThat(optional).isEmpty(); - } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @@ -80,4 +43,4 @@ public UniqueIdGenerator generator() { } } //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/Users.java b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index 2a0e431..2654f2c 100644 --- a/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter06/03 - Writing API Documentation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,128 +1,87 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.AdditionalAnswers; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.verify; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@RunWith(SpringRunner.class) @WebMvcTest(UserRestController.class) -@ActiveProfiles(SpringProfiles.TEST) -public class UserRestControllerTest { +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> +class UserRestControllerTest { @Autowired - private MockMvc mvc; + private MockMvc mockMvc; - //tag::extra-fields[] - @Autowired - private ObjectMapper objectMapper; //<1> @MockBean - private UserService service; //<2> - //end::extra-fields[] + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } - //tag::test-create-officer[] @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<1> - - mvc.perform(post("/api/users") //<2> - .contentType(MediaType.APPLICATION_JSON_UTF8) //<3> - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andExpect(jsonPath("id").exists()) //<6> - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); //<7> - } - //end::test-create-officer[] - - @TestConfiguration - @Import(OAuth2ServerConfiguration.class) - static class TestConfig { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } } diff --git a/chapter06/03 - Writing API Documentation/src/test/resources/application-test.properties b/chapter06/03 - Writing API Documentation/src/test/resources/application-test.properties deleted file mode 100644 index 78c3fdb..0000000 --- a/chapter06/03 - Writing API Documentation/src/test/resources/application-test.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret \ No newline at end of file diff --git a/chapter06/04 - Generating snippets/.gitignore b/chapter06/04 - Generating snippets/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/chapter06/04 - Generating snippets/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/chapter06/04 - Generating snippets/.mvn/wrapper/maven-wrapper.jar b/chapter06/04 - Generating snippets/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter06/04 - Generating snippets/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter06/04 - Generating snippets/.mvn/wrapper/maven-wrapper.properties b/chapter06/04 - Generating snippets/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter06/04 - Generating snippets/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter06/04 - Generating snippets/docker-compose.yaml b/chapter06/04 - Generating snippets/docker-compose.yaml new file mode 100644 index 0000000..7f8428d --- /dev/null +++ b/chapter06/04 - Generating snippets/docker-compose.yaml @@ -0,0 +1,14 @@ +version: '3' +services: + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter06/04 - Generating snippets/mvnw b/chapter06/04 - Generating snippets/mvnw index 5bf251c..66df285 100755 --- a/chapter06/04 - Generating snippets/mvnw +++ b/chapter06/04 - Generating snippets/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter06/04 - Generating snippets/mvnw.cmd b/chapter06/04 - Generating snippets/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter06/04 - Generating snippets/mvnw.cmd +++ b/chapter06/04 - Generating snippets/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter06/04 - Generating snippets/pom.xml b/chapter06/04 - Generating snippets/pom.xml index 0715bbe..f48de47 100644 --- a/chapter06/04 - Generating snippets/pom.xml +++ b/chapter06/04 - Generating snippets/pom.xml @@ -1,214 +1,183 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 29.0-jre - - - 2.0.3.RELEASE - - + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - com.google.guava - guava - ${guava.version} - - - - org.projectlombok - lombok - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter06/04 - Generating snippets/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter06/04 - Generating snippets/src/docs/asciidoc/Copsboot REST API Guide.adoc similarity index 91% rename from chapter06/04 - Generating snippets/src/main/asciidoc/Copsboot REST API Guide.adoc rename to chapter06/04 - Generating snippets/src/docs/asciidoc/Copsboot REST API Guide.adoc index 255bc8e..b0b91ae 100644 --- a/chapter06/04 - Generating snippets/src/main/asciidoc/Copsboot REST API Guide.adoc +++ b/chapter06/04 - Generating snippets/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -11,4 +11,4 @@ The Copsboot project uses a REST API for interfacing with the server. This documentation covers version {project-version} of the application. -include::_users.adoc[] \ No newline at end of file +include::_users.adoc[] diff --git a/chapter06/04 - Generating snippets/src/main/asciidoc/_users.adoc b/chapter06/04 - Generating snippets/src/docs/asciidoc/_users.adoc similarity index 56% rename from chapter06/04 - Generating snippets/src/main/asciidoc/_users.adoc rename to chapter06/04 - Generating snippets/src/docs/asciidoc/_users.adoc index a033db8..2becf75 100644 --- a/chapter06/04 - Generating snippets/src/main/asciidoc/_users.adoc +++ b/chapter06/04 - Generating snippets/src/docs/asciidoc/_users.adoc @@ -7,12 +7,12 @@ The API allows to get information on the currently logged on user via a `GET` on `/api/users/me`. If you are not a logged on user, the following response will be returned: -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] +operation::own-details-unauthorized[snippets='http-request,http-response'] //end::initial-doc[] If you do log on as a user, you get more information on that user: -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] +operation::own-details[snippets='http-request,http-response,response-fields'] //tag::create-user[] @@ -20,5 +20,5 @@ operation::authenticated-officer-details-example[snippets='http-request,http-res To create an new user, do a `POST` on `/api/users`: -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/User.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/User.java +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserNotFoundException.java deleted file mode 100644 index 1f65f04..0000000 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> -public class UserNotFoundException extends RuntimeException { - public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); - } -} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserService.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserService.java index 9e155a3..61846a5 100644 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserService.java @@ -1,9 +1,28 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } - Optional getUser(UserId userId); + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } + // end::createUser[] } diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index 7ab85e9..0000000 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..0d8f0ab --- /dev/null +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,16 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); //<.> + String email = jwt.getClaimAsString("email"); //<.> + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/UserRestController.java index c74ccd8..796adc1 100644 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,41 +1,52 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] - @PostMapping //<1> - @ResponseStatus(HttpStatus.CREATED) //<2> - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { //<3> - User officer = service.createOfficer(parameters.getEmail(), //<4> - parameters.getPassword()); - return UserDto.fromUser(officer); //<5> + // tag::createUser[] + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/Entity.java b/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/Entity.java index 96cadf0..3a45231 100644 --- a/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter06/04 - Generating snippets/src/main/java/com/example/orm/jpa/Entity.java @@ -1,5 +1,6 @@ package com.example.orm.jpa; + /** * Interface for entity objects. * diff --git a/chapter06/04 - Generating snippets/src/main/resources/application-dev.properties b/chapter06/04 - Generating snippets/src/main/resources/application-dev.properties deleted file mode 100644 index 819196a..0000000 --- a/chapter06/04 - Generating snippets/src/main/resources/application-dev.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN \ No newline at end of file diff --git a/chapter06/04 - Generating snippets/src/main/resources/application.properties b/chapter06/04 - Generating snippets/src/main/resources/application.properties index e69de29..22c3363 100644 --- a/chapter06/04 - Generating snippets/src/main/resources/application.properties +++ b/chapter06/04 - Generating snippets/src/main/resources/application.properties @@ -0,0 +1,3 @@ +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..73e7b68 100644 --- a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,13 @@ package com.example.copsboot; -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index ad7aa55..b37e583 100644 --- a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -2,13 +2,11 @@ import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -17,7 +15,6 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest public class UserRepositoryTest { @@ -27,50 +24,16 @@ public class UserRepositoryTest { //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - assertThat(repository.count()).isEqualTo(1L); //<7> + assertThat(repository.count()).isEqualTo(1L); } //end::testStoreUser[] - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); - - assertThat(optional).isEmpty(); - } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @@ -80,4 +43,4 @@ public UniqueIdGenerator generator() { } } //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/Users.java b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index f7b7e09..aae4b66 100644 --- a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,162 +1,126 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; //tag::class-setup[] -@RunWith(SpringRunner.class) @WebMvcTest(UserRestController.class) -@ActiveProfiles(SpringProfiles.TEST) +@AutoConfigureAddonsWebmvcResourceServerSecurity +@Import(WebSecurityConfiguration.class) +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs public class UserRestControllerDocumentation { - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; //end::class-setup[] - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] - //tag::test-config[] + //tag::testconfig[] @TestConfiguration - @Import(OAuth2ServerConfiguration.class) static class TestConfig { @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - } - //end::test-config[] + //end::testconfig[] } diff --git a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index 23d72ad..bcc571f 100644 --- a/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter06/04 - Generating snippets/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,125 +1,89 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.verify; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @WebMvcTest(UserRestController.class) -@ActiveProfiles(SpringProfiles.TEST) -public class UserRestControllerTest { -//end::class-annotations[] +@AutoConfigureAddonsWebmvcResourceServerSecurity +@Import(WebSecurityConfiguration.class) +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private MockMvc mvc; + private MockMvc mockMvc; - @Autowired - private ObjectMapper objectMapper; @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); - } - - @TestConfiguration - @Import(OAuth2ServerConfiguration.class) - static class TestConfig { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } } diff --git a/chapter06/04 - Generating snippets/src/test/resources/application-test.properties b/chapter06/04 - Generating snippets/src/test/resources/application-test.properties deleted file mode 100644 index 78c3fdb..0000000 --- a/chapter06/04 - Generating snippets/src/test/resources/application-test.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret \ No newline at end of file diff --git a/chapter06/05 - Refactoring/.mvn/wrapper/maven-wrapper.jar b/chapter06/05 - Refactoring/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter06/05 - Refactoring/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter06/05 - Refactoring/.mvn/wrapper/maven-wrapper.properties b/chapter06/05 - Refactoring/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter06/05 - Refactoring/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter06/05 - Refactoring/docker-compose.yaml b/chapter06/05 - Refactoring/docker-compose.yaml new file mode 100644 index 0000000..7f8428d --- /dev/null +++ b/chapter06/05 - Refactoring/docker-compose.yaml @@ -0,0 +1,14 @@ +version: '3' +services: + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter06/05 - Refactoring/mvnw b/chapter06/05 - Refactoring/mvnw index 5bf251c..66df285 100755 --- a/chapter06/05 - Refactoring/mvnw +++ b/chapter06/05 - Refactoring/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter06/05 - Refactoring/mvnw.cmd b/chapter06/05 - Refactoring/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter06/05 - Refactoring/mvnw.cmd +++ b/chapter06/05 - Refactoring/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter06/05 - Refactoring/pom.xml b/chapter06/05 - Refactoring/pom.xml index 0715bbe..f48de47 100644 --- a/chapter06/05 - Refactoring/pom.xml +++ b/chapter06/05 - Refactoring/pom.xml @@ -1,214 +1,183 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 29.0-jre - - - 2.0.3.RELEASE - - + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - com.google.guava - guava - ${guava.version} - - - - org.projectlombok - lombok - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter06/05 - Refactoring/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter06/05 - Refactoring/src/docs/asciidoc/Copsboot REST API Guide.adoc similarity index 91% rename from chapter06/05 - Refactoring/src/main/asciidoc/Copsboot REST API Guide.adoc rename to chapter06/05 - Refactoring/src/docs/asciidoc/Copsboot REST API Guide.adoc index 255bc8e..b0b91ae 100644 --- a/chapter06/05 - Refactoring/src/main/asciidoc/Copsboot REST API Guide.adoc +++ b/chapter06/05 - Refactoring/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -11,4 +11,4 @@ The Copsboot project uses a REST API for interfacing with the server. This documentation covers version {project-version} of the application. -include::_users.adoc[] \ No newline at end of file +include::_users.adoc[] diff --git a/chapter06/05 - Refactoring/src/main/asciidoc/_users.adoc b/chapter06/05 - Refactoring/src/docs/asciidoc/_users.adoc similarity index 56% rename from chapter06/05 - Refactoring/src/main/asciidoc/_users.adoc rename to chapter06/05 - Refactoring/src/docs/asciidoc/_users.adoc index a033db8..2becf75 100644 --- a/chapter06/05 - Refactoring/src/main/asciidoc/_users.adoc +++ b/chapter06/05 - Refactoring/src/docs/asciidoc/_users.adoc @@ -7,12 +7,12 @@ The API allows to get information on the currently logged on user via a `GET` on `/api/users/me`. If you are not a logged on user, the following response will be returned: -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] +operation::own-details-unauthorized[snippets='http-request,http-response'] //end::initial-doc[] If you do log on as a user, you get more information on that user: -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] +operation::own-details[snippets='http-request,http-response,response-fields'] //tag::create-user[] @@ -20,5 +20,5 @@ operation::authenticated-officer-details-example[snippets='http-request,http-res To create an new user, do a `POST` on `/api/users`: -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/User.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/User.java +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserNotFoundException.java deleted file mode 100644 index 1f65f04..0000000 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> -public class UserNotFoundException extends RuntimeException { - public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); - } -} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserService.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserService.java index 9e155a3..61846a5 100644 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserService.java @@ -1,9 +1,28 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } - Optional getUser(UserId userId); + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } + // end::createUser[] } diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index 7ab85e9..0000000 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..0d8f0ab --- /dev/null +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,16 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); //<.> + String email = jwt.getClaimAsString("email"); //<.> + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/UserRestController.java index c74ccd8..796adc1 100644 --- a/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter06/05 - Refactoring/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,41 +1,52 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] - @PostMapping //<1> - @ResponseStatus(HttpStatus.CREATED) //<2> - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { //<3> - User officer = service.createOfficer(parameters.getEmail(), //<4> - parameters.getPassword()); - return UserDto.fromUser(officer); //<5> + // tag::createUser[] + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/Entity.java b/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/Entity.java index a573e0e..3a45231 100644 --- a/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter06/05 - Refactoring/src/main/java/com/example/orm/jpa/Entity.java @@ -1,6 +1,5 @@ package com.example.orm.jpa; -import java.io.Serializable; /** * Interface for entity objects. diff --git a/chapter06/05 - Refactoring/src/main/resources/application-dev.properties b/chapter06/05 - Refactoring/src/main/resources/application-dev.properties deleted file mode 100644 index 819196a..0000000 --- a/chapter06/05 - Refactoring/src/main/resources/application-dev.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN \ No newline at end of file diff --git a/chapter06/05 - Refactoring/src/main/resources/application.properties b/chapter06/05 - Refactoring/src/main/resources/application.properties index e69de29..22c3363 100644 --- a/chapter06/05 - Refactoring/src/main/resources/application.properties +++ b/chapter06/05 - Refactoring/src/main/resources/application.properties @@ -0,0 +1,3 @@ +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..73e7b68 100644 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,13 @@ package com.example.copsboot; -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java new file mode 100644 index 0000000..3ddeac0 --- /dev/null +++ b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java @@ -0,0 +1,30 @@ +package com.example.copsboot.infrastructure.test; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//tag::class[] +@Retention(RetentionPolicy.RUNTIME) +@CopsbootControllerTest +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class) +public @interface CopsbootControllerDocumentationTest { + + @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> + Class[] value() default {}; + + @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") //<6> + Class[] controllers() default {}; +} +//end::class[] diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java new file mode 100644 index 0000000..02e070e --- /dev/null +++ b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java @@ -0,0 +1,21 @@ +package com.example.copsboot.infrastructure.test; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +class CopsbootControllerDocumentationTestConfiguration { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java index c33238a..6696635 100644 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java +++ b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java @@ -1,10 +1,10 @@ package com.example.copsboot.infrastructure.test; -import com.example.copsboot.infrastructure.SpringProfiles; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,23 +12,12 @@ /** * Custom annotation for all {@link org.springframework.stereotype.Controller Controller} tests on the project. By using * this single annotation, everything is configured properly to test a controller: - *
    - *
  • Import of {@link CopsbootControllerTestConfiguration}
  • - *
  • test profile active
  • - *
- *

- * Example usage: - *

- * @RunWith(SpringRunner.class)
- * @CopsbootControllerTest(UserController.class)
- * public class UserControllerTest {
- * 
*/ //tag::class[] -@Retention(RetentionPolicy.RUNTIME) //<1> -@WebMvcTest //<2> -@ContextConfiguration(classes = CopsbootControllerTestConfiguration.class) //<3> -@ActiveProfiles(SpringProfiles.TEST) //<4> +@Retention(RetentionPolicy.RUNTIME) //<.> +@WebMvcTest //<.> +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> public @interface CopsbootControllerTest { @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java deleted file mode 100644 index 7231430..0000000 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.infrastructure.test; - -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; - -@TestConfiguration -@Import(OAuth2ServerConfiguration.class) -public class CopsbootControllerTestConfiguration { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - -} diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index ad7aa55..b37e583 100644 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -2,13 +2,11 @@ import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -17,7 +15,6 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest public class UserRepositoryTest { @@ -27,50 +24,16 @@ public class UserRepositoryTest { //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - assertThat(repository.count()).isEqualTo(1L); //<7> + assertThat(repository.count()).isEqualTo(1L); } //end::testStoreUser[] - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); - - assertThat(optional).isEmpty(); - } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @@ -80,4 +43,4 @@ public UniqueIdGenerator generator() { } } //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/Users.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index e0d24b0..c142293 100644 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,133 +1,124 @@ package com.example.copsboot.user.web; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs public class UserRestControllerDocumentation { -//end::class-annotations[] - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; - + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] + + //tag::testconfig[] + @TestConfiguration + static class TestConfig { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } + //end::testconfig[] } diff --git a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index 9014594..2acf875 100644 --- a/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter06/05 - Refactoring/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,97 +1,84 @@ package com.example.copsboot.user.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.verify; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) -public class UserRestControllerTest { -//end::class-annotations[] - @Autowired - private MockMvc mvc; +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } } diff --git a/chapter06/05 - Refactoring/src/test/resources/application-test.properties b/chapter06/05 - Refactoring/src/test/resources/application-test.properties deleted file mode 100644 index 78c3fdb..0000000 --- a/chapter06/05 - Refactoring/src/test/resources/application-test.properties +++ /dev/null @@ -1,2 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret \ No newline at end of file diff --git a/chapter07/01 - postgresql/.mvn/wrapper/maven-wrapper.jar b/chapter07/01 - postgresql/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter07/01 - postgresql/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter07/01 - postgresql/.mvn/wrapper/maven-wrapper.properties b/chapter07/01 - postgresql/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter07/01 - postgresql/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter07/01 - postgresql/docker-compose.yaml b/chapter07/01 - postgresql/docker-compose.yaml new file mode 100644 index 0000000..92cea56 --- /dev/null +++ b/chapter07/01 - postgresql/docker-compose.yaml @@ -0,0 +1,20 @@ +version: '3' +services: + db: + image: 'postgres:16.0' + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: my-postgres-db-pwd + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter07/01 - postgresql/mvnw b/chapter07/01 - postgresql/mvnw index 5bf251c..66df285 100755 --- a/chapter07/01 - postgresql/mvnw +++ b/chapter07/01 - postgresql/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter07/01 - postgresql/mvnw.cmd b/chapter07/01 - postgresql/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter07/01 - postgresql/mvnw.cmd +++ b/chapter07/01 - postgresql/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter07/01 - postgresql/pom.xml b/chapter07/01 - postgresql/pom.xml index 9edcbaf..7eb0063 100644 --- a/chapter07/01 - postgresql/pom.xml +++ b/chapter07/01 - postgresql/pom.xml @@ -1,220 +1,190 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 29.0-jre + + + org.postgresql + postgresql + runtime + + + + + org.flywaydb + flyway-core + + - - 2.0.3.RELEASE - - + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-configuration-processor - true - - - com.google.guava - guava - ${guava.version} - - - org.projectlombok - lombok - - - - org.postgresql - postgresql - - - - - org.flywaydb - flyway-core - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter07/01 - postgresql/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter07/01 - postgresql/src/docs/asciidoc/Copsboot REST API Guide.adoc similarity index 91% rename from chapter07/01 - postgresql/src/main/asciidoc/Copsboot REST API Guide.adoc rename to chapter07/01 - postgresql/src/docs/asciidoc/Copsboot REST API Guide.adoc index 255bc8e..b0b91ae 100644 --- a/chapter07/01 - postgresql/src/main/asciidoc/Copsboot REST API Guide.adoc +++ b/chapter07/01 - postgresql/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -11,4 +11,4 @@ The Copsboot project uses a REST API for interfacing with the server. This documentation covers version {project-version} of the application. -include::_users.adoc[] \ No newline at end of file +include::_users.adoc[] diff --git a/chapter07/01 - postgresql/src/main/asciidoc/_users.adoc b/chapter07/01 - postgresql/src/docs/asciidoc/_users.adoc similarity index 56% rename from chapter07/01 - postgresql/src/main/asciidoc/_users.adoc rename to chapter07/01 - postgresql/src/docs/asciidoc/_users.adoc index a033db8..2becf75 100644 --- a/chapter07/01 - postgresql/src/main/asciidoc/_users.adoc +++ b/chapter07/01 - postgresql/src/docs/asciidoc/_users.adoc @@ -7,12 +7,12 @@ The API allows to get information on the currently logged on user via a `GET` on `/api/users/me`. If you are not a logged on user, the following response will be returned: -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] +operation::own-details-unauthorized[snippets='http-request,http-response'] //end::initial-doc[] If you do log on as a user, you get more information on that user: -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] +operation::own-details[snippets='http-request,http-response,response-fields'] //tag::create-user[] @@ -20,5 +20,5 @@ operation::authenticated-officer-details-example[snippets='http-request,http-res To create an new user, do a `POST` on `/api/users`: -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/User.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/User.java +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserNotFoundException.java deleted file mode 100644 index 1f65f04..0000000 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> -public class UserNotFoundException extends RuntimeException { - public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); - } -} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserService.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserService.java index 9e155a3..61846a5 100644 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserService.java @@ -1,9 +1,28 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } - Optional getUser(UserId userId); + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } + // end::createUser[] } diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index 7ab85e9..0000000 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..0d8f0ab --- /dev/null +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,16 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); //<.> + String email = jwt.getClaimAsString("email"); //<.> + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/UserRestController.java index c74ccd8..796adc1 100644 --- a/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter07/01 - postgresql/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,41 +1,52 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] - @PostMapping //<1> - @ResponseStatus(HttpStatus.CREATED) //<2> - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { //<3> - User officer = service.createOfficer(parameters.getEmail(), //<4> - parameters.getPassword()); - return UserDto.fromUser(officer); //<5> + // tag::createUser[] + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/Entity.java b/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/Entity.java index a573e0e..3a45231 100644 --- a/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter07/01 - postgresql/src/main/java/com/example/orm/jpa/Entity.java @@ -1,6 +1,5 @@ package com.example.orm.jpa; -import java.io.Serializable; /** * Interface for entity objects. diff --git a/chapter07/01 - postgresql/src/main/resources/application-dev.properties b/chapter07/01 - postgresql/src/main/resources/application-dev.properties deleted file mode 100644 index f72b4c7..0000000 --- a/chapter07/01 - postgresql/src/main/resources/application-dev.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter07/01 - postgresql/src/main/resources/application-local.properties b/chapter07/01 - postgresql/src/main/resources/application-local.properties index c14a8c4..8fbe161 100644 --- a/chapter07/01 - postgresql/src/main/resources/application-local.properties +++ b/chapter07/01 - postgresql/src/main/resources/application-local.properties @@ -3,14 +3,9 @@ spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.username=postgres spring.datasource.password=my-postgres-db-pwd spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata -spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create -spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=create.sql - - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata +spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create +spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=create.sql +spring.jpa.properties.hibernate.hbm2ddl.delimiter=; diff --git a/chapter07/01 - postgresql/src/main/resources/application.properties b/chapter07/01 - postgresql/src/main/resources/application.properties index e69de29..22c3363 100644 --- a/chapter07/01 - postgresql/src/main/resources/application.properties +++ b/chapter07/01 - postgresql/src/main/resources/application.properties @@ -0,0 +1,3 @@ +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter07/01 - postgresql/src/main/resources/db/migration/V1.0.0.1__users.sql b/chapter07/01 - postgresql/src/main/resources/db/migration/V1.0.0.1__users.sql new file mode 100644 index 0000000..d1939fa --- /dev/null +++ b/chapter07/01 - postgresql/src/main/resources/db/migration/V1.0.0.1__users.sql @@ -0,0 +1,7 @@ +CREATE TABLE copsboot_user +( + id uuid NOT NULL PRIMARY KEY, + auth_server_id uuid, + email VARCHAR(255), + mobile_token VARCHAR(255) +); diff --git a/chapter07/01 - postgresql/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql b/chapter07/01 - postgresql/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql deleted file mode 100644 index 485336f..0000000 --- a/chapter07/01 - postgresql/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,42 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(255) PRIMARY KEY, - resource_ids VARCHAR(255), - client_secret VARCHAR(255), - scope VARCHAR(255), - authorized_grant_types VARCHAR(255), - web_server_redirect_uri VARCHAR(255), - authorities VARCHAR(255), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(255) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255), - authentication BLOB, - refresh_token VARCHAR(255) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(255), - token BLOB, - authentication BLOB -); - -CREATE TABLE oauth_code ( - activationCode VARCHAR(255), - authentication BLOB -); \ No newline at end of file diff --git a/chapter07/01 - postgresql/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql b/chapter07/01 - postgresql/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql deleted file mode 100644 index 7c3fdf3..0000000 --- a/chapter07/01 - postgresql/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(256) PRIMARY KEY, - resource_ids VARCHAR(256), - client_secret VARCHAR(256), - scope VARCHAR(256), - authorized_grant_types VARCHAR(256), - web_server_redirect_uri VARCHAR(256), - authorities VARCHAR(256), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(256) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256), - authentication BYTEA, - refresh_token VARCHAR(256) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(256), - token BYTEA, - authentication BYTEA -); - -CREATE TABLE oauth_code ( - code VARCHAR(256), - authentication BYTEA -); - -CREATE TABLE oauth_approvals ( - userId VARCHAR(256), - clientId VARCHAR(256), - scope VARCHAR(256), - status VARCHAR(10), - expiresAt TIMESTAMP, - lastModifiedAt TIMESTAMP -); - diff --git a/chapter07/01 - postgresql/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql b/chapter07/01 - postgresql/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql deleted file mode 100644 index 122b1fc..0000000 --- a/chapter07/01 - postgresql/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE copsboot_user ( - id UUID NOT NULL, - email VARCHAR(255), - password VARCHAR(255), - PRIMARY KEY (id) -); - -CREATE TABLE user_roles ( - user_id UUID NOT NULL, - roles VARCHAR(255) -); - -ALTER TABLE user_roles - ADD CONSTRAINT FK7je59ku3x462eqxu4ss3das1s -FOREIGN KEY (user_id) -REFERENCES copsboot_user; diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..73e7b68 100644 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,13 @@ package com.example.copsboot; -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java new file mode 100644 index 0000000..3ddeac0 --- /dev/null +++ b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java @@ -0,0 +1,30 @@ +package com.example.copsboot.infrastructure.test; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//tag::class[] +@Retention(RetentionPolicy.RUNTIME) +@CopsbootControllerTest +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class) +public @interface CopsbootControllerDocumentationTest { + + @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> + Class[] value() default {}; + + @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") //<6> + Class[] controllers() default {}; +} +//end::class[] diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java new file mode 100644 index 0000000..02e070e --- /dev/null +++ b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java @@ -0,0 +1,21 @@ +package com.example.copsboot.infrastructure.test; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +class CopsbootControllerDocumentationTestConfiguration { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java index c33238a..6696635 100644 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java +++ b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java @@ -1,10 +1,10 @@ package com.example.copsboot.infrastructure.test; -import com.example.copsboot.infrastructure.SpringProfiles; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,23 +12,12 @@ /** * Custom annotation for all {@link org.springframework.stereotype.Controller Controller} tests on the project. By using * this single annotation, everything is configured properly to test a controller: - *
    - *
  • Import of {@link CopsbootControllerTestConfiguration}
  • - *
  • test profile active
  • - *
- *

- * Example usage: - *

- * @RunWith(SpringRunner.class)
- * @CopsbootControllerTest(UserController.class)
- * public class UserControllerTest {
- * 
*/ //tag::class[] -@Retention(RetentionPolicy.RUNTIME) //<1> -@WebMvcTest //<2> -@ContextConfiguration(classes = CopsbootControllerTestConfiguration.class) //<3> -@ActiveProfiles(SpringProfiles.TEST) //<4> +@Retention(RetentionPolicy.RUNTIME) //<.> +@WebMvcTest //<.> +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> public @interface CopsbootControllerTest { @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java deleted file mode 100644 index 7231430..0000000 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.infrastructure.test; - -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; - -@TestConfiguration -@Import(OAuth2ServerConfiguration.class) -public class CopsbootControllerTestConfiguration { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - -} diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index 3217c4a..b37e583 100644 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -1,16 +1,12 @@ package com.example.copsboot.user; -import com.example.copsboot.infrastructure.SpringProfiles; import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -19,9 +15,7 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest -@ActiveProfiles(SpringProfiles.TEST) public class UserRepositoryTest { @Autowired @@ -30,50 +24,16 @@ public class UserRepositoryTest { //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - assertThat(repository.count()).isEqualTo(1L); //<7> + assertThat(repository.count()).isEqualTo(1L); } //end::testStoreUser[] - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); - - assertThat(optional).isEmpty(); - } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @@ -83,4 +43,4 @@ public UniqueIdGenerator generator() { } } //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/Users.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index e0d24b0..805c501 100644 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,133 +1,94 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.infrastructure.test.CopsbootControllerDocumentationTest; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) -@CopsbootControllerTest(UserRestController.class) +@CopsbootControllerDocumentationTest(UserRestController.class) public class UserRestControllerDocumentation { -//end::class-annotations[] - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] } diff --git a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index 9014594..2acf875 100644 --- a/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter07/01 - postgresql/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,97 +1,84 @@ package com.example.copsboot.user.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.verify; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) -public class UserRestControllerTest { -//end::class-annotations[] - @Autowired - private MockMvc mvc; +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } } diff --git a/chapter07/01 - postgresql/src/test/resources/application-test.properties b/chapter07/01 - postgresql/src/test/resources/application-test.properties deleted file mode 100644 index 02b4003..0000000 --- a/chapter07/01 - postgresql/src/test/resources/application-test.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter07/02 - testcontainers/.mvn/wrapper/maven-wrapper.jar b/chapter07/02 - testcontainers/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter07/02 - testcontainers/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter07/02 - testcontainers/.mvn/wrapper/maven-wrapper.properties b/chapter07/02 - testcontainers/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter07/02 - testcontainers/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter07/02 - testcontainers/docker-compose.yaml b/chapter07/02 - testcontainers/docker-compose.yaml new file mode 100644 index 0000000..92cea56 --- /dev/null +++ b/chapter07/02 - testcontainers/docker-compose.yaml @@ -0,0 +1,20 @@ +version: '3' +services: + db: + image: 'postgres:16.0' + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: my-postgres-db-pwd + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter07/02 - testcontainers/mvnw b/chapter07/02 - testcontainers/mvnw index 5bf251c..66df285 100755 --- a/chapter07/02 - testcontainers/mvnw +++ b/chapter07/02 - testcontainers/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter07/02 - testcontainers/mvnw.cmd b/chapter07/02 - testcontainers/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter07/02 - testcontainers/mvnw.cmd +++ b/chapter07/02 - testcontainers/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter07/02 - testcontainers/pom.xml b/chapter07/02 - testcontainers/pom.xml index 059f9dd..43db322 100644 --- a/chapter07/02 - testcontainers/pom.xml +++ b/chapter07/02 - testcontainers/pom.xml @@ -1,231 +1,208 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 29.0-jre + + + org.postgresql + postgresql + runtime + + + + + org.flywaydb + flyway-core + + - - 2.0.3.RELEASE - 1.11.2 - - + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-configuration-processor - true - - - com.google.guava - guava - ${guava.version} - - - org.projectlombok - lombok - - - org.postgresql - postgresql - - - org.flywaydb - flyway-core - + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - org.testcontainers - postgresql - ${testcontainers.version} - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter07/02 - testcontainers/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter07/02 - testcontainers/src/docs/asciidoc/Copsboot REST API Guide.adoc similarity index 91% rename from chapter07/02 - testcontainers/src/main/asciidoc/Copsboot REST API Guide.adoc rename to chapter07/02 - testcontainers/src/docs/asciidoc/Copsboot REST API Guide.adoc index 255bc8e..b0b91ae 100644 --- a/chapter07/02 - testcontainers/src/main/asciidoc/Copsboot REST API Guide.adoc +++ b/chapter07/02 - testcontainers/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -11,4 +11,4 @@ The Copsboot project uses a REST API for interfacing with the server. This documentation covers version {project-version} of the application. -include::_users.adoc[] \ No newline at end of file +include::_users.adoc[] diff --git a/chapter07/02 - testcontainers/src/main/asciidoc/_users.adoc b/chapter07/02 - testcontainers/src/docs/asciidoc/_users.adoc similarity index 56% rename from chapter07/02 - testcontainers/src/main/asciidoc/_users.adoc rename to chapter07/02 - testcontainers/src/docs/asciidoc/_users.adoc index a033db8..2becf75 100644 --- a/chapter07/02 - testcontainers/src/main/asciidoc/_users.adoc +++ b/chapter07/02 - testcontainers/src/docs/asciidoc/_users.adoc @@ -7,12 +7,12 @@ The API allows to get information on the currently logged on user via a `GET` on `/api/users/me`. If you are not a logged on user, the following response will be returned: -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] +operation::own-details-unauthorized[snippets='http-request,http-response'] //end::initial-doc[] If you do log on as a user, you get more information on that user: -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] +operation::own-details[snippets='http-request,http-response,response-fields'] //tag::create-user[] @@ -20,5 +20,5 @@ operation::authenticated-officer-details-example[snippets='http-request,http-res To create an new user, do a `POST` on `/api/users`: -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java index 344a5fe..fb1cc59 100644 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java @@ -6,6 +6,7 @@ public final class SpringProfiles { public static final String TEST = "test"; public static final String STAGING = "staging"; public static final String PROD = "prod"; + public static final String REPOSITORY_TEST = "repository-test"; public static final String INTEGRATION_TEST = "integration-test"; private SpringProfiles() { diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/User.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/User.java +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserNotFoundException.java deleted file mode 100644 index 1f65f04..0000000 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> -public class UserNotFoundException extends RuntimeException { - public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); - } -} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserService.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserService.java index 9e155a3..61846a5 100644 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserService.java @@ -1,9 +1,28 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } - Optional getUser(UserId userId); + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } + // end::createUser[] } diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index 7ab85e9..0000000 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..0d8f0ab --- /dev/null +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,16 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); //<.> + String email = jwt.getClaimAsString("email"); //<.> + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/UserRestController.java index c74ccd8..796adc1 100644 --- a/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter07/02 - testcontainers/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,41 +1,52 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] - @PostMapping //<1> - @ResponseStatus(HttpStatus.CREATED) //<2> - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { //<3> - User officer = service.createOfficer(parameters.getEmail(), //<4> - parameters.getPassword()); - return UserDto.fromUser(officer); //<5> + // tag::createUser[] + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/Entity.java b/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/Entity.java index a573e0e..3a45231 100644 --- a/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter07/02 - testcontainers/src/main/java/com/example/orm/jpa/Entity.java @@ -1,6 +1,5 @@ package com.example.orm.jpa; -import java.io.Serializable; /** * Interface for entity objects. diff --git a/chapter07/02 - testcontainers/src/main/resources/application-dev.properties b/chapter07/02 - testcontainers/src/main/resources/application-dev.properties deleted file mode 100644 index f72b4c7..0000000 --- a/chapter07/02 - testcontainers/src/main/resources/application-dev.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter07/02 - testcontainers/src/main/resources/application-local.properties b/chapter07/02 - testcontainers/src/main/resources/application-local.properties index 8f13f3f..7e354d5 100644 --- a/chapter07/02 - testcontainers/src/main/resources/application-local.properties +++ b/chapter07/02 - testcontainers/src/main/resources/application-local.properties @@ -3,13 +3,9 @@ spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.username=postgres spring.datasource.password=my-postgres-db-pwd spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata -spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create -spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=create.sql - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +#spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=create.sql +#spring.jpa.properties.hibernate.hbm2ddl.delimiter=; diff --git a/chapter07/02 - testcontainers/src/main/resources/application.properties b/chapter07/02 - testcontainers/src/main/resources/application.properties index e69de29..22c3363 100644 --- a/chapter07/02 - testcontainers/src/main/resources/application.properties +++ b/chapter07/02 - testcontainers/src/main/resources/application.properties @@ -0,0 +1,3 @@ +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter07/02 - testcontainers/src/main/resources/db/migration/V1.0.0.1__users.sql b/chapter07/02 - testcontainers/src/main/resources/db/migration/V1.0.0.1__users.sql new file mode 100644 index 0000000..d1939fa --- /dev/null +++ b/chapter07/02 - testcontainers/src/main/resources/db/migration/V1.0.0.1__users.sql @@ -0,0 +1,7 @@ +CREATE TABLE copsboot_user +( + id uuid NOT NULL PRIMARY KEY, + auth_server_id uuid, + email VARCHAR(255), + mobile_token VARCHAR(255) +); diff --git a/chapter07/02 - testcontainers/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql b/chapter07/02 - testcontainers/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql deleted file mode 100644 index 485336f..0000000 --- a/chapter07/02 - testcontainers/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,42 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(255) PRIMARY KEY, - resource_ids VARCHAR(255), - client_secret VARCHAR(255), - scope VARCHAR(255), - authorized_grant_types VARCHAR(255), - web_server_redirect_uri VARCHAR(255), - authorities VARCHAR(255), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(255) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255), - authentication BLOB, - refresh_token VARCHAR(255) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(255), - token BLOB, - authentication BLOB -); - -CREATE TABLE oauth_code ( - activationCode VARCHAR(255), - authentication BLOB -); \ No newline at end of file diff --git a/chapter07/02 - testcontainers/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql b/chapter07/02 - testcontainers/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql deleted file mode 100644 index 7c3fdf3..0000000 --- a/chapter07/02 - testcontainers/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(256) PRIMARY KEY, - resource_ids VARCHAR(256), - client_secret VARCHAR(256), - scope VARCHAR(256), - authorized_grant_types VARCHAR(256), - web_server_redirect_uri VARCHAR(256), - authorities VARCHAR(256), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(256) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256), - authentication BYTEA, - refresh_token VARCHAR(256) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(256), - token BYTEA, - authentication BYTEA -); - -CREATE TABLE oauth_code ( - code VARCHAR(256), - authentication BYTEA -); - -CREATE TABLE oauth_approvals ( - userId VARCHAR(256), - clientId VARCHAR(256), - scope VARCHAR(256), - status VARCHAR(10), - expiresAt TIMESTAMP, - lastModifiedAt TIMESTAMP -); - diff --git a/chapter07/02 - testcontainers/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql b/chapter07/02 - testcontainers/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql deleted file mode 100644 index 122b1fc..0000000 --- a/chapter07/02 - testcontainers/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE copsboot_user ( - id UUID NOT NULL, - email VARCHAR(255), - password VARCHAR(255), - PRIMARY KEY (id) -); - -CREATE TABLE user_roles ( - user_id UUID NOT NULL, - roles VARCHAR(255) -); - -ALTER TABLE user_roles - ADD CONSTRAINT FK7je59ku3x462eqxu4ss3das1s -FOREIGN KEY (user_id) -REFERENCES copsboot_user; diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..5feb390 100644 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,16 @@ package com.example.copsboot; import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java new file mode 100644 index 0000000..3ddeac0 --- /dev/null +++ b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java @@ -0,0 +1,30 @@ +package com.example.copsboot.infrastructure.test; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//tag::class[] +@Retention(RetentionPolicy.RUNTIME) +@CopsbootControllerTest +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class) +public @interface CopsbootControllerDocumentationTest { + + @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> + Class[] value() default {}; + + @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") //<6> + Class[] controllers() default {}; +} +//end::class[] diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java new file mode 100644 index 0000000..02e070e --- /dev/null +++ b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java @@ -0,0 +1,21 @@ +package com.example.copsboot.infrastructure.test; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +class CopsbootControllerDocumentationTestConfiguration { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java index c33238a..6696635 100644 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java +++ b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java @@ -1,10 +1,10 @@ package com.example.copsboot.infrastructure.test; -import com.example.copsboot.infrastructure.SpringProfiles; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,23 +12,12 @@ /** * Custom annotation for all {@link org.springframework.stereotype.Controller Controller} tests on the project. By using * this single annotation, everything is configured properly to test a controller: - *
    - *
  • Import of {@link CopsbootControllerTestConfiguration}
  • - *
  • test profile active
  • - *
- *

- * Example usage: - *

- * @RunWith(SpringRunner.class)
- * @CopsbootControllerTest(UserController.class)
- * public class UserControllerTest {
- * 
*/ //tag::class[] -@Retention(RetentionPolicy.RUNTIME) //<1> -@WebMvcTest //<2> -@ContextConfiguration(classes = CopsbootControllerTestConfiguration.class) //<3> -@ActiveProfiles(SpringProfiles.TEST) //<4> +@Retention(RetentionPolicy.RUNTIME) //<.> +@WebMvcTest //<.> +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> public @interface CopsbootControllerTest { @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java deleted file mode 100644 index 7231430..0000000 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.infrastructure.test; - -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; - -@TestConfiguration -@Import(OAuth2ServerConfiguration.class) -public class CopsbootControllerTestConfiguration { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - -} diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java deleted file mode 100644 index 720f959..0000000 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.copsboot.user; - -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.util.HashSet; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@DataJpaTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> -@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) //<2> -public class UserRepositoryIntegrationTest { - @Autowired - private UserRepository repository; - @PersistenceContext - private EntityManager entityManager; - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - public void testSaveUser() { - Set roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); - - assertThat(repository.count()).isEqualTo(1L); - - entityManager.flush(); //<3> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM user_roles", Long.class)).isEqualTo(1L); - assertThat(jdbcTemplate.queryForObject("SELECT roles FROM user_roles", String.class)).isEqualTo("OFFICER"); - } -} diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index 3217c4a..19c23fe 100644 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -3,14 +3,16 @@ import com.example.copsboot.infrastructure.SpringProfiles; import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -19,62 +21,34 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest -@ActiveProfiles(SpringProfiles.TEST) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) //<2> public class UserRepositoryTest { @Autowired private UserRepository repository; + @PersistenceContext + private EntityManager entityManager; + @Autowired + private JdbcTemplate jdbcTemplate; - //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> - - assertThat(repository.count()).isEqualTo(1L); //<7> - } - //end::testStoreUser[] + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } + assertThat(repository.count()).isEqualTo(1L); - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); + entityManager.flush(); //<3> - assertThat(optional).isEmpty(); + assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> + assertThat(jdbcTemplate.queryForObject("SELECT email FROM copsboot_user", String.class)).isEqualTo("alex.foley@beverly-hills.com"); } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @Bean @@ -82,5 +56,4 @@ public UniqueIdGenerator generator() { return new InMemoryUniqueIdGenerator(); } } - //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/Users.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index e0d24b0..805c501 100644 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,133 +1,94 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.infrastructure.test.CopsbootControllerDocumentationTest; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) -@CopsbootControllerTest(UserRestController.class) +@CopsbootControllerDocumentationTest(UserRestController.class) public class UserRestControllerDocumentation { -//end::class-annotations[] - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] } diff --git a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index 9014594..2acf875 100644 --- a/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter07/02 - testcontainers/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,97 +1,84 @@ package com.example.copsboot.user.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.verify; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) -public class UserRestControllerTest { -//end::class-annotations[] - @Autowired - private MockMvc mvc; +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } } diff --git a/chapter07/02 - testcontainers/src/test/resources/application-integration-test.properties b/chapter07/02 - testcontainers/src/test/resources/application-integration-test.properties index 159536c..c61e563 100644 --- a/chapter07/02 - testcontainers/src/test/resources/application-integration-test.properties +++ b/chapter07/02 - testcontainers/src/test/resources/application-integration-test.properties @@ -1,11 +1,6 @@ -spring.datasource.url=jdbc:tc:postgresql://localhost/copsbootdb +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.username=user spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none - -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter07/02 - testcontainers/src/test/resources/application-repository-test.properties b/chapter07/02 - testcontainers/src/test/resources/application-repository-test.properties new file mode 100644 index 0000000..c61e563 --- /dev/null +++ b/chapter07/02 - testcontainers/src/test/resources/application-repository-test.properties @@ -0,0 +1,6 @@ +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver +spring.datasource.username=user +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter07/02 - testcontainers/src/test/resources/application-test.properties b/chapter07/02 - testcontainers/src/test/resources/application-test.properties deleted file mode 100644 index 02b4003..0000000 --- a/chapter07/02 - testcontainers/src/test/resources/application-test.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter07/02 - testcontainers/src/test/resources/logback-test.xml b/chapter07/02 - testcontainers/src/test/resources/logback-test.xml index bf47fec..164429c 100644 --- a/chapter07/02 - testcontainers/src/test/resources/logback-test.xml +++ b/chapter07/02 - testcontainers/src/test/resources/logback-test.xml @@ -5,7 +5,7 @@ - + @@ -17,14 +17,8 @@ - - - - - - - \ No newline at end of file + diff --git a/chapter08/01 - builtin/.mvn/wrapper/maven-wrapper.jar b/chapter08/01 - builtin/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter08/01 - builtin/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter08/01 - builtin/.mvn/wrapper/maven-wrapper.properties b/chapter08/01 - builtin/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter08/01 - builtin/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter08/01 - builtin/docker-compose.yaml b/chapter08/01 - builtin/docker-compose.yaml new file mode 100644 index 0000000..92cea56 --- /dev/null +++ b/chapter08/01 - builtin/docker-compose.yaml @@ -0,0 +1,20 @@ +version: '3' +services: + db: + image: 'postgres:16.0' + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: my-postgres-db-pwd + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter08/01 - builtin/mvnw b/chapter08/01 - builtin/mvnw index 5bf251c..66df285 100755 --- a/chapter08/01 - builtin/mvnw +++ b/chapter08/01 - builtin/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter08/01 - builtin/mvnw.cmd b/chapter08/01 - builtin/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter08/01 - builtin/mvnw.cmd +++ b/chapter08/01 - builtin/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter08/01 - builtin/pom.xml b/chapter08/01 - builtin/pom.xml index 059f9dd..43db322 100644 --- a/chapter08/01 - builtin/pom.xml +++ b/chapter08/01 - builtin/pom.xml @@ -1,231 +1,208 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 29.0-jre + + + org.postgresql + postgresql + runtime + + + + + org.flywaydb + flyway-core + + - - 2.0.3.RELEASE - 1.11.2 - - + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-configuration-processor - true - - - com.google.guava - guava - ${guava.version} - - - org.projectlombok - lombok - - - org.postgresql - postgresql - - - org.flywaydb - flyway-core - + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - org.testcontainers - postgresql - ${testcontainers.version} - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter08/01 - builtin/src/docs/asciidoc/Copsboot REST API Guide.adoc b/chapter08/01 - builtin/src/docs/asciidoc/Copsboot REST API Guide.adoc new file mode 100644 index 0000000..b0b91ae --- /dev/null +++ b/chapter08/01 - builtin/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -0,0 +1,14 @@ += Copsboot REST API Guide +:icons: font +:toc: +:toclevels: 2 + +:numbered: + +== Introduction + +The Copsboot project uses a REST API for interfacing with the server. + +This documentation covers version {project-version} of the application. + +include::_users.adoc[] diff --git a/chapter08/01 - builtin/src/docs/asciidoc/_users.adoc b/chapter08/01 - builtin/src/docs/asciidoc/_users.adoc new file mode 100644 index 0000000..2becf75 --- /dev/null +++ b/chapter08/01 - builtin/src/docs/asciidoc/_users.adoc @@ -0,0 +1,24 @@ +//tag::initial-doc[] +== User Management + +=== User information + +The API allows to get information on the currently logged on user +via a `GET` on `/api/users/me`. If you are not a logged on user, the +following response will be returned: + +operation::own-details-unauthorized[snippets='http-request,http-response'] +//end::initial-doc[] + +If you do log on as a user, you get more information on that user: + +operation::own-details[snippets='http-request,http-response,response-fields'] + + +//tag::create-user[] +=== Create a user + +To create an new user, do a `POST` on `/api/users`: + +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter08/01 - builtin/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter08/01 - builtin/src/main/asciidoc/Copsboot REST API Guide.adoc deleted file mode 100644 index 255bc8e..0000000 --- a/chapter08/01 - builtin/src/main/asciidoc/Copsboot REST API Guide.adoc +++ /dev/null @@ -1,14 +0,0 @@ -= Copsboot REST API Guide -:icons: font -:toc: -:toclevels: 2 - -:numbered: - -== Introduction - -The Copsboot project uses a REST API for interfacing with the server. - -This documentation covers version {project-version} of the application. - -include::_users.adoc[] \ No newline at end of file diff --git a/chapter08/01 - builtin/src/main/asciidoc/_users.adoc b/chapter08/01 - builtin/src/main/asciidoc/_users.adoc deleted file mode 100644 index a033db8..0000000 --- a/chapter08/01 - builtin/src/main/asciidoc/_users.adoc +++ /dev/null @@ -1,24 +0,0 @@ -//tag::initial-doc[] -== User Management - -=== User information - -The API allows to get information on the currently logged on user -via a `GET` on `/api/users/me`. If you are not a logged on user, the -following response will be returned: - -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] -//end::initial-doc[] - -If you do log on as a user, you get more information on that user: - -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] - - -//tag::create-user[] -=== Create a user - -To create an new user, do a `POST` on `/api/users`: - -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java index 344a5fe..fb1cc59 100644 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java @@ -6,6 +6,7 @@ public final class SpringProfiles { public static final String TEST = "test"; public static final String STAGING = "staging"; public static final String PROD = "prod"; + public static final String REPOSITORY_TEST = "repository-test"; public static final String INTEGRATION_TEST = "integration-test"; private SpringProfiles() { diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java index 9c92c49..8d26775 100644 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java @@ -1,11 +1,4 @@ package com.example.copsboot.infrastructure.mvc; -import lombok.Value; - -//tag::class[] -@Value -public class FieldErrorResponse { - private String fieldName; - private String errorMessage; +public record FieldErrorResponse(String fieldName, String errorMesesage) { } -//end::class[] \ No newline at end of file diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/User.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/User.java +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserNotFoundException.java deleted file mode 100644 index 1f65f04..0000000 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> -public class UserNotFoundException extends RuntimeException { - public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); - } -} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserService.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserService.java index 9e155a3..61846a5 100644 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserService.java @@ -1,9 +1,28 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } - Optional getUser(UserId userId); + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } + // end::createUser[] } diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index 7ab85e9..0000000 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..b87302d --- /dev/null +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,17 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import jakarta.validation.constraints.NotEmpty; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(@NotEmpty String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + String email = jwt.getClaimAsString("email"); + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/UserRestController.java index b5aa1a8..e0a6545 100644 --- a/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter08/01 - builtin/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,49 +1,53 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import lombok.Value; -import org.springframework.beans.factory.annotation.Autowired; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.Valid; -import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import java.util.stream.Collectors; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] + // tag::createUser[] @PostMapping @ResponseStatus(HttpStatus.CREATED) - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { - User officer = service.createOfficer(parameters.getEmail(), - parameters.getPassword()); - return UserDto.fromUser(officer); + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/Entity.java b/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/Entity.java index a573e0e..3a45231 100644 --- a/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter08/01 - builtin/src/main/java/com/example/orm/jpa/Entity.java @@ -1,6 +1,5 @@ package com.example.orm.jpa; -import java.io.Serializable; /** * Interface for entity objects. diff --git a/chapter08/01 - builtin/src/main/resources/application-dev.properties b/chapter08/01 - builtin/src/main/resources/application-dev.properties deleted file mode 100644 index f72b4c7..0000000 --- a/chapter08/01 - builtin/src/main/resources/application-dev.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter08/01 - builtin/src/main/resources/application-local.properties b/chapter08/01 - builtin/src/main/resources/application-local.properties index 8f13f3f..7e354d5 100644 --- a/chapter08/01 - builtin/src/main/resources/application-local.properties +++ b/chapter08/01 - builtin/src/main/resources/application-local.properties @@ -3,13 +3,9 @@ spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.username=postgres spring.datasource.password=my-postgres-db-pwd spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata -spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create -spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=create.sql - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +#spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=create.sql +#spring.jpa.properties.hibernate.hbm2ddl.delimiter=; diff --git a/chapter08/01 - builtin/src/main/resources/application.properties b/chapter08/01 - builtin/src/main/resources/application.properties index e69de29..3e80adf 100644 --- a/chapter08/01 - builtin/src/main/resources/application.properties +++ b/chapter08/01 - builtin/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/copsboot + +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter08/01 - builtin/src/main/resources/db/migration/V1.0.0.1__users.sql b/chapter08/01 - builtin/src/main/resources/db/migration/V1.0.0.1__users.sql new file mode 100644 index 0000000..d1939fa --- /dev/null +++ b/chapter08/01 - builtin/src/main/resources/db/migration/V1.0.0.1__users.sql @@ -0,0 +1,7 @@ +CREATE TABLE copsboot_user +( + id uuid NOT NULL PRIMARY KEY, + auth_server_id uuid, + email VARCHAR(255), + mobile_token VARCHAR(255) +); diff --git a/chapter08/01 - builtin/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql b/chapter08/01 - builtin/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql deleted file mode 100644 index 485336f..0000000 --- a/chapter08/01 - builtin/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,42 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(255) PRIMARY KEY, - resource_ids VARCHAR(255), - client_secret VARCHAR(255), - scope VARCHAR(255), - authorized_grant_types VARCHAR(255), - web_server_redirect_uri VARCHAR(255), - authorities VARCHAR(255), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(255) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255), - authentication BLOB, - refresh_token VARCHAR(255) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(255), - token BLOB, - authentication BLOB -); - -CREATE TABLE oauth_code ( - activationCode VARCHAR(255), - authentication BLOB -); \ No newline at end of file diff --git a/chapter08/01 - builtin/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql b/chapter08/01 - builtin/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql deleted file mode 100644 index 7c3fdf3..0000000 --- a/chapter08/01 - builtin/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(256) PRIMARY KEY, - resource_ids VARCHAR(256), - client_secret VARCHAR(256), - scope VARCHAR(256), - authorized_grant_types VARCHAR(256), - web_server_redirect_uri VARCHAR(256), - authorities VARCHAR(256), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(256) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256), - authentication BYTEA, - refresh_token VARCHAR(256) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(256), - token BYTEA, - authentication BYTEA -); - -CREATE TABLE oauth_code ( - code VARCHAR(256), - authentication BYTEA -); - -CREATE TABLE oauth_approvals ( - userId VARCHAR(256), - clientId VARCHAR(256), - scope VARCHAR(256), - status VARCHAR(10), - expiresAt TIMESTAMP, - lastModifiedAt TIMESTAMP -); - diff --git a/chapter08/01 - builtin/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql b/chapter08/01 - builtin/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql deleted file mode 100644 index 122b1fc..0000000 --- a/chapter08/01 - builtin/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE copsboot_user ( - id UUID NOT NULL, - email VARCHAR(255), - password VARCHAR(255), - PRIMARY KEY (id) -); - -CREATE TABLE user_roles ( - user_id UUID NOT NULL, - roles VARCHAR(255) -); - -ALTER TABLE user_roles - ADD CONSTRAINT FK7je59ku3x462eqxu4ss3das1s -FOREIGN KEY (user_id) -REFERENCES copsboot_user; diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..5feb390 100644 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter08/01 - builtin/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,16 @@ package com.example.copsboot; import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java new file mode 100644 index 0000000..3ddeac0 --- /dev/null +++ b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java @@ -0,0 +1,30 @@ +package com.example.copsboot.infrastructure.test; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//tag::class[] +@Retention(RetentionPolicy.RUNTIME) +@CopsbootControllerTest +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class) +public @interface CopsbootControllerDocumentationTest { + + @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> + Class[] value() default {}; + + @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") //<6> + Class[] controllers() default {}; +} +//end::class[] diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java new file mode 100644 index 0000000..02e070e --- /dev/null +++ b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java @@ -0,0 +1,21 @@ +package com.example.copsboot.infrastructure.test; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +class CopsbootControllerDocumentationTestConfiguration { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java index c33238a..6696635 100644 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java +++ b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java @@ -1,10 +1,10 @@ package com.example.copsboot.infrastructure.test; -import com.example.copsboot.infrastructure.SpringProfiles; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,23 +12,12 @@ /** * Custom annotation for all {@link org.springframework.stereotype.Controller Controller} tests on the project. By using * this single annotation, everything is configured properly to test a controller: - *
    - *
  • Import of {@link CopsbootControllerTestConfiguration}
  • - *
  • test profile active
  • - *
- *

- * Example usage: - *

- * @RunWith(SpringRunner.class)
- * @CopsbootControllerTest(UserController.class)
- * public class UserControllerTest {
- * 
*/ //tag::class[] -@Retention(RetentionPolicy.RUNTIME) //<1> -@WebMvcTest //<2> -@ContextConfiguration(classes = CopsbootControllerTestConfiguration.class) //<3> -@ActiveProfiles(SpringProfiles.TEST) //<4> +@Retention(RetentionPolicy.RUNTIME) //<.> +@WebMvcTest //<.> +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> public @interface CopsbootControllerTest { @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java deleted file mode 100644 index 7231430..0000000 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.infrastructure.test; - -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; - -@TestConfiguration -@Import(OAuth2ServerConfiguration.class) -public class CopsbootControllerTestConfiguration { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - -} diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java deleted file mode 100644 index 720f959..0000000 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.copsboot.user; - -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.util.HashSet; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@DataJpaTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> -@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) //<2> -public class UserRepositoryIntegrationTest { - @Autowired - private UserRepository repository; - @PersistenceContext - private EntityManager entityManager; - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - public void testSaveUser() { - Set roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); - - assertThat(repository.count()).isEqualTo(1L); - - entityManager.flush(); //<3> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM user_roles", Long.class)).isEqualTo(1L); - assertThat(jdbcTemplate.queryForObject("SELECT roles FROM user_roles", String.class)).isEqualTo("OFFICER"); - } -} diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index 3217c4a..19c23fe 100644 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -3,14 +3,16 @@ import com.example.copsboot.infrastructure.SpringProfiles; import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -19,62 +21,34 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest -@ActiveProfiles(SpringProfiles.TEST) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) //<2> public class UserRepositoryTest { @Autowired private UserRepository repository; + @PersistenceContext + private EntityManager entityManager; + @Autowired + private JdbcTemplate jdbcTemplate; - //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> - - assertThat(repository.count()).isEqualTo(1L); //<7> - } - //end::testStoreUser[] + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } + assertThat(repository.count()).isEqualTo(1L); - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); + entityManager.flush(); //<3> - assertThat(optional).isEmpty(); + assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> + assertThat(jdbcTemplate.queryForObject("SELECT email FROM copsboot_user", String.class)).isEqualTo("alex.foley@beverly-hills.com"); } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @Bean @@ -82,5 +56,4 @@ public UniqueIdGenerator generator() { return new InMemoryUniqueIdGenerator(); } } - //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/Users.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index e0d24b0..805c501 100644 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,133 +1,94 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.infrastructure.test.CopsbootControllerDocumentationTest; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) -@CopsbootControllerTest(UserRestController.class) +@CopsbootControllerDocumentationTest(UserRestController.class) public class UserRestControllerDocumentation { -//end::class-annotations[] - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] } diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index 519fa70..a20d744 100644 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,120 +1,104 @@ package com.example.copsboot.user.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) -public class UserRestControllerTest { -//end::class-annotations[] - @Autowired - private MockMvc mvc; +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } - //tag::pwdshort[] + // tag::emptyToken[] @Test - public void testCreateOfficerIfPasswordIsTooShort() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "pwd"; // <1> - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isBadRequest()) //<2> - .andDo(print()); //<3> - - verify(service, never()).createOfficer(email, password); //<4> + void givenEmptyMobileToken_badRequestIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "" + } + """)) //<.> + .andExpect(status().isBadRequest()) //<.> + .andDo(print()); //<.> + + verify(userService, never()).createUser(any(CreateUserParameters.class)); //<.> } - //end::pwdshort[] + // end::emptyToken[] } diff --git a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerWithResponseBodyValidationTest.java b/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerWithResponseBodyValidationTest.java deleted file mode 100644 index 4ee1390..0000000 --- a/chapter08/01 - builtin/src/test/java/com/example/copsboot/user/web/UserRestControllerWithResponseBodyValidationTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.copsboot.user.web; - -import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; - -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -//tag::class-annotations[] -@RunWith(SpringRunner.class) -@CopsbootControllerTest(UserRestController.class) -public class UserRestControllerWithResponseBodyValidationTest { - //end::class-annotations[] - @Autowired - private MockMvc mvc; - - @Autowired - private ObjectMapper objectMapper; - @MockBean - private UserService service; - - //tag::pwdshort[] - @Test - public void testCreateOfficerIfPasswordIsTooShort() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("errors[0].fieldName").value("password")); //<1> - - verify(service, never()).createOfficer(email, password); - } - //end::pwdshort[] -} diff --git a/chapter08/01 - builtin/src/test/resources/application-integration-test.properties b/chapter08/01 - builtin/src/test/resources/application-integration-test.properties index 159536c..c61e563 100644 --- a/chapter08/01 - builtin/src/test/resources/application-integration-test.properties +++ b/chapter08/01 - builtin/src/test/resources/application-integration-test.properties @@ -1,11 +1,6 @@ -spring.datasource.url=jdbc:tc:postgresql://localhost/copsbootdb +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.username=user spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none - -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter08/01 - builtin/src/test/resources/application-repository-test.properties b/chapter08/01 - builtin/src/test/resources/application-repository-test.properties new file mode 100644 index 0000000..c61e563 --- /dev/null +++ b/chapter08/01 - builtin/src/test/resources/application-repository-test.properties @@ -0,0 +1,6 @@ +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver +spring.datasource.username=user +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter08/01 - builtin/src/test/resources/application-test.properties b/chapter08/01 - builtin/src/test/resources/application-test.properties deleted file mode 100644 index 02b4003..0000000 --- a/chapter08/01 - builtin/src/test/resources/application-test.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter08/01 - builtin/src/test/resources/logback-test.xml b/chapter08/01 - builtin/src/test/resources/logback-test.xml index bf47fec..164429c 100644 --- a/chapter08/01 - builtin/src/test/resources/logback-test.xml +++ b/chapter08/01 - builtin/src/test/resources/logback-test.xml @@ -5,7 +5,7 @@ - + @@ -17,14 +17,8 @@ - - - - - - - \ No newline at end of file + diff --git a/chapter08/02 - customfield/.mvn/wrapper/maven-wrapper.jar b/chapter08/02 - customfield/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter08/02 - customfield/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter08/02 - customfield/.mvn/wrapper/maven-wrapper.properties b/chapter08/02 - customfield/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter08/02 - customfield/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter08/02 - customfield/docker-compose.yaml b/chapter08/02 - customfield/docker-compose.yaml new file mode 100644 index 0000000..92cea56 --- /dev/null +++ b/chapter08/02 - customfield/docker-compose.yaml @@ -0,0 +1,20 @@ +version: '3' +services: + db: + image: 'postgres:16.0' + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: my-postgres-db-pwd + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter08/02 - customfield/mvnw b/chapter08/02 - customfield/mvnw index 5bf251c..66df285 100755 --- a/chapter08/02 - customfield/mvnw +++ b/chapter08/02 - customfield/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter08/02 - customfield/mvnw.cmd b/chapter08/02 - customfield/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter08/02 - customfield/mvnw.cmd +++ b/chapter08/02 - customfield/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter08/02 - customfield/pom.xml b/chapter08/02 - customfield/pom.xml index 059f9dd..43db322 100644 --- a/chapter08/02 - customfield/pom.xml +++ b/chapter08/02 - customfield/pom.xml @@ -1,231 +1,208 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 29.0-jre + + + org.postgresql + postgresql + runtime + + + + + org.flywaydb + flyway-core + + - - 2.0.3.RELEASE - 1.11.2 - - + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-configuration-processor - true - - - com.google.guava - guava - ${guava.version} - - - org.projectlombok - lombok - - - org.postgresql - postgresql - - - org.flywaydb - flyway-core - + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - org.testcontainers - postgresql - ${testcontainers.version} - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter08/02 - customfield/src/docs/asciidoc/Copsboot REST API Guide.adoc b/chapter08/02 - customfield/src/docs/asciidoc/Copsboot REST API Guide.adoc new file mode 100644 index 0000000..b0b91ae --- /dev/null +++ b/chapter08/02 - customfield/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -0,0 +1,14 @@ += Copsboot REST API Guide +:icons: font +:toc: +:toclevels: 2 + +:numbered: + +== Introduction + +The Copsboot project uses a REST API for interfacing with the server. + +This documentation covers version {project-version} of the application. + +include::_users.adoc[] diff --git a/chapter08/02 - customfield/src/docs/asciidoc/_users.adoc b/chapter08/02 - customfield/src/docs/asciidoc/_users.adoc new file mode 100644 index 0000000..2becf75 --- /dev/null +++ b/chapter08/02 - customfield/src/docs/asciidoc/_users.adoc @@ -0,0 +1,24 @@ +//tag::initial-doc[] +== User Management + +=== User information + +The API allows to get information on the currently logged on user +via a `GET` on `/api/users/me`. If you are not a logged on user, the +following response will be returned: + +operation::own-details-unauthorized[snippets='http-request,http-response'] +//end::initial-doc[] + +If you do log on as a user, you get more information on that user: + +operation::own-details[snippets='http-request,http-response,response-fields'] + + +//tag::create-user[] +=== Create a user + +To create an new user, do a `POST` on `/api/users`: + +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter08/02 - customfield/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter08/02 - customfield/src/main/asciidoc/Copsboot REST API Guide.adoc deleted file mode 100644 index 255bc8e..0000000 --- a/chapter08/02 - customfield/src/main/asciidoc/Copsboot REST API Guide.adoc +++ /dev/null @@ -1,14 +0,0 @@ -= Copsboot REST API Guide -:icons: font -:toc: -:toclevels: 2 - -:numbered: - -== Introduction - -The Copsboot project uses a REST API for interfacing with the server. - -This documentation covers version {project-version} of the application. - -include::_users.adoc[] \ No newline at end of file diff --git a/chapter08/02 - customfield/src/main/asciidoc/_users.adoc b/chapter08/02 - customfield/src/main/asciidoc/_users.adoc deleted file mode 100644 index a033db8..0000000 --- a/chapter08/02 - customfield/src/main/asciidoc/_users.adoc +++ /dev/null @@ -1,24 +0,0 @@ -//tag::initial-doc[] -== User Management - -=== User information - -The API allows to get information on the currently logged on user -via a `GET` on `/api/users/me`. If you are not a logged on user, the -following response will be returned: - -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] -//end::initial-doc[] - -If you do log on as a user, you get more information on that user: - -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] - - -//tag::create-user[] -=== Create a user - -To create an new user, do a `POST` on `/api/users`: - -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java index 344a5fe..fb1cc59 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java @@ -6,6 +6,7 @@ public final class SpringProfiles { public static final String TEST = "test"; public static final String STAGING = "staging"; public static final String PROD = "prod"; + public static final String REPOSITORY_TEST = "repository-test"; public static final String INTEGRATION_TEST = "integration-test"; private SpringProfiles() { diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java index 9c92c49..8d26775 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java @@ -1,11 +1,4 @@ package com.example.copsboot.infrastructure.mvc; -import lombok.Value; - -//tag::class[] -@Value -public class FieldErrorResponse { - private String fieldName; - private String errorMessage; +public record FieldErrorResponse(String fieldName, String errorMesesage) { } -//end::class[] \ No newline at end of file diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/CreateReportParameters.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/CreateReportParameters.java new file mode 100644 index 0000000..64aeea6 --- /dev/null +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/CreateReportParameters.java @@ -0,0 +1,8 @@ +package com.example.copsboot.report; + +import com.example.copsboot.user.UserId; + +import java.time.Instant; + +public record CreateReportParameters(UserId userId, Instant dateTime, String description) { +} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/Report.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/Report.java index af25bfd..b10756f 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/Report.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/Report.java @@ -1,37 +1,36 @@ package com.example.copsboot.report; -import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; import com.example.orm.jpa.AbstractEntity; import com.example.util.ArtifactForFramework; +import jakarta.persistence.Entity; + +import java.time.Instant; -import javax.persistence.Entity; -import javax.persistence.ManyToOne; -import java.time.ZonedDateTime; //tag::class[] @Entity public class Report extends AbstractEntity { - @ManyToOne - private User reporter; - private ZonedDateTime dateTime; + + private UserId reporterId; + private Instant dateTime; private String description; @ArtifactForFramework protected Report() { } - - public Report(ReportId id, User reporter, ZonedDateTime dateTime, String description) { + public Report(ReportId id, UserId reporterId, Instant dateTime, String description) { super(id); - this.reporter = reporter; + this.reporterId = reporterId; this.dateTime = dateTime; this.description = description; } - public User getReporter() { - return reporter; + public UserId getReporterId() { + return reporterId; } - public ZonedDateTime getDateTime() { + public Instant getDateTime() { return dateTime; } diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/ReportService.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/ReportService.java index f6ed620..613248b 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/ReportService.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/ReportService.java @@ -1,9 +1,23 @@ package com.example.copsboot.report; -import com.example.copsboot.user.UserId; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.ZonedDateTime; -public interface ReportService { - Report createReport(UserId reporterId, ZonedDateTime dateTime, String description); +@Service +@Transactional +public class ReportService { + private final ReportRepository repository; + + public ReportService(ReportRepository repository) { + this.repository = repository; + } + + public Report createReport(CreateReportParameters parameters) { + return repository.save(new Report(repository.nextId(), + parameters.userId(), + parameters.dateTime(), + parameters.description())); + } } diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/ReportServiceImpl.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/ReportServiceImpl.java deleted file mode 100644 index 67c21e1..0000000 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/ReportServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserNotFoundException; -import com.example.copsboot.user.UserService; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.ZonedDateTime; - -@Service -@Transactional -public class ReportServiceImpl implements ReportService { - private final ReportRepository repository; - private final UserService userService; - - public ReportServiceImpl(ReportRepository repository, UserService userService) { - this.repository = repository; - this.userService = userService; - } - - @Override - public Report createReport(UserId reporterId, ZonedDateTime dateTime, String description) { - return repository.save(new Report(repository.nextId(), - userService.getUser(reporterId) - .orElseThrow(() -> new UserNotFoundException(reporterId)), - dateTime, - description)); - } -} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java deleted file mode 100644 index e506d1b..0000000 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.copsboot.report.web; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.ZonedDateTime; - -//tag::class[] -@Data -@AllArgsConstructor -@NoArgsConstructor -public class CreateReportParameters { - private ZonedDateTime dateTime; - private String description; -} -//end::class[] \ No newline at end of file diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java new file mode 100644 index 0000000..bd8d8ef --- /dev/null +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java @@ -0,0 +1,12 @@ +package com.example.copsboot.report.web; + +import com.example.copsboot.report.CreateReportParameters; +import com.example.copsboot.user.UserId; + +import java.time.Instant; + +public record CreateReportRequest(Instant dateTime, String description) { + public CreateReportParameters toParameters(UserId userId) { + return new CreateReportParameters(userId, dateTime, description); + } +} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/ReportDto.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/ReportDto.java index 0adc7f8..28e606e 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/ReportDto.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/ReportDto.java @@ -2,23 +2,21 @@ import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; -import lombok.Value; +import com.example.copsboot.user.UserService; -import java.time.ZonedDateTime; +import java.time.Instant; //tag::class[] -@Value -public class ReportDto { - private ReportId id; - private String reporter; - private ZonedDateTime dateTime; - private String description; +public record ReportDto(ReportId id, + String reporter, + Instant dateTime, + String description) { - public static ReportDto fromReport(Report report) { + public static ReportDto fromReport(Report report, UserService userService) { return new ReportDto(report.getId(), - report.getReporter().getEmail(), - report.getDateTime(), - report.getDescription()); + userService.getUserById(report.getReporterId()).getEmail(), + report.getDateTime(), + report.getDescription()); } } //end::class[] diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/ReportRestController.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/ReportRestController.java index 28b79ae..83f9d54 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/ReportRestController.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/report/web/ReportRestController.java @@ -1,31 +1,42 @@ package com.example.copsboot.report.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.report.CreateReportParameters; import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportService; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserNotFoundException; +import com.example.copsboot.user.UserService; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.UUID; //tag::class[] @RestController @RequestMapping("/api/reports") public class ReportRestController { private final ReportService service; + private final UserService userService; - public ReportRestController(ReportService service) { + public ReportRestController(ReportService service, UserService userService) { this.service = service; + this.userService = userService; } @PostMapping @ResponseStatus(HttpStatus.CREATED) - public ReportDto createReport(@AuthenticationPrincipal ApplicationUserDetails userDetails, - @Valid @RequestBody CreateReportParameters parameters) { - return ReportDto.fromReport(service.createReport(userDetails.getUserId(), - parameters.getDateTime(), - parameters.getDescription())); + public ReportDto createReport(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateReportRequest request) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + User user = userService.findUserByAuthServerId(authServerId) + .orElseThrow(() -> new UserNotFoundException(authServerId)); + CreateReportParameters parameters = request.toParameters(user.getId()); + Report report = service.createReport(parameters); + return ReportDto.fromReport(report, userService); } } //end::class[] diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/User.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/User.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java new file mode 100644 index 0000000..2a434e3 --- /dev/null +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class UserIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(UserId attribute) { + return attribute.getId(); + } + + @Override + public UserId convertToEntityAttribute(UUID dbData) { + return new UserId(dbData); + } +} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserNotFoundException.java index 1f65f04..97d0813 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserNotFoundException.java @@ -3,9 +3,13 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> +@ResponseStatus(HttpStatus.NOT_FOUND) public class UserNotFoundException extends RuntimeException { public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); + super(String.format("Unable to find user with id %s", userId)); + } + + public UserNotFoundException(AuthServerId authServerId) { + super(String.format("Unable to find user with auth server id %s", authServerId)); } } diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserService.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserService.java index 9e155a3..ec5aa13 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserService.java @@ -1,9 +1,33 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } + + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } - Optional getUser(UserId userId); + public User getUserById(UserId userId) { + return repository.findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + } + // end::createUser[] } diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index 7ab85e9..0000000 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..b87302d --- /dev/null +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,17 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import jakarta.validation.constraints.NotEmpty; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(@NotEmpty String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + String email = jwt.getClaimAsString("email"); + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/UserRestController.java index b5aa1a8..e0a6545 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter08/02 - customfield/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,49 +1,53 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import lombok.Value; -import org.springframework.beans.factory.annotation.Autowired; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.Valid; -import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import java.util.stream.Collectors; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] + // tag::createUser[] @PostMapping @ResponseStatus(HttpStatus.CREATED) - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { - User officer = service.createOfficer(parameters.getEmail(), - parameters.getPassword()); - return UserDto.fromUser(officer); + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/Entity.java b/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/Entity.java index a573e0e..3a45231 100644 --- a/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter08/02 - customfield/src/main/java/com/example/orm/jpa/Entity.java @@ -1,6 +1,5 @@ package com.example.orm.jpa; -import java.io.Serializable; /** * Interface for entity objects. diff --git a/chapter08/02 - customfield/src/main/resources/application-dev.properties b/chapter08/02 - customfield/src/main/resources/application-dev.properties deleted file mode 100644 index f72b4c7..0000000 --- a/chapter08/02 - customfield/src/main/resources/application-dev.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter08/02 - customfield/src/main/resources/application-local.properties b/chapter08/02 - customfield/src/main/resources/application-local.properties index 8f13f3f..7e354d5 100644 --- a/chapter08/02 - customfield/src/main/resources/application-local.properties +++ b/chapter08/02 - customfield/src/main/resources/application-local.properties @@ -3,13 +3,9 @@ spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.username=postgres spring.datasource.password=my-postgres-db-pwd spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata -spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create -spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=create.sql - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +#spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=create.sql +#spring.jpa.properties.hibernate.hbm2ddl.delimiter=; diff --git a/chapter08/02 - customfield/src/main/resources/application.properties b/chapter08/02 - customfield/src/main/resources/application.properties index e69de29..3e80adf 100644 --- a/chapter08/02 - customfield/src/main/resources/application.properties +++ b/chapter08/02 - customfield/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/copsboot + +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter08/02 - customfield/src/main/resources/db/migration/V1.0.0.1__users.sql b/chapter08/02 - customfield/src/main/resources/db/migration/V1.0.0.1__users.sql new file mode 100644 index 0000000..d1939fa --- /dev/null +++ b/chapter08/02 - customfield/src/main/resources/db/migration/V1.0.0.1__users.sql @@ -0,0 +1,7 @@ +CREATE TABLE copsboot_user +( + id uuid NOT NULL PRIMARY KEY, + auth_server_id uuid, + email VARCHAR(255), + mobile_token VARCHAR(255) +); diff --git a/chapter08/02 - customfield/src/main/resources/db/migration/V1.0.0.2__reports.sql b/chapter08/02 - customfield/src/main/resources/db/migration/V1.0.0.2__reports.sql new file mode 100644 index 0000000..cc2e26c --- /dev/null +++ b/chapter08/02 - customfield/src/main/resources/db/migration/V1.0.0.2__reports.sql @@ -0,0 +1,8 @@ +CREATE TABLE report +( + date_time TIMESTAMP(6) WITH TIME ZONE, + id uuid NOT NULL, + description VARCHAR(255), + reporter_id uuid, + PRIMARY KEY (id) +); diff --git a/chapter08/02 - customfield/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql b/chapter08/02 - customfield/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql deleted file mode 100644 index 485336f..0000000 --- a/chapter08/02 - customfield/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,42 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(255) PRIMARY KEY, - resource_ids VARCHAR(255), - client_secret VARCHAR(255), - scope VARCHAR(255), - authorized_grant_types VARCHAR(255), - web_server_redirect_uri VARCHAR(255), - authorities VARCHAR(255), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(255) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255), - authentication BLOB, - refresh_token VARCHAR(255) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(255), - token BLOB, - authentication BLOB -); - -CREATE TABLE oauth_code ( - activationCode VARCHAR(255), - authentication BLOB -); \ No newline at end of file diff --git a/chapter08/02 - customfield/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql b/chapter08/02 - customfield/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql deleted file mode 100644 index 7c3fdf3..0000000 --- a/chapter08/02 - customfield/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(256) PRIMARY KEY, - resource_ids VARCHAR(256), - client_secret VARCHAR(256), - scope VARCHAR(256), - authorized_grant_types VARCHAR(256), - web_server_redirect_uri VARCHAR(256), - authorities VARCHAR(256), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(256) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256), - authentication BYTEA, - refresh_token VARCHAR(256) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(256), - token BYTEA, - authentication BYTEA -); - -CREATE TABLE oauth_code ( - code VARCHAR(256), - authentication BYTEA -); - -CREATE TABLE oauth_approvals ( - userId VARCHAR(256), - clientId VARCHAR(256), - scope VARCHAR(256), - status VARCHAR(10), - expiresAt TIMESTAMP, - lastModifiedAt TIMESTAMP -); - diff --git a/chapter08/02 - customfield/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql b/chapter08/02 - customfield/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql deleted file mode 100644 index 122b1fc..0000000 --- a/chapter08/02 - customfield/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE copsboot_user ( - id UUID NOT NULL, - email VARCHAR(255), - password VARCHAR(255), - PRIMARY KEY (id) -); - -CREATE TABLE user_roles ( - user_id UUID NOT NULL, - roles VARCHAR(255) -); - -ALTER TABLE user_roles - ADD CONSTRAINT FK7je59ku3x462eqxu4ss3das1s -FOREIGN KEY (user_id) -REFERENCES copsboot_user; diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..5feb390 100644 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter08/02 - customfield/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,16 @@ package com.example.copsboot; import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java new file mode 100644 index 0000000..3ddeac0 --- /dev/null +++ b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java @@ -0,0 +1,30 @@ +package com.example.copsboot.infrastructure.test; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//tag::class[] +@Retention(RetentionPolicy.RUNTIME) +@CopsbootControllerTest +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class) +public @interface CopsbootControllerDocumentationTest { + + @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> + Class[] value() default {}; + + @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") //<6> + Class[] controllers() default {}; +} +//end::class[] diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java new file mode 100644 index 0000000..02e070e --- /dev/null +++ b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java @@ -0,0 +1,21 @@ +package com.example.copsboot.infrastructure.test; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +class CopsbootControllerDocumentationTestConfiguration { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java index c33238a..6696635 100644 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java +++ b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java @@ -1,10 +1,10 @@ package com.example.copsboot.infrastructure.test; -import com.example.copsboot.infrastructure.SpringProfiles; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,23 +12,12 @@ /** * Custom annotation for all {@link org.springframework.stereotype.Controller Controller} tests on the project. By using * this single annotation, everything is configured properly to test a controller: - *
    - *
  • Import of {@link CopsbootControllerTestConfiguration}
  • - *
  • test profile active
  • - *
- *

- * Example usage: - *

- * @RunWith(SpringRunner.class)
- * @CopsbootControllerTest(UserController.class)
- * public class UserControllerTest {
- * 
*/ //tag::class[] -@Retention(RetentionPolicy.RUNTIME) //<1> -@WebMvcTest //<2> -@ContextConfiguration(classes = CopsbootControllerTestConfiguration.class) //<3> -@ActiveProfiles(SpringProfiles.TEST) //<4> +@Retention(RetentionPolicy.RUNTIME) //<.> +@WebMvcTest //<.> +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> public @interface CopsbootControllerTest { @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java deleted file mode 100644 index 7231430..0000000 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.infrastructure.test; - -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; - -@TestConfiguration -@Import(OAuth2ServerConfiguration.class) -public class CopsbootControllerTestConfiguration { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - -} diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java index 51e8f79..dcf5eee 100644 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java +++ b/chapter08/02 - customfield/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java @@ -1,61 +1,76 @@ package com.example.copsboot.report.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; +import com.example.copsboot.report.CreateReportParameters; import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; import com.example.copsboot.report.ReportService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; +import com.example.copsboot.user.UserService; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.time.ZonedDateTime; +import java.time.Instant; +import java.util.Optional; import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; //tag::class[] -@RunWith(SpringRunner.class) @CopsbootControllerTest(ReportRestController.class) public class ReportRestControllerTest { @Autowired - private MockMvc mvc; - - @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; @MockBean private ReportService service; + @MockBean + private UserService userService; @Test public void officerIsAbleToPostAReport() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - ZonedDateTime dateTime = ZonedDateTime.parse("2018-04-11T22:59:03.189+02:00"); - String description = "This is a test report description."; - CreateReportParameters parameters = new CreateReportParameters(dateTime, - description); - when(service.createReport(eq(Users.officer().getId()), any(ZonedDateTime.class), eq(description))) - .thenReturn(new Report(new ReportId(UUID.randomUUID()), Users.officer(), dateTime, description)); - mvc.perform(post("/api/reports") - .header(HEADER_AUTHORIZATION, bearer(accessToken)) - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("reporter").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("dateTime").value("2018-04-11T22:59:03.189+02:00")) - .andExpect(jsonPath("description").value(description)); + UserId userId = new UserId(UUID.randomUUID()); + AuthServerId authServerId = new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); + User user = new User(userId, + "wim@example.com", + authServerId, + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"); + when(userService.findUserByAuthServerId(authServerId)) + .thenReturn(Optional.of(user)); + when(userService.getUserById(userId)) + .thenReturn(user); + when(service.createReport(any(CreateReportParameters.class))) + .thenReturn(new Report(new ReportId(UUID.randomUUID()), + userId, + Instant.parse("2023-04-11T22:59:03.189+02:00"), + "This is a test report description.")); + mockMvc.perform(post("/api/reports") + .with(jwt().jwt(builder -> builder.subject(authServerId.value().toString()) + .claim("email", "wim@example.com")) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "dateTime": "2023-04-11T22:59:03.189+02:00", + "description": "This is a test report description." + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("id").exists()) + .andExpect(jsonPath("reporter").value("wim@example.com")) + .andExpect(jsonPath("dateTime").value("2023-04-11T20:59:03.189Z")) + .andExpect(jsonPath("description").value("This is a test report description.")); } } -//end::class[] \ No newline at end of file +//end::class[] diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java deleted file mode 100644 index 720f959..0000000 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.copsboot.user; - -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.util.HashSet; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@DataJpaTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> -@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) //<2> -public class UserRepositoryIntegrationTest { - @Autowired - private UserRepository repository; - @PersistenceContext - private EntityManager entityManager; - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - public void testSaveUser() { - Set roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); - - assertThat(repository.count()).isEqualTo(1L); - - entityManager.flush(); //<3> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM user_roles", Long.class)).isEqualTo(1L); - assertThat(jdbcTemplate.queryForObject("SELECT roles FROM user_roles", String.class)).isEqualTo("OFFICER"); - } -} diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index 3217c4a..19c23fe 100644 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -3,14 +3,16 @@ import com.example.copsboot.infrastructure.SpringProfiles; import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -19,62 +21,34 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest -@ActiveProfiles(SpringProfiles.TEST) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) //<2> public class UserRepositoryTest { @Autowired private UserRepository repository; + @PersistenceContext + private EntityManager entityManager; + @Autowired + private JdbcTemplate jdbcTemplate; - //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> - - assertThat(repository.count()).isEqualTo(1L); //<7> - } - //end::testStoreUser[] + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } + assertThat(repository.count()).isEqualTo(1L); - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); + entityManager.flush(); //<3> - assertThat(optional).isEmpty(); + assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> + assertThat(jdbcTemplate.queryForObject("SELECT email FROM copsboot_user", String.class)).isEqualTo("alex.foley@beverly-hills.com"); } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @Bean @@ -82,5 +56,4 @@ public UniqueIdGenerator generator() { return new InMemoryUniqueIdGenerator(); } } - //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/Users.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index e0d24b0..805c501 100644 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,133 +1,94 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.infrastructure.test.CopsbootControllerDocumentationTest; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) -@CopsbootControllerTest(UserRestController.class) +@CopsbootControllerDocumentationTest(UserRestController.class) public class UserRestControllerDocumentation { -//end::class-annotations[] - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] } diff --git a/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index eb07c50..a20d744 100644 --- a/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter08/02 - customfield/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,118 +1,104 @@ package com.example.copsboot.user.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) -public class UserRestControllerTest { -//end::class-annotations[] - @Autowired - private MockMvc mvc; +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } + // tag::emptyToken[] @Test - public void testCreateOfficerIfPasswordIsTooShort() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("errors[0].fieldName").value("password")); - - verify(service, never()).createOfficer(email, password); + void givenEmptyMobileToken_badRequestIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "" + } + """)) //<.> + .andExpect(status().isBadRequest()) //<.> + .andDo(print()); //<.> + + verify(userService, never()).createUser(any(CreateUserParameters.class)); //<.> } + // end::emptyToken[] } diff --git a/chapter08/02 - customfield/src/test/resources/application-integration-test.properties b/chapter08/02 - customfield/src/test/resources/application-integration-test.properties index 159536c..c61e563 100644 --- a/chapter08/02 - customfield/src/test/resources/application-integration-test.properties +++ b/chapter08/02 - customfield/src/test/resources/application-integration-test.properties @@ -1,11 +1,6 @@ -spring.datasource.url=jdbc:tc:postgresql://localhost/copsbootdb +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.username=user spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none - -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter08/02 - customfield/src/test/resources/application-repository-test.properties b/chapter08/02 - customfield/src/test/resources/application-repository-test.properties new file mode 100644 index 0000000..c61e563 --- /dev/null +++ b/chapter08/02 - customfield/src/test/resources/application-repository-test.properties @@ -0,0 +1,6 @@ +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver +spring.datasource.username=user +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter08/02 - customfield/src/test/resources/application-test.properties b/chapter08/02 - customfield/src/test/resources/application-test.properties deleted file mode 100644 index 02b4003..0000000 --- a/chapter08/02 - customfield/src/test/resources/application-test.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter08/02 - customfield/src/test/resources/logback-test.xml b/chapter08/02 - customfield/src/test/resources/logback-test.xml index bf47fec..164429c 100644 --- a/chapter08/02 - customfield/src/test/resources/logback-test.xml +++ b/chapter08/02 - customfield/src/test/resources/logback-test.xml @@ -5,7 +5,7 @@ - + @@ -17,14 +17,8 @@ - - - - - - - \ No newline at end of file + diff --git a/chapter08/03 - customfieldfinal/.mvn/wrapper/maven-wrapper.jar b/chapter08/03 - customfieldfinal/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter08/03 - customfieldfinal/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter08/03 - customfieldfinal/.mvn/wrapper/maven-wrapper.properties b/chapter08/03 - customfieldfinal/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter08/03 - customfieldfinal/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter08/03 - customfieldfinal/docker-compose.yaml b/chapter08/03 - customfieldfinal/docker-compose.yaml new file mode 100644 index 0000000..92cea56 --- /dev/null +++ b/chapter08/03 - customfieldfinal/docker-compose.yaml @@ -0,0 +1,20 @@ +version: '3' +services: + db: + image: 'postgres:16.0' + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: my-postgres-db-pwd + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter08/03 - customfieldfinal/mvnw b/chapter08/03 - customfieldfinal/mvnw index 5bf251c..66df285 100755 --- a/chapter08/03 - customfieldfinal/mvnw +++ b/chapter08/03 - customfieldfinal/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter08/03 - customfieldfinal/mvnw.cmd b/chapter08/03 - customfieldfinal/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter08/03 - customfieldfinal/mvnw.cmd +++ b/chapter08/03 - customfieldfinal/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter08/03 - customfieldfinal/pom.xml b/chapter08/03 - customfieldfinal/pom.xml index 059f9dd..43db322 100644 --- a/chapter08/03 - customfieldfinal/pom.xml +++ b/chapter08/03 - customfieldfinal/pom.xml @@ -1,231 +1,208 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 29.0-jre + + + org.postgresql + postgresql + runtime + + + + + org.flywaydb + flyway-core + + - - 2.0.3.RELEASE - 1.11.2 - - + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-configuration-processor - true - - - com.google.guava - guava - ${guava.version} - - - org.projectlombok - lombok - - - org.postgresql - postgresql - - - org.flywaydb - flyway-core - + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - org.testcontainers - postgresql - ${testcontainers.version} - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter08/03 - customfieldfinal/src/docs/asciidoc/Copsboot REST API Guide.adoc b/chapter08/03 - customfieldfinal/src/docs/asciidoc/Copsboot REST API Guide.adoc new file mode 100644 index 0000000..b0b91ae --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -0,0 +1,14 @@ += Copsboot REST API Guide +:icons: font +:toc: +:toclevels: 2 + +:numbered: + +== Introduction + +The Copsboot project uses a REST API for interfacing with the server. + +This documentation covers version {project-version} of the application. + +include::_users.adoc[] diff --git a/chapter08/03 - customfieldfinal/src/docs/asciidoc/_users.adoc b/chapter08/03 - customfieldfinal/src/docs/asciidoc/_users.adoc new file mode 100644 index 0000000..2becf75 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/docs/asciidoc/_users.adoc @@ -0,0 +1,24 @@ +//tag::initial-doc[] +== User Management + +=== User information + +The API allows to get information on the currently logged on user +via a `GET` on `/api/users/me`. If you are not a logged on user, the +following response will be returned: + +operation::own-details-unauthorized[snippets='http-request,http-response'] +//end::initial-doc[] + +If you do log on as a user, you get more information on that user: + +operation::own-details[snippets='http-request,http-response,response-fields'] + + +//tag::create-user[] +=== Create a user + +To create an new user, do a `POST` on `/api/users`: + +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter08/03 - customfieldfinal/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter08/03 - customfieldfinal/src/main/asciidoc/Copsboot REST API Guide.adoc deleted file mode 100644 index 255bc8e..0000000 --- a/chapter08/03 - customfieldfinal/src/main/asciidoc/Copsboot REST API Guide.adoc +++ /dev/null @@ -1,14 +0,0 @@ -= Copsboot REST API Guide -:icons: font -:toc: -:toclevels: 2 - -:numbered: - -== Introduction - -The Copsboot project uses a REST API for interfacing with the server. - -This documentation covers version {project-version} of the application. - -include::_users.adoc[] \ No newline at end of file diff --git a/chapter08/03 - customfieldfinal/src/main/asciidoc/_users.adoc b/chapter08/03 - customfieldfinal/src/main/asciidoc/_users.adoc deleted file mode 100644 index a033db8..0000000 --- a/chapter08/03 - customfieldfinal/src/main/asciidoc/_users.adoc +++ /dev/null @@ -1,24 +0,0 @@ -//tag::initial-doc[] -== User Management - -=== User information - -The API allows to get information on the currently logged on user -via a `GET` on `/api/users/me`. If you are not a logged on user, the -following response will be returned: - -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] -//end::initial-doc[] - -If you do log on as a user, you get more information on that user: - -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] - - -//tag::create-user[] -=== Create a user - -To create an new user, do a `POST` on `/api/users`: - -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java index 344a5fe..fb1cc59 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java @@ -6,6 +6,7 @@ public final class SpringProfiles { public static final String TEST = "test"; public static final String STAGING = "staging"; public static final String PROD = "prod"; + public static final String REPOSITORY_TEST = "repository-test"; public static final String INTEGRATION_TEST = "integration-test"; private SpringProfiles() { diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java index 9c92c49..8d26775 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java @@ -1,11 +1,4 @@ package com.example.copsboot.infrastructure.mvc; -import lombok.Value; - -//tag::class[] -@Value -public class FieldErrorResponse { - private String fieldName; - private String errorMessage; +public record FieldErrorResponse(String fieldName, String errorMesesage) { } -//end::class[] \ No newline at end of file diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/CreateReportParameters.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/CreateReportParameters.java new file mode 100644 index 0000000..64aeea6 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/CreateReportParameters.java @@ -0,0 +1,8 @@ +package com.example.copsboot.report; + +import com.example.copsboot.user.UserId; + +import java.time.Instant; + +public record CreateReportParameters(UserId userId, Instant dateTime, String description) { +} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/Report.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/Report.java index c0f9c66..b10756f 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/Report.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/Report.java @@ -1,36 +1,36 @@ package com.example.copsboot.report; -import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; import com.example.orm.jpa.AbstractEntity; import com.example.util.ArtifactForFramework; +import jakarta.persistence.Entity; + +import java.time.Instant; -import javax.persistence.Entity; -import javax.persistence.ManyToOne; -import java.time.ZonedDateTime; //tag::class[] @Entity public class Report extends AbstractEntity { - @ManyToOne - private User reporter; - private ZonedDateTime dateTime; + + private UserId reporterId; + private Instant dateTime; private String description; @ArtifactForFramework protected Report() { } - public Report(ReportId id, User reporter, ZonedDateTime dateTime, String description) { + public Report(ReportId id, UserId reporterId, Instant dateTime, String description) { super(id); - this.reporter = reporter; + this.reporterId = reporterId; this.dateTime = dateTime; this.description = description; } - public User getReporter() { - return reporter; + public UserId getReporterId() { + return reporterId; } - public ZonedDateTime getDateTime() { + public Instant getDateTime() { return dateTime; } diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/ReportService.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/ReportService.java index f6ed620..613248b 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/ReportService.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/ReportService.java @@ -1,9 +1,23 @@ package com.example.copsboot.report; -import com.example.copsboot.user.UserId; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.ZonedDateTime; -public interface ReportService { - Report createReport(UserId reporterId, ZonedDateTime dateTime, String description); +@Service +@Transactional +public class ReportService { + private final ReportRepository repository; + + public ReportService(ReportRepository repository) { + this.repository = repository; + } + + public Report createReport(CreateReportParameters parameters) { + return repository.save(new Report(repository.nextId(), + parameters.userId(), + parameters.dateTime(), + parameters.description())); + } } diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/ReportServiceImpl.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/ReportServiceImpl.java deleted file mode 100644 index 67c21e1..0000000 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/ReportServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserNotFoundException; -import com.example.copsboot.user.UserService; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.ZonedDateTime; - -@Service -@Transactional -public class ReportServiceImpl implements ReportService { - private final ReportRepository repository; - private final UserService userService; - - public ReportServiceImpl(ReportRepository repository, UserService userService) { - this.repository = repository; - this.userService = userService; - } - - @Override - public Report createReport(UserId reporterId, ZonedDateTime dateTime, String description) { - return repository.save(new Report(repository.nextId(), - userService.getUser(reporterId) - .orElseThrow(() -> new UserNotFoundException(reporterId)), - dateTime, - description)); - } -} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java deleted file mode 100644 index 9a169e4..0000000 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.report.web; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.ZonedDateTime; - -//tag::class[] -@Data -@AllArgsConstructor -@NoArgsConstructor -public class CreateReportParameters { - private ZonedDateTime dateTime; - - @ValidReportDescription - private String description; -} -//end::class[] \ No newline at end of file diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java new file mode 100644 index 0000000..2c7fac1 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java @@ -0,0 +1,12 @@ +package com.example.copsboot.report.web; + +import com.example.copsboot.report.CreateReportParameters; +import com.example.copsboot.user.UserId; + +import java.time.Instant; + +public record CreateReportRequest(Instant dateTime, @ValidReportDescription String description) { + public CreateReportParameters toParameters(UserId userId) { + return new CreateReportParameters(userId, dateTime, description); + } +} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java index e16971d..aa30ca4 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java @@ -1,7 +1,7 @@ package com.example.copsboot.report.web; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; public class ReportDescriptionValidator implements ConstraintValidator { //<1> diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportDto.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportDto.java index 0adc7f8..28e606e 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportDto.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportDto.java @@ -2,23 +2,21 @@ import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; -import lombok.Value; +import com.example.copsboot.user.UserService; -import java.time.ZonedDateTime; +import java.time.Instant; //tag::class[] -@Value -public class ReportDto { - private ReportId id; - private String reporter; - private ZonedDateTime dateTime; - private String description; +public record ReportDto(ReportId id, + String reporter, + Instant dateTime, + String description) { - public static ReportDto fromReport(Report report) { + public static ReportDto fromReport(Report report, UserService userService) { return new ReportDto(report.getId(), - report.getReporter().getEmail(), - report.getDateTime(), - report.getDescription()); + userService.getUserById(report.getReporterId()).getEmail(), + report.getDateTime(), + report.getDescription()); } } //end::class[] diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportRestController.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportRestController.java index 28b79ae..83f9d54 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportRestController.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ReportRestController.java @@ -1,31 +1,42 @@ package com.example.copsboot.report.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.report.CreateReportParameters; import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportService; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserNotFoundException; +import com.example.copsboot.user.UserService; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.UUID; //tag::class[] @RestController @RequestMapping("/api/reports") public class ReportRestController { private final ReportService service; + private final UserService userService; - public ReportRestController(ReportService service) { + public ReportRestController(ReportService service, UserService userService) { this.service = service; + this.userService = userService; } @PostMapping @ResponseStatus(HttpStatus.CREATED) - public ReportDto createReport(@AuthenticationPrincipal ApplicationUserDetails userDetails, - @Valid @RequestBody CreateReportParameters parameters) { - return ReportDto.fromReport(service.createReport(userDetails.getUserId(), - parameters.getDateTime(), - parameters.getDescription())); + public ReportDto createReport(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateReportRequest request) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + User user = userService.findUserByAuthServerId(authServerId) + .orElseThrow(() -> new UserNotFoundException(authServerId)); + CreateReportParameters parameters = request.toParameters(user.getId()); + Report report = service.createReport(parameters); + return ReportDto.fromReport(report, userService); } } //end::class[] diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java index 41d39e9..ba8fa56 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java @@ -1,7 +1,7 @@ package com.example.copsboot.report.web; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -16,4 +16,4 @@ Class[] groups() default {}; //<5> Class[] payload() default {}; //<6> -} \ No newline at end of file +} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/User.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/User.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java new file mode 100644 index 0000000..2a434e3 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class UserIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(UserId attribute) { + return attribute.getId(); + } + + @Override + public UserId convertToEntityAttribute(UUID dbData) { + return new UserId(dbData); + } +} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserNotFoundException.java index 1f65f04..97d0813 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserNotFoundException.java @@ -3,9 +3,13 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> +@ResponseStatus(HttpStatus.NOT_FOUND) public class UserNotFoundException extends RuntimeException { public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); + super(String.format("Unable to find user with id %s", userId)); + } + + public UserNotFoundException(AuthServerId authServerId) { + super(String.format("Unable to find user with auth server id %s", authServerId)); } } diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserService.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserService.java index 9e155a3..ec5aa13 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserService.java @@ -1,9 +1,33 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } + + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } - Optional getUser(UserId userId); + public User getUserById(UserId userId) { + return repository.findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + } + // end::createUser[] } diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index 7ab85e9..0000000 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..b87302d --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,17 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import jakarta.validation.constraints.NotEmpty; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(@NotEmpty String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + String email = jwt.getClaimAsString("email"); + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/UserRestController.java index b5aa1a8..e0a6545 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,49 +1,53 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import lombok.Value; -import org.springframework.beans.factory.annotation.Autowired; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.Valid; -import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import java.util.stream.Collectors; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] + // tag::createUser[] @PostMapping @ResponseStatus(HttpStatus.CREATED) - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { - User officer = service.createOfficer(parameters.getEmail(), - parameters.getPassword()); - return UserDto.fromUser(officer); + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/Entity.java b/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/Entity.java index a573e0e..3a45231 100644 --- a/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter08/03 - customfieldfinal/src/main/java/com/example/orm/jpa/Entity.java @@ -1,6 +1,5 @@ package com.example.orm.jpa; -import java.io.Serializable; /** * Interface for entity objects. diff --git a/chapter08/03 - customfieldfinal/src/main/resources/application-dev.properties b/chapter08/03 - customfieldfinal/src/main/resources/application-dev.properties deleted file mode 100644 index f72b4c7..0000000 --- a/chapter08/03 - customfieldfinal/src/main/resources/application-dev.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter08/03 - customfieldfinal/src/main/resources/application-local.properties b/chapter08/03 - customfieldfinal/src/main/resources/application-local.properties index 8f13f3f..7e354d5 100644 --- a/chapter08/03 - customfieldfinal/src/main/resources/application-local.properties +++ b/chapter08/03 - customfieldfinal/src/main/resources/application-local.properties @@ -3,13 +3,9 @@ spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.username=postgres spring.datasource.password=my-postgres-db-pwd spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata -spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create -spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=create.sql - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +#spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=create.sql +#spring.jpa.properties.hibernate.hbm2ddl.delimiter=; diff --git a/chapter08/03 - customfieldfinal/src/main/resources/application.properties b/chapter08/03 - customfieldfinal/src/main/resources/application.properties index e69de29..3e80adf 100644 --- a/chapter08/03 - customfieldfinal/src/main/resources/application.properties +++ b/chapter08/03 - customfieldfinal/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/copsboot + +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter08/03 - customfieldfinal/src/main/resources/db/migration/V1.0.0.1__users.sql b/chapter08/03 - customfieldfinal/src/main/resources/db/migration/V1.0.0.1__users.sql new file mode 100644 index 0000000..d1939fa --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/resources/db/migration/V1.0.0.1__users.sql @@ -0,0 +1,7 @@ +CREATE TABLE copsboot_user +( + id uuid NOT NULL PRIMARY KEY, + auth_server_id uuid, + email VARCHAR(255), + mobile_token VARCHAR(255) +); diff --git a/chapter08/03 - customfieldfinal/src/main/resources/db/migration/V1.0.0.2__reports.sql b/chapter08/03 - customfieldfinal/src/main/resources/db/migration/V1.0.0.2__reports.sql new file mode 100644 index 0000000..cc2e26c --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/main/resources/db/migration/V1.0.0.2__reports.sql @@ -0,0 +1,8 @@ +CREATE TABLE report +( + date_time TIMESTAMP(6) WITH TIME ZONE, + id uuid NOT NULL, + description VARCHAR(255), + reporter_id uuid, + PRIMARY KEY (id) +); diff --git a/chapter08/03 - customfieldfinal/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql b/chapter08/03 - customfieldfinal/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql deleted file mode 100644 index 485336f..0000000 --- a/chapter08/03 - customfieldfinal/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,42 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(255) PRIMARY KEY, - resource_ids VARCHAR(255), - client_secret VARCHAR(255), - scope VARCHAR(255), - authorized_grant_types VARCHAR(255), - web_server_redirect_uri VARCHAR(255), - authorities VARCHAR(255), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(255) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255), - authentication BLOB, - refresh_token VARCHAR(255) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(255), - token BLOB, - authentication BLOB -); - -CREATE TABLE oauth_code ( - activationCode VARCHAR(255), - authentication BLOB -); \ No newline at end of file diff --git a/chapter08/03 - customfieldfinal/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql b/chapter08/03 - customfieldfinal/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql deleted file mode 100644 index 7c3fdf3..0000000 --- a/chapter08/03 - customfieldfinal/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(256) PRIMARY KEY, - resource_ids VARCHAR(256), - client_secret VARCHAR(256), - scope VARCHAR(256), - authorized_grant_types VARCHAR(256), - web_server_redirect_uri VARCHAR(256), - authorities VARCHAR(256), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(256) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256), - authentication BYTEA, - refresh_token VARCHAR(256) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(256), - token BYTEA, - authentication BYTEA -); - -CREATE TABLE oauth_code ( - code VARCHAR(256), - authentication BYTEA -); - -CREATE TABLE oauth_approvals ( - userId VARCHAR(256), - clientId VARCHAR(256), - scope VARCHAR(256), - status VARCHAR(10), - expiresAt TIMESTAMP, - lastModifiedAt TIMESTAMP -); - diff --git a/chapter08/03 - customfieldfinal/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql b/chapter08/03 - customfieldfinal/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql deleted file mode 100644 index 122b1fc..0000000 --- a/chapter08/03 - customfieldfinal/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE copsboot_user ( - id UUID NOT NULL, - email VARCHAR(255), - password VARCHAR(255), - PRIMARY KEY (id) -); - -CREATE TABLE user_roles ( - user_id UUID NOT NULL, - roles VARCHAR(255) -); - -ALTER TABLE user_roles - ADD CONSTRAINT FK7je59ku3x462eqxu4ss3das1s -FOREIGN KEY (user_id) -REFERENCES copsboot_user; diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..5feb390 100644 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,16 @@ package com.example.copsboot; import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java new file mode 100644 index 0000000..3ddeac0 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java @@ -0,0 +1,30 @@ +package com.example.copsboot.infrastructure.test; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//tag::class[] +@Retention(RetentionPolicy.RUNTIME) +@CopsbootControllerTest +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class) +public @interface CopsbootControllerDocumentationTest { + + @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> + Class[] value() default {}; + + @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") //<6> + Class[] controllers() default {}; +} +//end::class[] diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java new file mode 100644 index 0000000..02e070e --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java @@ -0,0 +1,21 @@ +package com.example.copsboot.infrastructure.test; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +class CopsbootControllerDocumentationTestConfiguration { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java index c33238a..6696635 100644 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java +++ b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java @@ -1,10 +1,10 @@ package com.example.copsboot.infrastructure.test; -import com.example.copsboot.infrastructure.SpringProfiles; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,23 +12,12 @@ /** * Custom annotation for all {@link org.springframework.stereotype.Controller Controller} tests on the project. By using * this single annotation, everything is configured properly to test a controller: - *
    - *
  • Import of {@link CopsbootControllerTestConfiguration}
  • - *
  • test profile active
  • - *
- *

- * Example usage: - *

- * @RunWith(SpringRunner.class)
- * @CopsbootControllerTest(UserController.class)
- * public class UserControllerTest {
- * 
*/ //tag::class[] -@Retention(RetentionPolicy.RUNTIME) //<1> -@WebMvcTest //<2> -@ContextConfiguration(classes = CopsbootControllerTestConfiguration.class) //<3> -@ActiveProfiles(SpringProfiles.TEST) //<4> +@Retention(RetentionPolicy.RUNTIME) //<.> +@WebMvcTest //<.> +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> public @interface CopsbootControllerTest { @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java deleted file mode 100644 index 7231430..0000000 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.infrastructure.test; - -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; - -@TestConfiguration -@Import(OAuth2ServerConfiguration.class) -public class CopsbootControllerTestConfiguration { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - -} diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/Reports.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/Reports.java deleted file mode 100644 index 2302dc9..0000000 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/Reports.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.Users; - -import java.time.ZonedDateTime; -import java.util.UUID; - -public class Reports { - public static Report createRandomReport(String description) { - return new Report(new ReportId(UUID.randomUUID()), - Users.newRandomOfficer(), - ZonedDateTime.now(), - description); - } -} diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java index 199247a..1b1ec35 100644 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java +++ b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java @@ -1,12 +1,12 @@ package com.example.copsboot.report.web; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; import org.junit.Test; -import javax.validation.ConstraintViolation; -import javax.validation.Validation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import java.time.ZonedDateTime; +import java.time.Instant; import java.util.Set; import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; @@ -16,25 +16,27 @@ public class ReportDescriptionValidatorTest { //tag::invalid[] @Test public void givenEmptyString_notValid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); //<1> - Validator validator = factory.getValidator(); //<2> + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { //<1> + Validator validator = factory.getValidator(); //<2> - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), ""); - Set> violationSet = validator.validate(parameters); //<3> - assertThat(violationSet).hasViolationOnPath("description"); //<4> + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), ""); + Set> violationSet = validator.validate(parameters); //<3> + assertThat(violationSet).hasViolationOnPath("description"); //<4> + } } //end::invalid[] //tag::valid[] @Test public void givenSuspectWordPresent_valid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - Validator validator = factory.getValidator(); - - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), - "The suspect was wearing a black hat."); - Set> violationSet = validator.validate(parameters); - assertThat(violationSet).hasNoViolations(); + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat."); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } } //end::valid[] -} \ No newline at end of file +} diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java index 49705e9..d6c6e5f 100644 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java +++ b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java @@ -1,61 +1,76 @@ package com.example.copsboot.report.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; +import com.example.copsboot.report.CreateReportParameters; import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; import com.example.copsboot.report.ReportService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; +import com.example.copsboot.user.UserService; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.time.ZonedDateTime; +import java.time.Instant; +import java.util.Optional; import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; //tag::class[] -@RunWith(SpringRunner.class) @CopsbootControllerTest(ReportRestController.class) public class ReportRestControllerTest { @Autowired - private MockMvc mvc; - - @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; @MockBean private ReportService service; + @MockBean + private UserService userService; @Test public void officerIsAbleToPostAReport() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - ZonedDateTime dateTime = ZonedDateTime.parse("2018-04-11T22:59:03.189+02:00"); - String description = "The suspect is wearing a black hat."; - CreateReportParameters parameters = new CreateReportParameters(dateTime, - description); - when(service.createReport(eq(Users.officer().getId()), any(ZonedDateTime.class), eq(description))) - .thenReturn(new Report(new ReportId(UUID.randomUUID()), Users.officer(), dateTime, description)); - mvc.perform(post("/api/reports") - .header(HEADER_AUTHORIZATION, bearer(accessToken)) - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("reporter").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("dateTime").value("2018-04-11T22:59:03.189+02:00")) - .andExpect(jsonPath("description").value(description)); + UserId userId = new UserId(UUID.randomUUID()); + AuthServerId authServerId = new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); + User user = new User(userId, + "wim@example.com", + authServerId, + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"); + when(userService.findUserByAuthServerId(authServerId)) + .thenReturn(Optional.of(user)); + when(userService.getUserById(userId)) + .thenReturn(user); + when(service.createReport(any(CreateReportParameters.class))) + .thenReturn(new Report(new ReportId(UUID.randomUUID()), + userId, + Instant.parse("2023-04-11T22:59:03.189+02:00"), + "This is a test report description. The suspect was wearing a black hat.")); + mockMvc.perform(post("/api/reports") + .with(jwt().jwt(builder -> builder.subject(authServerId.value().toString()) + .claim("email", "wim@example.com")) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "dateTime": "2023-04-11T22:59:03.189+02:00", + "description": "This is a test report description. The suspect was wearing a black hat." + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("id").exists()) + .andExpect(jsonPath("reporter").value("wim@example.com")) + .andExpect(jsonPath("dateTime").value("2023-04-11T20:59:03.189Z")) + .andExpect(jsonPath("description").value("This is a test report description. The suspect was wearing a black hat.")); } } -//end::class[] \ No newline at end of file +//end::class[] diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java deleted file mode 100644 index 720f959..0000000 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.copsboot.user; - -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.util.HashSet; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@DataJpaTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> -@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) //<2> -public class UserRepositoryIntegrationTest { - @Autowired - private UserRepository repository; - @PersistenceContext - private EntityManager entityManager; - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - public void testSaveUser() { - Set roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); - - assertThat(repository.count()).isEqualTo(1L); - - entityManager.flush(); //<3> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM user_roles", Long.class)).isEqualTo(1L); - assertThat(jdbcTemplate.queryForObject("SELECT roles FROM user_roles", String.class)).isEqualTo("OFFICER"); - } -} diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index 3217c4a..19c23fe 100644 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -3,14 +3,16 @@ import com.example.copsboot.infrastructure.SpringProfiles; import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -19,62 +21,34 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest -@ActiveProfiles(SpringProfiles.TEST) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) //<2> public class UserRepositoryTest { @Autowired private UserRepository repository; + @PersistenceContext + private EntityManager entityManager; + @Autowired + private JdbcTemplate jdbcTemplate; - //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> - - assertThat(repository.count()).isEqualTo(1L); //<7> - } - //end::testStoreUser[] + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } + assertThat(repository.count()).isEqualTo(1L); - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); + entityManager.flush(); //<3> - assertThat(optional).isEmpty(); + assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> + assertThat(jdbcTemplate.queryForObject("SELECT email FROM copsboot_user", String.class)).isEqualTo("alex.foley@beverly-hills.com"); } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @Bean @@ -82,5 +56,4 @@ public UniqueIdGenerator generator() { return new InMemoryUniqueIdGenerator(); } } - //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/Users.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index e0d24b0..805c501 100644 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,133 +1,94 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.infrastructure.test.CopsbootControllerDocumentationTest; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) -@CopsbootControllerTest(UserRestController.class) +@CopsbootControllerDocumentationTest(UserRestController.class) public class UserRestControllerDocumentation { -//end::class-annotations[] - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] } diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index eb07c50..a20d744 100644 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,118 +1,104 @@ package com.example.copsboot.user.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) -public class UserRestControllerTest { -//end::class-annotations[] - @Autowired - private MockMvc mvc; +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } + // tag::emptyToken[] @Test - public void testCreateOfficerIfPasswordIsTooShort() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("errors[0].fieldName").value("password")); - - verify(service, never()).createOfficer(email, password); + void givenEmptyMobileToken_badRequestIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "" + } + """)) //<.> + .andExpect(status().isBadRequest()) //<.> + .andDo(print()); //<.> + + verify(userService, never()).createUser(any(CreateUserParameters.class)); //<.> } + // end::emptyToken[] } diff --git a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java index 77c5f4c..21556a5 100644 --- a/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java +++ b/chapter08/03 - customfieldfinal/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java @@ -2,7 +2,8 @@ import org.assertj.core.api.AbstractAssert; -import javax.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolation; + import java.util.Set; import java.util.stream.Collectors; @@ -21,12 +22,12 @@ public ConstraintViolationSetAssert hasViolationOnPath(String path) { // check condition if (!containsViolationWithPath(actual, path)) { failWithMessage("There was no violation with path <%s>. Violation paths: <%s>", path, actual.stream() - .map(violation -> violation - .getPropertyPath() - .toString()) - .collect( - Collectors - .toList())); + .map(violation -> violation + .getPropertyPath() + .toString()) + .collect( + Collectors + .toList())); } return this; diff --git a/chapter08/03 - customfieldfinal/src/test/resources/application-integration-test.properties b/chapter08/03 - customfieldfinal/src/test/resources/application-integration-test.properties index 159536c..c61e563 100644 --- a/chapter08/03 - customfieldfinal/src/test/resources/application-integration-test.properties +++ b/chapter08/03 - customfieldfinal/src/test/resources/application-integration-test.properties @@ -1,11 +1,6 @@ -spring.datasource.url=jdbc:tc:postgresql://localhost/copsbootdb +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.username=user spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none - -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter08/03 - customfieldfinal/src/test/resources/application-repository-test.properties b/chapter08/03 - customfieldfinal/src/test/resources/application-repository-test.properties new file mode 100644 index 0000000..c61e563 --- /dev/null +++ b/chapter08/03 - customfieldfinal/src/test/resources/application-repository-test.properties @@ -0,0 +1,6 @@ +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver +spring.datasource.username=user +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter08/03 - customfieldfinal/src/test/resources/application-test.properties b/chapter08/03 - customfieldfinal/src/test/resources/application-test.properties deleted file mode 100644 index 02b4003..0000000 --- a/chapter08/03 - customfieldfinal/src/test/resources/application-test.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter08/03 - customfieldfinal/src/test/resources/logback-test.xml b/chapter08/03 - customfieldfinal/src/test/resources/logback-test.xml index bf47fec..164429c 100644 --- a/chapter08/03 - customfieldfinal/src/test/resources/logback-test.xml +++ b/chapter08/03 - customfieldfinal/src/test/resources/logback-test.xml @@ -5,7 +5,7 @@ - + @@ -17,14 +17,8 @@ - - - - - - - \ No newline at end of file + diff --git a/chapter08/04 - objectvalidation/.mvn/wrapper/maven-wrapper.jar b/chapter08/04 - objectvalidation/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter08/04 - objectvalidation/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter08/04 - objectvalidation/.mvn/wrapper/maven-wrapper.properties b/chapter08/04 - objectvalidation/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter08/04 - objectvalidation/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter08/04 - objectvalidation/docker-compose.yaml b/chapter08/04 - objectvalidation/docker-compose.yaml new file mode 100644 index 0000000..92cea56 --- /dev/null +++ b/chapter08/04 - objectvalidation/docker-compose.yaml @@ -0,0 +1,20 @@ +version: '3' +services: + db: + image: 'postgres:16.0' + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: my-postgres-db-pwd + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter08/04 - objectvalidation/mvnw b/chapter08/04 - objectvalidation/mvnw index 5bf251c..66df285 100755 --- a/chapter08/04 - objectvalidation/mvnw +++ b/chapter08/04 - objectvalidation/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter08/04 - objectvalidation/mvnw.cmd b/chapter08/04 - objectvalidation/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter08/04 - objectvalidation/mvnw.cmd +++ b/chapter08/04 - objectvalidation/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter08/04 - objectvalidation/pom.xml b/chapter08/04 - objectvalidation/pom.xml index 059f9dd..43db322 100644 --- a/chapter08/04 - objectvalidation/pom.xml +++ b/chapter08/04 - objectvalidation/pom.xml @@ -1,231 +1,208 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 29.0-jre + + + org.postgresql + postgresql + runtime + + + + + org.flywaydb + flyway-core + + - - 2.0.3.RELEASE - 1.11.2 - - + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-configuration-processor - true - - - com.google.guava - guava - ${guava.version} - - - org.projectlombok - lombok - - - org.postgresql - postgresql - - - org.flywaydb - flyway-core - + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - org.testcontainers - postgresql - ${testcontainers.version} - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter08/04 - objectvalidation/src/docs/asciidoc/Copsboot REST API Guide.adoc b/chapter08/04 - objectvalidation/src/docs/asciidoc/Copsboot REST API Guide.adoc new file mode 100644 index 0000000..b0b91ae --- /dev/null +++ b/chapter08/04 - objectvalidation/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -0,0 +1,14 @@ += Copsboot REST API Guide +:icons: font +:toc: +:toclevels: 2 + +:numbered: + +== Introduction + +The Copsboot project uses a REST API for interfacing with the server. + +This documentation covers version {project-version} of the application. + +include::_users.adoc[] diff --git a/chapter08/04 - objectvalidation/src/docs/asciidoc/_users.adoc b/chapter08/04 - objectvalidation/src/docs/asciidoc/_users.adoc new file mode 100644 index 0000000..2becf75 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/docs/asciidoc/_users.adoc @@ -0,0 +1,24 @@ +//tag::initial-doc[] +== User Management + +=== User information + +The API allows to get information on the currently logged on user +via a `GET` on `/api/users/me`. If you are not a logged on user, the +following response will be returned: + +operation::own-details-unauthorized[snippets='http-request,http-response'] +//end::initial-doc[] + +If you do log on as a user, you get more information on that user: + +operation::own-details[snippets='http-request,http-response,response-fields'] + + +//tag::create-user[] +=== Create a user + +To create an new user, do a `POST` on `/api/users`: + +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter08/04 - objectvalidation/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter08/04 - objectvalidation/src/main/asciidoc/Copsboot REST API Guide.adoc deleted file mode 100644 index 255bc8e..0000000 --- a/chapter08/04 - objectvalidation/src/main/asciidoc/Copsboot REST API Guide.adoc +++ /dev/null @@ -1,14 +0,0 @@ -= Copsboot REST API Guide -:icons: font -:toc: -:toclevels: 2 - -:numbered: - -== Introduction - -The Copsboot project uses a REST API for interfacing with the server. - -This documentation covers version {project-version} of the application. - -include::_users.adoc[] \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/main/asciidoc/_users.adoc b/chapter08/04 - objectvalidation/src/main/asciidoc/_users.adoc deleted file mode 100644 index a033db8..0000000 --- a/chapter08/04 - objectvalidation/src/main/asciidoc/_users.adoc +++ /dev/null @@ -1,24 +0,0 @@ -//tag::initial-doc[] -== User Management - -=== User information - -The API allows to get information on the currently logged on user -via a `GET` on `/api/users/me`. If you are not a logged on user, the -following response will be returned: - -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] -//end::initial-doc[] - -If you do log on as a user, you get more information on that user: - -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] - - -//tag::create-user[] -=== Create a user - -To create an new user, do a `POST` on `/api/users`: - -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java index 344a5fe..fb1cc59 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java @@ -6,6 +6,7 @@ public final class SpringProfiles { public static final String TEST = "test"; public static final String STAGING = "staging"; public static final String PROD = "prod"; + public static final String REPOSITORY_TEST = "repository-test"; public static final String INTEGRATION_TEST = "integration-test"; private SpringProfiles() { diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java index 9c92c49..8d26775 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java @@ -1,11 +1,4 @@ package com.example.copsboot.infrastructure.mvc; -import lombok.Value; - -//tag::class[] -@Value -public class FieldErrorResponse { - private String fieldName; - private String errorMessage; +public record FieldErrorResponse(String fieldName, String errorMesesage) { } -//end::class[] \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/CreateReportParameters.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/CreateReportParameters.java new file mode 100644 index 0000000..64aeea6 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/CreateReportParameters.java @@ -0,0 +1,8 @@ +package com.example.copsboot.report; + +import com.example.copsboot.user.UserId; + +import java.time.Instant; + +public record CreateReportParameters(UserId userId, Instant dateTime, String description) { +} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/Report.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/Report.java index c0f9c66..b10756f 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/Report.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/Report.java @@ -1,36 +1,36 @@ package com.example.copsboot.report; -import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; import com.example.orm.jpa.AbstractEntity; import com.example.util.ArtifactForFramework; +import jakarta.persistence.Entity; + +import java.time.Instant; -import javax.persistence.Entity; -import javax.persistence.ManyToOne; -import java.time.ZonedDateTime; //tag::class[] @Entity public class Report extends AbstractEntity { - @ManyToOne - private User reporter; - private ZonedDateTime dateTime; + + private UserId reporterId; + private Instant dateTime; private String description; @ArtifactForFramework protected Report() { } - public Report(ReportId id, User reporter, ZonedDateTime dateTime, String description) { + public Report(ReportId id, UserId reporterId, Instant dateTime, String description) { super(id); - this.reporter = reporter; + this.reporterId = reporterId; this.dateTime = dateTime; this.description = description; } - public User getReporter() { - return reporter; + public UserId getReporterId() { + return reporterId; } - public ZonedDateTime getDateTime() { + public Instant getDateTime() { return dateTime; } diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/ReportService.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/ReportService.java index f6ed620..613248b 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/ReportService.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/ReportService.java @@ -1,9 +1,23 @@ package com.example.copsboot.report; -import com.example.copsboot.user.UserId; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.ZonedDateTime; -public interface ReportService { - Report createReport(UserId reporterId, ZonedDateTime dateTime, String description); +@Service +@Transactional +public class ReportService { + private final ReportRepository repository; + + public ReportService(ReportRepository repository) { + this.repository = repository; + } + + public Report createReport(CreateReportParameters parameters) { + return repository.save(new Report(repository.nextId(), + parameters.userId(), + parameters.dateTime(), + parameters.description())); + } } diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/ReportServiceImpl.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/ReportServiceImpl.java deleted file mode 100644 index 67c21e1..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/ReportServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserNotFoundException; -import com.example.copsboot.user.UserService; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.ZonedDateTime; - -@Service -@Transactional -public class ReportServiceImpl implements ReportService { - private final ReportRepository repository; - private final UserService userService; - - public ReportServiceImpl(ReportRepository repository, UserService userService) { - this.repository = repository; - this.userService = userService; - } - - @Override - public Report createReport(UserId reporterId, ZonedDateTime dateTime, String description) { - return repository.save(new Report(repository.nextId(), - userService.getUser(reporterId) - .orElseThrow(() -> new UserNotFoundException(reporterId)), - dateTime, - description)); - } -} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java deleted file mode 100644 index 1fd3b6e..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.example.copsboot.report.web; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.ZonedDateTime; - -//tag::class[] -@Data -@AllArgsConstructor -@NoArgsConstructor -@ValidCreateReportParameters -public class CreateReportParameters { - private ZonedDateTime dateTime; - - @ValidReportDescription - private String description; - - private boolean trafficIncident; - private int numberOfInvolvedCars; -} -//end::class[] \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportParametersValidator.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportParametersValidator.java deleted file mode 100644 index 80c38ed..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportParametersValidator.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.report.web; - -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; - -//tag::class[] -public class CreateReportParametersValidator implements ConstraintValidator { //<1> - - @Override - public void initialize(ValidCreateReportParameters constraintAnnotation) { - } - - @Override - public boolean isValid(CreateReportParameters value, ConstraintValidatorContext context) { - boolean result = true; - if (value.isTrafficIncident() && value.getNumberOfInvolvedCars() <= 0) { //<2> - result = false; - } - return result; - } -} //end::class[] diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java new file mode 100644 index 0000000..c39c4e9 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java @@ -0,0 +1,16 @@ +package com.example.copsboot.report.web; + +import com.example.copsboot.report.CreateReportParameters; +import com.example.copsboot.user.UserId; + +import java.time.Instant; + +@ValidCreateReportRequest +public record CreateReportRequest(Instant dateTime, + @ValidReportDescription String description, + boolean trafficIncident, + int numberOfInvolvedCars) { + public CreateReportParameters toParameters(UserId userId) { + return new CreateReportParameters(userId, dateTime, description); + } +} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java new file mode 100644 index 0000000..fbad4ea --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java @@ -0,0 +1,21 @@ +package com.example.copsboot.report.web; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +//tag::class[] +public class CreateReportRequestValidator implements ConstraintValidator { //<1> + + @Override + public void initialize(ValidCreateReportRequest constraintAnnotation) { + } + + @Override + public boolean isValid(CreateReportRequest value, ConstraintValidatorContext context) { + boolean result = true; + if (value.trafficIncident() && value.numberOfInvolvedCars() <= 0) { //<2> + result = false; + } + return result; + } +} //end::class[] diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java index e16971d..aa30ca4 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java @@ -1,7 +1,7 @@ package com.example.copsboot.report.web; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; public class ReportDescriptionValidator implements ConstraintValidator { //<1> diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportDto.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportDto.java index 0adc7f8..28e606e 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportDto.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportDto.java @@ -2,23 +2,21 @@ import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; -import lombok.Value; +import com.example.copsboot.user.UserService; -import java.time.ZonedDateTime; +import java.time.Instant; //tag::class[] -@Value -public class ReportDto { - private ReportId id; - private String reporter; - private ZonedDateTime dateTime; - private String description; +public record ReportDto(ReportId id, + String reporter, + Instant dateTime, + String description) { - public static ReportDto fromReport(Report report) { + public static ReportDto fromReport(Report report, UserService userService) { return new ReportDto(report.getId(), - report.getReporter().getEmail(), - report.getDateTime(), - report.getDescription()); + userService.getUserById(report.getReporterId()).getEmail(), + report.getDateTime(), + report.getDescription()); } } //end::class[] diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportRestController.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportRestController.java index 28b79ae..83f9d54 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportRestController.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ReportRestController.java @@ -1,31 +1,42 @@ package com.example.copsboot.report.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.report.CreateReportParameters; import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportService; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserNotFoundException; +import com.example.copsboot.user.UserService; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.UUID; //tag::class[] @RestController @RequestMapping("/api/reports") public class ReportRestController { private final ReportService service; + private final UserService userService; - public ReportRestController(ReportService service) { + public ReportRestController(ReportService service, UserService userService) { this.service = service; + this.userService = userService; } @PostMapping @ResponseStatus(HttpStatus.CREATED) - public ReportDto createReport(@AuthenticationPrincipal ApplicationUserDetails userDetails, - @Valid @RequestBody CreateReportParameters parameters) { - return ReportDto.fromReport(service.createReport(userDetails.getUserId(), - parameters.getDateTime(), - parameters.getDescription())); + public ReportDto createReport(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateReportRequest request) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + User user = userService.findUserByAuthServerId(authServerId) + .orElseThrow(() -> new UserNotFoundException(authServerId)); + CreateReportParameters parameters = request.toParameters(user.getId()); + Report report = service.createReport(parameters); + return ReportDto.fromReport(report, userService); } } //end::class[] diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ValidCreateReportParameters.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java similarity index 67% rename from chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ValidCreateReportParameters.java rename to chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java index 1dc95fd..895ce6c 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ValidCreateReportParameters.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java @@ -1,7 +1,7 @@ package com.example.copsboot.report.web; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -10,11 +10,11 @@ //tag::class[] @Target(ElementType.TYPE) //<1> @Retention(RetentionPolicy.RUNTIME) -@Constraint(validatedBy = {CreateReportParametersValidator.class}) //<2> -public @interface ValidCreateReportParameters { +@Constraint(validatedBy = {CreateReportRequestValidator.class}) //<2> +public @interface ValidCreateReportRequest { String message() default "Invalid report"; Class[] groups() default {}; Class[] payload() default {}; -}//end::class[] \ No newline at end of file +}//end::class[] diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java index 41d39e9..ba8fa56 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java @@ -1,7 +1,7 @@ package com.example.copsboot.report.web; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -16,4 +16,4 @@ Class[] groups() default {}; //<5> Class[] payload() default {}; //<6> -} \ No newline at end of file +} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/User.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/User.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java new file mode 100644 index 0000000..2a434e3 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class UserIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(UserId attribute) { + return attribute.getId(); + } + + @Override + public UserId convertToEntityAttribute(UUID dbData) { + return new UserId(dbData); + } +} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserNotFoundException.java index 1f65f04..97d0813 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserNotFoundException.java @@ -3,9 +3,13 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> +@ResponseStatus(HttpStatus.NOT_FOUND) public class UserNotFoundException extends RuntimeException { public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); + super(String.format("Unable to find user with id %s", userId)); + } + + public UserNotFoundException(AuthServerId authServerId) { + super(String.format("Unable to find user with auth server id %s", authServerId)); } } diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..43f7e98 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,9 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); } //end::class[] diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserService.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserService.java index 9e155a3..ec5aa13 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserService.java @@ -1,9 +1,33 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } + + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } - Optional getUser(UserId userId); + public User getUserById(UserId userId) { + return repository.findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + } + // end::createUser[] } diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 9856e84..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } -} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index 7ab85e9..0000000 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..b87302d --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,17 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import jakarta.validation.constraints.NotEmpty; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +public record CreateUserRequest(@NotEmpty String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + String email = jwt.getClaimAsString("email"); + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/UserRestController.java index b5aa1a8..e0a6545 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,49 +1,53 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import lombok.Value; -import org.springframework.beans.factory.annotation.Autowired; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.Valid; -import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import java.util.stream.Collectors; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] + // tag::createUser[] @PostMapping @ResponseStatus(HttpStatus.CREATED) - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { - User officer = service.createOfficer(parameters.getEmail(), - parameters.getPassword()); - return UserDto.fromUser(officer); + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/Entity.java b/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/Entity.java index a573e0e..3a45231 100644 --- a/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter08/04 - objectvalidation/src/main/java/com/example/orm/jpa/Entity.java @@ -1,6 +1,5 @@ package com.example.orm.jpa; -import java.io.Serializable; /** * Interface for entity objects. diff --git a/chapter08/04 - objectvalidation/src/main/resources/application-dev.properties b/chapter08/04 - objectvalidation/src/main/resources/application-dev.properties deleted file mode 100644 index f72b4c7..0000000 --- a/chapter08/04 - objectvalidation/src/main/resources/application-dev.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/main/resources/application-local.properties b/chapter08/04 - objectvalidation/src/main/resources/application-local.properties index 8f13f3f..7e354d5 100644 --- a/chapter08/04 - objectvalidation/src/main/resources/application-local.properties +++ b/chapter08/04 - objectvalidation/src/main/resources/application-local.properties @@ -3,13 +3,9 @@ spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.username=postgres spring.datasource.password=my-postgres-db-pwd spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata -spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create -spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=create.sql - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +#spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=create.sql +#spring.jpa.properties.hibernate.hbm2ddl.delimiter=; diff --git a/chapter08/04 - objectvalidation/src/main/resources/application.properties b/chapter08/04 - objectvalidation/src/main/resources/application.properties index e69de29..3e80adf 100644 --- a/chapter08/04 - objectvalidation/src/main/resources/application.properties +++ b/chapter08/04 - objectvalidation/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/copsboot + +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter08/04 - objectvalidation/src/main/resources/db/migration/V1.0.0.1__users.sql b/chapter08/04 - objectvalidation/src/main/resources/db/migration/V1.0.0.1__users.sql new file mode 100644 index 0000000..d1939fa --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/resources/db/migration/V1.0.0.1__users.sql @@ -0,0 +1,7 @@ +CREATE TABLE copsboot_user +( + id uuid NOT NULL PRIMARY KEY, + auth_server_id uuid, + email VARCHAR(255), + mobile_token VARCHAR(255) +); diff --git a/chapter08/04 - objectvalidation/src/main/resources/db/migration/V1.0.0.2__reports.sql b/chapter08/04 - objectvalidation/src/main/resources/db/migration/V1.0.0.2__reports.sql new file mode 100644 index 0000000..cc2e26c --- /dev/null +++ b/chapter08/04 - objectvalidation/src/main/resources/db/migration/V1.0.0.2__reports.sql @@ -0,0 +1,8 @@ +CREATE TABLE report +( + date_time TIMESTAMP(6) WITH TIME ZONE, + id uuid NOT NULL, + description VARCHAR(255), + reporter_id uuid, + PRIMARY KEY (id) +); diff --git a/chapter08/04 - objectvalidation/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql b/chapter08/04 - objectvalidation/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql deleted file mode 100644 index 485336f..0000000 --- a/chapter08/04 - objectvalidation/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,42 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(255) PRIMARY KEY, - resource_ids VARCHAR(255), - client_secret VARCHAR(255), - scope VARCHAR(255), - authorized_grant_types VARCHAR(255), - web_server_redirect_uri VARCHAR(255), - authorities VARCHAR(255), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(255) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255), - authentication BLOB, - refresh_token VARCHAR(255) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(255), - token BLOB, - authentication BLOB -); - -CREATE TABLE oauth_code ( - activationCode VARCHAR(255), - authentication BLOB -); \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql b/chapter08/04 - objectvalidation/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql deleted file mode 100644 index 7c3fdf3..0000000 --- a/chapter08/04 - objectvalidation/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(256) PRIMARY KEY, - resource_ids VARCHAR(256), - client_secret VARCHAR(256), - scope VARCHAR(256), - authorized_grant_types VARCHAR(256), - web_server_redirect_uri VARCHAR(256), - authorities VARCHAR(256), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(256) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256), - authentication BYTEA, - refresh_token VARCHAR(256) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(256), - token BYTEA, - authentication BYTEA -); - -CREATE TABLE oauth_code ( - code VARCHAR(256), - authentication BYTEA -); - -CREATE TABLE oauth_approvals ( - userId VARCHAR(256), - clientId VARCHAR(256), - scope VARCHAR(256), - status VARCHAR(10), - expiresAt TIMESTAMP, - lastModifiedAt TIMESTAMP -); - diff --git a/chapter08/04 - objectvalidation/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql b/chapter08/04 - objectvalidation/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql deleted file mode 100644 index 122b1fc..0000000 --- a/chapter08/04 - objectvalidation/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE copsboot_user ( - id UUID NOT NULL, - email VARCHAR(255), - password VARCHAR(255), - PRIMARY KEY (id) -); - -CREATE TABLE user_roles ( - user_id UUID NOT NULL, - roles VARCHAR(255) -); - -ALTER TABLE user_roles - ADD CONSTRAINT FK7je59ku3x462eqxu4ss3das1s -FOREIGN KEY (user_id) -REFERENCES copsboot_user; diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..5feb390 100644 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,16 @@ package com.example.copsboot; import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java new file mode 100644 index 0000000..3ddeac0 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java @@ -0,0 +1,30 @@ +package com.example.copsboot.infrastructure.test; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//tag::class[] +@Retention(RetentionPolicy.RUNTIME) +@CopsbootControllerTest +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class) +public @interface CopsbootControllerDocumentationTest { + + @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> + Class[] value() default {}; + + @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") //<6> + Class[] controllers() default {}; +} +//end::class[] diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java new file mode 100644 index 0000000..02e070e --- /dev/null +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java @@ -0,0 +1,21 @@ +package com.example.copsboot.infrastructure.test; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +class CopsbootControllerDocumentationTestConfiguration { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java index c33238a..6696635 100644 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java @@ -1,10 +1,10 @@ package com.example.copsboot.infrastructure.test; -import com.example.copsboot.infrastructure.SpringProfiles; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,23 +12,12 @@ /** * Custom annotation for all {@link org.springframework.stereotype.Controller Controller} tests on the project. By using * this single annotation, everything is configured properly to test a controller: - *
    - *
  • Import of {@link CopsbootControllerTestConfiguration}
  • - *
  • test profile active
  • - *
- *

- * Example usage: - *

- * @RunWith(SpringRunner.class)
- * @CopsbootControllerTest(UserController.class)
- * public class UserControllerTest {
- * 
*/ //tag::class[] -@Retention(RetentionPolicy.RUNTIME) //<1> -@WebMvcTest //<2> -@ContextConfiguration(classes = CopsbootControllerTestConfiguration.class) //<3> -@ActiveProfiles(SpringProfiles.TEST) //<4> +@Retention(RetentionPolicy.RUNTIME) //<.> +@WebMvcTest //<.> +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> public @interface CopsbootControllerTest { @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java deleted file mode 100644 index 7231430..0000000 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.infrastructure.test; - -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; - -@TestConfiguration -@Import(OAuth2ServerConfiguration.class) -public class CopsbootControllerTestConfiguration { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - -} diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/Reports.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/Reports.java deleted file mode 100644 index 2302dc9..0000000 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/Reports.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.Users; - -import java.time.ZonedDateTime; -import java.util.UUID; - -public class Reports { - public static Report createRandomReport(String description) { - return new Report(new ReportId(UUID.randomUUID()), - Users.newRandomOfficer(), - ZonedDateTime.now(), - description); - } -} diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/CreateReportParametersValidatorTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/CreateReportParametersValidatorTest.java deleted file mode 100644 index bc37179..0000000 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/CreateReportParametersValidatorTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.example.copsboot.report.web; - - -import org.junit.Test; - -import javax.validation.ConstraintViolation; -import javax.validation.Validation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import java.time.ZonedDateTime; -import java.util.Set; - -import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; - -public class CreateReportParametersValidatorTest { - //tag::invalid[] - @Test - public void givenTrafficIndicentButInvolvedCarsZero_invalid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - Validator validator = factory.getValidator(); - - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), - "The suspect was wearing a black hat", - true, - 0); - Set> violationSet = validator.validate(parameters); - assertThat(violationSet).hasViolationOnPath(""); - } - //end::invalid[] - - //tag::valid[] - @Test - public void givenTrafficIndicent_involvedCarsMustBePositive() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - Validator validator = factory.getValidator(); - - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), - "The suspect was wearing a black hat.", - true, - 2); - Set> violationSet = validator.validate(parameters); - assertThat(violationSet).hasNoViolations(); - } - //end::valid[] - - //tag::valid-no-cars[] - @Test - public void givenNoTrafficIndicent_involvedCarsDoesNotMatter() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - Validator validator = factory.getValidator(); - - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), - "The suspect was wearing a black hat.", - false, - 0); - Set> violationSet = validator.validate(parameters); - assertThat(violationSet).hasNoViolations(); - } - //end::valid-no-cars[] -} \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java new file mode 100644 index 0000000..a6bb390 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java @@ -0,0 +1,63 @@ +package com.example.copsboot.report.web; + + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.Test; + +import java.time.Instant; +import java.util.Set; + +import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; + +public class CreateReportRequestValidatorTest { + //tag::invalid[] + @Test + public void givenTrafficIndicentButInvolvedCarsZero_invalid() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat", + true, + 0); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasViolationOnPath(""); + } + } + //end::invalid[] + + //tag::valid[] + @Test + public void givenTrafficIndicent_involvedCarsMustBePositive() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", + true, + 2); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } + } + //end::valid[] + + //tag::valid-no-cars[] + @Test + public void givenNoTrafficIndicent_involvedCarsDoesNotMatter() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", + false, + 0); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } + } + //end::valid-no-cars[] +} diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java index fe3a377..f40d47c 100644 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java @@ -1,12 +1,12 @@ package com.example.copsboot.report.web; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; import org.junit.Test; -import javax.validation.ConstraintViolation; -import javax.validation.Validation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import java.time.ZonedDateTime; +import java.time.Instant; import java.util.Set; import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; @@ -16,26 +16,27 @@ public class ReportDescriptionValidatorTest { //tag::invalid[] @Test public void givenEmptyString_notValid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); //<1> - Validator validator = factory.getValidator(); //<2> + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { //<1> + Validator validator = factory.getValidator(); //<2> - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), "", false, 0); - Set> violationSet = validator.validate(parameters); //<3> - assertThat(violationSet).hasViolationOnPath("description"); //<4> + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), "", false, 0); + Set> violationSet = validator.validate(parameters); //<3> + assertThat(violationSet).hasViolationOnPath("description"); //<4> + } } //end::invalid[] //tag::valid[] @Test public void givenSuspectWordPresent_valid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - Validator validator = factory.getValidator(); - - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), - "The suspect was wearing a black hat.", - false, 0); - Set> violationSet = validator.validate(parameters); - assertThat(violationSet).hasNoViolations(); + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", false, 0); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } } //end::valid[] -} \ No newline at end of file +} diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java index d46c329..d6c6e5f 100644 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java @@ -1,63 +1,76 @@ package com.example.copsboot.report.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; +import com.example.copsboot.report.CreateReportParameters; import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; import com.example.copsboot.report.ReportService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; +import com.example.copsboot.user.UserService; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.time.ZonedDateTime; +import java.time.Instant; +import java.util.Optional; import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; //tag::class[] -@RunWith(SpringRunner.class) @CopsbootControllerTest(ReportRestController.class) public class ReportRestControllerTest { @Autowired - private MockMvc mvc; - - @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; @MockBean private ReportService service; + @MockBean + private UserService userService; @Test public void officerIsAbleToPostAReport() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - ZonedDateTime dateTime = ZonedDateTime.parse("2018-04-11T22:59:03.189+02:00"); - String description = "The suspect is wearing a black hat."; - CreateReportParameters parameters = new CreateReportParameters(dateTime, - description, - false, - 0); - when(service.createReport(eq(Users.officer().getId()), any(ZonedDateTime.class), eq(description))) - .thenReturn(new Report(new ReportId(UUID.randomUUID()), Users.officer(), dateTime, description)); - mvc.perform(post("/api/reports") - .header(HEADER_AUTHORIZATION, bearer(accessToken)) - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("reporter").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("dateTime").value("2018-04-11T22:59:03.189+02:00")) - .andExpect(jsonPath("description").value(description)); + UserId userId = new UserId(UUID.randomUUID()); + AuthServerId authServerId = new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); + User user = new User(userId, + "wim@example.com", + authServerId, + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"); + when(userService.findUserByAuthServerId(authServerId)) + .thenReturn(Optional.of(user)); + when(userService.getUserById(userId)) + .thenReturn(user); + when(service.createReport(any(CreateReportParameters.class))) + .thenReturn(new Report(new ReportId(UUID.randomUUID()), + userId, + Instant.parse("2023-04-11T22:59:03.189+02:00"), + "This is a test report description. The suspect was wearing a black hat.")); + mockMvc.perform(post("/api/reports") + .with(jwt().jwt(builder -> builder.subject(authServerId.value().toString()) + .claim("email", "wim@example.com")) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "dateTime": "2023-04-11T22:59:03.189+02:00", + "description": "This is a test report description. The suspect was wearing a black hat." + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("id").exists()) + .andExpect(jsonPath("reporter").value("wim@example.com")) + .andExpect(jsonPath("dateTime").value("2023-04-11T20:59:03.189Z")) + .andExpect(jsonPath("description").value("This is a test report description. The suspect was wearing a black hat.")); } } -//end::class[] \ No newline at end of file +//end::class[] diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java deleted file mode 100644 index 720f959..0000000 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.copsboot.user; - -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.util.HashSet; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@DataJpaTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> -@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) //<2> -public class UserRepositoryIntegrationTest { - @Autowired - private UserRepository repository; - @PersistenceContext - private EntityManager entityManager; - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - public void testSaveUser() { - Set roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); - - assertThat(repository.count()).isEqualTo(1L); - - entityManager.flush(); //<3> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM user_roles", Long.class)).isEqualTo(1L); - assertThat(jdbcTemplate.queryForObject("SELECT roles FROM user_roles", String.class)).isEqualTo("OFFICER"); - } -} diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index 3217c4a..19c23fe 100644 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -3,14 +3,16 @@ import com.example.copsboot.infrastructure.SpringProfiles; import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -19,62 +21,34 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest -@ActiveProfiles(SpringProfiles.TEST) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) //<2> public class UserRepositoryTest { @Autowired private UserRepository repository; + @PersistenceContext + private EntityManager entityManager; + @Autowired + private JdbcTemplate jdbcTemplate; - //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> - - assertThat(repository.count()).isEqualTo(1L); //<7> - } - //end::testStoreUser[] + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } + assertThat(repository.count()).isEqualTo(1L); - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); + entityManager.flush(); //<3> - assertThat(optional).isEmpty(); + assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> + assertThat(jdbcTemplate.queryForObject("SELECT email FROM copsboot_user", String.class)).isEqualTo("alex.foley@beverly-hills.com"); } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @Bean @@ -82,5 +56,4 @@ public UniqueIdGenerator generator() { return new InMemoryUniqueIdGenerator(); } } - //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/Users.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index e0d24b0..805c501 100644 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,133 +1,94 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.infrastructure.test.CopsbootControllerDocumentationTest; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) -@CopsbootControllerTest(UserRestController.class) +@CopsbootControllerDocumentationTest(UserRestController.class) public class UserRestControllerDocumentation { -//end::class-annotations[] - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] } diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index eb07c50..a20d744 100644 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,118 +1,104 @@ package com.example.copsboot.user.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) -public class UserRestControllerTest { -//end::class-annotations[] - @Autowired - private MockMvc mvc; +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } + // tag::emptyToken[] @Test - public void testCreateOfficerIfPasswordIsTooShort() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("errors[0].fieldName").value("password")); - - verify(service, never()).createOfficer(email, password); + void givenEmptyMobileToken_badRequestIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "" + } + """)) //<.> + .andExpect(status().isBadRequest()) //<.> + .andDo(print()); //<.> + + verify(userService, never()).createUser(any(CreateUserParameters.class)); //<.> } + // end::emptyToken[] } diff --git a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java index 77c5f4c..21556a5 100644 --- a/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java +++ b/chapter08/04 - objectvalidation/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java @@ -2,7 +2,8 @@ import org.assertj.core.api.AbstractAssert; -import javax.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolation; + import java.util.Set; import java.util.stream.Collectors; @@ -21,12 +22,12 @@ public ConstraintViolationSetAssert hasViolationOnPath(String path) { // check condition if (!containsViolationWithPath(actual, path)) { failWithMessage("There was no violation with path <%s>. Violation paths: <%s>", path, actual.stream() - .map(violation -> violation - .getPropertyPath() - .toString()) - .collect( - Collectors - .toList())); + .map(violation -> violation + .getPropertyPath() + .toString()) + .collect( + Collectors + .toList())); } return this; diff --git a/chapter08/04 - objectvalidation/src/test/resources/application-integration-test.properties b/chapter08/04 - objectvalidation/src/test/resources/application-integration-test.properties index 159536c..c61e563 100644 --- a/chapter08/04 - objectvalidation/src/test/resources/application-integration-test.properties +++ b/chapter08/04 - objectvalidation/src/test/resources/application-integration-test.properties @@ -1,11 +1,6 @@ -spring.datasource.url=jdbc:tc:postgresql://localhost/copsbootdb +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.username=user spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none - -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter08/04 - objectvalidation/src/test/resources/application-repository-test.properties b/chapter08/04 - objectvalidation/src/test/resources/application-repository-test.properties new file mode 100644 index 0000000..c61e563 --- /dev/null +++ b/chapter08/04 - objectvalidation/src/test/resources/application-repository-test.properties @@ -0,0 +1,6 @@ +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver +spring.datasource.username=user +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter08/04 - objectvalidation/src/test/resources/application-test.properties b/chapter08/04 - objectvalidation/src/test/resources/application-test.properties deleted file mode 100644 index 02b4003..0000000 --- a/chapter08/04 - objectvalidation/src/test/resources/application-test.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter08/04 - objectvalidation/src/test/resources/logback-test.xml b/chapter08/04 - objectvalidation/src/test/resources/logback-test.xml index bf47fec..164429c 100644 --- a/chapter08/04 - objectvalidation/src/test/resources/logback-test.xml +++ b/chapter08/04 - objectvalidation/src/test/resources/logback-test.xml @@ -5,7 +5,7 @@ - + @@ -17,14 +17,8 @@ - - - - - - - \ No newline at end of file + diff --git a/chapter08/05 - validatorspringbean/.mvn/wrapper/maven-wrapper.jar b/chapter08/05 - validatorspringbean/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter08/05 - validatorspringbean/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter08/05 - validatorspringbean/.mvn/wrapper/maven-wrapper.properties b/chapter08/05 - validatorspringbean/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter08/05 - validatorspringbean/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter08/05 - validatorspringbean/docker-compose.yaml b/chapter08/05 - validatorspringbean/docker-compose.yaml new file mode 100644 index 0000000..92cea56 --- /dev/null +++ b/chapter08/05 - validatorspringbean/docker-compose.yaml @@ -0,0 +1,20 @@ +version: '3' +services: + db: + image: 'postgres:16.0' + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: my-postgres-db-pwd + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter08/05 - validatorspringbean/mvnw b/chapter08/05 - validatorspringbean/mvnw index 5bf251c..66df285 100755 --- a/chapter08/05 - validatorspringbean/mvnw +++ b/chapter08/05 - validatorspringbean/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter08/05 - validatorspringbean/mvnw.cmd b/chapter08/05 - validatorspringbean/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter08/05 - validatorspringbean/mvnw.cmd +++ b/chapter08/05 - validatorspringbean/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter08/05 - validatorspringbean/pom.xml b/chapter08/05 - validatorspringbean/pom.xml index 059f9dd..43db322 100644 --- a/chapter08/05 - validatorspringbean/pom.xml +++ b/chapter08/05 - validatorspringbean/pom.xml @@ -1,231 +1,208 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 29.0-jre + + + org.postgresql + postgresql + runtime + + + + + org.flywaydb + flyway-core + + - - 2.0.3.RELEASE - 1.11.2 - - + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-configuration-processor - true - - - com.google.guava - guava - ${guava.version} - - - org.projectlombok - lombok - - - org.postgresql - postgresql - - - org.flywaydb - flyway-core - + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - org.testcontainers - postgresql - ${testcontainers.version} - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter08/05 - validatorspringbean/src/docs/asciidoc/Copsboot REST API Guide.adoc b/chapter08/05 - validatorspringbean/src/docs/asciidoc/Copsboot REST API Guide.adoc new file mode 100644 index 0000000..b0b91ae --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -0,0 +1,14 @@ += Copsboot REST API Guide +:icons: font +:toc: +:toclevels: 2 + +:numbered: + +== Introduction + +The Copsboot project uses a REST API for interfacing with the server. + +This documentation covers version {project-version} of the application. + +include::_users.adoc[] diff --git a/chapter08/05 - validatorspringbean/src/docs/asciidoc/_users.adoc b/chapter08/05 - validatorspringbean/src/docs/asciidoc/_users.adoc new file mode 100644 index 0000000..2becf75 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/docs/asciidoc/_users.adoc @@ -0,0 +1,24 @@ +//tag::initial-doc[] +== User Management + +=== User information + +The API allows to get information on the currently logged on user +via a `GET` on `/api/users/me`. If you are not a logged on user, the +following response will be returned: + +operation::own-details-unauthorized[snippets='http-request,http-response'] +//end::initial-doc[] + +If you do log on as a user, you get more information on that user: + +operation::own-details[snippets='http-request,http-response,response-fields'] + + +//tag::create-user[] +=== Create a user + +To create an new user, do a `POST` on `/api/users`: + +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter08/05 - validatorspringbean/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter08/05 - validatorspringbean/src/main/asciidoc/Copsboot REST API Guide.adoc deleted file mode 100644 index 255bc8e..0000000 --- a/chapter08/05 - validatorspringbean/src/main/asciidoc/Copsboot REST API Guide.adoc +++ /dev/null @@ -1,14 +0,0 @@ -= Copsboot REST API Guide -:icons: font -:toc: -:toclevels: 2 - -:numbered: - -== Introduction - -The Copsboot project uses a REST API for interfacing with the server. - -This documentation covers version {project-version} of the application. - -include::_users.adoc[] \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/main/asciidoc/_users.adoc b/chapter08/05 - validatorspringbean/src/main/asciidoc/_users.adoc deleted file mode 100644 index a033db8..0000000 --- a/chapter08/05 - validatorspringbean/src/main/asciidoc/_users.adoc +++ /dev/null @@ -1,24 +0,0 @@ -//tag::initial-doc[] -== User Management - -=== User information - -The API allows to get information on the currently logged on user -via a `GET` on `/api/users/me`. If you are not a logged on user, the -following response will be returned: - -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] -//end::initial-doc[] - -If you do log on as a user, you get more information on that user: - -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] - - -//tag::create-user[] -=== Create a user - -To create an new user, do a `POST` on `/api/users`: - -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java index 344a5fe..fb1cc59 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java @@ -6,6 +6,7 @@ public final class SpringProfiles { public static final String TEST = "test"; public static final String STAGING = "staging"; public static final String PROD = "prod"; + public static final String REPOSITORY_TEST = "repository-test"; public static final String INTEGRATION_TEST = "integration-test"; private SpringProfiles() { diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java index 9c92c49..8d26775 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java @@ -1,11 +1,4 @@ package com.example.copsboot.infrastructure.mvc; -import lombok.Value; - -//tag::class[] -@Value -public class FieldErrorResponse { - private String fieldName; - private String errorMessage; +public record FieldErrorResponse(String fieldName, String errorMesesage) { } -//end::class[] \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/CreateReportParameters.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/CreateReportParameters.java new file mode 100644 index 0000000..64aeea6 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/CreateReportParameters.java @@ -0,0 +1,8 @@ +package com.example.copsboot.report; + +import com.example.copsboot.user.UserId; + +import java.time.Instant; + +public record CreateReportParameters(UserId userId, Instant dateTime, String description) { +} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/Report.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/Report.java index c0f9c66..b10756f 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/Report.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/Report.java @@ -1,36 +1,36 @@ package com.example.copsboot.report; -import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; import com.example.orm.jpa.AbstractEntity; import com.example.util.ArtifactForFramework; +import jakarta.persistence.Entity; + +import java.time.Instant; -import javax.persistence.Entity; -import javax.persistence.ManyToOne; -import java.time.ZonedDateTime; //tag::class[] @Entity public class Report extends AbstractEntity { - @ManyToOne - private User reporter; - private ZonedDateTime dateTime; + + private UserId reporterId; + private Instant dateTime; private String description; @ArtifactForFramework protected Report() { } - public Report(ReportId id, User reporter, ZonedDateTime dateTime, String description) { + public Report(ReportId id, UserId reporterId, Instant dateTime, String description) { super(id); - this.reporter = reporter; + this.reporterId = reporterId; this.dateTime = dateTime; this.description = description; } - public User getReporter() { - return reporter; + public UserId getReporterId() { + return reporterId; } - public ZonedDateTime getDateTime() { + public Instant getDateTime() { return dateTime; } diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/ReportService.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/ReportService.java index f6ed620..613248b 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/ReportService.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/ReportService.java @@ -1,9 +1,23 @@ package com.example.copsboot.report; -import com.example.copsboot.user.UserId; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.ZonedDateTime; -public interface ReportService { - Report createReport(UserId reporterId, ZonedDateTime dateTime, String description); +@Service +@Transactional +public class ReportService { + private final ReportRepository repository; + + public ReportService(ReportRepository repository) { + this.repository = repository; + } + + public Report createReport(CreateReportParameters parameters) { + return repository.save(new Report(repository.nextId(), + parameters.userId(), + parameters.dateTime(), + parameters.description())); + } } diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/ReportServiceImpl.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/ReportServiceImpl.java deleted file mode 100644 index 67c21e1..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/ReportServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserNotFoundException; -import com.example.copsboot.user.UserService; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.ZonedDateTime; - -@Service -@Transactional -public class ReportServiceImpl implements ReportService { - private final ReportRepository repository; - private final UserService userService; - - public ReportServiceImpl(ReportRepository repository, UserService userService) { - this.repository = repository; - this.userService = userService; - } - - @Override - public Report createReport(UserId reporterId, ZonedDateTime dateTime, String description) { - return repository.save(new Report(repository.nextId(), - userService.getUser(reporterId) - .orElseThrow(() -> new UserNotFoundException(reporterId)), - dateTime, - description)); - } -} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java deleted file mode 100644 index 9a169e4..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.report.web; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.ZonedDateTime; - -//tag::class[] -@Data -@AllArgsConstructor -@NoArgsConstructor -public class CreateReportParameters { - private ZonedDateTime dateTime; - - @ValidReportDescription - private String description; -} -//end::class[] \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java new file mode 100644 index 0000000..6a0ce81 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java @@ -0,0 +1,14 @@ +package com.example.copsboot.report.web; + +import com.example.copsboot.report.CreateReportParameters; +import com.example.copsboot.user.UserId; + +import java.time.Instant; + +@ValidCreateReportRequest +public record CreateReportRequest(Instant dateTime, @ValidReportDescription String description, + boolean trafficIncident, int numberOfInvolvedCars) { + public CreateReportParameters toParameters(UserId userId) { + return new CreateReportParameters(userId, dateTime, description); + } +} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java new file mode 100644 index 0000000..fbad4ea --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java @@ -0,0 +1,21 @@ +package com.example.copsboot.report.web; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +//tag::class[] +public class CreateReportRequestValidator implements ConstraintValidator { //<1> + + @Override + public void initialize(ValidCreateReportRequest constraintAnnotation) { + } + + @Override + public boolean isValid(CreateReportRequest value, ConstraintValidatorContext context) { + boolean result = true; + if (value.trafficIncident() && value.numberOfInvolvedCars() <= 0) { //<2> + result = false; + } + return result; + } +} //end::class[] diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java index e16971d..aa30ca4 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java @@ -1,7 +1,7 @@ package com.example.copsboot.report.web; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; public class ReportDescriptionValidator implements ConstraintValidator { //<1> diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportDto.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportDto.java index 0adc7f8..28e606e 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportDto.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportDto.java @@ -2,23 +2,21 @@ import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; -import lombok.Value; +import com.example.copsboot.user.UserService; -import java.time.ZonedDateTime; +import java.time.Instant; //tag::class[] -@Value -public class ReportDto { - private ReportId id; - private String reporter; - private ZonedDateTime dateTime; - private String description; +public record ReportDto(ReportId id, + String reporter, + Instant dateTime, + String description) { - public static ReportDto fromReport(Report report) { + public static ReportDto fromReport(Report report, UserService userService) { return new ReportDto(report.getId(), - report.getReporter().getEmail(), - report.getDateTime(), - report.getDescription()); + userService.getUserById(report.getReporterId()).getEmail(), + report.getDateTime(), + report.getDescription()); } } //end::class[] diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportRestController.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportRestController.java index 1f4eb4b..3fcc153 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportRestController.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ReportRestController.java @@ -1,33 +1,45 @@ package com.example.copsboot.report.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.report.CreateReportParameters; import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportService; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserNotFoundException; +import com.example.copsboot.user.UserService; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.UUID; //tag::class[] @RestController @RequestMapping("/api/reports") public class ReportRestController { private final ReportService service; + private final UserService userService; - public ReportRestController(ReportService service) { + public ReportRestController(ReportService service, UserService userService) { this.service = service; + this.userService = userService; } - //tag::create-report-method-signature[] + + // tag::create-report-method-signature[] @PostMapping @ResponseStatus(HttpStatus.CREATED) - public ReportDto createReport(@AuthenticationPrincipal ApplicationUserDetails userDetails, - @Valid @RequestBody CreateReportParameters parameters) { - //end::create-report-method-signature[] - return ReportDto.fromReport(service.createReport(userDetails.getUserId(), - parameters.getDateTime(), - parameters.getDescription())); + public ReportDto createReport(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateReportRequest request) { + // end::create-report-method-signature[] + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + User user = userService.findUserByAuthServerId(authServerId) + .orElseThrow(() -> new UserNotFoundException(authServerId)); + CreateReportParameters parameters = request.toParameters(user.getId()); + Report report = service.createReport(parameters); + return ReportDto.fromReport(report, userService); } } //end::class[] diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java new file mode 100644 index 0000000..895ce6c --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java @@ -0,0 +1,20 @@ +package com.example.copsboot.report.web; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +//tag::class[] +@Target(ElementType.TYPE) //<1> +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = {CreateReportRequestValidator.class}) //<2> +public @interface ValidCreateReportRequest { + String message() default "Invalid report"; + + Class[] groups() default {}; + + Class[] payload() default {}; +}//end::class[] diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java index 41d39e9..ba8fa56 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java @@ -1,7 +1,7 @@ package com.example.copsboot.report.web; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -16,4 +16,4 @@ Class[] groups() default {}; //<5> Class[] payload() default {}; //<6> -} \ No newline at end of file +} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/User.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/User.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java new file mode 100644 index 0000000..2a434e3 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class UserIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(UserId attribute) { + return attribute.getId(); + } + + @Override + public UserId convertToEntityAttribute(UUID dbData) { + return new UserId(dbData); + } +} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserNotFoundException.java index 1f65f04..97d0813 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserNotFoundException.java @@ -3,9 +3,13 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> +@ResponseStatus(HttpStatus.NOT_FOUND) public class UserNotFoundException extends RuntimeException { public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); + super(String.format("Unable to find user with id %s", userId)); + } + + public UserNotFoundException(AuthServerId authServerId) { + super(String.format("Unable to find user with auth server id %s", authServerId)); } } diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..741d2e0 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,11 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); + + Optional findByMobileToken(String mobileToken); } //end::class[] diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserService.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserService.java index d5630f0..ba1d4ab 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserService.java @@ -1,11 +1,37 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } + + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } - Optional getUser(UserId userId); + public User getUserById(UserId userId) { + return repository.findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + } - Optional findUserByEmail(String email); + public Optional findUserByMobileToken(String mobileToken) { + return repository.findByMobileToken(mobileToken); + } + // end::createUser[] } diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 6918081..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } - - @Override - public Optional findUserByEmail(String email) { - return repository.findByEmailIgnoreCase(email); - } -} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index f96ee54..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -@ValidCreateUserParameters -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java deleted file mode 100644 index 3f86d70..0000000 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.copsboot.user.web; - -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; - -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; - -//tag::class[] -public class CreateUserParametersValidator implements ConstraintValidator { - - private final UserService userService; - - @Autowired - public CreateUserParametersValidator(UserService userService) { //<1> - this.userService = userService; - } - - @Override - public void initialize(ValidCreateUserParameters constraintAnnotation) { - - } - - @Override - public boolean isValid(CreateOfficerParameters userParameters, ConstraintValidatorContext context) { - - boolean result = true; - - if (userService.findUserByEmail(userParameters.getEmail()).isPresent()) { //<2> - context.buildConstraintViolationWithTemplate( - "There is already a user with the given email address.") - .addPropertyNode("email").addConstraintViolation(); //<3> - - result = false; //<4> - } - - return result; - } -} -//end::class[] diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..83c56a1 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,18 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import jakarta.validation.constraints.NotEmpty; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +@ValidCreateUserRequest +public record CreateUserRequest(@NotEmpty String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + String email = jwt.getClaimAsString("email"); + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java new file mode 100644 index 0000000..bdd4aa5 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java @@ -0,0 +1,40 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.UserService; +import org.springframework.beans.factory.annotation.Autowired; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +//tag::class[] +public class CreateUserRequestValidator implements ConstraintValidator { + + private final UserService userService; + + @Autowired + public CreateUserRequestValidator(UserService userService) { //<1> + this.userService = userService; + } + + @Override + public void initialize(ValidCreateUserRequest constraintAnnotation) { + + } + + @Override + public boolean isValid(CreateUserRequest userRequest, ConstraintValidatorContext context) { + + boolean result = true; + + if (userService.findUserByMobileToken(userRequest.mobileToken()).isPresent()) { //<2> + context.buildConstraintViolationWithTemplate( + "There is already a user with the given mobile token.") + .addPropertyNode("mobileToken").addConstraintViolation(); //<3> + + result = false; //<4> + } + + return result; + } +} +//end::class[] diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/UserRestController.java index b5aa1a8..e0a6545 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,49 +1,53 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import lombok.Value; -import org.springframework.beans.factory.annotation.Autowired; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.Valid; -import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import java.util.stream.Collectors; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] + // tag::createUser[] @PostMapping @ResponseStatus(HttpStatus.CREATED) - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { - User officer = service.createOfficer(parameters.getEmail(), - parameters.getPassword()); - return UserDto.fromUser(officer); + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/ValidCreateUserParameters.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/ValidCreateUserRequest.java similarity index 70% rename from chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/ValidCreateUserParameters.java rename to chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/ValidCreateUserRequest.java index a7ec388..e6a975e 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/ValidCreateUserParameters.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/copsboot/user/web/ValidCreateUserRequest.java @@ -1,7 +1,7 @@ package com.example.copsboot.user.web; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -10,8 +10,8 @@ //tag::class[] @Target(ElementType.TYPE) // <1> @Retention(RetentionPolicy.RUNTIME) -@Constraint(validatedBy = {CreateUserParametersValidator.class}) //<2> -public @interface ValidCreateUserParameters { +@Constraint(validatedBy = {CreateUserRequestValidator.class}) //<2> +public @interface ValidCreateUserRequest { String message() default "Invalid user"; Class[] groups() default {}; diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/Entity.java b/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/Entity.java index 96cadf0..3a45231 100644 --- a/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter08/05 - validatorspringbean/src/main/java/com/example/orm/jpa/Entity.java @@ -1,5 +1,6 @@ package com.example.orm.jpa; + /** * Interface for entity objects. * diff --git a/chapter08/05 - validatorspringbean/src/main/resources/application-dev.properties b/chapter08/05 - validatorspringbean/src/main/resources/application-dev.properties deleted file mode 100644 index f72b4c7..0000000 --- a/chapter08/05 - validatorspringbean/src/main/resources/application-dev.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/main/resources/application-local.properties b/chapter08/05 - validatorspringbean/src/main/resources/application-local.properties index 8f13f3f..7e354d5 100644 --- a/chapter08/05 - validatorspringbean/src/main/resources/application-local.properties +++ b/chapter08/05 - validatorspringbean/src/main/resources/application-local.properties @@ -3,13 +3,9 @@ spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.username=postgres spring.datasource.password=my-postgres-db-pwd spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata -spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create -spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=create.sql - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +#spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=create.sql +#spring.jpa.properties.hibernate.hbm2ddl.delimiter=; diff --git a/chapter08/05 - validatorspringbean/src/main/resources/application.properties b/chapter08/05 - validatorspringbean/src/main/resources/application.properties index e69de29..3e80adf 100644 --- a/chapter08/05 - validatorspringbean/src/main/resources/application.properties +++ b/chapter08/05 - validatorspringbean/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/copsboot + +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter08/05 - validatorspringbean/src/main/resources/db/migration/V1.0.0.1__users.sql b/chapter08/05 - validatorspringbean/src/main/resources/db/migration/V1.0.0.1__users.sql new file mode 100644 index 0000000..d1939fa --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/resources/db/migration/V1.0.0.1__users.sql @@ -0,0 +1,7 @@ +CREATE TABLE copsboot_user +( + id uuid NOT NULL PRIMARY KEY, + auth_server_id uuid, + email VARCHAR(255), + mobile_token VARCHAR(255) +); diff --git a/chapter08/05 - validatorspringbean/src/main/resources/db/migration/V1.0.0.2__reports.sql b/chapter08/05 - validatorspringbean/src/main/resources/db/migration/V1.0.0.2__reports.sql new file mode 100644 index 0000000..cc2e26c --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/main/resources/db/migration/V1.0.0.2__reports.sql @@ -0,0 +1,8 @@ +CREATE TABLE report +( + date_time TIMESTAMP(6) WITH TIME ZONE, + id uuid NOT NULL, + description VARCHAR(255), + reporter_id uuid, + PRIMARY KEY (id) +); diff --git a/chapter08/05 - validatorspringbean/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql b/chapter08/05 - validatorspringbean/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql deleted file mode 100644 index 485336f..0000000 --- a/chapter08/05 - validatorspringbean/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,42 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(255) PRIMARY KEY, - resource_ids VARCHAR(255), - client_secret VARCHAR(255), - scope VARCHAR(255), - authorized_grant_types VARCHAR(255), - web_server_redirect_uri VARCHAR(255), - authorities VARCHAR(255), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(255) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255), - authentication BLOB, - refresh_token VARCHAR(255) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(255), - token BLOB, - authentication BLOB -); - -CREATE TABLE oauth_code ( - activationCode VARCHAR(255), - authentication BLOB -); \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql b/chapter08/05 - validatorspringbean/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql deleted file mode 100644 index 7c3fdf3..0000000 --- a/chapter08/05 - validatorspringbean/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(256) PRIMARY KEY, - resource_ids VARCHAR(256), - client_secret VARCHAR(256), - scope VARCHAR(256), - authorized_grant_types VARCHAR(256), - web_server_redirect_uri VARCHAR(256), - authorities VARCHAR(256), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(256) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256), - authentication BYTEA, - refresh_token VARCHAR(256) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(256), - token BYTEA, - authentication BYTEA -); - -CREATE TABLE oauth_code ( - code VARCHAR(256), - authentication BYTEA -); - -CREATE TABLE oauth_approvals ( - userId VARCHAR(256), - clientId VARCHAR(256), - scope VARCHAR(256), - status VARCHAR(10), - expiresAt TIMESTAMP, - lastModifiedAt TIMESTAMP -); - diff --git a/chapter08/05 - validatorspringbean/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql b/chapter08/05 - validatorspringbean/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql deleted file mode 100644 index 122b1fc..0000000 --- a/chapter08/05 - validatorspringbean/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE copsboot_user ( - id UUID NOT NULL, - email VARCHAR(255), - password VARCHAR(255), - PRIMARY KEY (id) -); - -CREATE TABLE user_roles ( - user_id UUID NOT NULL, - roles VARCHAR(255) -); - -ALTER TABLE user_roles - ADD CONSTRAINT FK7je59ku3x462eqxu4ss3das1s -FOREIGN KEY (user_id) -REFERENCES copsboot_user; diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..5feb390 100644 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,16 @@ package com.example.copsboot; import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java new file mode 100644 index 0000000..3ddeac0 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java @@ -0,0 +1,30 @@ +package com.example.copsboot.infrastructure.test; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//tag::class[] +@Retention(RetentionPolicy.RUNTIME) +@CopsbootControllerTest +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class) +public @interface CopsbootControllerDocumentationTest { + + @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> + Class[] value() default {}; + + @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") //<6> + Class[] controllers() default {}; +} +//end::class[] diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java new file mode 100644 index 0000000..02e070e --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java @@ -0,0 +1,21 @@ +package com.example.copsboot.infrastructure.test; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +class CopsbootControllerDocumentationTestConfiguration { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java index c33238a..6696635 100644 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java @@ -1,10 +1,10 @@ package com.example.copsboot.infrastructure.test; -import com.example.copsboot.infrastructure.SpringProfiles; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,23 +12,12 @@ /** * Custom annotation for all {@link org.springframework.stereotype.Controller Controller} tests on the project. By using * this single annotation, everything is configured properly to test a controller: - *
    - *
  • Import of {@link CopsbootControllerTestConfiguration}
  • - *
  • test profile active
  • - *
- *

- * Example usage: - *

- * @RunWith(SpringRunner.class)
- * @CopsbootControllerTest(UserController.class)
- * public class UserControllerTest {
- * 
*/ //tag::class[] -@Retention(RetentionPolicy.RUNTIME) //<1> -@WebMvcTest //<2> -@ContextConfiguration(classes = CopsbootControllerTestConfiguration.class) //<3> -@ActiveProfiles(SpringProfiles.TEST) //<4> +@Retention(RetentionPolicy.RUNTIME) //<.> +@WebMvcTest //<.> +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> public @interface CopsbootControllerTest { @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java deleted file mode 100644 index 7231430..0000000 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.infrastructure.test; - -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; - -@TestConfiguration -@Import(OAuth2ServerConfiguration.class) -public class CopsbootControllerTestConfiguration { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - -} diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/Reports.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/Reports.java deleted file mode 100644 index 2302dc9..0000000 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/Reports.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.Users; - -import java.time.ZonedDateTime; -import java.util.UUID; - -public class Reports { - public static Report createRandomReport(String description) { - return new Report(new ReportId(UUID.randomUUID()), - Users.newRandomOfficer(), - ZonedDateTime.now(), - description); - } -} diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java new file mode 100644 index 0000000..a6bb390 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java @@ -0,0 +1,63 @@ +package com.example.copsboot.report.web; + + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.Test; + +import java.time.Instant; +import java.util.Set; + +import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; + +public class CreateReportRequestValidatorTest { + //tag::invalid[] + @Test + public void givenTrafficIndicentButInvolvedCarsZero_invalid() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat", + true, + 0); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasViolationOnPath(""); + } + } + //end::invalid[] + + //tag::valid[] + @Test + public void givenTrafficIndicent_involvedCarsMustBePositive() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", + true, + 2); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } + } + //end::valid[] + + //tag::valid-no-cars[] + @Test + public void givenNoTrafficIndicent_involvedCarsDoesNotMatter() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", + false, + 0); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } + } + //end::valid-no-cars[] +} diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java index 199247a..f40d47c 100644 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java @@ -1,12 +1,12 @@ package com.example.copsboot.report.web; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; import org.junit.Test; -import javax.validation.ConstraintViolation; -import javax.validation.Validation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import java.time.ZonedDateTime; +import java.time.Instant; import java.util.Set; import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; @@ -16,25 +16,27 @@ public class ReportDescriptionValidatorTest { //tag::invalid[] @Test public void givenEmptyString_notValid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); //<1> - Validator validator = factory.getValidator(); //<2> + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { //<1> + Validator validator = factory.getValidator(); //<2> - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), ""); - Set> violationSet = validator.validate(parameters); //<3> - assertThat(violationSet).hasViolationOnPath("description"); //<4> + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), "", false, 0); + Set> violationSet = validator.validate(parameters); //<3> + assertThat(violationSet).hasViolationOnPath("description"); //<4> + } } //end::invalid[] //tag::valid[] @Test public void givenSuspectWordPresent_valid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - Validator validator = factory.getValidator(); - - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), - "The suspect was wearing a black hat."); - Set> violationSet = validator.validate(parameters); - assertThat(violationSet).hasNoViolations(); + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", false, 0); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } } //end::valid[] -} \ No newline at end of file +} diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java index 49705e9..d6c6e5f 100644 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java @@ -1,61 +1,76 @@ package com.example.copsboot.report.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; +import com.example.copsboot.report.CreateReportParameters; import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; import com.example.copsboot.report.ReportService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; +import com.example.copsboot.user.UserService; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.time.ZonedDateTime; +import java.time.Instant; +import java.util.Optional; import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; //tag::class[] -@RunWith(SpringRunner.class) @CopsbootControllerTest(ReportRestController.class) public class ReportRestControllerTest { @Autowired - private MockMvc mvc; - - @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; @MockBean private ReportService service; + @MockBean + private UserService userService; @Test public void officerIsAbleToPostAReport() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - ZonedDateTime dateTime = ZonedDateTime.parse("2018-04-11T22:59:03.189+02:00"); - String description = "The suspect is wearing a black hat."; - CreateReportParameters parameters = new CreateReportParameters(dateTime, - description); - when(service.createReport(eq(Users.officer().getId()), any(ZonedDateTime.class), eq(description))) - .thenReturn(new Report(new ReportId(UUID.randomUUID()), Users.officer(), dateTime, description)); - mvc.perform(post("/api/reports") - .header(HEADER_AUTHORIZATION, bearer(accessToken)) - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("reporter").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("dateTime").value("2018-04-11T22:59:03.189+02:00")) - .andExpect(jsonPath("description").value(description)); + UserId userId = new UserId(UUID.randomUUID()); + AuthServerId authServerId = new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); + User user = new User(userId, + "wim@example.com", + authServerId, + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"); + when(userService.findUserByAuthServerId(authServerId)) + .thenReturn(Optional.of(user)); + when(userService.getUserById(userId)) + .thenReturn(user); + when(service.createReport(any(CreateReportParameters.class))) + .thenReturn(new Report(new ReportId(UUID.randomUUID()), + userId, + Instant.parse("2023-04-11T22:59:03.189+02:00"), + "This is a test report description. The suspect was wearing a black hat.")); + mockMvc.perform(post("/api/reports") + .with(jwt().jwt(builder -> builder.subject(authServerId.value().toString()) + .claim("email", "wim@example.com")) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "dateTime": "2023-04-11T22:59:03.189+02:00", + "description": "This is a test report description. The suspect was wearing a black hat." + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("id").exists()) + .andExpect(jsonPath("reporter").value("wim@example.com")) + .andExpect(jsonPath("dateTime").value("2023-04-11T20:59:03.189Z")) + .andExpect(jsonPath("description").value("This is a test report description. The suspect was wearing a black hat.")); } } -//end::class[] \ No newline at end of file +//end::class[] diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java deleted file mode 100644 index 720f959..0000000 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.copsboot.user; - -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.util.HashSet; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@DataJpaTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> -@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) //<2> -public class UserRepositoryIntegrationTest { - @Autowired - private UserRepository repository; - @PersistenceContext - private EntityManager entityManager; - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - public void testSaveUser() { - Set roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); - - assertThat(repository.count()).isEqualTo(1L); - - entityManager.flush(); //<3> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM user_roles", Long.class)).isEqualTo(1L); - assertThat(jdbcTemplate.queryForObject("SELECT roles FROM user_roles", String.class)).isEqualTo("OFFICER"); - } -} diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index 3217c4a..19c23fe 100644 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -3,14 +3,16 @@ import com.example.copsboot.infrastructure.SpringProfiles; import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -19,62 +21,34 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest -@ActiveProfiles(SpringProfiles.TEST) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) //<2> public class UserRepositoryTest { @Autowired private UserRepository repository; + @PersistenceContext + private EntityManager entityManager; + @Autowired + private JdbcTemplate jdbcTemplate; - //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> - - assertThat(repository.count()).isEqualTo(1L); //<7> - } - //end::testStoreUser[] + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } + assertThat(repository.count()).isEqualTo(1L); - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); + entityManager.flush(); //<3> - assertThat(optional).isEmpty(); + assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> + assertThat(jdbcTemplate.queryForObject("SELECT email FROM copsboot_user", String.class)).isEqualTo("alex.foley@beverly-hills.com"); } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @Bean @@ -82,5 +56,4 @@ public UniqueIdGenerator generator() { return new InMemoryUniqueIdGenerator(); } } - //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/Users.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java deleted file mode 100644 index 7b94df3..0000000 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.copsboot.user.web; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserService; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.validation.ConstraintViolation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; - -import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; -import static org.mockito.Mockito.when; - -//tag::class[] -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@ActiveProfiles(SpringProfiles.TEST) -public class CreateUserParametersValidatorTest { - - @MockBean - private UserService userService; //<2> - @Autowired - private PasswordEncoder encoder; - @Autowired - private ValidatorFactory factory; //<3> - - @Test - public void invalidIfAlreadyUserWithGivenEmail() { - - String email = "wim.deblauwe@example.com"; - when(userService.findUserByEmail(email)) - .thenReturn(Optional.of( - User.createOfficer(new UserId(UUID.randomUUID()), - email, - encoder.encode("testing1234")))); - - Validator validator = factory.getValidator(); //<4> - - CreateOfficerParameters userParameters = new CreateOfficerParameters(); - userParameters.setEmail(email); - userParameters.setPassword("my-secret-pwd-1234"); - Set> violationSet = validator.validate(userParameters); //<5> - assertThat(violationSet).hasViolationSize(2) - .hasViolationOnPath("email"); //<6> - } - - @Test - public void validIfNoUserWithGivenEmail() { - String email = "wim.deblauwe@example.com"; - when(userService.findUserByEmail(email)) - .thenReturn(Optional.empty()); - - Validator validator = factory.getValidator(); - - CreateOfficerParameters userParameters = new CreateOfficerParameters(); - userParameters.setEmail(email); - userParameters.setPassword("my-secret-pwd-1234"); - Set> violationSet = validator.validate(userParameters); - assertThat(violationSet).hasNoViolations(); - } -} -//end::class[] \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java new file mode 100644 index 0000000..d058abd --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java @@ -0,0 +1,65 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.infrastructure.SpringProfiles; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; +import com.example.copsboot.user.UserService; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; + +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; +import static org.mockito.Mockito.when; + +//tag::class[] +@SpringBootTest //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) +public class CreateUserRequestValidatorTest { + + @MockBean + private UserService userService; //<2> + @Autowired + private ValidatorFactory factory; //<3> + + @Test + public void invalidIfAlreadyUserWithGivenMobileToken() { + + String mobileToken = "abc123"; + when(userService.findUserByMobileToken(mobileToken)) + .thenReturn(Optional.of(new User(new UserId(UUID.randomUUID()), + "wim@example.com", + new AuthServerId(UUID.randomUUID()), + mobileToken))); + + Validator validator = factory.getValidator(); //<4> + + CreateUserRequest request = new CreateUserRequest(mobileToken); + Set> violationSet = validator.validate(request); //<5> + assertThat(violationSet).hasViolationSize(2) + .hasViolationOnPath("mobileToken"); //<6> + } + + @Test + public void validIfNoUserWithGivenMobileToken() { + String mobileToken = "abc123"; + when(userService.findUserByMobileToken(mobileToken)) + .thenReturn(Optional.empty()); + + Validator validator = factory.getValidator(); + + CreateUserRequest request = new CreateUserRequest(mobileToken); + Set> violationSet = validator.validate(request); + assertThat(violationSet).hasNoViolations(); + } +} +//end::class[] diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index b1c3165..805c501 100644 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,134 +1,94 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.infrastructure.test.CopsbootControllerDocumentationTest; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) -@CopsbootControllerTest(UserRestController.class) +@CopsbootControllerDocumentationTest(UserRestController.class) public class UserRestControllerDocumentation { -//end::class-annotations[] - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - when(service.findUserByEmail(email)).thenReturn(Optional.empty()); - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] } diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index f55e6c6..a20d744 100644 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,121 +1,104 @@ package com.example.copsboot.user.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) -public class UserRestControllerTest { -//end::class-annotations[] - @Autowired - private MockMvc mvc; +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - when(service.findUserByEmail(email)).thenReturn(Optional.empty()); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } + // tag::emptyToken[] @Test - public void testCreateOfficerIfPasswordIsTooShort() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.findUserByEmail(email)).thenReturn(Optional.empty()); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("errors[0].fieldName").value("password")); - - verify(service, never()).createOfficer(email, password); + void givenEmptyMobileToken_badRequestIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "" + } + """)) //<.> + .andExpect(status().isBadRequest()) //<.> + .andDo(print()); //<.> + + verify(userService, never()).createUser(any(CreateUserParameters.class)); //<.> } + // end::emptyToken[] } diff --git a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java index 77c5f4c..21556a5 100644 --- a/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java +++ b/chapter08/05 - validatorspringbean/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java @@ -2,7 +2,8 @@ import org.assertj.core.api.AbstractAssert; -import javax.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolation; + import java.util.Set; import java.util.stream.Collectors; @@ -21,12 +22,12 @@ public ConstraintViolationSetAssert hasViolationOnPath(String path) { // check condition if (!containsViolationWithPath(actual, path)) { failWithMessage("There was no violation with path <%s>. Violation paths: <%s>", path, actual.stream() - .map(violation -> violation - .getPropertyPath() - .toString()) - .collect( - Collectors - .toList())); + .map(violation -> violation + .getPropertyPath() + .toString()) + .collect( + Collectors + .toList())); } return this; diff --git a/chapter08/05 - validatorspringbean/src/test/resources/application-integration-test.properties b/chapter08/05 - validatorspringbean/src/test/resources/application-integration-test.properties index 159536c..c61e563 100644 --- a/chapter08/05 - validatorspringbean/src/test/resources/application-integration-test.properties +++ b/chapter08/05 - validatorspringbean/src/test/resources/application-integration-test.properties @@ -1,11 +1,6 @@ -spring.datasource.url=jdbc:tc:postgresql://localhost/copsbootdb +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.username=user spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none - -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter08/05 - validatorspringbean/src/test/resources/application-repository-test.properties b/chapter08/05 - validatorspringbean/src/test/resources/application-repository-test.properties new file mode 100644 index 0000000..c61e563 --- /dev/null +++ b/chapter08/05 - validatorspringbean/src/test/resources/application-repository-test.properties @@ -0,0 +1,6 @@ +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver +spring.datasource.username=user +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter08/05 - validatorspringbean/src/test/resources/application-test.properties b/chapter08/05 - validatorspringbean/src/test/resources/application-test.properties deleted file mode 100644 index 02b4003..0000000 --- a/chapter08/05 - validatorspringbean/src/test/resources/application-test.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter08/05 - validatorspringbean/src/test/resources/logback-test.xml b/chapter08/05 - validatorspringbean/src/test/resources/logback-test.xml index bf47fec..164429c 100644 --- a/chapter08/05 - validatorspringbean/src/test/resources/logback-test.xml +++ b/chapter08/05 - validatorspringbean/src/test/resources/logback-test.xml @@ -5,7 +5,7 @@ - + @@ -17,14 +17,8 @@ - - - - - - - \ No newline at end of file + diff --git a/chapter09/01 - fileupload/.mvn/wrapper/maven-wrapper.jar b/chapter09/01 - fileupload/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter09/01 - fileupload/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter09/01 - fileupload/.mvn/wrapper/maven-wrapper.properties b/chapter09/01 - fileupload/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter09/01 - fileupload/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter09/01 - fileupload/docker-compose.yaml b/chapter09/01 - fileupload/docker-compose.yaml new file mode 100644 index 0000000..92cea56 --- /dev/null +++ b/chapter09/01 - fileupload/docker-compose.yaml @@ -0,0 +1,20 @@ +version: '3' +services: + db: + image: 'postgres:16.0' + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: my-postgres-db-pwd + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter09/01 - fileupload/mvnw b/chapter09/01 - fileupload/mvnw index 5bf251c..66df285 100755 --- a/chapter09/01 - fileupload/mvnw +++ b/chapter09/01 - fileupload/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter09/01 - fileupload/mvnw.cmd b/chapter09/01 - fileupload/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter09/01 - fileupload/mvnw.cmd +++ b/chapter09/01 - fileupload/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter09/01 - fileupload/pom.xml b/chapter09/01 - fileupload/pom.xml index 059f9dd..43db322 100644 --- a/chapter09/01 - fileupload/pom.xml +++ b/chapter09/01 - fileupload/pom.xml @@ -1,231 +1,208 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 - - - 1.5.6 - - - 29.0-jre + + + org.postgresql + postgresql + runtime + + + + + org.flywaydb + flyway-core + + - - 2.0.3.RELEASE - 1.11.2 - - + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-configuration-processor - true - - - com.google.guava - guava - ${guava.version} - - - org.projectlombok - lombok - - - org.postgresql - postgresql - - - org.flywaydb - flyway-core - + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - org.testcontainers - postgresql - ${testcontainers.version} - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter09/01 - fileupload/src/docs/asciidoc/Copsboot REST API Guide.adoc b/chapter09/01 - fileupload/src/docs/asciidoc/Copsboot REST API Guide.adoc new file mode 100644 index 0000000..b0b91ae --- /dev/null +++ b/chapter09/01 - fileupload/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -0,0 +1,14 @@ += Copsboot REST API Guide +:icons: font +:toc: +:toclevels: 2 + +:numbered: + +== Introduction + +The Copsboot project uses a REST API for interfacing with the server. + +This documentation covers version {project-version} of the application. + +include::_users.adoc[] diff --git a/chapter09/01 - fileupload/src/docs/asciidoc/_users.adoc b/chapter09/01 - fileupload/src/docs/asciidoc/_users.adoc new file mode 100644 index 0000000..2becf75 --- /dev/null +++ b/chapter09/01 - fileupload/src/docs/asciidoc/_users.adoc @@ -0,0 +1,24 @@ +//tag::initial-doc[] +== User Management + +=== User information + +The API allows to get information on the currently logged on user +via a `GET` on `/api/users/me`. If you are not a logged on user, the +following response will be returned: + +operation::own-details-unauthorized[snippets='http-request,http-response'] +//end::initial-doc[] + +If you do log on as a user, you get more information on that user: + +operation::own-details[snippets='http-request,http-response,response-fields'] + + +//tag::create-user[] +=== Create a user + +To create an new user, do a `POST` on `/api/users`: + +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter09/01 - fileupload/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter09/01 - fileupload/src/main/asciidoc/Copsboot REST API Guide.adoc deleted file mode 100644 index 255bc8e..0000000 --- a/chapter09/01 - fileupload/src/main/asciidoc/Copsboot REST API Guide.adoc +++ /dev/null @@ -1,14 +0,0 @@ -= Copsboot REST API Guide -:icons: font -:toc: -:toclevels: 2 - -:numbered: - -== Introduction - -The Copsboot project uses a REST API for interfacing with the server. - -This documentation covers version {project-version} of the application. - -include::_users.adoc[] \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/main/asciidoc/_users.adoc b/chapter09/01 - fileupload/src/main/asciidoc/_users.adoc deleted file mode 100644 index a033db8..0000000 --- a/chapter09/01 - fileupload/src/main/asciidoc/_users.adoc +++ /dev/null @@ -1,24 +0,0 @@ -//tag::initial-doc[] -== User Management - -=== User information - -The API allows to get information on the currently logged on user -via a `GET` on `/api/users/me`. If you are not a logged on user, the -following response will be returned: - -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] -//end::initial-doc[] - -If you do log on as a user, you get more information on that user: - -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] - - -//tag::create-user[] -=== Create a user - -To create an new user, do a `POST` on `/api/users`: - -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java index 344a5fe..fb1cc59 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java @@ -6,6 +6,7 @@ public final class SpringProfiles { public static final String TEST = "test"; public static final String STAGING = "staging"; public static final String PROD = "prod"; + public static final String REPOSITORY_TEST = "repository-test"; public static final String INTEGRATION_TEST = "integration-test"; private SpringProfiles() { diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java index 9c92c49..8d26775 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java @@ -1,11 +1,4 @@ package com.example.copsboot.infrastructure.mvc; -import lombok.Value; - -//tag::class[] -@Value -public class FieldErrorResponse { - private String fieldName; - private String errorMessage; +public record FieldErrorResponse(String fieldName, String errorMesesage) { } -//end::class[] \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/mvc/RestControllerExceptionHandler.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/mvc/RestControllerExceptionHandler.java index f302a4f..b356ea7 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/mvc/RestControllerExceptionHandler.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/mvc/RestControllerExceptionHandler.java @@ -1,7 +1,6 @@ package com.example.copsboot.infrastructure.mvc; import org.springframework.http.HttpStatus; -import org.springframework.validation.BindException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -14,29 +13,17 @@ import java.util.stream.Collectors; //tag::class[] -@ControllerAdvice +@ControllerAdvice //<1> public class RestControllerExceptionHandler { - @ExceptionHandler - @ResponseBody - @ResponseStatus(HttpStatus.BAD_REQUEST) - public Map> handle(MethodArgumentNotValidException exception) { + @ExceptionHandler //<2> + @ResponseBody //<3> + @ResponseStatus(HttpStatus.BAD_REQUEST) //<4> + public Map> handle(MethodArgumentNotValidException exception) { //<5> return error(exception.getBindingResult() .getFieldErrors() .stream() - .map(fieldError -> new FieldErrorResponse(fieldError.getField(), - fieldError.getDefaultMessage())) - .collect(Collectors.toList())); - } - - @ExceptionHandler - @ResponseBody - @ResponseStatus(HttpStatus.BAD_REQUEST) - public Map handle(BindException exception) { - return error(exception.getBindingResult() - .getFieldErrors() - .stream() - .map(fieldError -> new FieldErrorResponse(fieldError.getField(), + .map(fieldError -> new FieldErrorResponse(fieldError.getField(), //<6> fieldError.getDefaultMessage())) .collect(Collectors.toList())); } diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index e8dc16a..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import static java.lang.String.format; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/CreateReportParameters.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/CreateReportParameters.java new file mode 100644 index 0000000..64aeea6 --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/CreateReportParameters.java @@ -0,0 +1,8 @@ +package com.example.copsboot.report; + +import com.example.copsboot.user.UserId; + +import java.time.Instant; + +public record CreateReportParameters(UserId userId, Instant dateTime, String description) { +} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/Report.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/Report.java index c0f9c66..b10756f 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/Report.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/Report.java @@ -1,36 +1,36 @@ package com.example.copsboot.report; -import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; import com.example.orm.jpa.AbstractEntity; import com.example.util.ArtifactForFramework; +import jakarta.persistence.Entity; + +import java.time.Instant; -import javax.persistence.Entity; -import javax.persistence.ManyToOne; -import java.time.ZonedDateTime; //tag::class[] @Entity public class Report extends AbstractEntity { - @ManyToOne - private User reporter; - private ZonedDateTime dateTime; + + private UserId reporterId; + private Instant dateTime; private String description; @ArtifactForFramework protected Report() { } - public Report(ReportId id, User reporter, ZonedDateTime dateTime, String description) { + public Report(ReportId id, UserId reporterId, Instant dateTime, String description) { super(id); - this.reporter = reporter; + this.reporterId = reporterId; this.dateTime = dateTime; this.description = description; } - public User getReporter() { - return reporter; + public UserId getReporterId() { + return reporterId; } - public ZonedDateTime getDateTime() { + public Instant getDateTime() { return dateTime; } diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/ReportService.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/ReportService.java index 4d02935..613248b 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/ReportService.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/ReportService.java @@ -1,10 +1,23 @@ package com.example.copsboot.report; -import com.example.copsboot.user.UserId; -import org.springframework.web.multipart.MultipartFile; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.ZonedDateTime; -public interface ReportService { - Report createReport(UserId reporterId, ZonedDateTime dateTime, String description, MultipartFile image); +@Service +@Transactional +public class ReportService { + private final ReportRepository repository; + + public ReportService(ReportRepository repository) { + this.repository = repository; + } + + public Report createReport(CreateReportParameters parameters) { + return repository.save(new Report(repository.nextId(), + parameters.userId(), + parameters.dateTime(), + parameters.description())); + } } diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/ReportServiceImpl.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/ReportServiceImpl.java deleted file mode 100644 index 403fd0e..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/ReportServiceImpl.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserNotFoundException; -import com.example.copsboot.user.UserService; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -import java.time.ZonedDateTime; - -@Service -@Transactional -public class ReportServiceImpl implements ReportService { - private final ReportRepository repository; - private final UserService userService; - - public ReportServiceImpl(ReportRepository repository, UserService userService) { - this.repository = repository; - this.userService = userService; - } - - @Override - public Report createReport(UserId reporterId, ZonedDateTime dateTime, String description, MultipartFile image) { - return repository.save(new Report(repository.nextId(), - userService.getUser(reporterId) - .orElseThrow(() -> new UserNotFoundException(reporterId)), - dateTime, - description)); - } -} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java deleted file mode 100644 index efeb69b..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.report.web; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.web.multipart.MultipartFile; - -import javax.validation.constraints.NotNull; -import java.time.ZonedDateTime; - -//tag::class[] -@Data -@AllArgsConstructor -@NoArgsConstructor -public class CreateReportParameters { - @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) - private ZonedDateTime dateTime; - - @ValidReportDescription - private String description; - - @NotNull - private MultipartFile image; //<1> -} -//end::class[] \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java new file mode 100644 index 0000000..d4b215f --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java @@ -0,0 +1,21 @@ +package com.example.copsboot.report.web; + +import com.example.copsboot.report.CreateReportParameters; +import com.example.copsboot.user.UserId; +import jakarta.validation.constraints.NotNull; +import org.springframework.web.multipart.MultipartFile; + +import java.time.Instant; + +@ValidCreateReportRequest +public record CreateReportRequest( + Instant dateTime, + @ValidReportDescription String description, + boolean trafficIncident, + int numberOfInvolvedCars, + @NotNull MultipartFile image //<.> +) { + public CreateReportParameters toParameters(UserId userId) { + return new CreateReportParameters(userId, dateTime, description); + } +} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java new file mode 100644 index 0000000..fbad4ea --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java @@ -0,0 +1,21 @@ +package com.example.copsboot.report.web; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +//tag::class[] +public class CreateReportRequestValidator implements ConstraintValidator { //<1> + + @Override + public void initialize(ValidCreateReportRequest constraintAnnotation) { + } + + @Override + public boolean isValid(CreateReportRequest value, ConstraintValidatorContext context) { + boolean result = true; + if (value.trafficIncident() && value.numberOfInvolvedCars() <= 0) { //<2> + result = false; + } + return result; + } +} //end::class[] diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java index d1bff04..aa30ca4 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java @@ -1,10 +1,9 @@ package com.example.copsboot.report.web; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; -public class ReportDescriptionValidator - implements ConstraintValidator { //<1> +public class ReportDescriptionValidator implements ConstraintValidator { //<1> @Override public void initialize(ValidReportDescription constraintAnnotation) { //<2> diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportDto.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportDto.java index 0adc7f8..28e606e 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportDto.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportDto.java @@ -2,23 +2,21 @@ import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; -import lombok.Value; +import com.example.copsboot.user.UserService; -import java.time.ZonedDateTime; +import java.time.Instant; //tag::class[] -@Value -public class ReportDto { - private ReportId id; - private String reporter; - private ZonedDateTime dateTime; - private String description; +public record ReportDto(ReportId id, + String reporter, + Instant dateTime, + String description) { - public static ReportDto fromReport(Report report) { + public static ReportDto fromReport(Report report, UserService userService) { return new ReportDto(report.getId(), - report.getReporter().getEmail(), - report.getDateTime(), - report.getDescription()); + userService.getUserById(report.getReporterId()).getEmail(), + report.getDateTime(), + report.getDescription()); } } //end::class[] diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportRestController.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportRestController.java index 6de180b..12387e0 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportRestController.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ReportRestController.java @@ -1,36 +1,44 @@ package com.example.copsboot.report.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.report.CreateReportParameters; +import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportService; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserNotFoundException; +import com.example.copsboot.user.UserService; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.UUID; //tag::class[] @RestController @RequestMapping("/api/reports") public class ReportRestController { private final ReportService service; + private final UserService userService; - public ReportRestController(ReportService service) { + public ReportRestController(ReportService service, UserService userService) { this.service = service; + this.userService = userService; } - //tag::create-report-method-signature[] + // tag::create-report-method-signature[] @PostMapping @ResponseStatus(HttpStatus.CREATED) - public ReportDto createReport(@AuthenticationPrincipal ApplicationUserDetails userDetails, - @Valid CreateReportParameters parameters) { - //end::create-report-method-signature[] - return ReportDto.fromReport(service.createReport(userDetails.getUserId(), - parameters.getDateTime(), - parameters.getDescription(), - parameters.getImage())); + public ReportDto createReport(@AuthenticationPrincipal Jwt jwt, + @Valid CreateReportRequest request) { + // end::create-report-method-signature[] + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + User user = userService.findUserByAuthServerId(authServerId) + .orElseThrow(() -> new UserNotFoundException(authServerId)); + CreateReportParameters parameters = request.toParameters(user.getId()); + Report report = service.createReport(parameters); + return ReportDto.fromReport(report, userService); } } //end::class[] diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java new file mode 100644 index 0000000..895ce6c --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java @@ -0,0 +1,20 @@ +package com.example.copsboot.report.web; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +//tag::class[] +@Target(ElementType.TYPE) //<1> +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = {CreateReportRequestValidator.class}) //<2> +public @interface ValidCreateReportRequest { + String message() default "Invalid report"; + + Class[] groups() default {}; + + Class[] payload() default {}; +}//end::class[] diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java index 41d39e9..ba8fa56 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java @@ -1,7 +1,7 @@ package com.example.copsboot.report.web; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -16,4 +16,4 @@ Class[] groups() default {}; //<5> Class[] payload() default {}; //<6> -} \ No newline at end of file +} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/User.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/User.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java new file mode 100644 index 0000000..2a434e3 --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class UserIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(UserId attribute) { + return attribute.getId(); + } + + @Override + public UserId convertToEntityAttribute(UUID dbData) { + return new UserId(dbData); + } +} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserNotFoundException.java index 1f65f04..97d0813 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserNotFoundException.java @@ -3,9 +3,13 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> +@ResponseStatus(HttpStatus.NOT_FOUND) public class UserNotFoundException extends RuntimeException { public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); + super(String.format("Unable to find user with id %s", userId)); + } + + public UserNotFoundException(AuthServerId authServerId) { + super(String.format("Unable to find user with auth server id %s", authServerId)); } } diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..741d2e0 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,11 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); + + Optional findByMobileToken(String mobileToken); } //end::class[] diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserService.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserService.java index d5630f0..ba1d4ab 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserService.java @@ -1,11 +1,37 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } + + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } - Optional getUser(UserId userId); + public User getUserById(UserId userId) { + return repository.findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + } - Optional findUserByEmail(String email); + public Optional findUserByMobileToken(String mobileToken) { + return repository.findByMobileToken(mobileToken); + } + // end::createUser[] } diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 6918081..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } - - @Override - public Optional findUserByEmail(String email) { - return repository.findByEmailIgnoreCase(email); - } -} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index f96ee54..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -@ValidCreateUserParameters -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java deleted file mode 100644 index 3f86d70..0000000 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.copsboot.user.web; - -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; - -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; - -//tag::class[] -public class CreateUserParametersValidator implements ConstraintValidator { - - private final UserService userService; - - @Autowired - public CreateUserParametersValidator(UserService userService) { //<1> - this.userService = userService; - } - - @Override - public void initialize(ValidCreateUserParameters constraintAnnotation) { - - } - - @Override - public boolean isValid(CreateOfficerParameters userParameters, ConstraintValidatorContext context) { - - boolean result = true; - - if (userService.findUserByEmail(userParameters.getEmail()).isPresent()) { //<2> - context.buildConstraintViolationWithTemplate( - "There is already a user with the given email address.") - .addPropertyNode("email").addConstraintViolation(); //<3> - - result = false; //<4> - } - - return result; - } -} -//end::class[] diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..83c56a1 --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,18 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import jakarta.validation.constraints.NotEmpty; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +@ValidCreateUserRequest +public record CreateUserRequest(@NotEmpty String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + String email = jwt.getClaimAsString("email"); + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java new file mode 100644 index 0000000..bdd4aa5 --- /dev/null +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java @@ -0,0 +1,40 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.UserService; +import org.springframework.beans.factory.annotation.Autowired; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +//tag::class[] +public class CreateUserRequestValidator implements ConstraintValidator { + + private final UserService userService; + + @Autowired + public CreateUserRequestValidator(UserService userService) { //<1> + this.userService = userService; + } + + @Override + public void initialize(ValidCreateUserRequest constraintAnnotation) { + + } + + @Override + public boolean isValid(CreateUserRequest userRequest, ConstraintValidatorContext context) { + + boolean result = true; + + if (userService.findUserByMobileToken(userRequest.mobileToken()).isPresent()) { //<2> + context.buildConstraintViolationWithTemplate( + "There is already a user with the given mobile token.") + .addPropertyNode("mobileToken").addConstraintViolation(); //<3> + + result = false; //<4> + } + + return result; + } +} +//end::class[] diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/UserRestController.java index b5aa1a8..e0a6545 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,49 +1,53 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import lombok.Value; -import org.springframework.beans.factory.annotation.Autowired; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.Valid; -import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import java.util.stream.Collectors; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] + // tag::createUser[] @PostMapping @ResponseStatus(HttpStatus.CREATED) - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { - User officer = service.createOfficer(parameters.getEmail(), - parameters.getPassword()); - return UserDto.fromUser(officer); + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/ValidCreateUserParameters.java b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/ValidCreateUserRequest.java similarity index 70% rename from chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/ValidCreateUserParameters.java rename to chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/ValidCreateUserRequest.java index a7ec388..e6a975e 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/ValidCreateUserParameters.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/copsboot/user/web/ValidCreateUserRequest.java @@ -1,7 +1,7 @@ package com.example.copsboot.user.web; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -10,8 +10,8 @@ //tag::class[] @Target(ElementType.TYPE) // <1> @Retention(RetentionPolicy.RUNTIME) -@Constraint(validatedBy = {CreateUserParametersValidator.class}) //<2> -public @interface ValidCreateUserParameters { +@Constraint(validatedBy = {CreateUserRequestValidator.class}) //<2> +public @interface ValidCreateUserRequest { String message() default "Invalid user"; Class[] groups() default {}; diff --git a/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/Entity.java b/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/Entity.java index 96cadf0..3a45231 100644 --- a/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter09/01 - fileupload/src/main/java/com/example/orm/jpa/Entity.java @@ -1,5 +1,6 @@ package com.example.orm.jpa; + /** * Interface for entity objects. * diff --git a/chapter09/01 - fileupload/src/main/resources/application-dev.properties b/chapter09/01 - fileupload/src/main/resources/application-dev.properties deleted file mode 100644 index f72b4c7..0000000 --- a/chapter09/01 - fileupload/src/main/resources/application-dev.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/main/resources/application-local.properties b/chapter09/01 - fileupload/src/main/resources/application-local.properties index 8f13f3f..7e354d5 100644 --- a/chapter09/01 - fileupload/src/main/resources/application-local.properties +++ b/chapter09/01 - fileupload/src/main/resources/application-local.properties @@ -3,13 +3,9 @@ spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.username=postgres spring.datasource.password=my-postgres-db-pwd spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata -spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create -spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=create.sql - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +#spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=create.sql +#spring.jpa.properties.hibernate.hbm2ddl.delimiter=; diff --git a/chapter09/01 - fileupload/src/main/resources/application.properties b/chapter09/01 - fileupload/src/main/resources/application.properties index e69de29..3e80adf 100644 --- a/chapter09/01 - fileupload/src/main/resources/application.properties +++ b/chapter09/01 - fileupload/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/copsboot + +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ diff --git a/chapter09/01 - fileupload/src/main/resources/db/migration/V1.0.0.1__users.sql b/chapter09/01 - fileupload/src/main/resources/db/migration/V1.0.0.1__users.sql new file mode 100644 index 0000000..d1939fa --- /dev/null +++ b/chapter09/01 - fileupload/src/main/resources/db/migration/V1.0.0.1__users.sql @@ -0,0 +1,7 @@ +CREATE TABLE copsboot_user +( + id uuid NOT NULL PRIMARY KEY, + auth_server_id uuid, + email VARCHAR(255), + mobile_token VARCHAR(255) +); diff --git a/chapter09/01 - fileupload/src/main/resources/db/migration/V1.0.0.2__reports.sql b/chapter09/01 - fileupload/src/main/resources/db/migration/V1.0.0.2__reports.sql new file mode 100644 index 0000000..cc2e26c --- /dev/null +++ b/chapter09/01 - fileupload/src/main/resources/db/migration/V1.0.0.2__reports.sql @@ -0,0 +1,8 @@ +CREATE TABLE report +( + date_time TIMESTAMP(6) WITH TIME ZONE, + id uuid NOT NULL, + description VARCHAR(255), + reporter_id uuid, + PRIMARY KEY (id) +); diff --git a/chapter09/01 - fileupload/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql b/chapter09/01 - fileupload/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql deleted file mode 100644 index 485336f..0000000 --- a/chapter09/01 - fileupload/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,42 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(255) PRIMARY KEY, - resource_ids VARCHAR(255), - client_secret VARCHAR(255), - scope VARCHAR(255), - authorized_grant_types VARCHAR(255), - web_server_redirect_uri VARCHAR(255), - authorities VARCHAR(255), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(255) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255), - authentication BLOB, - refresh_token VARCHAR(255) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(255), - token BLOB, - authentication BLOB -); - -CREATE TABLE oauth_code ( - activationCode VARCHAR(255), - authentication BLOB -); \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql b/chapter09/01 - fileupload/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql deleted file mode 100644 index 7c3fdf3..0000000 --- a/chapter09/01 - fileupload/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(256) PRIMARY KEY, - resource_ids VARCHAR(256), - client_secret VARCHAR(256), - scope VARCHAR(256), - authorized_grant_types VARCHAR(256), - web_server_redirect_uri VARCHAR(256), - authorities VARCHAR(256), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(256) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256), - authentication BYTEA, - refresh_token VARCHAR(256) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(256), - token BYTEA, - authentication BYTEA -); - -CREATE TABLE oauth_code ( - code VARCHAR(256), - authentication BYTEA -); - -CREATE TABLE oauth_approvals ( - userId VARCHAR(256), - clientId VARCHAR(256), - scope VARCHAR(256), - status VARCHAR(10), - expiresAt TIMESTAMP, - lastModifiedAt TIMESTAMP -); - diff --git a/chapter09/01 - fileupload/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql b/chapter09/01 - fileupload/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql deleted file mode 100644 index 122b1fc..0000000 --- a/chapter09/01 - fileupload/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE copsboot_user ( - id UUID NOT NULL, - email VARCHAR(255), - password VARCHAR(255), - PRIMARY KEY (id) -); - -CREATE TABLE user_roles ( - user_id UUID NOT NULL, - roles VARCHAR(255) -); - -ALTER TABLE user_roles - ADD CONSTRAINT FK7je59ku3x462eqxu4ss3das1s -FOREIGN KEY (user_id) -REFERENCES copsboot_user; diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..5feb390 100644 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,16 @@ package com.example.copsboot; import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java new file mode 100644 index 0000000..3ddeac0 --- /dev/null +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java @@ -0,0 +1,30 @@ +package com.example.copsboot.infrastructure.test; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//tag::class[] +@Retention(RetentionPolicy.RUNTIME) +@CopsbootControllerTest +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class) +public @interface CopsbootControllerDocumentationTest { + + @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> + Class[] value() default {}; + + @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") //<6> + Class[] controllers() default {}; +} +//end::class[] diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java new file mode 100644 index 0000000..02e070e --- /dev/null +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java @@ -0,0 +1,21 @@ +package com.example.copsboot.infrastructure.test; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +class CopsbootControllerDocumentationTestConfiguration { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java index c33238a..6696635 100644 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java @@ -1,10 +1,10 @@ package com.example.copsboot.infrastructure.test; -import com.example.copsboot.infrastructure.SpringProfiles; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,23 +12,12 @@ /** * Custom annotation for all {@link org.springframework.stereotype.Controller Controller} tests on the project. By using * this single annotation, everything is configured properly to test a controller: - *
    - *
  • Import of {@link CopsbootControllerTestConfiguration}
  • - *
  • test profile active
  • - *
- *

- * Example usage: - *

- * @RunWith(SpringRunner.class)
- * @CopsbootControllerTest(UserController.class)
- * public class UserControllerTest {
- * 
*/ //tag::class[] -@Retention(RetentionPolicy.RUNTIME) //<1> -@WebMvcTest //<2> -@ContextConfiguration(classes = CopsbootControllerTestConfiguration.class) //<3> -@ActiveProfiles(SpringProfiles.TEST) //<4> +@Retention(RetentionPolicy.RUNTIME) //<.> +@WebMvcTest //<.> +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> public @interface CopsbootControllerTest { @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java deleted file mode 100644 index 7231430..0000000 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.infrastructure.test; - -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; - -@TestConfiguration -@Import(OAuth2ServerConfiguration.class) -public class CopsbootControllerTestConfiguration { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - -} diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/Reports.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/Reports.java deleted file mode 100644 index 2302dc9..0000000 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/Reports.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.Users; - -import java.time.ZonedDateTime; -import java.util.UUID; - -public class Reports { - public static Report createRandomReport(String description) { - return new Report(new ReportId(UUID.randomUUID()), - Users.newRandomOfficer(), - ZonedDateTime.now(), - description); - } -} diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java new file mode 100644 index 0000000..04744f4 --- /dev/null +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java @@ -0,0 +1,75 @@ +package com.example.copsboot.report.web; + + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.jetbrains.annotations.NotNull; +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; + +import java.time.Instant; +import java.util.Set; + +import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; + +public class CreateReportRequestValidatorTest { + //tag::invalid[] + @Test + public void givenTrafficIndicentButInvolvedCarsZero_invalid() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat", + true, + 0, + createImage()); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasViolationOnPath(""); + } + } + //end::invalid[] + + //tag::valid[] + @Test + public void givenTrafficIndicent_involvedCarsMustBePositive() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", + true, + 2, + createImage()); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } + } + //end::valid[] + + //tag::valid-no-cars[] + @Test + public void givenNoTrafficIndicent_involvedCarsDoesNotMatter() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", + false, + 0, + createImage()); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } + } + //end::valid-no-cars[] + + @NotNull + private static MockMultipartFile createImage() { + return new MockMultipartFile("image", "picture.png", MediaType.IMAGE_PNG_VALUE, new byte[]{1, 2, 3}); + } + +} diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java index 0715dc3..b3b6724 100644 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java @@ -1,13 +1,15 @@ package com.example.copsboot.report.web; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.jetbrains.annotations.NotNull; import org.junit.Test; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; -import javax.validation.ConstraintViolation; -import javax.validation.Validation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import java.time.ZonedDateTime; +import java.time.Instant; import java.util.Set; import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; @@ -17,33 +19,34 @@ public class ReportDescriptionValidatorTest { //tag::invalid[] @Test public void givenEmptyString_notValid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); //<1> - Validator validator = factory.getValidator(); //<2> - - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), "", createMockImage()); - Set> violationSet = validator.validate(parameters); //<3> - assertThat(violationSet).hasViolationOnPath("description"); //<4> + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { //<1> + Validator validator = factory.getValidator(); //<2> + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), "", false, 0, + createImage()); + Set> violationSet = validator.validate(parameters); //<3> + assertThat(violationSet).hasViolationOnPath("description"); //<4> + } } //end::invalid[] //tag::valid[] @Test public void givenSuspectWordPresent_valid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - Validator validator = factory.getValidator(); - - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), - "The suspect was wearing a black hat.", - createMockImage()); - Set> violationSet = validator.validate(parameters); - assertThat(violationSet).hasNoViolations(); + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", false, 0, + createImage()); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } } //end::valid[] - private MockMultipartFile createMockImage() { - return new MockMultipartFile("image", - "picture.png", - "image/png", - new byte[]{1, 2, 3}); + @NotNull + private static MockMultipartFile createImage() { + return new MockMultipartFile("image", "picture.png", MediaType.IMAGE_PNG_VALUE, new byte[]{1, 2, 3}); } -} \ No newline at end of file +} diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java index 535b38d..88f289a 100644 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java @@ -1,69 +1,76 @@ package com.example.copsboot.report.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; +import com.example.copsboot.report.CreateReportParameters; import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; import com.example.copsboot.report.ReportService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; +import com.example.copsboot.user.UserService; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.time.ZonedDateTime; +import java.time.Instant; +import java.util.Optional; import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; //tag::class[] -@RunWith(SpringRunner.class) @CopsbootControllerTest(ReportRestController.class) public class ReportRestControllerTest { @Autowired - private MockMvc mvc; - + private MockMvc mockMvc; @MockBean private ReportService service; + @MockBean + private UserService userService; @Test public void officerIsAbleToPostAReport() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - String dateTime = "2018-04-11T22:59:03.189+02:00"; - String description = "The suspect is wearing a black hat."; - MockMultipartFile image = createMockImage(); - when(service.createReport(eq(Users.officer().getId()), - any(ZonedDateTime.class), - eq(description), - any(MockMultipartFile.class))) - .thenReturn(new Report(new ReportId(UUID.randomUUID()), Users.officer(), ZonedDateTime.parse(dateTime), description)); - - mvc.perform(fileUpload("/api/reports") //<1> - .file(image) //<2> - .header(HEADER_AUTHORIZATION, bearer(accessToken)) - .param("dateTime", dateTime) //<3> - .param("description", description)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("reporter").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("dateTime").value(dateTime)) - .andExpect(jsonPath("description").value(description)); - } - private MockMultipartFile createMockImage() { //<4> - return new MockMultipartFile("image", - "picture.png", - "image/png", - new byte[]{1, 2, 3}); + UserId userId = new UserId(UUID.randomUUID()); + AuthServerId authServerId = new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); + User user = new User(userId, + "wim@example.com", + authServerId, + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"); + when(userService.findUserByAuthServerId(authServerId)) + .thenReturn(Optional.of(user)); + when(userService.getUserById(userId)) + .thenReturn(user); + when(service.createReport(any(CreateReportParameters.class))) + .thenReturn(new Report(new ReportId(UUID.randomUUID()), + userId, + Instant.parse("2023-04-11T22:59:03.189+02:00"), + "This is a test report description. The suspect was wearing a black hat.")); + mockMvc.perform(multipart("/api/reports") //<.> + .file(new MockMultipartFile("image", "picture.png", MediaType.IMAGE_PNG_VALUE, new byte[]{1,2,3})) //<.> + .param("dateTime", "2023-04-11T22:59:03.189+02:00") //<.> + .param("description", "This is a test report description. The suspect was wearing a black hat.") + .param("trafficIncident", "false") + .param("numberOfInvolvedCars", "0") + .with(jwt().jwt(builder -> builder.subject(authServerId.value().toString()) + .claim("email", "wim@example.com")) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("id").exists()) + .andExpect(jsonPath("reporter").value("wim@example.com")) + .andExpect(jsonPath("dateTime").value("2023-04-11T20:59:03.189Z")) + .andExpect(jsonPath("description").value("This is a test report description. The suspect was wearing a black hat.")); } } -//end::class[] \ No newline at end of file +//end::class[] diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java deleted file mode 100644 index 720f959..0000000 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.copsboot.user; - -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.util.HashSet; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@DataJpaTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> -@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) //<2> -public class UserRepositoryIntegrationTest { - @Autowired - private UserRepository repository; - @PersistenceContext - private EntityManager entityManager; - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - public void testSaveUser() { - Set roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); - - assertThat(repository.count()).isEqualTo(1L); - - entityManager.flush(); //<3> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM user_roles", Long.class)).isEqualTo(1L); - assertThat(jdbcTemplate.queryForObject("SELECT roles FROM user_roles", String.class)).isEqualTo("OFFICER"); - } -} diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index 3217c4a..19c23fe 100644 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -3,14 +3,16 @@ import com.example.copsboot.infrastructure.SpringProfiles; import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -19,62 +21,34 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest -@ActiveProfiles(SpringProfiles.TEST) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) //<2> public class UserRepositoryTest { @Autowired private UserRepository repository; + @PersistenceContext + private EntityManager entityManager; + @Autowired + private JdbcTemplate jdbcTemplate; - //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> - - assertThat(repository.count()).isEqualTo(1L); //<7> - } - //end::testStoreUser[] + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } + assertThat(repository.count()).isEqualTo(1L); - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); + entityManager.flush(); //<3> - assertThat(optional).isEmpty(); + assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> + assertThat(jdbcTemplate.queryForObject("SELECT email FROM copsboot_user", String.class)).isEqualTo("alex.foley@beverly-hills.com"); } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @Bean @@ -82,5 +56,4 @@ public UniqueIdGenerator generator() { return new InMemoryUniqueIdGenerator(); } } - //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/Users.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java deleted file mode 100644 index 7b94df3..0000000 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.copsboot.user.web; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserService; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.validation.ConstraintViolation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; - -import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; -import static org.mockito.Mockito.when; - -//tag::class[] -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@ActiveProfiles(SpringProfiles.TEST) -public class CreateUserParametersValidatorTest { - - @MockBean - private UserService userService; //<2> - @Autowired - private PasswordEncoder encoder; - @Autowired - private ValidatorFactory factory; //<3> - - @Test - public void invalidIfAlreadyUserWithGivenEmail() { - - String email = "wim.deblauwe@example.com"; - when(userService.findUserByEmail(email)) - .thenReturn(Optional.of( - User.createOfficer(new UserId(UUID.randomUUID()), - email, - encoder.encode("testing1234")))); - - Validator validator = factory.getValidator(); //<4> - - CreateOfficerParameters userParameters = new CreateOfficerParameters(); - userParameters.setEmail(email); - userParameters.setPassword("my-secret-pwd-1234"); - Set> violationSet = validator.validate(userParameters); //<5> - assertThat(violationSet).hasViolationSize(2) - .hasViolationOnPath("email"); //<6> - } - - @Test - public void validIfNoUserWithGivenEmail() { - String email = "wim.deblauwe@example.com"; - when(userService.findUserByEmail(email)) - .thenReturn(Optional.empty()); - - Validator validator = factory.getValidator(); - - CreateOfficerParameters userParameters = new CreateOfficerParameters(); - userParameters.setEmail(email); - userParameters.setPassword("my-secret-pwd-1234"); - Set> violationSet = validator.validate(userParameters); - assertThat(violationSet).hasNoViolations(); - } -} -//end::class[] \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java new file mode 100644 index 0000000..d058abd --- /dev/null +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java @@ -0,0 +1,65 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.infrastructure.SpringProfiles; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; +import com.example.copsboot.user.UserService; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; + +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; +import static org.mockito.Mockito.when; + +//tag::class[] +@SpringBootTest //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) +public class CreateUserRequestValidatorTest { + + @MockBean + private UserService userService; //<2> + @Autowired + private ValidatorFactory factory; //<3> + + @Test + public void invalidIfAlreadyUserWithGivenMobileToken() { + + String mobileToken = "abc123"; + when(userService.findUserByMobileToken(mobileToken)) + .thenReturn(Optional.of(new User(new UserId(UUID.randomUUID()), + "wim@example.com", + new AuthServerId(UUID.randomUUID()), + mobileToken))); + + Validator validator = factory.getValidator(); //<4> + + CreateUserRequest request = new CreateUserRequest(mobileToken); + Set> violationSet = validator.validate(request); //<5> + assertThat(violationSet).hasViolationSize(2) + .hasViolationOnPath("mobileToken"); //<6> + } + + @Test + public void validIfNoUserWithGivenMobileToken() { + String mobileToken = "abc123"; + when(userService.findUserByMobileToken(mobileToken)) + .thenReturn(Optional.empty()); + + Validator validator = factory.getValidator(); + + CreateUserRequest request = new CreateUserRequest(mobileToken); + Set> violationSet = validator.validate(request); + assertThat(violationSet).hasNoViolations(); + } +} +//end::class[] diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index b1c3165..805c501 100644 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,134 +1,94 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.infrastructure.test.CopsbootControllerDocumentationTest; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) -@CopsbootControllerTest(UserRestController.class) +@CopsbootControllerDocumentationTest(UserRestController.class) public class UserRestControllerDocumentation { -//end::class-annotations[] - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - when(service.findUserByEmail(email)).thenReturn(Optional.empty()); - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] } diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index f55e6c6..a20d744 100644 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,121 +1,104 @@ package com.example.copsboot.user.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) -public class UserRestControllerTest { -//end::class-annotations[] - @Autowired - private MockMvc mvc; +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - when(service.findUserByEmail(email)).thenReturn(Optional.empty()); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } + // tag::emptyToken[] @Test - public void testCreateOfficerIfPasswordIsTooShort() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.findUserByEmail(email)).thenReturn(Optional.empty()); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("errors[0].fieldName").value("password")); - - verify(service, never()).createOfficer(email, password); + void givenEmptyMobileToken_badRequestIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "" + } + """)) //<.> + .andExpect(status().isBadRequest()) //<.> + .andDo(print()); //<.> + + verify(userService, never()).createUser(any(CreateUserParameters.class)); //<.> } + // end::emptyToken[] } diff --git a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java index 77c5f4c..21556a5 100644 --- a/chapter09/01 - fileupload/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java +++ b/chapter09/01 - fileupload/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java @@ -2,7 +2,8 @@ import org.assertj.core.api.AbstractAssert; -import javax.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolation; + import java.util.Set; import java.util.stream.Collectors; @@ -21,12 +22,12 @@ public ConstraintViolationSetAssert hasViolationOnPath(String path) { // check condition if (!containsViolationWithPath(actual, path)) { failWithMessage("There was no violation with path <%s>. Violation paths: <%s>", path, actual.stream() - .map(violation -> violation - .getPropertyPath() - .toString()) - .collect( - Collectors - .toList())); + .map(violation -> violation + .getPropertyPath() + .toString()) + .collect( + Collectors + .toList())); } return this; diff --git a/chapter09/01 - fileupload/src/test/resources/application-integration-test.properties b/chapter09/01 - fileupload/src/test/resources/application-integration-test.properties index 159536c..c61e563 100644 --- a/chapter09/01 - fileupload/src/test/resources/application-integration-test.properties +++ b/chapter09/01 - fileupload/src/test/resources/application-integration-test.properties @@ -1,11 +1,6 @@ -spring.datasource.url=jdbc:tc:postgresql://localhost/copsbootdb +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.username=user spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none - -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter09/01 - fileupload/src/test/resources/application-repository-test.properties b/chapter09/01 - fileupload/src/test/resources/application-repository-test.properties new file mode 100644 index 0000000..c61e563 --- /dev/null +++ b/chapter09/01 - fileupload/src/test/resources/application-repository-test.properties @@ -0,0 +1,6 @@ +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver +spring.datasource.username=user +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter09/01 - fileupload/src/test/resources/application-test.properties b/chapter09/01 - fileupload/src/test/resources/application-test.properties deleted file mode 100644 index 02b4003..0000000 --- a/chapter09/01 - fileupload/src/test/resources/application-test.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter09/01 - fileupload/src/test/resources/logback-test.xml b/chapter09/01 - fileupload/src/test/resources/logback-test.xml index bf47fec..164429c 100644 --- a/chapter09/01 - fileupload/src/test/resources/logback-test.xml +++ b/chapter09/01 - fileupload/src/test/resources/logback-test.xml @@ -5,7 +5,7 @@ - + @@ -17,14 +17,8 @@ - - - - - - - \ No newline at end of file + diff --git a/chapter09/02 - validation/.mvn/wrapper/maven-wrapper.jar b/chapter09/02 - validation/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/chapter09/02 - validation/.mvn/wrapper/maven-wrapper.jar differ diff --git a/chapter09/02 - validation/.mvn/wrapper/maven-wrapper.properties b/chapter09/02 - validation/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/chapter09/02 - validation/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/chapter09/02 - validation/docker-compose.yaml b/chapter09/02 - validation/docker-compose.yaml new file mode 100644 index 0000000..92cea56 --- /dev/null +++ b/chapter09/02 - validation/docker-compose.yaml @@ -0,0 +1,20 @@ +version: '3' +services: + db: + image: 'postgres:16.0' + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: my-postgres-db-pwd + identity: + image: 'quay.io/keycloak/keycloak:22.0.1' + entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm + ports: + - '8180:8080' + environment: + KEYCLOAK_LOGLEVEL: 'INFO' + KEYCLOAK_ADMIN: 'admin' + KEYCLOAK_ADMIN_PASSWORD: 'admin-secret' + KC_HOSTNAME: 'localhost' + KC_HEALTH_ENABLED: 'true' + KC_METRICS_ENABLED: 'true' diff --git a/chapter09/02 - validation/mvnw b/chapter09/02 - validation/mvnw index 5bf251c..66df285 100755 --- a/chapter09/02 - validation/mvnw +++ b/chapter09/02 - validation/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -36,6 +35,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -50,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -58,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -68,69 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -# For Migwn, ensure paths are in UNIX format before anything is touched +# For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -146,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -160,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -181,45 +150,159 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -echo $MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/chapter09/02 - validation/mvnw.cmd b/chapter09/02 - validation/mvnw.cmd index 019bd74..95ba6f5 100644 --- a/chapter09/02 - validation/mvnw.cmd +++ b/chapter09/02 - validation/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,15 +18,14 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -35,7 +34,9 @@ @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME @@ -44,8 +45,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal @@ -115,11 +116,72 @@ for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do s :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end @@ -129,15 +191,15 @@ set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause +if "%MAVEN_BATCH_PAUSE%"=="on" pause -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% -exit /B %ERROR_CODE% +cmd /C exit /B %ERROR_CODE% diff --git a/chapter09/02 - validation/pom.xml b/chapter09/02 - validation/pom.xml index 8edf789..e5f26ec 100644 --- a/chapter09/02 - validation/pom.xml +++ b/chapter09/02 - validation/pom.xml @@ -1,240 +1,225 @@ - - 4.0.0 + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + copsboot + 0.0.1-SNAPSHOT + copsboot + Demo project for Spring Boot + + + 17 + 27.1-jre + + - com.example.copsboot - copsboot - 0.0.1-SNAPSHOT - jar + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + 7.1.9 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + - copsboot - Demo project for Spring Boot + + com.google.guava + guava + ${guava.version} + - - org.springframework.boot - spring-boot-starter-parent - 2.1.4.RELEASE - - - - - - UTF-8 - UTF-8 - 1.8 + + + org.postgresql + postgresql + runtime + + + + + org.flywaydb + flyway-core + + - - 1.5.6 + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + com.c4-soft.springaddons + spring-addons-starter-oidc-test + 7.1.9 + test + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + - - 29.0-jre + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + + + io.rest-assured + rest-assured + test + + + + + com.github.dasniko + testcontainers-keycloak + 3.0.0 + test + + - - 2.0.3.RELEASE - 1.11.2 - 3.3.0 - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - 2.1.4.RELEASE - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-configuration-processor - true - - - com.google.guava - guava - ${guava.version} - - - org.projectlombok - lombok - - - org.postgresql - postgresql - - - org.flywaydb - flyway-core - + - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - com.h2database - h2 - runtime - - - org.assertj - assertj-core - test - - - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - org.testcontainers - postgresql - ${testcontainers.version} - test - - - - - io.rest-assured - rest-assured - ${rest-assured.version} - test - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.16 - - - org.asciidoctor - asciidoctorj - 1.5.7 - - - org.springframework.restdocs - spring-restdocs-asciidoctor - ${spring-restdocs.version} - - - org.jruby - jruby-complete - 9.1.17.0 - - - - - generate-docs - prepare-package - - process-asciidoc - - - html - - - - generate-docs-pdf - prepare-package - - process-asciidoc - - - pdf - - - - - html - book - - ${project.version} - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + false + + **/*.java + + + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + + + + generate-docs-pdf + prepare-package + + process-asciidoc + + + pdf + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + org.asciidoctor + asciidoctorj-pdf + 2.3.9 + + + + book + + ${project.version} + + + + + + + + + + + ci + - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - true - false - - **/*.java - - - + + org.asciidoctor + asciidoctor-maven-plugin + - - - - - ci - - - - org.asciidoctor - asciidoctor-maven-plugin - ${asciidoctor-maven-plugin.version} - - - generate-docs - prepare-package - - process-asciidoc - - - - - - - - - - - + + + + diff --git a/chapter09/02 - validation/src/docs/asciidoc/Copsboot REST API Guide.adoc b/chapter09/02 - validation/src/docs/asciidoc/Copsboot REST API Guide.adoc new file mode 100644 index 0000000..b0b91ae --- /dev/null +++ b/chapter09/02 - validation/src/docs/asciidoc/Copsboot REST API Guide.adoc @@ -0,0 +1,14 @@ += Copsboot REST API Guide +:icons: font +:toc: +:toclevels: 2 + +:numbered: + +== Introduction + +The Copsboot project uses a REST API for interfacing with the server. + +This documentation covers version {project-version} of the application. + +include::_users.adoc[] diff --git a/chapter09/02 - validation/src/docs/asciidoc/_users.adoc b/chapter09/02 - validation/src/docs/asciidoc/_users.adoc new file mode 100644 index 0000000..2becf75 --- /dev/null +++ b/chapter09/02 - validation/src/docs/asciidoc/_users.adoc @@ -0,0 +1,24 @@ +//tag::initial-doc[] +== User Management + +=== User information + +The API allows to get information on the currently logged on user +via a `GET` on `/api/users/me`. If you are not a logged on user, the +following response will be returned: + +operation::own-details-unauthorized[snippets='http-request,http-response'] +//end::initial-doc[] + +If you do log on as a user, you get more information on that user: + +operation::own-details[snippets='http-request,http-response,response-fields'] + + +//tag::create-user[] +=== Create a user + +To create an new user, do a `POST` on `/api/users`: + +operation::create-user[snippets='http-request,request-fields,http-response,response-fields'] +//end::create-user[] diff --git a/chapter09/02 - validation/src/main/asciidoc/Copsboot REST API Guide.adoc b/chapter09/02 - validation/src/main/asciidoc/Copsboot REST API Guide.adoc deleted file mode 100644 index 255bc8e..0000000 --- a/chapter09/02 - validation/src/main/asciidoc/Copsboot REST API Guide.adoc +++ /dev/null @@ -1,14 +0,0 @@ -= Copsboot REST API Guide -:icons: font -:toc: -:toclevels: 2 - -:numbered: - -== Introduction - -The Copsboot project uses a REST API for interfacing with the server. - -This documentation covers version {project-version} of the application. - -include::_users.adoc[] \ No newline at end of file diff --git a/chapter09/02 - validation/src/main/asciidoc/_users.adoc b/chapter09/02 - validation/src/main/asciidoc/_users.adoc deleted file mode 100644 index a033db8..0000000 --- a/chapter09/02 - validation/src/main/asciidoc/_users.adoc +++ /dev/null @@ -1,24 +0,0 @@ -//tag::initial-doc[] -== User Management - -=== User information - -The API allows to get information on the currently logged on user -via a `GET` on `/api/users/me`. If you are not a logged on user, the -following response will be returned: - -operation::own-user-details-when-not-logged-in-example[snippets='http-request,http-response'] -//end::initial-doc[] - -If you do log on as a user, you get more information on that user: - -operation::authenticated-officer-details-example[snippets='http-request,http-response,response-fields'] - - -//tag::create-user[] -=== Create a user - -To create an new user, do a `POST` on `/api/users`: - -operation::create-officer-example[snippets='http-request,request-fields,http-response,response-fields'] -//end::create-user[] \ No newline at end of file diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/CopsbootApplication.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/CopsbootApplication.java index f4e3307..7b031d7 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/CopsbootApplication.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/CopsbootApplication.java @@ -1,40 +1,13 @@ package com.example.copsboot; -import com.example.orm.jpa.InMemoryUniqueIdGenerator; -import com.example.orm.jpa.UniqueIdGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; - -import javax.sql.DataSource; -import java.util.UUID; @SpringBootApplication public class CopsbootApplication { - public static void main(String[] args) { - SpringApplication.run(CopsbootApplication.class, args); - } - - @Bean - public UniqueIdGenerator uniqueIdGenerator() { - return new InMemoryUniqueIdGenerator(); - } - - //tag::supporting-beans[] - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + public static void main(String[] args) { + SpringApplication.run(CopsbootApplication.class, args); + } - @Bean - public TokenStore tokenStore() { - return new InMemoryTokenStore(); - } - //end::supporting-beans[] } diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java new file mode 100644 index 0000000..cb552d7 --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/CopsbootApplicationConfiguration.java @@ -0,0 +1,18 @@ +package com.example.copsboot; + +import com.example.orm.jpa.InMemoryUniqueIdGenerator; +import com.example.orm.jpa.UniqueIdGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.UUID; + +@Configuration +public class CopsbootApplicationConfiguration { + + @Bean + public UniqueIdGenerator uniqueIdGenerator() { + return new InMemoryUniqueIdGenerator(); + } + +} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java deleted file mode 100644 index 74f702f..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/DevelopmentDbInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.copsboot; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component //<1> -@Profile(SpringProfiles.DEV) //<2> -public class DevelopmentDbInitializer implements ApplicationRunner { - - private final UserService userService; - - @Autowired - public DevelopmentDbInitializer(UserService userService) { //<3> - this.userService = userService; - } - - @Override - public void run(ApplicationArguments applicationArguments) { //<4> - createTestUsers(); - } - - private void createTestUsers() { - userService.createOfficer("officer@example.com", "officer"); //<5> - } -} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java index 344a5fe..fb1cc59 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/SpringProfiles.java @@ -6,6 +6,7 @@ public final class SpringProfiles { public static final String TEST = "test"; public static final String STAGING = "staging"; public static final String PROD = "prod"; + public static final String REPOSITORY_TEST = "repository-test"; public static final String INTEGRATION_TEST = "integration-test"; private SpringProfiles() { diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java deleted file mode 100644 index d541b38..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/json/EntityIdJsonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.infrastructure.json; - -import com.example.orm.jpa.EntityId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.springframework.boot.jackson.JsonComponent; - -import java.io.IOException; - -@JsonComponent //<1> -public class EntityIdJsonSerializer extends JsonSerializer { //<2> - - @Override - public void serialize(EntityId entityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(entityId.asString()); //<3> - } - -} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java index 9c92c49..8d26775 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/mvc/FieldErrorResponse.java @@ -1,11 +1,4 @@ package com.example.copsboot.infrastructure.mvc; -import lombok.Value; - -//tag::class[] -@Value -public class FieldErrorResponse { - private String fieldName; - private String errorMessage; +public record FieldErrorResponse(String fieldName, String errorMesesage) { } -//end::class[] \ No newline at end of file diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/mvc/RestControllerExceptionHandler.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/mvc/RestControllerExceptionHandler.java index a5acde8..b9fabac 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/mvc/RestControllerExceptionHandler.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/mvc/RestControllerExceptionHandler.java @@ -2,14 +2,12 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.ui.Model; -import org.springframework.validation.BindException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.multipart.MultipartException; +import org.springframework.web.multipart.MaxUploadSizeExceededException; import java.util.Collections; import java.util.List; @@ -17,42 +15,30 @@ import java.util.stream.Collectors; //tag::class[] -@ControllerAdvice +@ControllerAdvice //<1> public class RestControllerExceptionHandler { - @ExceptionHandler - @ResponseBody - @ResponseStatus(HttpStatus.BAD_REQUEST) - public Map> handle(MethodArgumentNotValidException exception) { + @ExceptionHandler //<2> + @ResponseBody //<3> + @ResponseStatus(HttpStatus.BAD_REQUEST) //<4> + public Map> handle(MethodArgumentNotValidException exception) { //<5> return error(exception.getBindingResult() - .getFieldErrors() - .stream() - .map(fieldError -> new FieldErrorResponse(fieldError.getField(), - fieldError.getDefaultMessage())) - .collect(Collectors.toList())); + .getFieldErrors() + .stream() + .map(fieldError -> new FieldErrorResponse(fieldError.getField(), //<6> + fieldError.getDefaultMessage())) + .collect(Collectors.toList())); } - @ExceptionHandler - @ResponseBody - @ResponseStatus(HttpStatus.BAD_REQUEST) - public Map> handle(BindException exception) { - return error(exception.getBindingResult() - .getFieldErrors() - .stream() - .map(fieldError -> new FieldErrorResponse(fieldError.getField(), - fieldError.getDefaultMessage())) - .collect(Collectors.toList())); + // tag::maxUploadSizeExceeded[] + @ExceptionHandler(MaxUploadSizeExceededException.class) + public ResponseEntity> maxUploadSizeExceeded(MaxUploadSizeExceededException e) { + return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE) + .body(Map.of("code", "MAX_UPLOAD_SIZE_EXCEEDED", + "description", e.getMessage())); } + // end::maxUploadSizeExceeded[] - //tag::multipart-exception[] - @ExceptionHandler(MultipartException.class) - public ResponseEntity handleMultipartException(MultipartException e, Model model) { - model.addAttribute("exception", e); - return ResponseEntity - .badRequest() - .body(e.getMessage()); - } - //end::multipart-exception[] private Map> error(List errors) { return Collections.singletonMap("errors", errors); diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java deleted file mode 100644 index 8d02905..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User { - - private static final String ROLE_PREFIX = "ROLE_"; - - private final UserId userId; - - public ApplicationUserDetails(User user) { - super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles())); - this.userId = user.getId(); - } - - public UserId getUserId() { - return userId; - } - - private static Collection createAuthorities(Set roles) { - return roles.stream() - .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name())) - .collect(Collectors.toSet()); - } -} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java deleted file mode 100644 index d28668e..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsService.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -@Service //<1> -public class ApplicationUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - @Autowired - public ApplicationUserDetailsService(UserRepository userRepository) { // <2> - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) { - User user = userRepository.findByEmailIgnoreCase(username) //<3> - .orElseThrow(() -> new UsernameNotFoundException( //<4> - String.format("User with email %s could not be found", - username))); - return new ApplicationUserDetails(user); //<5> - } -} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java deleted file mode 100644 index e8ad97c..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfiguration.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.token.TokenStore; - -@Configuration -public class OAuth2ServerConfiguration { - - private static final String RESOURCE_ID = "copsboot-service"; - - @Configuration - @EnableResourceServer - @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { - - @Override - public void configure(ResourceServerSecurityConfigurer resources) throws Exception { - resources.resourceId(RESOURCE_ID); - } - - //tag::configure[] - @Override - public void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() - .and() - .antMatcher("/api/**") - .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/users").permitAll() //<1> - .anyRequest().authenticated(); - } - //end::configure[] - } - - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private UserDetailsService userDetailsService; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Autowired - private TokenStore tokenStore; - - @Autowired - private SecurityConfiguration securityConfiguration; - - @Override - public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { - security.passwordEncoder(passwordEncoder); - } - - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.inMemory() - .withClient(securityConfiguration.getMobileAppClientId()) - .authorizedGrantTypes("password", "refresh_token") - .scopes("mobile_app") - .resourceIds(RESOURCE_ID) - .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret())); - } - - @Override - public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore) - .authenticationManager(authenticationManager) - .userDetailsService(userDetailsService); - } - } - - @Configuration - public static class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter { - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - } -} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java deleted file mode 100644 index c246162..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/SecurityConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component //<1> -@ConfigurationProperties(prefix = "copsboot-security") //<2> -public class SecurityConfiguration { - private String mobileAppClientId; - private String mobileAppClientSecret; - - public String getMobileAppClientId() { - return mobileAppClientId; - } - - public void setMobileAppClientId(String mobileAppClientId) { - this.mobileAppClientId = mobileAppClientId; - } - - public String getMobileAppClientSecret() { - return mobileAppClientSecret; - } - - public void setMobileAppClientSecret(String mobileAppClientSecret) { - this.mobileAppClientSecret = mobileAppClientSecret; - } -} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java new file mode 100644 index 0000000..9fca2b6 --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/infrastructure/security/WebSecurityConfiguration.java @@ -0,0 +1,19 @@ +package com.example.copsboot.infrastructure.security; + +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity //<.> +public class WebSecurityConfiguration { + + @Bean + ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() { //<.> + return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated(); + } +} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/CreateReportParameters.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/CreateReportParameters.java new file mode 100644 index 0000000..64aeea6 --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/CreateReportParameters.java @@ -0,0 +1,8 @@ +package com.example.copsboot.report; + +import com.example.copsboot.user.UserId; + +import java.time.Instant; + +public record CreateReportParameters(UserId userId, Instant dateTime, String description) { +} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/Report.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/Report.java index c0f9c66..b10756f 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/Report.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/Report.java @@ -1,36 +1,36 @@ package com.example.copsboot.report; -import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; import com.example.orm.jpa.AbstractEntity; import com.example.util.ArtifactForFramework; +import jakarta.persistence.Entity; + +import java.time.Instant; -import javax.persistence.Entity; -import javax.persistence.ManyToOne; -import java.time.ZonedDateTime; //tag::class[] @Entity public class Report extends AbstractEntity { - @ManyToOne - private User reporter; - private ZonedDateTime dateTime; + + private UserId reporterId; + private Instant dateTime; private String description; @ArtifactForFramework protected Report() { } - public Report(ReportId id, User reporter, ZonedDateTime dateTime, String description) { + public Report(ReportId id, UserId reporterId, Instant dateTime, String description) { super(id); - this.reporter = reporter; + this.reporterId = reporterId; this.dateTime = dateTime; this.description = description; } - public User getReporter() { - return reporter; + public UserId getReporterId() { + return reporterId; } - public ZonedDateTime getDateTime() { + public Instant getDateTime() { return dateTime; } diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/ReportService.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/ReportService.java index 4d02935..613248b 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/ReportService.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/ReportService.java @@ -1,10 +1,23 @@ package com.example.copsboot.report; -import com.example.copsboot.user.UserId; -import org.springframework.web.multipart.MultipartFile; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.ZonedDateTime; -public interface ReportService { - Report createReport(UserId reporterId, ZonedDateTime dateTime, String description, MultipartFile image); +@Service +@Transactional +public class ReportService { + private final ReportRepository repository; + + public ReportService(ReportRepository repository) { + this.repository = repository; + } + + public Report createReport(CreateReportParameters parameters) { + return repository.save(new Report(repository.nextId(), + parameters.userId(), + parameters.dateTime(), + parameters.description())); + } } diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/ReportServiceImpl.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/ReportServiceImpl.java deleted file mode 100644 index 403fd0e..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/ReportServiceImpl.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserNotFoundException; -import com.example.copsboot.user.UserService; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -import java.time.ZonedDateTime; - -@Service -@Transactional -public class ReportServiceImpl implements ReportService { - private final ReportRepository repository; - private final UserService userService; - - public ReportServiceImpl(ReportRepository repository, UserService userService) { - this.repository = repository; - this.userService = userService; - } - - @Override - public Report createReport(UserId reporterId, ZonedDateTime dateTime, String description, MultipartFile image) { - return repository.save(new Report(repository.nextId(), - userService.getUser(reporterId) - .orElseThrow(() -> new UserNotFoundException(reporterId)), - dateTime, - description)); - } -} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java deleted file mode 100644 index efeb69b..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportParameters.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.report.web; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.web.multipart.MultipartFile; - -import javax.validation.constraints.NotNull; -import java.time.ZonedDateTime; - -//tag::class[] -@Data -@AllArgsConstructor -@NoArgsConstructor -public class CreateReportParameters { - @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) - private ZonedDateTime dateTime; - - @ValidReportDescription - private String description; - - @NotNull - private MultipartFile image; //<1> -} -//end::class[] \ No newline at end of file diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java new file mode 100644 index 0000000..d4b215f --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportRequest.java @@ -0,0 +1,21 @@ +package com.example.copsboot.report.web; + +import com.example.copsboot.report.CreateReportParameters; +import com.example.copsboot.user.UserId; +import jakarta.validation.constraints.NotNull; +import org.springframework.web.multipart.MultipartFile; + +import java.time.Instant; + +@ValidCreateReportRequest +public record CreateReportRequest( + Instant dateTime, + @ValidReportDescription String description, + boolean trafficIncident, + int numberOfInvolvedCars, + @NotNull MultipartFile image //<.> +) { + public CreateReportParameters toParameters(UserId userId) { + return new CreateReportParameters(userId, dateTime, description); + } +} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java new file mode 100644 index 0000000..fbad4ea --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/CreateReportRequestValidator.java @@ -0,0 +1,21 @@ +package com.example.copsboot.report.web; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +//tag::class[] +public class CreateReportRequestValidator implements ConstraintValidator { //<1> + + @Override + public void initialize(ValidCreateReportRequest constraintAnnotation) { + } + + @Override + public boolean isValid(CreateReportRequest value, ConstraintValidatorContext context) { + boolean result = true; + if (value.trafficIncident() && value.numberOfInvolvedCars() <= 0) { //<2> + result = false; + } + return result; + } +} //end::class[] diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java index d1bff04..aa30ca4 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportDescriptionValidator.java @@ -1,10 +1,9 @@ package com.example.copsboot.report.web; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; -public class ReportDescriptionValidator - implements ConstraintValidator { //<1> +public class ReportDescriptionValidator implements ConstraintValidator { //<1> @Override public void initialize(ValidReportDescription constraintAnnotation) { //<2> diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportDto.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportDto.java index 0adc7f8..28e606e 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportDto.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportDto.java @@ -2,23 +2,21 @@ import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; -import lombok.Value; +import com.example.copsboot.user.UserService; -import java.time.ZonedDateTime; +import java.time.Instant; //tag::class[] -@Value -public class ReportDto { - private ReportId id; - private String reporter; - private ZonedDateTime dateTime; - private String description; +public record ReportDto(ReportId id, + String reporter, + Instant dateTime, + String description) { - public static ReportDto fromReport(Report report) { + public static ReportDto fromReport(Report report, UserService userService) { return new ReportDto(report.getId(), - report.getReporter().getEmail(), - report.getDateTime(), - report.getDescription()); + userService.getUserById(report.getReporterId()).getEmail(), + report.getDateTime(), + report.getDescription()); } } //end::class[] diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportRestController.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportRestController.java index 6de180b..12387e0 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportRestController.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ReportRestController.java @@ -1,36 +1,44 @@ package com.example.copsboot.report.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.report.CreateReportParameters; +import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportService; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserNotFoundException; +import com.example.copsboot.user.UserService; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; +import java.util.UUID; //tag::class[] @RestController @RequestMapping("/api/reports") public class ReportRestController { private final ReportService service; + private final UserService userService; - public ReportRestController(ReportService service) { + public ReportRestController(ReportService service, UserService userService) { this.service = service; + this.userService = userService; } - //tag::create-report-method-signature[] + // tag::create-report-method-signature[] @PostMapping @ResponseStatus(HttpStatus.CREATED) - public ReportDto createReport(@AuthenticationPrincipal ApplicationUserDetails userDetails, - @Valid CreateReportParameters parameters) { - //end::create-report-method-signature[] - return ReportDto.fromReport(service.createReport(userDetails.getUserId(), - parameters.getDateTime(), - parameters.getDescription(), - parameters.getImage())); + public ReportDto createReport(@AuthenticationPrincipal Jwt jwt, + @Valid CreateReportRequest request) { + // end::create-report-method-signature[] + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + User user = userService.findUserByAuthServerId(authServerId) + .orElseThrow(() -> new UserNotFoundException(authServerId)); + CreateReportParameters parameters = request.toParameters(user.getId()); + Report report = service.createReport(parameters); + return ReportDto.fromReport(report, userService); } } //end::class[] diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java new file mode 100644 index 0000000..895ce6c --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ValidCreateReportRequest.java @@ -0,0 +1,20 @@ +package com.example.copsboot.report.web; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +//tag::class[] +@Target(ElementType.TYPE) //<1> +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = {CreateReportRequestValidator.class}) //<2> +public @interface ValidCreateReportRequest { + String message() default "Invalid report"; + + Class[] groups() default {}; + + Class[] payload() default {}; +}//end::class[] diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java index 41d39e9..ba8fa56 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/report/web/ValidReportDescription.java @@ -1,7 +1,7 @@ package com.example.copsboot.report.web; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -16,4 +16,4 @@ Class[] groups() default {}; //<5> Class[] payload() default {}; //<6> -} \ No newline at end of file +} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/AuthServerId.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/AuthServerId.java new file mode 100644 index 0000000..1705863 --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/AuthServerId.java @@ -0,0 +1,11 @@ +package com.example.copsboot.user; + +import org.springframework.util.Assert; + +import java.util.UUID; + +public record AuthServerId(UUID value) { + public AuthServerId { + Assert.notNull(value, "The AuthServerId value should not be null"); + } +} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java new file mode 100644 index 0000000..f2c86b3 --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/AuthServerIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class AuthServerIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(AuthServerId attribute) { + return attribute.value(); + } + + @Override + public AuthServerId convertToEntityAttribute(UUID dbData) { + return new AuthServerId(dbData); + } +} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/CreateUserParameters.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/CreateUserParameters.java new file mode 100644 index 0000000..2f7b0b2 --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/CreateUserParameters.java @@ -0,0 +1,4 @@ +package com.example.copsboot.user; + +public record CreateUserParameters(AuthServerId authServerId, String email, String mobileToken) { +} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/User.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/User.java index 236cd6d..32d02a4 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/User.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/User.java @@ -1,53 +1,37 @@ package com.example.copsboot.user; import com.example.orm.jpa.AbstractEntity; -import com.google.common.collect.Sets; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Set; - +import jakarta.persistence.Entity; +import jakarta.persistence.Table; @Entity @Table(name = "copsboot_user") public class User extends AbstractEntity { private String email; - private String password; - - @ElementCollection(fetch = FetchType.EAGER) - @Enumerated(EnumType.STRING) - @NotNull - private Set roles; + private AuthServerId authServerId; //<.> + private String mobileToken; //<.> protected User() { } - public User(UserId id, String email, String password, Set roles) { + public User(UserId id, String email, AuthServerId authServerId, String mobileToken) { //<.> super(id); this.email = email; - this.password = password; - this.roles = roles; - } - - public static User createOfficer(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.OFFICER)); - } - - public static User createCaptain(UserId userId, String email, String encodedPassword) { - return new User(userId, email, encodedPassword, Sets.newHashSet(UserRole.CAPTAIN)); + this.authServerId = authServerId; + this.mobileToken = mobileToken; } public String getEmail() { return email; } - public String getPassword() { - return password; + public AuthServerId getAuthServerId() { //<.> + return authServerId; } - public Set getRoles() { - return roles; + public String getMobileToken() { + return mobileToken; } } diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java new file mode 100644 index 0000000..2a434e3 --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserIdAttributeConverter.java @@ -0,0 +1,19 @@ +package com.example.copsboot.user; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter(autoApply = true) +public class UserIdAttributeConverter implements AttributeConverter { + @Override + public UUID convertToDatabaseColumn(UserId attribute) { + return attribute.getId(); + } + + @Override + public UserId convertToEntityAttribute(UUID dbData) { + return new UserId(dbData); + } +} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserNotFoundException.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserNotFoundException.java index 1f65f04..97d0813 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserNotFoundException.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserNotFoundException.java @@ -3,9 +3,13 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -@ResponseStatus(HttpStatus.NOT_FOUND) //<1> +@ResponseStatus(HttpStatus.NOT_FOUND) public class UserNotFoundException extends RuntimeException { public UserNotFoundException(UserId userId) { - super(String.format("Could not find user with id %s", userId.asString())); + super(String.format("Unable to find user with id %s", userId)); + } + + public UserNotFoundException(AuthServerId authServerId) { + super(String.format("Unable to find user with auth server id %s", authServerId)); } } diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserRepository.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserRepository.java index 2359735..741d2e0 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserRepository.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserRepository.java @@ -3,9 +3,11 @@ import org.springframework.data.repository.CrudRepository; import java.util.Optional; -import java.util.UUID; + //tag::class[] public interface UserRepository extends CrudRepository, UserRepositoryCustom { - Optional findByEmailIgnoreCase(String email); + Optional findByAuthServerId(AuthServerId authServerId); + + Optional findByMobileToken(String mobileToken); } //end::class[] diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserService.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserService.java index d5630f0..ba1d4ab 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserService.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserService.java @@ -1,11 +1,37 @@ package com.example.copsboot.user; +import org.springframework.stereotype.Service; + import java.util.Optional; -public interface UserService { - User createOfficer(String email, String password); +@Service +public class UserService { + private final UserRepository repository; //<.> + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public Optional findUserByAuthServerId(AuthServerId authServerId) { //<.> + return repository.findByAuthServerId(authServerId); + } + + // tag::createUser[] + public User createUser(CreateUserParameters createUserParameters) { + UserId userId = repository.nextId(); + User user = new User(userId, createUserParameters.email(), + createUserParameters.authServerId(), + createUserParameters.mobileToken()); + return repository.save(user); + } - Optional getUser(UserId userId); + public User getUserById(UserId userId) { + return repository.findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + } - Optional findUserByEmail(String email); + public Optional findUserByMobileToken(String mobileToken) { + return repository.findByMobileToken(mobileToken); + } + // end::createUser[] } diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserServiceImpl.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserServiceImpl.java deleted file mode 100644 index 6918081..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/UserServiceImpl.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository repository; - private final PasswordEncoder passwordEncoder; - - @Autowired - public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { - this.repository = repository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public User createOfficer(String email, String password) { - User user = User.createOfficer(repository.nextId(), email, passwordEncoder.encode(password)); - return repository.save(user); - } - - @Override - public Optional getUser(UserId userId) { - return repository.findById(userId); - } - - @Override - public Optional findUserByEmail(String email) { - return repository.findByEmailIgnoreCase(email); - } -} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java deleted file mode 100644 index f96ee54..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateOfficerParameters.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.copsboot.user.web; - -import lombok.Data; -import org.hibernate.validator.constraints.Email; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -@Data -@ValidCreateUserParameters -public class CreateOfficerParameters { - @NotNull - @Email - private String email; - - @NotNull - @Size(min = 6, max = 1000) - private String password; -} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java deleted file mode 100644 index 3f86d70..0000000 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserParametersValidator.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.copsboot.user.web; - -import com.example.copsboot.user.UserService; -import org.springframework.beans.factory.annotation.Autowired; - -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; - -//tag::class[] -public class CreateUserParametersValidator implements ConstraintValidator { - - private final UserService userService; - - @Autowired - public CreateUserParametersValidator(UserService userService) { //<1> - this.userService = userService; - } - - @Override - public void initialize(ValidCreateUserParameters constraintAnnotation) { - - } - - @Override - public boolean isValid(CreateOfficerParameters userParameters, ConstraintValidatorContext context) { - - boolean result = true; - - if (userService.findUserByEmail(userParameters.getEmail()).isPresent()) { //<2> - context.buildConstraintViolationWithTemplate( - "There is already a user with the given email address.") - .addPropertyNode("email").addConstraintViolation(); //<3> - - result = false; //<4> - } - - return result; - } -} -//end::class[] diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java new file mode 100644 index 0000000..83c56a1 --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserRequest.java @@ -0,0 +1,18 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; +import jakarta.validation.constraints.NotEmpty; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +@ValidCreateUserRequest +public record CreateUserRequest(@NotEmpty String mobileToken) { //<.> + + public CreateUserParameters toParameters(Jwt jwt) { + AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject())); + String email = jwt.getClaimAsString("email"); + return new CreateUserParameters(authServerId, email, mobileToken); + } +} diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java new file mode 100644 index 0000000..bdd4aa5 --- /dev/null +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/CreateUserRequestValidator.java @@ -0,0 +1,40 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.user.UserService; +import org.springframework.beans.factory.annotation.Autowired; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +//tag::class[] +public class CreateUserRequestValidator implements ConstraintValidator { + + private final UserService userService; + + @Autowired + public CreateUserRequestValidator(UserService userService) { //<1> + this.userService = userService; + } + + @Override + public void initialize(ValidCreateUserRequest constraintAnnotation) { + + } + + @Override + public boolean isValid(CreateUserRequest userRequest, ConstraintValidatorContext context) { + + boolean result = true; + + if (userService.findUserByMobileToken(userRequest.mobileToken()).isPresent()) { //<2> + context.buildConstraintViolationWithTemplate( + "There is already a user with the given mobile token.") + .addPropertyNode("mobileToken").addConstraintViolation(); //<3> + + result = false; //<4> + } + + return result; + } +} +//end::class[] diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/UserDto.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/UserDto.java index 3769d1a..2fac96c 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/UserDto.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/UserDto.java @@ -1,21 +1,14 @@ package com.example.copsboot.user.web; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserRole; -import lombok.Value; -import java.util.Set; - -@Value -public class UserDto { - private final UserId id; - private final String email; - private final Set roles; +import java.util.UUID; +public record UserDto(UUID userId, String email, UUID authServerId, String mobileToken) { public static UserDto fromUser(User user) { - return new UserDto(user.getId(), - user.getEmail(), - user.getRoles()); + return new UserDto(user.getId().getId(), + user.getEmail(), + user.getAuthServerId().value(), + user.getMobileToken()); } } diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/UserRestController.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/UserRestController.java index b5aa1a8..e0a6545 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/UserRestController.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/UserRestController.java @@ -1,49 +1,53 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.security.ApplicationUserDetails; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.CreateUserParameters; import com.example.copsboot.user.User; -import com.example.copsboot.user.UserNotFoundException; import com.example.copsboot.user.UserService; -import lombok.Value; -import org.springframework.beans.factory.annotation.Autowired; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.Valid; -import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import java.util.stream.Collectors; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/users") public class UserRestController { + private final UserService userService; - private final UserService service; - - @Autowired - public UserRestController(UserService service) { - this.service = service; + public UserRestController(UserService userService) { + this.userService = userService; } - @GetMapping("/me") - public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) { - User user = service.getUser(userDetails.getUserId()) - .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId())); - return UserDto.fromUser(user); + // tag::myself[] + @GetMapping("/me") //<.> + public Map myself(@AuthenticationPrincipal Jwt jwt) { //<.> + Optional userByAuthServerId = userService.findUserByAuthServerId(new AuthServerId(UUID.fromString(jwt.getSubject()))); + + Map result = new HashMap<>(); + userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().asString())); + result.put("subject", jwt.getSubject()); + result.put("claims", jwt.getClaims()); + + return result; } + // end::myself[] - //tag::post[] + // tag::createUser[] @PostMapping @ResponseStatus(HttpStatus.CREATED) - public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters) { - User officer = service.createOfficer(parameters.getEmail(), - parameters.getPassword()); - return UserDto.fromUser(officer); + @PreAuthorize("hasRole('OFFICER')") + public UserDto createUser(@AuthenticationPrincipal Jwt jwt, + @Valid @RequestBody CreateUserRequest request) { + CreateUserParameters parameters = request.toParameters(jwt); + User user = userService.createUser(parameters); + return UserDto.fromUser(user); } - //end::post[] + // end::createUser[] } diff --git a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/ValidCreateUserParameters.java b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/ValidCreateUserRequest.java similarity index 70% rename from chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/ValidCreateUserParameters.java rename to chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/ValidCreateUserRequest.java index a7ec388..e6a975e 100644 --- a/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/ValidCreateUserParameters.java +++ b/chapter09/02 - validation/src/main/java/com/example/copsboot/user/web/ValidCreateUserRequest.java @@ -1,7 +1,7 @@ package com.example.copsboot.user.web; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -10,8 +10,8 @@ //tag::class[] @Target(ElementType.TYPE) // <1> @Retention(RetentionPolicy.RUNTIME) -@Constraint(validatedBy = {CreateUserParametersValidator.class}) //<2> -public @interface ValidCreateUserParameters { +@Constraint(validatedBy = {CreateUserRequestValidator.class}) //<2> +public @interface ValidCreateUserRequest { String message() default "Invalid user"; Class[] groups() default {}; diff --git a/chapter09/02 - validation/src/main/java/com/example/orm/jpa/AbstractEntity.java b/chapter09/02 - validation/src/main/java/com/example/orm/jpa/AbstractEntity.java index dfa9f1e..275804e 100644 --- a/chapter09/02 - validation/src/main/java/com/example/orm/jpa/AbstractEntity.java +++ b/chapter09/02 - validation/src/main/java/com/example/orm/jpa/AbstractEntity.java @@ -2,8 +2,8 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.EmbeddedId; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; import java.util.Objects; import static com.google.common.base.MoreObjects.toStringHelper; diff --git a/chapter09/02 - validation/src/main/java/com/example/orm/jpa/AbstractEntityId.java b/chapter09/02 - validation/src/main/java/com/example/orm/jpa/AbstractEntityId.java index b9ddc5b..f50c4e4 100755 --- a/chapter09/02 - validation/src/main/java/com/example/orm/jpa/AbstractEntityId.java +++ b/chapter09/02 - validation/src/main/java/com/example/orm/jpa/AbstractEntityId.java @@ -2,7 +2,7 @@ import com.example.util.ArtifactForFramework; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.util.Objects; diff --git a/chapter09/02 - validation/src/main/java/com/example/orm/jpa/Entity.java b/chapter09/02 - validation/src/main/java/com/example/orm/jpa/Entity.java index 96cadf0..3a45231 100644 --- a/chapter09/02 - validation/src/main/java/com/example/orm/jpa/Entity.java +++ b/chapter09/02 - validation/src/main/java/com/example/orm/jpa/Entity.java @@ -1,5 +1,6 @@ package com.example.orm.jpa; + /** * Interface for entity objects. * diff --git a/chapter09/02 - validation/src/main/java/com/example/util/ArtifactForFramework.java b/chapter09/02 - validation/src/main/java/com/example/util/ArtifactForFramework.java index 9e2ef24..5d4ec38 100644 --- a/chapter09/02 - validation/src/main/java/com/example/util/ArtifactForFramework.java +++ b/chapter09/02 - validation/src/main/java/com/example/util/ArtifactForFramework.java @@ -1,8 +1,4 @@ package com.example.util; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -@Retention(value = RetentionPolicy.SOURCE) public @interface ArtifactForFramework { } diff --git a/chapter09/02 - validation/src/main/resources/application-dev.properties b/chapter09/02 - validation/src/main/resources/application-dev.properties deleted file mode 100644 index f72b4c7..0000000 --- a/chapter09/02 - validation/src/main/resources/application-dev.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter09/02 - validation/src/main/resources/application-local.properties b/chapter09/02 - validation/src/main/resources/application-local.properties index 8f13f3f..7e354d5 100644 --- a/chapter09/02 - validation/src/main/resources/application-local.properties +++ b/chapter09/02 - validation/src/main/resources/application-local.properties @@ -3,13 +3,9 @@ spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.username=postgres spring.datasource.password=my-postgres-db-pwd spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -copsboot-security.mobile-app-client-id=copsboot-mobile-client -copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN - -spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata -spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create -spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=create.sql - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +#spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create +#spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=create.sql +#spring.jpa.properties.hibernate.hbm2ddl.delimiter=; diff --git a/chapter09/02 - validation/src/main/resources/application.properties b/chapter09/02 - validation/src/main/resources/application.properties index 32bb668..e791e0e 100644 --- a/chapter09/02 - validation/src/main/resources/application.properties +++ b/chapter09/02 - validation/src/main/resources/application.properties @@ -1,2 +1,6 @@ +com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot +com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles +com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_ + spring.servlet.multipart.max-file-size=1MB -spring.servlet.multipart.max-request-size=10MB \ No newline at end of file +spring.servlet.multipart.max-request-size=10MB diff --git a/chapter09/02 - validation/src/main/resources/db/migration/V1.0.0.1__users.sql b/chapter09/02 - validation/src/main/resources/db/migration/V1.0.0.1__users.sql new file mode 100644 index 0000000..d1939fa --- /dev/null +++ b/chapter09/02 - validation/src/main/resources/db/migration/V1.0.0.1__users.sql @@ -0,0 +1,7 @@ +CREATE TABLE copsboot_user +( + id uuid NOT NULL PRIMARY KEY, + auth_server_id uuid, + email VARCHAR(255), + mobile_token VARCHAR(255) +); diff --git a/chapter09/02 - validation/src/main/resources/db/migration/V1.0.0.2__reports.sql b/chapter09/02 - validation/src/main/resources/db/migration/V1.0.0.2__reports.sql new file mode 100644 index 0000000..cc2e26c --- /dev/null +++ b/chapter09/02 - validation/src/main/resources/db/migration/V1.0.0.2__reports.sql @@ -0,0 +1,8 @@ +CREATE TABLE report +( + date_time TIMESTAMP(6) WITH TIME ZONE, + id uuid NOT NULL, + description VARCHAR(255), + reporter_id uuid, + PRIMARY KEY (id) +); diff --git a/chapter09/02 - validation/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql b/chapter09/02 - validation/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql deleted file mode 100644 index 485336f..0000000 --- a/chapter09/02 - validation/src/main/resources/db/migration/h2/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,42 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(255) PRIMARY KEY, - resource_ids VARCHAR(255), - client_secret VARCHAR(255), - scope VARCHAR(255), - authorized_grant_types VARCHAR(255), - web_server_redirect_uri VARCHAR(255), - authorities VARCHAR(255), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(255) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(255), - token BLOB, - authentication_id VARCHAR(255), - user_name VARCHAR(255), - client_id VARCHAR(255), - authentication BLOB, - refresh_token VARCHAR(255) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(255), - token BLOB, - authentication BLOB -); - -CREATE TABLE oauth_code ( - activationCode VARCHAR(255), - authentication BLOB -); \ No newline at end of file diff --git a/chapter09/02 - validation/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql b/chapter09/02 - validation/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql deleted file mode 100644 index 7c3fdf3..0000000 --- a/chapter09/02 - validation/src/main/resources/db/migration/postgresql/V1.0.0.1__authentication.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE oauth_client_details ( - client_id VARCHAR(256) PRIMARY KEY, - resource_ids VARCHAR(256), - client_secret VARCHAR(256), - scope VARCHAR(256), - authorized_grant_types VARCHAR(256), - web_server_redirect_uri VARCHAR(256), - authorities VARCHAR(256), - access_token_validity INTEGER, - refresh_token_validity INTEGER, - additional_information VARCHAR(4096), - autoapprove VARCHAR(256) -); - -CREATE TABLE oauth_client_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256) -); - -CREATE TABLE oauth_access_token ( - token_id VARCHAR(256), - token BYTEA, - authentication_id VARCHAR(256), - user_name VARCHAR(256), - client_id VARCHAR(256), - authentication BYTEA, - refresh_token VARCHAR(256) -); - -CREATE TABLE oauth_refresh_token ( - token_id VARCHAR(256), - token BYTEA, - authentication BYTEA -); - -CREATE TABLE oauth_code ( - code VARCHAR(256), - authentication BYTEA -); - -CREATE TABLE oauth_approvals ( - userId VARCHAR(256), - clientId VARCHAR(256), - scope VARCHAR(256), - status VARCHAR(10), - expiresAt TIMESTAMP, - lastModifiedAt TIMESTAMP -); - diff --git a/chapter09/02 - validation/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql b/chapter09/02 - validation/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql deleted file mode 100644 index 122b1fc..0000000 --- a/chapter09/02 - validation/src/main/resources/db/migration/postgresql/V1.0.0.2__users.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE copsboot_user ( - id UUID NOT NULL, - email VARCHAR(255), - password VARCHAR(255), - PRIMARY KEY (id) -); - -CREATE TABLE user_roles ( - user_id UUID NOT NULL, - roles VARCHAR(255) -); - -ALTER TABLE user_roles - ADD CONSTRAINT FK7je59ku3x462eqxu4ss3das1s -FOREIGN KEY (user_id) -REFERENCES copsboot_user; diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java index add5a9b..5feb390 100644 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/CopsbootApplicationTests.java @@ -1,19 +1,16 @@ package com.example.copsboot; import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) @SpringBootTest -@ActiveProfiles(SpringProfiles.TEST) -public class CopsbootApplicationTests { +@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) +class CopsbootApplicationTests { - @Test - public void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java deleted file mode 100644 index 71946be..0000000 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/ApplicationUserDetailsServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.copsboot.infrastructure.security; - - -import com.example.copsboot.user.UserRepository; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ApplicationUserDetailsServiceTest { - - @Test - public void givenExistingUsername_whenLoadingUser_userIsReturned() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); // <1> - when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL)) // <2> - .thenReturn(Optional.of(Users.officer())); - - UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL); //<3> - assertThat(userDetails).isNotNull(); - assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL); //<4> - assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) - .contains("ROLE_OFFICER"); //<5> - assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class, //<6> - applicationUserDetails -> { - assertThat(applicationUserDetails.getUserId()) - .isEqualTo(Users.officer().getId()); - }); - } - - @Test//(expected = UsernameNotFoundException.class) //<7> - public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() { - UserRepository repository = mock(UserRepository.class); - ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository); - when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> service.loadUserByUsername("i@donotexist.com")) - .isInstanceOf(UsernameNotFoundException.class); - - //service.loadUserByUsername("i@donotexist.com"); - - } -} \ No newline at end of file diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java deleted file mode 100644 index 9357ee6..0000000 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/OAuth2ServerConfigurationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@AutoConfigureMockMvc //<2> -@ActiveProfiles(SpringProfiles.TEST) -public class OAuth2ServerConfigurationTest { - - @Autowired - private MockMvc mvc; //<3> - - @Autowired - private UserService userService; //<4> - - @Test - public void testGetAccessTokenAsOfficer() throws Exception { - - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> - - String clientId = "test-client-id"; - String clientSecret = "test-client-secret"; - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("username", Users.OFFICER_EMAIL); - params.add("password", Users.OFFICER_PASSWORD); - - mvc.perform(post("/oauth/token") //<6> - .params(params) //<7> - .with(httpBasic(clientId, clientSecret)) //<8> - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andDo(print()) //<9> - .andExpect(jsonPath("access_token").isString()) //<10> - .andExpect(jsonPath("token_type").value("bearer")) - .andExpect(jsonPath("refresh_token").isString()) - .andExpect(jsonPath("expires_in").isNumber()) - .andExpect(jsonPath("scope").value("mobile_app")) - ; - } - -} \ No newline at end of file diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java deleted file mode 100644 index af48af9..0000000 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForMockMvc.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import org.springframework.boot.json.JacksonJsonParser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class SecurityHelperForMockMvc { - - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static final String HEADER_AUTHORIZATION = "Authorization"; - - /** - * Allows to get an access token for the given user in the context of a spring (unit) test - * using MockMVC. - * - * @param mvc the MockMvc instance - * @param username the username - * @param password the password - * @return the access_token to be used in the Authorization header - * @throws Exception if no token could be obtained. - */ - public static String obtainAccessToken(MockMvc mvc, String username, String password) throws Exception { - - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "password"); - params.add("client_id", UNIT_TEST_CLIENT_ID); - params.add("client_secret", UNIT_TEST_CLIENT_SECRET); - params.add("username", username); - params.add("password", password); - - ResultActions result - = mvc.perform(post("/oauth/token") - .params(params) - .with(httpBasic(UNIT_TEST_CLIENT_ID, UNIT_TEST_CLIENT_SECRET)) - .accept("application/json;charset=UTF-8")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")); - - String resultString = result.andReturn().getResponse().getContentAsString(); - - JacksonJsonParser jsonParser = new JacksonJsonParser(); - return jsonParser.parseMap(resultString).get("access_token").toString(); - } - - public static String bearer(String accessToken) { - return "Bearer " + accessToken; - } -} \ No newline at end of file diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForRestAssured.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForRestAssured.java deleted file mode 100644 index 62c1957..0000000 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/SecurityHelperForRestAssured.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import io.restassured.specification.RequestSpecification; -import org.springframework.security.oauth2.client.OAuth2RestTemplate; -import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails; -import org.springframework.security.oauth2.common.OAuth2AccessToken; - -import static io.restassured.RestAssured.given; -import static java.lang.String.format; - -public class SecurityHelperForRestAssured { - private static final String UNIT_TEST_CLIENT_ID = "test-client-id"; //<1> - private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret"; //<2> - - public static RequestSpecification givenAuthenticatedUser(int serverPort, String username, String password) { - OAuth2RestTemplate template = new OAuth2RestTemplate(createResourceOwnerPasswordResourceDetails(serverPort, - username, - password)); - OAuth2AccessToken accessToken = template.getAccessToken(); - - return given().auth().preemptive().oauth2(accessToken.getValue()); - } - - private static ResourceOwnerPasswordResourceDetails createResourceOwnerPasswordResourceDetails(int serverPort, String username, String password) { - ResourceOwnerPasswordResourceDetails details = new ResourceOwnerPasswordResourceDetails(); - details.setAccessTokenUri(String.format("http://localhost:%s/oauth/token", serverPort)); - details.setUsername(username); - details.setPassword(password); - details.setClientId(UNIT_TEST_CLIENT_ID); - details.setClientSecret(UNIT_TEST_CLIENT_SECRET); - return details; - } -} diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java deleted file mode 100644 index 5cc112c..0000000 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/security/StubUserDetailsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.copsboot.infrastructure.security; - -import com.example.copsboot.user.Users; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class StubUserDetailsService implements UserDetailsService { - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - switch (username) { - case Users.OFFICER_EMAIL: - return new ApplicationUserDetails(Users.officer()); - case Users.CAPTAIN_EMAIL: - return new ApplicationUserDetails(Users.captain()); - default: - throw new UsernameNotFoundException(username); - } - } -} diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java new file mode 100644 index 0000000..3ddeac0 --- /dev/null +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTest.java @@ -0,0 +1,30 @@ +package com.example.copsboot.infrastructure.test; + +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//tag::class[] +@Retention(RetentionPolicy.RUNTIME) +@CopsbootControllerTest +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class) +public @interface CopsbootControllerDocumentationTest { + + @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> + Class[] value() default {}; + + @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") //<6> + Class[] controllers() default {}; +} +//end::class[] diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java new file mode 100644 index 0000000..02e070e --- /dev/null +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerDocumentationTestConfiguration.java @@ -0,0 +1,21 @@ +package com.example.copsboot.infrastructure.test; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +class CopsbootControllerDocumentationTestConfiguration { + @Bean + public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() { + return configurer -> configurer.operationPreprocessors() + .withRequestDefaults(prettyPrint()) + .withResponseDefaults(prettyPrint(), + modifyHeaders().removeMatching("X.*") + .removeMatching("Pragma") + .removeMatching("Expires")); + } + } diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java index c33238a..6696635 100644 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTest.java @@ -1,10 +1,10 @@ package com.example.copsboot.infrastructure.test; -import com.example.copsboot.infrastructure.SpringProfiles; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.example.copsboot.infrastructure.security.WebSecurityConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,23 +12,12 @@ /** * Custom annotation for all {@link org.springframework.stereotype.Controller Controller} tests on the project. By using * this single annotation, everything is configured properly to test a controller: - *
    - *
  • Import of {@link CopsbootControllerTestConfiguration}
  • - *
  • test profile active
  • - *
- *

- * Example usage: - *

- * @RunWith(SpringRunner.class)
- * @CopsbootControllerTest(UserController.class)
- * public class UserControllerTest {
- * 
*/ //tag::class[] -@Retention(RetentionPolicy.RUNTIME) //<1> -@WebMvcTest //<2> -@ContextConfiguration(classes = CopsbootControllerTestConfiguration.class) //<3> -@ActiveProfiles(SpringProfiles.TEST) //<4> +@Retention(RetentionPolicy.RUNTIME) //<.> +@WebMvcTest //<.> +@AutoConfigureAddonsWebmvcResourceServerSecurity //<.> +@Import(WebSecurityConfiguration.class) //<.> public @interface CopsbootControllerTest { @AliasFor(annotation = WebMvcTest.class, attribute = "value") //<5> diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java deleted file mode 100644 index 7231430..0000000 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/infrastructure/test/CopsbootControllerTestConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.copsboot.infrastructure.test; - -import com.example.copsboot.infrastructure.security.OAuth2ServerConfiguration; -import com.example.copsboot.infrastructure.security.SecurityConfiguration; -import com.example.copsboot.infrastructure.security.StubUserDetailsService; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; - -@TestConfiguration -@Import(OAuth2ServerConfiguration.class) -public class CopsbootControllerTestConfiguration { - @Bean - public UserDetailsService userDetailsService() { - return new StubUserDetailsService(); - } - - @Bean - public SecurityConfiguration securityConfiguration() { - return new SecurityConfiguration(); - } - -} diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/report/Reports.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/Reports.java deleted file mode 100644 index 2302dc9..0000000 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/report/Reports.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.copsboot.report; - -import com.example.copsboot.user.Users; - -import java.time.ZonedDateTime; -import java.util.UUID; - -public class Reports { - public static Report createRandomReport(String description) { - return new Report(new ReportId(UUID.randomUUID()), - Users.newRandomOfficer(), - ZonedDateTime.now(), - description); - } -} diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java new file mode 100644 index 0000000..04744f4 --- /dev/null +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/CreateReportRequestValidatorTest.java @@ -0,0 +1,75 @@ +package com.example.copsboot.report.web; + + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.jetbrains.annotations.NotNull; +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; + +import java.time.Instant; +import java.util.Set; + +import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; + +public class CreateReportRequestValidatorTest { + //tag::invalid[] + @Test + public void givenTrafficIndicentButInvolvedCarsZero_invalid() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat", + true, + 0, + createImage()); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasViolationOnPath(""); + } + } + //end::invalid[] + + //tag::valid[] + @Test + public void givenTrafficIndicent_involvedCarsMustBePositive() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", + true, + 2, + createImage()); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } + } + //end::valid[] + + //tag::valid-no-cars[] + @Test + public void givenNoTrafficIndicent_involvedCarsDoesNotMatter() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", + false, + 0, + createImage()); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } + } + //end::valid-no-cars[] + + @NotNull + private static MockMultipartFile createImage() { + return new MockMultipartFile("image", "picture.png", MediaType.IMAGE_PNG_VALUE, new byte[]{1, 2, 3}); + } + +} diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/KeycloakAdminClientFacade.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/KeycloakAdminClientFacade.java new file mode 100644 index 0000000..c919c86 --- /dev/null +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/KeycloakAdminClientFacade.java @@ -0,0 +1,66 @@ +package com.example.copsboot.report.web; + +import jakarta.ws.rs.core.Response; +import org.keycloak.admin.client.CreatedResponseUtil; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.*; +import org.keycloak.representations.idm.*; + +import java.util.Collections; +import java.util.List; + +public class KeycloakAdminClientFacade { + private final Keycloak keycloak; + + public KeycloakAdminClientFacade(Keycloak keycloak) { + this.keycloak = keycloak; + } + + public void createRealm(String realmName) { + RealmRepresentation realmRepresentation = new RealmRepresentation(); + realmRepresentation.setRealm(realmName); + realmRepresentation.setEnabled(true); + RealmsResource realmsResource = keycloak.realms(); + realmsResource.create(realmRepresentation); + } + + public void createRealmRole(String realmName, String roleName) { + RealmResource copsbootRealm = keycloak.realm(realmName); + RoleRepresentation roleRepresentation = new RoleRepresentation(); + roleRepresentation.setName(roleName); + copsbootRealm.roles().create(roleRepresentation); + } + + public void createUser(String realmName, String username, String password, String roleName) { + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setUsername(username); + userRepresentation.setEnabled(true); + CredentialRepresentation credentialRepresentation = new CredentialRepresentation(); + credentialRepresentation.setTemporary(false); + credentialRepresentation.setType(CredentialRepresentation.PASSWORD); + credentialRepresentation.setValue(password); + userRepresentation.setCredentials(List.of(credentialRepresentation)); + RealmResource realmResource = keycloak.realm(realmName); + UsersResource usersResource = realmResource.users(); + Response response = usersResource.create(userRepresentation); + String userId = CreatedResponseUtil.getCreatedId(response); + + UserResource userResource = usersResource.get(userId); + + userResource.resetPassword(credentialRepresentation); + RoleRepresentation roleRepresentation = realmResource.roles().get(roleName).toRepresentation(); + userResource.roles().realmLevel().add(Collections.singletonList(roleRepresentation)); + } + + public String createClient(String realmName, String clientId1) { + RealmResource realmResource = keycloak.realm(realmName); + ClientRepresentation clientRepresentation = new ClientRepresentation(); + clientRepresentation.setClientId(clientId1); + clientRepresentation.setDirectAccessGrantsEnabled(true); + Response response = realmResource.clients().create(clientRepresentation); + String clientId = CreatedResponseUtil.getCreatedId(response); + ClientResource clientResource = realmResource.clients().get(clientId); + CredentialRepresentation secret = clientResource.getSecret(); + return secret.getValue(); + } +} diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java index 0715dc3..b3b6724 100644 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportDescriptionValidatorTest.java @@ -1,13 +1,15 @@ package com.example.copsboot.report.web; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.jetbrains.annotations.NotNull; import org.junit.Test; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; -import javax.validation.ConstraintViolation; -import javax.validation.Validation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import java.time.ZonedDateTime; +import java.time.Instant; import java.util.Set; import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; @@ -17,33 +19,34 @@ public class ReportDescriptionValidatorTest { //tag::invalid[] @Test public void givenEmptyString_notValid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); //<1> - Validator validator = factory.getValidator(); //<2> - - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), "", createMockImage()); - Set> violationSet = validator.validate(parameters); //<3> - assertThat(violationSet).hasViolationOnPath("description"); //<4> + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { //<1> + Validator validator = factory.getValidator(); //<2> + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), "", false, 0, + createImage()); + Set> violationSet = validator.validate(parameters); //<3> + assertThat(violationSet).hasViolationOnPath("description"); //<4> + } } //end::invalid[] //tag::valid[] @Test public void givenSuspectWordPresent_valid() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - Validator validator = factory.getValidator(); - - CreateReportParameters parameters = new CreateReportParameters(ZonedDateTime.now(), - "The suspect was wearing a black hat.", - createMockImage()); - Set> violationSet = validator.validate(parameters); - assertThat(violationSet).hasNoViolations(); + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator validator = factory.getValidator(); + + CreateReportRequest parameters = new CreateReportRequest(Instant.now(), + "The suspect was wearing a black hat.", false, 0, + createImage()); + Set> violationSet = validator.validate(parameters); + assertThat(violationSet).hasNoViolations(); + } } //end::valid[] - private MockMultipartFile createMockImage() { - return new MockMultipartFile("image", - "picture.png", - "image/png", - new byte[]{1, 2, 3}); + @NotNull + private static MockMultipartFile createImage() { + return new MockMultipartFile("image", "picture.png", MediaType.IMAGE_PNG_VALUE, new byte[]{1, 2, 3}); } -} \ No newline at end of file +} diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportRestControllerIntegrationTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportRestControllerIntegrationTest.java index f89678c..5f34aef 100644 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportRestControllerIntegrationTest.java +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportRestControllerIntegrationTest.java @@ -1,58 +1,152 @@ package com.example.copsboot.report.web; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AddonsWebmvcTestConf; +import com.c4_soft.springaddons.security.oidc.starter.properties.OpenidProviderProperties; +import com.c4_soft.springaddons.security.oidc.starter.properties.SimpleAuthoritiesMappingProperties; import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.infrastructure.security.SecurityHelperForRestAssured; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; +import com.example.copsboot.user.UserRepository; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import dasniko.testcontainers.keycloak.KeycloakContainer; import io.restassured.RestAssured; import io.restassured.builder.MultiPartSpecBuilder; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import io.restassured.http.ContentType; +import net.bytebuddy.asm.Advice; +import org.junit.jupiter.api.*; +import org.keycloak.admin.client.Keycloak; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.util.Collections; + +import static io.restassured.RestAssured.given; //tag::class[] -@RunWith(SpringRunner.class) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) //<1> -@ActiveProfiles(SpringProfiles.TEST) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) //<.> +@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) public class ReportRestControllerIntegrationTest { + private static final String REALM_NAME = "copsboot"; + private static final String ROLE_NAME = "OFFICER"; + private static final String INTEGRATION_TEST_CLIENT_ID = "integration-test-client"; + private static final String TEST_USER_NAME = "wim@example.com"; + private static final String TEST_USER_PASSWORD = "test1234"; + @LocalServerPort - private int serverport; //<2> + private int serverport; //<.> - @Autowired - private UserService userService; + static KeycloakContainer keycloak = new KeycloakContainer("quay.io/keycloak/keycloak:22.0.1"); //<.> + private static String clientSecret; - @Before + @BeforeAll + static void beforeAll() { + keycloak.start(); //<.> + Keycloak client = keycloak.getKeycloakAdminClient(); //<.> + + KeycloakAdminClientFacade clientFacade = new KeycloakAdminClientFacade(client); //<.> + clientFacade.createRealm(REALM_NAME); + clientFacade.createRealmRole(REALM_NAME, ROLE_NAME); + clientFacade.createUser(REALM_NAME, TEST_USER_NAME, TEST_USER_PASSWORD, ROLE_NAME); + clientSecret = clientFacade.createClient(REALM_NAME, INTEGRATION_TEST_CLIENT_ID); + } + + @AfterAll + static void afterAll() { + keycloak.stop(); //<.> + } + + @AfterEach + void afterEach(@Autowired UserRepository userRepository) { + userRepository.deleteAll(); + } + + // tag::configureProperties[] + @DynamicPropertySource + static void configureProperties(DynamicPropertyRegistry registry) { + registry.add("com.c4-soft.springaddons.oidc.ops[0].iss", () -> keycloak.getAuthServerUrl() + "/realms/" + REALM_NAME); //<.> + registry.add("com.c4-soft.springaddons.oidc.ops[0].authorities[0].path", () -> "$.realm_access.roles"); //<.> + registry.add("com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix", () -> "ROLE_"); + } + // end::configureProperties[] + + @BeforeEach public void setup() { - RestAssured.port = serverport; //<3> - RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); //<4> + RestAssured.port = serverport; //<.> + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); //<.> } + // tag::officerIsUnableToPostAReportIfFileSizeIsTooBig[] @Test public void officerIsUnableToPostAReportIfFileSizeIsTooBig() { + String token = getToken(); //<.> + + given() + .header("Authorization", "Bearer " + token) //<.> + .contentType(ContentType.JSON) + .body(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """) + .post("/api/users") //<.> + .then() + .statusCode(HttpStatus.CREATED.value()); //<.> - userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); //<5> + given() + .header("Authorization", "Bearer " + token) + .multiPart(new MultiPartSpecBuilder(new byte[2_000_000]) //<.> + .fileName("picture.png") + .controlName("image") + .mimeType("image/png") + .build()) + .formParam("dateTime", "2018-04-11T22:59:03.189+02:00") + .formParam("description", "The suspect is wearing a black hat.") + .formParam("trafficIncident", "false") + .formParam("numberOfInvolvedCars", "0") + .when() + .post("/api/reports") + .then() + .statusCode(HttpStatus.PAYLOAD_TOO_LARGE.value()); //<.> + } + // end::officerIsUnableToPostAReportIfFileSizeIsTooBig[] + + // tag::getToken[] + private String getToken() { + RestTemplate restTemplate = new RestTemplate(); //<.> + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - String dateTime = "2018-04-11T22:59:03.189+02:00"; - String description = "The suspect is wearing a black hat."; + MultiValueMap map = new LinkedMultiValueMap<>(); + map.put("grant_type", Collections.singletonList("password")); //<.> + map.put("client_id", Collections.singletonList(INTEGRATION_TEST_CLIENT_ID)); + map.put("client_secret", Collections.singletonList(clientSecret)); + map.put("username", Collections.singletonList(TEST_USER_NAME)); + map.put("password", Collections.singletonList(TEST_USER_PASSWORD)); + KeycloakToken token = + restTemplate.postForObject( + keycloak.getAuthServerUrl() + "/realms/" + REALM_NAME + "/protocol/openid-connect/token", //<.> + new HttpEntity<>(map, httpHeaders), + KeycloakToken.class); + + assert token != null; + return token.accessToken(); //<.> + } - SecurityHelperForRestAssured.givenAuthenticatedUser(serverport, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD) //<6> - .when() - .multiPart("image", new MultiPartSpecBuilder(new byte[2_000_000]) //<7> - .fileName("picture.png") - .mimeType("image/png") - .build()) - .formParam("dateTime", dateTime) - .formParam("description", description) - .post("/api/reports") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()); //<8> + private record KeycloakToken(@JsonProperty("access_token") String accessToken) { //<.> } + // end::getToken[] } // end::class[] diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java index 11aa8df..88f289a 100644 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/report/web/ReportRestControllerTest.java @@ -1,69 +1,76 @@ package com.example.copsboot.report.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; +import com.example.copsboot.report.CreateReportParameters; import com.example.copsboot.report.Report; import com.example.copsboot.report.ReportId; import com.example.copsboot.report.ReportService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; +import com.example.copsboot.user.UserService; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.time.ZonedDateTime; +import java.time.Instant; +import java.util.Optional; import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; //tag::class[] -@RunWith(SpringRunner.class) @CopsbootControllerTest(ReportRestController.class) public class ReportRestControllerTest { @Autowired - private MockMvc mvc; - + private MockMvc mockMvc; @MockBean private ReportService service; + @MockBean + private UserService userService; @Test public void officerIsAbleToPostAReport() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - String dateTime = "2018-04-11T22:59:03.189+02:00"; - String description = "The suspect is wearing a black hat."; - MockMultipartFile image = createMockImage(); - when(service.createReport(eq(Users.officer().getId()), - any(ZonedDateTime.class), - eq(description), - any(MockMultipartFile.class))) - .thenReturn(new Report(new ReportId(UUID.randomUUID()), Users.officer(), ZonedDateTime.parse(dateTime), description)); - - mvc.perform(fileUpload("/api/reports") //<1> - .file(image) //<2> - .header(HEADER_AUTHORIZATION, bearer(accessToken)) - .param("dateTime", dateTime) //<3> - .param("description", description)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("reporter").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("dateTime").value(dateTime)) - .andExpect(jsonPath("description").value(description)); - } - private MockMultipartFile createMockImage() { //<4> - return new MockMultipartFile("image", - "picture.png", - "image/png", - new byte[10_000_000]); + UserId userId = new UserId(UUID.randomUUID()); + AuthServerId authServerId = new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); + User user = new User(userId, + "wim@example.com", + authServerId, + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"); + when(userService.findUserByAuthServerId(authServerId)) + .thenReturn(Optional.of(user)); + when(userService.getUserById(userId)) + .thenReturn(user); + when(service.createReport(any(CreateReportParameters.class))) + .thenReturn(new Report(new ReportId(UUID.randomUUID()), + userId, + Instant.parse("2023-04-11T22:59:03.189+02:00"), + "This is a test report description. The suspect was wearing a black hat.")); + mockMvc.perform(multipart("/api/reports") //<.> + .file(new MockMultipartFile("image", "picture.png", MediaType.IMAGE_PNG_VALUE, new byte[]{1,2,3})) //<.> + .param("dateTime", "2023-04-11T22:59:03.189+02:00") //<.> + .param("description", "This is a test report description. The suspect was wearing a black hat.") + .param("trafficIncident", "false") + .param("numberOfInvolvedCars", "0") + .with(jwt().jwt(builder -> builder.subject(authServerId.value().toString()) + .claim("email", "wim@example.com")) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("id").exists()) + .andExpect(jsonPath("reporter").value("wim@example.com")) + .andExpect(jsonPath("dateTime").value("2023-04-11T20:59:03.189Z")) + .andExpect(jsonPath("description").value("This is a test report description. The suspect was wearing a black hat.")); } } -//end::class[] \ No newline at end of file +//end::class[] diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java deleted file mode 100644 index 720f959..0000000 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/UserRepositoryIntegrationTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.copsboot.user; - -import com.example.copsboot.infrastructure.SpringProfiles; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.util.HashSet; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@DataJpaTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> -@ActiveProfiles(SpringProfiles.INTEGRATION_TEST) //<2> -public class UserRepositoryIntegrationTest { - @Autowired - private UserRepository repository; - @PersistenceContext - private EntityManager entityManager; - @Autowired - private JdbcTemplate jdbcTemplate; - - @Test - public void testSaveUser() { - Set roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); - - assertThat(repository.count()).isEqualTo(1L); - - entityManager.flush(); //<3> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> - assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM user_roles", Long.class)).isEqualTo(1L); - assertThat(jdbcTemplate.queryForObject("SELECT roles FROM user_roles", String.class)).isEqualTo("OFFICER"); - } -} diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java index 3217c4a..19c23fe 100644 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/UserRepositoryTest.java @@ -3,14 +3,16 @@ import com.example.copsboot.infrastructure.SpringProfiles; import com.example.orm.jpa.InMemoryUniqueIdGenerator; import com.example.orm.jpa.UniqueIdGenerator; -import org.junit.Test; -import org.junit.runner.RunWith; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; import java.util.HashSet; import java.util.Locale; @@ -19,62 +21,34 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @DataJpaTest -@ActiveProfiles(SpringProfiles.TEST) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) //<2> public class UserRepositoryTest { @Autowired private UserRepository repository; + @PersistenceContext + private EntityManager entityManager; + @Autowired + private JdbcTemplate jdbcTemplate; - //tag::testStoreUser[] @Test public void testStoreUser() { - HashSet roles = new HashSet<>(); - roles.add(UserRole.OFFICER); - User user = repository.save(new User(repository.nextId(), //<1> - "alex.foley@beverly-hills.com", - "my-secret-pwd", - roles)); - assertThat(user).isNotNull(); //<6> - - assertThat(repository.count()).isEqualTo(1L); //<7> - } - //end::testStoreUser[] + User user = repository.save(new User(repository.nextId(), + "alex.foley@beverly-hills.com", + new AuthServerId(UUID.randomUUID()), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + assertThat(user).isNotNull(); - //tag::find-by-email-tests[] - @Test - public void testFindByEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail()); - - assertThat(optional).isNotEmpty() - .contains(user); - } + assertThat(repository.count()).isEqualTo(1L); - @Test - public void testFindByEmailIgnoringCase() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase(user.getEmail() - .toUpperCase(Locale.US)); - - assertThat(optional).isNotEmpty() - .contains(user); - } - - @Test - public void testFindByEmail_unknownEmail() { - User user = Users.newRandomOfficer(); - repository.save(user); - Optional optional = repository.findByEmailIgnoreCase("will.not@find.me"); + entityManager.flush(); //<3> - assertThat(optional).isEmpty(); + assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user", Long.class)).isEqualTo(1L); //<4> + assertThat(jdbcTemplate.queryForObject("SELECT email FROM copsboot_user", String.class)).isEqualTo("alex.foley@beverly-hills.com"); } - //end::find-by-email-tests[] - //tag::testconfig[] @TestConfiguration static class TestConfig { @Bean @@ -82,5 +56,4 @@ public UniqueIdGenerator generator() { return new InMemoryUniqueIdGenerator(); } } - //end::testconfig[] -} \ No newline at end of file +} diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/Users.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/Users.java deleted file mode 100644 index 0020a96..0000000 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/Users.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.copsboot.user; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.UUID; - -public class Users { - private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); - - public static final String OFFICER_EMAIL = "officer@example.com"; - public static final String OFFICER_PASSWORD = "officer"; - public static final String CAPTAIN_EMAIL = "captain@example.com"; - public static final String CAPTAIN_PASSWORD = "captain"; - - private static User OFFICER = User.createOfficer(newRandomId(), - OFFICER_EMAIL, - PASSWORD_ENCODER.encode(OFFICER_PASSWORD)); - - private static User CAPTAIN = User.createCaptain(newRandomId(), - CAPTAIN_EMAIL, - PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD)); - - - public static UserId newRandomId() { - return new UserId(UUID.randomUUID()); - } - - public static User newRandomOfficer() { - return newRandomOfficer(newRandomId()); - } - - public static User newRandomOfficer(UserId userId) { - String uniqueId = userId.asString().substring(0, 5); - return User.createOfficer(userId, - "user-" + uniqueId + - "@example.com", - PASSWORD_ENCODER.encode("user")); - } - - public static User officer() { - return OFFICER; - } - - public static User captain() { - return CAPTAIN; - } - - private Users() { - } - - public static User newOfficer(String email, String password) { - return User.createOfficer(newRandomId(), email, PASSWORD_ENCODER.encode(password)); - } -} diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java deleted file mode 100644 index 7b94df3..0000000 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/CreateUserParametersValidatorTest.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.copsboot.user.web; - -import com.example.copsboot.infrastructure.SpringProfiles; -import com.example.copsboot.user.User; -import com.example.copsboot.user.UserId; -import com.example.copsboot.user.UserService; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import javax.validation.ConstraintViolation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; - -import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; -import static org.mockito.Mockito.when; - -//tag::class[] -@RunWith(SpringRunner.class) -@SpringBootTest //<1> -@ActiveProfiles(SpringProfiles.TEST) -public class CreateUserParametersValidatorTest { - - @MockBean - private UserService userService; //<2> - @Autowired - private PasswordEncoder encoder; - @Autowired - private ValidatorFactory factory; //<3> - - @Test - public void invalidIfAlreadyUserWithGivenEmail() { - - String email = "wim.deblauwe@example.com"; - when(userService.findUserByEmail(email)) - .thenReturn(Optional.of( - User.createOfficer(new UserId(UUID.randomUUID()), - email, - encoder.encode("testing1234")))); - - Validator validator = factory.getValidator(); //<4> - - CreateOfficerParameters userParameters = new CreateOfficerParameters(); - userParameters.setEmail(email); - userParameters.setPassword("my-secret-pwd-1234"); - Set> violationSet = validator.validate(userParameters); //<5> - assertThat(violationSet).hasViolationSize(2) - .hasViolationOnPath("email"); //<6> - } - - @Test - public void validIfNoUserWithGivenEmail() { - String email = "wim.deblauwe@example.com"; - when(userService.findUserByEmail(email)) - .thenReturn(Optional.empty()); - - Validator validator = factory.getValidator(); - - CreateOfficerParameters userParameters = new CreateOfficerParameters(); - userParameters.setEmail(email); - userParameters.setPassword("my-secret-pwd-1234"); - Set> violationSet = validator.validate(userParameters); - assertThat(violationSet).hasNoViolations(); - } -} -//end::class[] \ No newline at end of file diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java new file mode 100644 index 0000000..d058abd --- /dev/null +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/CreateUserRequestValidatorTest.java @@ -0,0 +1,65 @@ +package com.example.copsboot.user.web; + +import com.example.copsboot.infrastructure.SpringProfiles; +import com.example.copsboot.user.AuthServerId; +import com.example.copsboot.user.User; +import com.example.copsboot.user.UserId; +import com.example.copsboot.user.UserService; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; + +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import static com.example.copsboot.util.test.ConstraintViolationSetAssert.assertThat; +import static org.mockito.Mockito.when; + +//tag::class[] +@SpringBootTest //<1> +@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) +public class CreateUserRequestValidatorTest { + + @MockBean + private UserService userService; //<2> + @Autowired + private ValidatorFactory factory; //<3> + + @Test + public void invalidIfAlreadyUserWithGivenMobileToken() { + + String mobileToken = "abc123"; + when(userService.findUserByMobileToken(mobileToken)) + .thenReturn(Optional.of(new User(new UserId(UUID.randomUUID()), + "wim@example.com", + new AuthServerId(UUID.randomUUID()), + mobileToken))); + + Validator validator = factory.getValidator(); //<4> + + CreateUserRequest request = new CreateUserRequest(mobileToken); + Set> violationSet = validator.validate(request); //<5> + assertThat(violationSet).hasViolationSize(2) + .hasViolationOnPath("mobileToken"); //<6> + } + + @Test + public void validIfNoUserWithGivenMobileToken() { + String mobileToken = "abc123"; + when(userService.findUserByMobileToken(mobileToken)) + .thenReturn(Optional.empty()); + + Validator validator = factory.getValidator(); + + CreateUserRequest request = new CreateUserRequest(mobileToken); + Set> violationSet = validator.validate(request); + assertThat(violationSet).hasNoViolations(); + } +} +//end::class[] diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java index b1c3165..805c501 100644 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/UserRestControllerDocumentation.java @@ -1,134 +1,94 @@ package com.example.copsboot.user.web; -import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.infrastructure.test.CopsbootControllerDocumentationTest; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) -@CopsbootControllerTest(UserRestController.class) +@CopsbootControllerDocumentationTest(UserRestController.class) public class UserRestControllerDocumentation { -//end::class-annotations[] - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - private MockMvc mvc; @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean private UserService service; - //tag::setup-method[] - @Autowired - private WebApplicationContext context; //<1> - private RestDocumentationResultHandler resultHandler; //<2> - - @Before - public void setUp() { - resultHandler = document("{method-name}", //<3> - preprocessRequest(prettyPrint()), //<4> - preprocessResponse(prettyPrint(), //<5> - removeMatchingHeaders("X.*", //<6> - "Pragma", - "Expires"))); - mvc = MockMvcBuilders.webAppContextSetup(context) //<7> - .apply(springSecurity()) //<8> - .apply(documentationConfiguration(restDocumentation)) //<9> - .alwaysDo(resultHandler) //<10> - .build(); - } - //end::setup-method[] - //tag::not-logged-in[] @Test public void ownUserDetailsWhenNotLoggedInExample() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()) + .andDo(document("own-details-unauthorized")); } //end::not-logged-in[] //tag::officer-details[] @Test public void authenticatedOfficerDetailsExample() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andDo(resultHandler.document( - responseFields( - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))) + .andExpect(status().isOk()) + .andDo(document("own-details", + responseFields( + fieldWithPath("subject").description("The subject from the JWT token"), + subsectionWithPath("claims").description("The claims from the JWT token") + ))); } - //end::officer-details[] //tag::create-officer[] @Test public void createOfficerExample() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); //<1> - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); //<2> - when(service.findUserByEmail(email)).thenReturn(Optional.empty()); - - mvc.perform(post("/api/users") //<3> - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) //<4> - .andExpect(status().isCreated()) //<5> - .andDo(resultHandler.document( - requestFields( //<6> - fieldWithPath("email") - .description("The email address of the user to be created."), - fieldWithPath("password") - .description("The password for the new user.") - ), - responseFields( //<7> - fieldWithPath("id") - .description("The unique id of the user."), - fieldWithPath("email") - .description("The email address of the user."), - fieldWithPath("roles") - .description("The security roles of the user.")))); + UserId userId = new UserId(UUID.randomUUID()); + when(service.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andDo(document("create-user", + requestFields( // <.> + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ), + responseFields( // <.> + fieldWithPath("userId") + .description("The unique id of the user."), + fieldWithPath("email") + .description("The email address of the user."), + fieldWithPath("authServerId") + .description("The id of the user on the authorization server."), + fieldWithPath("mobileToken") + .description("The unique mobile token of the device (for push notifications).") + ))); } //end::create-officer[] } diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java index f55e6c6..a20d744 100644 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/user/web/UserRestControllerTest.java @@ -1,121 +1,104 @@ package com.example.copsboot.user.web; import com.example.copsboot.infrastructure.test.CopsbootControllerTest; -import com.example.copsboot.user.UserService; -import com.example.copsboot.user.Users; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.example.copsboot.user.*; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; +import java.util.UUID; -import static com.example.copsboot.infrastructure.security.SecurityHelperForMockMvc.*; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -//tag::class-annotations[] -@RunWith(SpringRunner.class) +// tag::class-annotations[] @CopsbootControllerTest(UserRestController.class) -public class UserRestControllerTest { -//end::class-annotations[] - @Autowired - private MockMvc mvc; +class UserRestControllerTest { + // end::class-annotations[] @Autowired - private ObjectMapper objectMapper; + private MockMvc mockMvc; + @MockBean - private UserService service; + private UserService userService; //<.> @Test - public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception { - mvc.perform(get("/api/users/me")) - .andExpect(status().isUnauthorized()); + void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception { + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isUnauthorized()); } @Test - public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD); - - when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")) - ; + void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception { + String subject = UUID.randomUUID().toString(); //<.> + mockMvc.perform(get("/api/users/me") + .with(jwt().jwt(builder -> builder.subject(subject)))) //<.> + .andExpect(status().isOk()) + .andExpect(jsonPath("subject").value(subject)) //<.> + .andExpect(jsonPath("claims").isMap()); } @Test - public void givenAuthenticatedAsCaptain_whenAskingMyDetails_detailsReturned() throws Exception { - String accessToken = obtainAccessToken(mvc, Users.CAPTAIN_EMAIL, Users.CAPTAIN_PASSWORD); - - when(service.getUser(Users.captain().getId())).thenReturn(Optional.of(Users.captain())); - - mvc.perform(get("/api/users/me") - .header(HEADER_AUTHORIZATION, bearer(accessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(Users.CAPTAIN_EMAIL)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles").value("CAPTAIN")); + void givenAuthenticatedOfficer_userIsCreated() throws Exception { //<.> + UserId userId = new UserId(UUID.randomUUID()); + when(userService.createUser(any(CreateUserParameters.class))) + .thenReturn(new User(userId, + "wim@example.com", + new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")), + "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0")); + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("userId").value(userId.asString())) + .andExpect(jsonPath("email").value("wim@example.com")) + .andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-d8b4ae2750ac")); } @Test - public void testCreateOfficer() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "my-super-secret-pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email, password)); - when(service.findUserByEmail(email)).thenReturn(Optional.empty()); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("id").exists()) - .andExpect(jsonPath("email").value(email)) - .andExpect(jsonPath("roles").isArray()) - .andExpect(jsonPath("roles[0]").value("OFFICER")); - - verify(service).createOfficer(email, password); + void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString()))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0" + } + """)) + .andExpect(status().isForbidden()); // <.> } + // tag::emptyToken[] @Test - public void testCreateOfficerIfPasswordIsTooShort() throws Exception { - String email = "wim.deblauwe@example.com"; - String password = "pwd"; - - CreateOfficerParameters parameters = new CreateOfficerParameters(); - parameters.setEmail(email); - parameters.setPassword(password); - - when(service.findUserByEmail(email)).thenReturn(Optional.empty()); - - mvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(parameters))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("errors[0].fieldName").value("password")); - - verify(service, never()).createOfficer(email, password); + void givenEmptyMobileToken_badRequestIsReturned() throws Exception { + mockMvc.perform(post("/api/users") + .with(jwt().jwt(builder -> builder.subject(UUID.randomUUID().toString())) + .authorities(new SimpleGrantedAuthority("ROLE_OFFICER"))) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "mobileToken": "" + } + """)) //<.> + .andExpect(status().isBadRequest()) //<.> + .andDo(print()); //<.> + + verify(userService, never()).createUser(any(CreateUserParameters.class)); //<.> } + // end::emptyToken[] } diff --git a/chapter09/02 - validation/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java b/chapter09/02 - validation/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java index 77c5f4c..21556a5 100644 --- a/chapter09/02 - validation/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java +++ b/chapter09/02 - validation/src/test/java/com/example/copsboot/util/test/ConstraintViolationSetAssert.java @@ -2,7 +2,8 @@ import org.assertj.core.api.AbstractAssert; -import javax.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolation; + import java.util.Set; import java.util.stream.Collectors; @@ -21,12 +22,12 @@ public ConstraintViolationSetAssert hasViolationOnPath(String path) { // check condition if (!containsViolationWithPath(actual, path)) { failWithMessage("There was no violation with path <%s>. Violation paths: <%s>", path, actual.stream() - .map(violation -> violation - .getPropertyPath() - .toString()) - .collect( - Collectors - .toList())); + .map(violation -> violation + .getPropertyPath() + .toString()) + .collect( + Collectors + .toList())); } return this; diff --git a/chapter09/02 - validation/src/test/resources/application-integration-test.properties b/chapter09/02 - validation/src/test/resources/application-integration-test.properties index 159536c..81fd275 100644 --- a/chapter09/02 - validation/src/test/resources/application-integration-test.properties +++ b/chapter09/02 - validation/src/test/resources/application-integration-test.properties @@ -1,11 +1,9 @@ -spring.datasource.url=jdbc:tc:postgresql://localhost/copsbootdb +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.username=user spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/postgresql \ No newline at end of file +#logging.level.org.springframework.security=DEBUG +#logging.level.org.springframework=DEBUG diff --git a/chapter09/02 - validation/src/test/resources/application-repository-test.properties b/chapter09/02 - validation/src/test/resources/application-repository-test.properties new file mode 100644 index 0000000..c61e563 --- /dev/null +++ b/chapter09/02 - validation/src/test/resources/application-repository-test.properties @@ -0,0 +1,6 @@ +spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver +spring.datasource.username=user +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=validate diff --git a/chapter09/02 - validation/src/test/resources/application-test.properties b/chapter09/02 - validation/src/test/resources/application-test.properties deleted file mode 100644 index 02b4003..0000000 --- a/chapter09/02 - validation/src/test/resources/application-test.properties +++ /dev/null @@ -1,5 +0,0 @@ -copsboot-security.mobile-app-client-id=test-client-id -copsboot-security.mobile-app-client-secret=test-client-secret - -spring.flyway.locations=classpath:db/migration/h2 -spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/chapter09/02 - validation/src/test/resources/jwt-officer.json b/chapter09/02 - validation/src/test/resources/jwt-officer.json new file mode 100644 index 0000000..cdf7177 --- /dev/null +++ b/chapter09/02 - validation/src/test/resources/jwt-officer.json @@ -0,0 +1,41 @@ +{ + "exp": 1694870234, + "iat": 1694869934, + "auth_time": 1694865932, + "jti": "7b933105-60b9-43ae-8725-a34bff521858", + "iss": "http://localhost:8180/realms/copsboot", + "aud": "account", + "sub": "eaa8b8a5-a264-48be-98de-d8b4ae2750ac", + "typ": "Bearer", + "azp": "copsboot-mobile-client", + "session_state": "2866bba7-d53f-498e-8830-4dcf0bcb865e", + "acr": "0", + "allowed-origins": [ + "https://oauth.pstmn.io" + ], + "realm_access": { + "roles": [ + "default-roles-copsboot", + "offline_access", + "OFFICER", + "uma_authorization" + ] + }, + "resource_access": { + "account": { + "roles": [ + "manage-account", + "manage-account-links", + "view-profile" + ] + } + }, + "scope": "email profile", + "sid": "2866bba7-d53f-498e-8830-4dcf0bcb865e", + "email_verified": false, + "name": "Wim Example", + "preferred_username": "wim@example.com", + "given_name": "Wim", + "family_name": "Example", + "email": "wim@example.com" +} diff --git a/chapter09/02 - validation/src/test/resources/logback-test.xml b/chapter09/02 - validation/src/test/resources/logback-test.xml index bf47fec..164429c 100644 --- a/chapter09/02 - validation/src/test/resources/logback-test.xml +++ b/chapter09/02 - validation/src/test/resources/logback-test.xml @@ -5,7 +5,7 @@ - + @@ -17,14 +17,8 @@ - - - - - - - \ No newline at end of file +