├── .dockerignore ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── LICENSE ├── README.md ├── auth-lib ├── .gitignore ├── .mvn │ └── wrapper │ │ └── maven-wrapper.properties ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── company │ │ │ └── secureapispring │ │ │ └── auth │ │ │ ├── AuthInfo.java │ │ │ ├── cache │ │ │ └── CacheName.java │ │ │ ├── domain │ │ │ └── AuditAwareImpl.java │ │ │ ├── entities │ │ │ ├── Organization.java │ │ │ └── User.java │ │ │ ├── interfaces │ │ │ └── HasOrganization.java │ │ │ ├── repositories │ │ │ ├── OrganizationRepository.java │ │ │ └── UserRepository.java │ │ │ └── services │ │ │ ├── AuthService.java │ │ │ ├── OrganizationService.java │ │ │ └── UserService.java │ └── resources │ │ └── liquibase │ │ └── changelog │ │ └── 000000_auth_initial_schema.xml │ └── test │ ├── java │ └── com │ │ └── company │ │ └── secureapispring │ │ └── auth │ │ ├── AbstractIT.java │ │ ├── AuthLibSpringBootApp.java │ │ ├── AuthLibSpringBootTest.java │ │ ├── services │ │ ├── AuthServiceIT.java │ │ ├── OrganizationServiceIT.java │ │ └── UserServiceIT.java │ │ └── utils │ │ └── TestJWTUtils.java │ └── resources │ ├── application.properties │ └── liquibase │ └── main.xml ├── customer-svc ├── .gitignore ├── .mvn │ └── wrapper │ │ └── maven-wrapper.properties ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── company │ │ │ └── secureapispring │ │ │ └── customer │ │ │ ├── CustomerSvcApp.java │ │ │ ├── cache │ │ │ ├── CacheConfig.java │ │ │ └── CacheName.java │ │ │ ├── controllers │ │ │ ├── AppController.java │ │ │ ├── CountryController.java │ │ │ └── CustomerController.java │ │ │ ├── entities │ │ │ ├── AbstractAuditingEntity.java │ │ │ ├── Country.java │ │ │ ├── Customer.java │ │ │ └── StateProvince.java │ │ │ ├── exceptions │ │ │ ├── BadRequestException.java │ │ │ └── GlobalExceptionHandler.java │ │ │ ├── repositories │ │ │ ├── CountryRepository.java │ │ │ ├── CustomerRepository.java │ │ │ └── StateProvinceRepository.java │ │ │ ├── security │ │ │ ├── OpenAPISecurityConfig.java │ │ │ └── SecurityConfig.java │ │ │ └── services │ │ │ ├── CountryService.java │ │ │ ├── CustomerService.java │ │ │ └── StateProvinceService.java │ ├── jenkins │ │ ├── Jenkinsfile │ │ └── README.md │ ├── kubernetes │ │ └── templates │ │ │ ├── config-map.yaml │ │ │ ├── deploy.yaml │ │ │ └── secret.yaml │ └── resources │ │ ├── application.properties │ │ ├── liquibase │ │ ├── changelog │ │ │ └── 000000_initial_schema.xml │ │ ├── data │ │ │ ├── countries.csv │ │ │ └── state-provinces.csv │ │ └── main.xml │ │ └── redisson.yaml │ └── test │ ├── java │ └── com │ │ └── company │ │ └── secureapispring │ │ └── customer │ │ ├── AbstractIT.java │ │ ├── CustomerSvcSpringBootAppTest.java │ │ ├── TestConfiguration.java │ │ ├── TestJWTUtils.java │ │ ├── controllers │ │ ├── AppControllerIT.java │ │ ├── CountryControllerIT.java │ │ └── CustomerControllerIT.java │ │ ├── factory │ │ ├── EntityBuilder.java │ │ └── EntityFactory.java │ │ └── services │ │ ├── CountryServiceIT.java │ │ └── StateProvinceServiceIT.java │ └── resources │ └── application.properties ├── docker ├── .env.example ├── jenkins │ ├── Dockerfile │ └── jenkins-agent.yml ├── keycloak │ ├── data │ │ └── initial-dev-realm.json │ └── keycloak.yml ├── postgresql │ ├── PostgreSQL.dockerfile │ ├── create-db.sh │ ├── init-user-db.sh │ └── postgresql.yml ├── redis.yml └── services.yml ├── mvnw ├── mvnw.cmd ├── pom.xml └── task.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | /docker 2 | /.idea 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .mvn/timing.properties 2 | .mvn/wrapper/maven-wrapper.jar 3 | .idea 4 | docker/.env 5 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Eli Barbosa 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Secure API with Spring Boot 3 2 | 3 | This Git monorepository contains a sample Java REST API application configured to use [Keycloak](https://www.keycloak.org) for access management. 4 | 5 | The project was bootstrapped using [Spring Initializer](https://start.spring.io/) with the following dependencies: 6 | 7 | - Spring Boot Web (spring-boot-starter-web) for building RESTful APIs. 8 | - JUnit Jupiter, Hamcrest, and Mockito (spring-boot-starter-test) for unit testing. 9 | - Spring Security OAuth2 Resource Server (spring-boot-starter-oauth2-resource-server) to enable OIDC integration with Keycloak. 10 | - Jacoco for generating test coverage reports. 11 | - Spring Data and Hibernate Validator for data access and validation. 12 | - Redis is used as 2nd level cache (Using Redisson driver). 13 | - Spring Doc Open API for API documentation. 14 | - Liquibase with PostgreSQL as the database for managing database migrations. 15 | - [TestContainers](https://www.testcontainers.org/) to run tests in an isolated PostgreSQL database environment. 16 | - [Data Faker](https://github.com/datafaker-net/datafaker) for generating test data. 17 | - [Lombok](https://projectlombok.org/) to reduce verbosity in the code. 18 | - Jenkins for deploying to a self-hosted Kubernetes server. 19 | 20 | ## Requirements 21 | 22 | 1. Java JDK version 21 or higher. 23 | 2. Docker version 27 or higher. 24 | 25 | ## Initial steps 26 | 27 | ### 1st step 28 | 29 | This example relies on Keycloak for authentication, PostgreSQL as the database as Redis for cache. To start these dependencies, run the following commands: 30 | 31 | ```bash 32 | ./task.sh services:up 33 | ``` 34 | 35 | ### 2nd step 36 | 37 | After cloning the repository, navigate to the root directory of the project and run the following command: 38 | 39 | ```bash 40 | ./mvnw clean install 41 | ``` 42 | 43 | ## 3rd step 44 | 45 | To run the customer service, use the following command in another terminal: 46 | 47 | ```bash 48 | ./mvnw -pl customer-svc spring-boot:run 49 | ``` 50 | 51 | ## Try it out 52 | 53 | You can test the API using the Swagger UI at the following URL: 54 | 55 | http://localhost:8081/swagger-ui/index.html 56 | 57 | 1. Click the **Authorize** button and enter `secure-api` in the **client_id** field. 58 | 2. Click the **Authorize** button of the popup, which will redirect you to the Keycloak authentication page. 59 | 60 | When the Keycloak service runs for the first time, it imports a realm called `App`, created exclusively for testing purposes. 61 | 62 | This realm contains two users for authentication: 63 | - The user with the administrator role (ROLE_ADMIN) has the username `admin` and password `password`. 64 | - The user with the analyst role (ROLE_ANALYST) has the username `analyst` and password `password`. 65 | 66 | Note: The ROLE_ADMIN user is permitted to modify customers, while the ROLE_ANALYST user can only list them. 67 | 68 | Once you have authenticated, you can begin testing the API endpoints using the **Try it out** button for each endpoint. 69 | 70 | If you would like to view the JWT token generated by Keycloak, use the following URL, which redirects the authentication flow to jwt.io: 71 | 72 | http://localhost:9080/realms/app/protocol/openid-connect/auth?response_type=token&client_id=secure-api&redirect_uri=https%3A%2F%2Fjwt%2Eio 73 | 74 | ## Keycloak 75 | 76 | Since Keycloak 25, the Organization feature is available to support multi-tenancy as can be seen [here](https://www.keycloak.org/2024/06/announcement-keycloak-organizations). 77 | 78 | When using the Customer API endpoints, the User and Organization of the Keycloak authenticated token are going to be synced in the Customers database. 79 | 80 | The admin console is accessible at http://localhost:9080/admin/master/console/#/. For testing purposes, the super admin user credentials are as follows: 81 | 82 | - Username: admin 83 | - Password: admin 84 | 85 | ## PostgreSQL 86 | 87 | The is available at localhost:5432. For testing purposes, the superuser credentials are: 88 | 89 | - Username: postgresql 90 | - Password: postgresql 91 | 92 | ## Modules 93 | 94 | The modules of this repository is based on the structure of a [Multi Module Project for Spring Boot](https://spring.io/guides/gs/multi-module/): 95 | 96 | The [auth-lib](./auth-lib/README.md) directory is a library designed to share the code need to handle authenticated resources, like authenticated user and organization for Multi-tenancy. 97 | 98 | The [customer-svc](./customer-svc/README.md) module is the main app of this repository, bootstrapped with the dependencies mentioned earlier. 99 | 100 | ## Docker 101 | 102 | The [docker](./docker) directory is not a Maven module. 103 | - The `jenkins` directory contains a Dockerfile that is used to build the Docker Agent image, which will be utilized in the Pipeline. 104 | - The `postgresql` directory contains a Dockerfile to build the image for PostgreSQL used in this example. For development convenience, when the container runs for the first time, it will create two databases by default: one for Keycloak and one for PostgreSQL. 105 | - The `keycloak` directory contains a Docker Compose template to run Keycloak. For convenience, when it runs for the first time, it will import a development realm and enable the organization feature. 106 | 107 | ## Jenkins 108 | 109 | See [here](./customer-svc/src/main/jenkins/README.md) for more details on how to build the Docker image using Jenkins, push the built image to the Docker Image Registry, and deploy it to Kubernetes. 110 | -------------------------------------------------------------------------------- /auth-lib/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | .mvn/wrapper/maven-wrapper.jar 11 | .idea 12 | -------------------------------------------------------------------------------- /auth-lib/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar 19 | -------------------------------------------------------------------------------- /auth-lib/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /usr/local/etc/mavenrc ] ; then 40 | . /usr/local/etc/mavenrc 41 | fi 42 | 43 | if [ -f /etc/mavenrc ] ; then 44 | . /etc/mavenrc 45 | fi 46 | 47 | if [ -f "$HOME/.mavenrc" ] ; then 48 | . "$HOME/.mavenrc" 49 | fi 50 | 51 | fi 52 | 53 | # OS specific support. $var _must_ be set to either true or false. 54 | cygwin=false; 55 | darwin=false; 56 | mingw=false 57 | case "`uname`" in 58 | CYGWIN*) cygwin=true ;; 59 | MINGW*) mingw=true;; 60 | Darwin*) darwin=true 61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 63 | if [ -z "$JAVA_HOME" ]; then 64 | if [ -x "/usr/libexec/java_home" ]; then 65 | export JAVA_HOME="`/usr/libexec/java_home`" 66 | else 67 | export JAVA_HOME="/Library/Java/Home" 68 | fi 69 | fi 70 | ;; 71 | esac 72 | 73 | if [ -z "$JAVA_HOME" ] ; then 74 | if [ -r /etc/gentoo-release ] ; then 75 | JAVA_HOME=`java-config --jre-home` 76 | fi 77 | fi 78 | 79 | if [ -z "$M2_HOME" ] ; then 80 | ## resolve links - $0 may be a link to maven's home 81 | PRG="$0" 82 | 83 | # need this for relative symlinks 84 | while [ -h "$PRG" ] ; do 85 | ls=`ls -ld "$PRG"` 86 | link=`expr "$ls" : '.*-> \(.*\)$'` 87 | if expr "$link" : '/.*' > /dev/null; then 88 | PRG="$link" 89 | else 90 | PRG="`dirname "$PRG"`/$link" 91 | fi 92 | done 93 | 94 | saveddir=`pwd` 95 | 96 | M2_HOME=`dirname "$PRG"`/.. 97 | 98 | # make it fully qualified 99 | M2_HOME=`cd "$M2_HOME" && pwd` 100 | 101 | cd "$saveddir" 102 | # echo Using m2 at $M2_HOME 103 | fi 104 | 105 | # For Cygwin, ensure paths are in UNIX format before anything is touched 106 | if $cygwin ; then 107 | [ -n "$M2_HOME" ] && 108 | M2_HOME=`cygpath --unix "$M2_HOME"` 109 | [ -n "$JAVA_HOME" ] && 110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 111 | [ -n "$CLASSPATH" ] && 112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 113 | fi 114 | 115 | # For Mingw, ensure paths are in UNIX format before anything is touched 116 | if $mingw ; then 117 | [ -n "$M2_HOME" ] && 118 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 119 | [ -n "$JAVA_HOME" ] && 120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 121 | fi 122 | 123 | if [ -z "$JAVA_HOME" ]; then 124 | javaExecutable="`which javac`" 125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 126 | # readlink(1) is not available as standard on Solaris 10. 127 | readLink=`which readlink` 128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 129 | if $darwin ; then 130 | javaHome="`dirname \"$javaExecutable\"`" 131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 132 | else 133 | javaExecutable="`readlink -f \"$javaExecutable\"`" 134 | fi 135 | javaHome="`dirname \"$javaExecutable\"`" 136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 137 | JAVA_HOME="$javaHome" 138 | export JAVA_HOME 139 | fi 140 | fi 141 | fi 142 | 143 | if [ -z "$JAVACMD" ] ; then 144 | if [ -n "$JAVA_HOME" ] ; then 145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 146 | # IBM's JDK on AIX uses strange locations for the executables 147 | JAVACMD="$JAVA_HOME/jre/sh/java" 148 | else 149 | JAVACMD="$JAVA_HOME/bin/java" 150 | fi 151 | else 152 | JAVACMD="`\\unset -f command; \\command -v java`" 153 | fi 154 | fi 155 | 156 | if [ ! -x "$JAVACMD" ] ; then 157 | echo "Error: JAVA_HOME is not defined correctly." >&2 158 | echo " We cannot execute $JAVACMD" >&2 159 | exit 1 160 | fi 161 | 162 | if [ -z "$JAVA_HOME" ] ; then 163 | echo "Warning: JAVA_HOME environment variable is not set." 164 | fi 165 | 166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 167 | 168 | # traverses directory structure from process work directory to filesystem root 169 | # first directory with .mvn subdirectory is considered project base directory 170 | find_maven_basedir() { 171 | 172 | if [ -z "$1" ] 173 | then 174 | echo "Path not specified to find_maven_basedir" 175 | return 1 176 | fi 177 | 178 | basedir="$1" 179 | wdir="$1" 180 | while [ "$wdir" != '/' ] ; do 181 | if [ -d "$wdir"/.mvn ] ; then 182 | basedir=$wdir 183 | break 184 | fi 185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 186 | if [ -d "${wdir}" ]; then 187 | wdir=`cd "$wdir/.."; pwd` 188 | fi 189 | # end of workaround 190 | done 191 | echo "${basedir}" 192 | } 193 | 194 | # concatenates all lines of a file 195 | concat_lines() { 196 | if [ -f "$1" ]; then 197 | echo "$(tr -s '\n' ' ' < "$1")" 198 | fi 199 | } 200 | 201 | BASE_DIR=`find_maven_basedir "$(pwd)"` 202 | if [ -z "$BASE_DIR" ]; then 203 | exit 1; 204 | fi 205 | 206 | ########################################################################################## 207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 208 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 209 | ########################################################################################## 210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Found .mvn/wrapper/maven-wrapper.jar" 213 | fi 214 | else 215 | if [ "$MVNW_VERBOSE" = true ]; then 216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 217 | fi 218 | if [ -n "$MVNW_REPOURL" ]; then 219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 220 | else 221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 222 | fi 223 | while IFS="=" read key value; do 224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 225 | esac 226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 227 | if [ "$MVNW_VERBOSE" = true ]; then 228 | echo "Downloading from: $jarUrl" 229 | fi 230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 231 | if $cygwin; then 232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 233 | fi 234 | 235 | if command -v wget > /dev/null; then 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Found wget ... using wget" 238 | fi 239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 241 | else 242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 243 | fi 244 | elif command -v curl > /dev/null; then 245 | if [ "$MVNW_VERBOSE" = true ]; then 246 | echo "Found curl ... using curl" 247 | fi 248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 249 | curl -o "$wrapperJarPath" "$jarUrl" -f 250 | else 251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 252 | fi 253 | 254 | else 255 | if [ "$MVNW_VERBOSE" = true ]; then 256 | echo "Falling back to using Java to download" 257 | fi 258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 259 | # For Cygwin, switch paths to Windows format before running javac 260 | if $cygwin; then 261 | javaClass=`cygpath --path --windows "$javaClass"` 262 | fi 263 | if [ -e "$javaClass" ]; then 264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 265 | if [ "$MVNW_VERBOSE" = true ]; then 266 | echo " - Compiling MavenWrapperDownloader.java ..." 267 | fi 268 | # Compiling the Java class 269 | ("$JAVA_HOME/bin/javac" "$javaClass") 270 | fi 271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 272 | # Running the downloader 273 | if [ "$MVNW_VERBOSE" = true ]; then 274 | echo " - Running MavenWrapperDownloader.java ..." 275 | fi 276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 277 | fi 278 | fi 279 | fi 280 | fi 281 | ########################################################################################## 282 | # End of extension 283 | ########################################################################################## 284 | 285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 286 | if [ "$MVNW_VERBOSE" = true ]; then 287 | echo $MAVEN_PROJECTBASEDIR 288 | fi 289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 290 | 291 | # For Cygwin, switch paths to Windows format before running java 292 | if $cygwin; then 293 | [ -n "$M2_HOME" ] && 294 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 295 | [ -n "$JAVA_HOME" ] && 296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 297 | [ -n "$CLASSPATH" ] && 298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 299 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 301 | fi 302 | 303 | # Provide a "standardized" way to retrieve the CLI args that will 304 | # work with both Windows and non-Windows executions. 305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 306 | export MAVEN_CMD_LINE_ARGS 307 | 308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 309 | 310 | exec "$JAVACMD" \ 311 | $MAVEN_OPTS \ 312 | $MAVEN_DEBUG_OPTS \ 313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 314 | "-Dmaven.home=${M2_HOME}" \ 315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 317 | -------------------------------------------------------------------------------- /auth-lib/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 124 | 125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% ^ 162 | %JVM_CONFIG_MAVEN_PROPS% ^ 163 | %MAVEN_OPTS% ^ 164 | %MAVEN_DEBUG_OPTS% ^ 165 | -classpath %WRAPPER_JAR% ^ 166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 168 | if ERRORLEVEL 1 goto error 169 | goto end 170 | 171 | :error 172 | set ERROR_CODE=1 173 | 174 | :end 175 | @endlocal & set ERROR_CODE=%ERROR_CODE% 176 | 177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 181 | :skipRcPost 182 | 183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 185 | 186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 187 | 188 | cmd /C exit /B %ERROR_CODE% 189 | -------------------------------------------------------------------------------- /auth-lib/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | auth-lib 6 | 0.0.1-SNAPSHOT 7 | 8 | com.company.secureapispring 9 | secure-api-spring-parent 10 | 0.0.1-SNAPSHOT 11 | 12 | 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-oauth2-resource-server 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-data-jpa 21 | 22 | 23 | org.testcontainers 24 | junit-jupiter 25 | test 26 | 27 | 28 | org.postgresql 29 | postgresql 30 | runtime 31 | 32 | 33 | 34 | 35 | org.testcontainers 36 | postgresql 37 | test 38 | 39 | 40 | org.liquibase 41 | liquibase-core 42 | test 43 | 44 | 45 | net.datafaker 46 | datafaker 47 | test 48 | 49 | 50 | 51 | 52 | 53 | 54 | org.apache.maven.plugins 55 | maven-jar-plugin 56 | 57 | 58 | 59 | test-jar 60 | 61 | 62 | 63 | 64 | 65 | org.apache.maven.plugins 66 | maven-surefire-plugin 67 | 68 | **/*IT.java 69 | 70 | 71 | 72 | org.jacoco 73 | jacoco-maven-plugin 74 | 75 | ${basedir}/target/coverage-reports/jacoco-unit.exec 76 | ${basedir}/target/coverage-reports/jacoco-unit.exec 77 | file 78 | true 79 | 80 | *MethodAccess 81 | 82 | 83 | 84 | 85 | jacoco-initialize 86 | 87 | prepare-agent 88 | 89 | test-compile 90 | 91 | 92 | jacoco-site 93 | verify 94 | 95 | report 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /auth-lib/src/main/java/com/company/secureapispring/auth/AuthInfo.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.auth; 2 | 3 | import com.company.secureapispring.auth.entities.Organization; 4 | import com.company.secureapispring.auth.entities.User; 5 | 6 | public record AuthInfo(User user, Organization organization) { 7 | } 8 | -------------------------------------------------------------------------------- /auth-lib/src/main/java/com/company/secureapispring/auth/cache/CacheName.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.auth.cache; 2 | 3 | public final class CacheName { 4 | public static final String USER_BY_USERNAME = "auth.userByUsername"; 5 | public static final String ORG_BY_ALIAS = "auth.orgByAlias"; 6 | private CacheName() {} 7 | } 8 | -------------------------------------------------------------------------------- /auth-lib/src/main/java/com/company/secureapispring/auth/domain/AuditAwareImpl.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.auth.domain; 2 | 3 | import org.springframework.data.domain.AuditorAware; 4 | import org.springframework.security.core.context.SecurityContextHolder; 5 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; 6 | 7 | import java.util.Map; 8 | import java.util.Optional; 9 | 10 | public class AuditAwareImpl implements AuditorAware { 11 | @Override 12 | public Optional getCurrentAuditor() { 13 | JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); 14 | if (jwtAuthenticationToken == null) { 15 | return Optional.empty(); 16 | } 17 | Map attrs = jwtAuthenticationToken.getTokenAttributes(); 18 | return Optional.of((String) attrs.get("preferred_username")); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /auth-lib/src/main/java/com/company/secureapispring/auth/entities/Organization.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.auth.entities; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.ToString; 7 | import org.hibernate.annotations.CacheConcurrencyStrategy; 8 | import org.hibernate.annotations.Cache; 9 | import org.springframework.data.annotation.CreatedBy; 10 | import org.springframework.data.annotation.CreatedDate; 11 | import org.springframework.data.annotation.LastModifiedBy; 12 | import org.springframework.data.annotation.LastModifiedDate; 13 | 14 | import java.io.Serial; 15 | import java.io.Serializable; 16 | import java.time.Instant; 17 | 18 | @EqualsAndHashCode(onlyExplicitlyIncluded = true) 19 | @Data 20 | @Entity 21 | @Table(name = "organizations") 22 | @Cache(region = "organizations", usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) 23 | @ToString 24 | public class Organization implements Serializable { 25 | @Serial 26 | private static final long serialVersionUID = 1L; 27 | 28 | @Id 29 | @GeneratedValue(strategy = GenerationType.IDENTITY) 30 | @Column(name = "id") 31 | private Long id; 32 | 33 | @EqualsAndHashCode.Include 34 | @Column(name = "alias", length = 48, nullable = false, unique = true) 35 | private String alias; 36 | 37 | @CreatedBy 38 | @Column(name = "created_by", nullable = false, updatable = false) 39 | private String createdBy; 40 | 41 | @CreatedDate 42 | @Column(name = "created_date", nullable = false, updatable = false) 43 | private Instant createdDate = Instant.now(); 44 | 45 | @LastModifiedBy 46 | @Column(name = "last_modified_by") 47 | private String lastModifiedBy; 48 | 49 | @LastModifiedDate 50 | @Column(name = "last_modified_date") 51 | private Instant lastModifiedDate; 52 | } 53 | -------------------------------------------------------------------------------- /auth-lib/src/main/java/com/company/secureapispring/auth/entities/User.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.auth.entities; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | import org.hibernate.annotations.Cache; 7 | import org.hibernate.annotations.CacheConcurrencyStrategy; 8 | import org.springframework.data.annotation.CreatedDate; 9 | import org.springframework.data.annotation.LastModifiedDate; 10 | 11 | import java.io.Serial; 12 | import java.io.Serializable; 13 | import java.time.Instant; 14 | 15 | @EqualsAndHashCode(onlyExplicitlyIncluded = true) 16 | @Data 17 | @Entity 18 | @Table(name = "users") 19 | @Cache(region = "users", usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) 20 | public class User implements Serializable { 21 | 22 | @Serial 23 | private static final long serialVersionUID = 1L; 24 | 25 | @Id 26 | @GeneratedValue(strategy = GenerationType.IDENTITY) 27 | @Column(name = "id") 28 | private Long id; 29 | 30 | @Column(name = "given_name") 31 | private String givenName; 32 | 33 | @Column(name = "family_name") 34 | private String familyName; 35 | 36 | @EqualsAndHashCode.Include 37 | @Column(unique = true, nullable = false) 38 | private String username; 39 | 40 | @Column(unique = true, nullable = false) 41 | private String email; 42 | 43 | @Column(nullable = false) 44 | private boolean emailVerified = false; 45 | 46 | @CreatedDate 47 | @Column(name = "created_date", nullable = false, updatable = false) 48 | private Instant createdDate = Instant.now(); 49 | 50 | @LastModifiedDate 51 | @Column(name = "last_modified_date") 52 | private Instant lastModifiedDate; 53 | } 54 | -------------------------------------------------------------------------------- /auth-lib/src/main/java/com/company/secureapispring/auth/interfaces/HasOrganization.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.auth.interfaces; 2 | 3 | import com.company.secureapispring.auth.entities.Organization; 4 | 5 | public interface HasOrganization { 6 | void setOrganization(Organization entity); 7 | Organization getOrganization(); 8 | } 9 | -------------------------------------------------------------------------------- /auth-lib/src/main/java/com/company/secureapispring/auth/repositories/OrganizationRepository.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.auth.repositories; 2 | 3 | import com.company.secureapispring.auth.entities.Organization; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.repository.query.Param; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.util.Optional; 9 | 10 | @Repository 11 | public interface OrganizationRepository extends JpaRepository { 12 | Optional findByAlias(@Param("alias") String alias); 13 | } 14 | -------------------------------------------------------------------------------- /auth-lib/src/main/java/com/company/secureapispring/auth/repositories/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.auth.repositories; 2 | 3 | import com.company.secureapispring.auth.entities.User; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.repository.query.Param; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.util.Optional; 9 | 10 | @Repository 11 | public interface UserRepository extends JpaRepository { 12 | Optional findByUsername(@Param("username") String username); 13 | } 14 | -------------------------------------------------------------------------------- /auth-lib/src/main/java/com/company/secureapispring/auth/services/AuthService.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.auth.services; 2 | 3 | import com.company.secureapispring.auth.AuthInfo; 4 | import com.company.secureapispring.auth.entities.Organization; 5 | import com.company.secureapispring.auth.entities.User; 6 | import com.company.secureapispring.auth.interfaces.HasOrganization; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.security.access.AccessDeniedException; 9 | import org.springframework.security.core.context.SecurityContextHolder; 10 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; 11 | import org.springframework.stereotype.Service; 12 | import org.springframework.transaction.annotation.Transactional; 13 | 14 | import java.time.Instant; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | @RequiredArgsConstructor 19 | @Service 20 | @Transactional(readOnly = true) 21 | public class AuthService { 22 | private final OrganizationService organizationService; 23 | private final UserService userService; 24 | 25 | private User extractUser(Map attrs) { 26 | User detachedUser = new User(); 27 | detachedUser.setGivenName((String) attrs.get("given_name")); 28 | detachedUser.setFamilyName((String) attrs.get("family_name")); 29 | detachedUser.setUsername((String) attrs.get("preferred_username")); 30 | detachedUser.setEmail((String) attrs.get("email")); 31 | detachedUser.setEmailVerified(Boolean.TRUE.equals(attrs.get("email_verified"))); 32 | return userService.findByUsername(detachedUser.getUsername()) 33 | .map(user -> { 34 | boolean updated = !user.getEmail().equals(detachedUser.getEmail()); 35 | if (!user.getFamilyName().equals(detachedUser.getFamilyName())) { 36 | updated = true; 37 | } 38 | if (!user.getGivenName().equals(detachedUser.getGivenName())) { 39 | updated = true; 40 | } 41 | if (updated) { 42 | return userService.sync(user, detachedUser); 43 | } else { 44 | return user; 45 | } 46 | }) 47 | .orElseGet(() -> userService.sync(new User(), detachedUser)); 48 | } 49 | 50 | private Organization extractOrganization(Map attrs, User authUser) { 51 | List organizations = (List) attrs.get("organization"); 52 | if (organizations == null || organizations.isEmpty()) { 53 | throw new AccessDeniedException("Authentication does not have organization token."); 54 | } 55 | if (organizations.size() != 1) { 56 | throw new AccessDeniedException("More than one organization in the token is not handled."); 57 | } 58 | Organization detachedOrganization = new Organization(); 59 | detachedOrganization.setAlias(organizations.getFirst()); 60 | return organizationService.findByAlias(detachedOrganization.getAlias()) 61 | .orElseGet(() -> { 62 | detachedOrganization.setCreatedDate(Instant.now()); 63 | detachedOrganization.setCreatedBy(authUser.getUsername()); 64 | return organizationService.sync(new Organization(), detachedOrganization); 65 | }); 66 | } 67 | 68 | public AuthInfo getAuthInfo() { 69 | JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); 70 | if (jwtAuthenticationToken == null) { 71 | throw new AccessDeniedException("Not authenticated."); 72 | } 73 | Map attrs = jwtAuthenticationToken.getTokenAttributes(); 74 | User authUser = extractUser(attrs); 75 | Organization authOrganization = extractOrganization(attrs, authUser); 76 | return new AuthInfo(authUser, authOrganization); 77 | } 78 | 79 | public void setAuthOrganization(HasOrganization entity) { 80 | entity.setOrganization(getAuthInfo().organization()); 81 | } 82 | 83 | public void validateOwnership(HasOrganization entity) { 84 | if (!getAuthInfo().organization().equals(entity.getOrganization())) { 85 | throw new AccessDeniedException("The record does not belongs to your organization."); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /auth-lib/src/main/java/com/company/secureapispring/auth/services/OrganizationService.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.auth.services; 2 | 3 | import com.company.secureapispring.auth.cache.CacheName; 4 | import com.company.secureapispring.auth.entities.Organization; 5 | import com.company.secureapispring.auth.repositories.OrganizationRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.beans.BeanUtils; 8 | import org.springframework.cache.annotation.CacheEvict; 9 | import org.springframework.cache.annotation.Cacheable; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.transaction.annotation.Propagation; 12 | import org.springframework.transaction.annotation.Transactional; 13 | 14 | import java.util.Optional; 15 | 16 | @RequiredArgsConstructor 17 | @Service 18 | @Transactional(readOnly = true) 19 | public class OrganizationService { 20 | private final OrganizationRepository organizationRepository; 21 | 22 | @Cacheable(cacheNames = CacheName.ORG_BY_ALIAS, unless = "#result == null") 23 | public Optional findByAlias(String alias) { 24 | return organizationRepository.findByAlias(alias); 25 | } 26 | 27 | @CacheEvict(cacheNames = CacheName.USER_BY_USERNAME) 28 | @Transactional(propagation = Propagation.REQUIRES_NEW) 29 | public Organization sync(Organization organization, Organization detachedOrganization) { 30 | BeanUtils.copyProperties(detachedOrganization, organization, "id"); 31 | organization = organizationRepository.save(organization); 32 | organizationRepository.flush(); 33 | return organization; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /auth-lib/src/main/java/com/company/secureapispring/auth/services/UserService.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.auth.services; 2 | 3 | import com.company.secureapispring.auth.cache.CacheName; 4 | import com.company.secureapispring.auth.entities.User; 5 | import com.company.secureapispring.auth.repositories.UserRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.beans.BeanUtils; 8 | import org.springframework.cache.annotation.CacheEvict; 9 | import org.springframework.cache.annotation.Cacheable; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.transaction.annotation.Propagation; 12 | import org.springframework.transaction.annotation.Transactional; 13 | 14 | import java.util.Optional; 15 | 16 | @RequiredArgsConstructor 17 | @Service 18 | @Transactional(readOnly = true) 19 | public class UserService { 20 | private final UserRepository userRepository; 21 | 22 | @Cacheable(cacheNames = CacheName.USER_BY_USERNAME, unless = "#result == null") 23 | public Optional findByUsername(String username) { 24 | return userRepository.findByUsername(username); 25 | } 26 | 27 | @CacheEvict(cacheNames = CacheName.USER_BY_USERNAME) 28 | @Transactional(propagation = Propagation.REQUIRES_NEW) 29 | public User sync(User user, User detachedUser) { 30 | BeanUtils.copyProperties(detachedUser, user, "id"); 31 | user = userRepository.save(user); 32 | userRepository.flush(); 33 | return user; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /auth-lib/src/main/resources/liquibase/changelog/000000_auth_initial_schema.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /auth-lib/src/test/java/com/company/secureapispring/auth/AbstractIT.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.auth; 2 | 3 | import com.company.secureapispring.auth.entities.Organization; 4 | import com.company.secureapispring.auth.entities.User; 5 | import net.datafaker.Faker; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.cache.CacheManager; 9 | import org.springframework.test.context.event.annotation.AfterTestClass; 10 | import org.testcontainers.containers.PostgreSQLContainer; 11 | import org.testcontainers.utility.DockerImageName; 12 | 13 | import java.time.Instant; 14 | import java.time.temporal.ChronoUnit; 15 | import java.util.Objects; 16 | 17 | public class AbstractIT { 18 | protected int fakerCounter = 0; 19 | protected static final Faker faker = new Faker(); 20 | 21 | @Autowired 22 | protected CacheManager cacheManager; 23 | 24 | private static final PostgreSQLContainer dbContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:15.6-alpine")) 25 | .withDatabaseName("auth_lib_test") 26 | .withUsername("auth_lib_test") 27 | .withPassword("password"); 28 | 29 | static { 30 | dbContainer.start(); 31 | System.setProperty("DATASOURCE_JDBC_URL_TEST", dbContainer.getJdbcUrl()); 32 | System.setProperty("DATASOURCE_USERNAME_TEST", dbContainer.getUsername()); 33 | System.setProperty("DATASOURCE_PASSWORD_TEST", dbContainer.getPassword()); 34 | } 35 | 36 | @BeforeEach 37 | public void setup() { 38 | cacheManager.getCacheNames().forEach(s -> Objects.requireNonNull(cacheManager.getCache(s)).clear()); 39 | } 40 | 41 | @AfterTestClass 42 | public static void stopContainer() { 43 | dbContainer.stop(); 44 | } 45 | 46 | protected String generateUniqueEmail() { 47 | return faker.internet().emailAddress().replace("@", "+" + (fakerCounter++) + "@"); 48 | } 49 | 50 | protected User makeUser() { 51 | User user = new User(); 52 | user.setEmail(generateUniqueEmail()); 53 | user.setUsername(faker.internet().username()); 54 | user.setGivenName(faker.name().firstName()); 55 | user.setFamilyName(faker.name().lastName()); 56 | user.setEmailVerified(false); 57 | user.setCreatedDate(Instant.now().minusSeconds(86400).truncatedTo(ChronoUnit.MICROS)); 58 | user.setLastModifiedDate(Instant.now().minusSeconds(43200).truncatedTo(ChronoUnit.MICROS)); 59 | return user; 60 | } 61 | 62 | protected Organization makeOrganization() { 63 | String email = generateUniqueEmail(); 64 | String username = email.substring(0, email.indexOf("@")); 65 | 66 | Organization organization = new Organization(); 67 | organization.setAlias(faker.internet().uuid()); 68 | organization.setCreatedDate(Instant.now().minusSeconds(86400).truncatedTo(ChronoUnit.MICROS)); 69 | organization.setCreatedBy(username); 70 | organization.setLastModifiedDate(Instant.now().minusSeconds(43200).truncatedTo(ChronoUnit.MICROS)); 71 | organization.setLastModifiedBy(username); 72 | return organization; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /auth-lib/src/test/java/com/company/secureapispring/auth/AuthLibSpringBootApp.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.auth; 2 | 3 | import com.company.secureapispring.auth.cache.CacheName; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cache.CacheManager; 6 | import org.springframework.cache.annotation.EnableCaching; 7 | import org.springframework.cache.concurrent.ConcurrentMapCacheManager; 8 | import org.springframework.context.annotation.Bean; 9 | 10 | @EnableCaching 11 | @SpringBootApplication 12 | public class AuthLibSpringBootApp { 13 | @Bean 14 | public CacheManager cacheManager() { 15 | return new ConcurrentMapCacheManager(CacheName.USER_BY_USERNAME, CacheName.ORG_BY_ALIAS); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /auth-lib/src/test/java/com/company/secureapispring/auth/AuthLibSpringBootTest.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.auth; 2 | 3 | import org.springframework.boot.test.context.SpringBootTest; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Target(ElementType.TYPE) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @SpringBootTest(classes = {AuthLibSpringBootApp.class}) 13 | public @interface AuthLibSpringBootTest { 14 | } 15 | -------------------------------------------------------------------------------- /auth-lib/src/test/java/com/company/secureapispring/auth/services/AuthServiceIT.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.auth.services; 2 | 3 | import com.company.secureapispring.auth.AbstractIT; 4 | import com.company.secureapispring.auth.AuthInfo; 5 | import com.company.secureapispring.auth.AuthLibSpringBootTest; 6 | import com.company.secureapispring.auth.entities.Organization; 7 | import com.company.secureapispring.auth.entities.User; 8 | import com.company.secureapispring.auth.interfaces.HasOrganization; 9 | import com.company.secureapispring.auth.repositories.OrganizationRepository; 10 | import com.company.secureapispring.auth.repositories.UserRepository; 11 | import com.company.secureapispring.auth.utils.TestJWTUtils; 12 | import org.junit.jupiter.api.Assertions; 13 | import org.junit.jupiter.api.Test; 14 | import org.mockito.Mockito; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.test.mock.mockito.SpyBean; 17 | import org.springframework.security.access.AccessDeniedException; 18 | import org.springframework.security.core.context.SecurityContextHolder; 19 | import org.springframework.security.oauth2.jwt.Jwt; 20 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; 21 | 22 | import java.util.Arrays; 23 | import java.util.Collections; 24 | 25 | @AuthLibSpringBootTest 26 | public class AuthServiceIT extends AbstractIT { 27 | 28 | @Autowired 29 | private AuthService authService; 30 | 31 | @SpyBean 32 | private UserRepository userRepository; 33 | 34 | @SpyBean 35 | private OrganizationRepository organizationRepository; 36 | 37 | private AuthInfo mockAuthenticationContext() { 38 | User user = makeUser(); 39 | Organization organization = makeOrganization(); 40 | 41 | Jwt jwt = TestJWTUtils.createJwt(user, Collections.singletonList(organization.getAlias()), "ROLE_ADMIN"); 42 | JwtAuthenticationToken jwtAuthToken = new JwtAuthenticationToken(jwt); 43 | 44 | SecurityContextHolder.getContext().setAuthentication(jwtAuthToken); 45 | 46 | return new AuthInfo(user, organization); 47 | } 48 | 49 | @Test 50 | public void testGetAuthInfo() { 51 | AuthInfo expectedAuthInfo = mockAuthenticationContext(); 52 | 53 | AuthInfo authInfo = authService.getAuthInfo(); 54 | 55 | Assertions.assertEquals(expectedAuthInfo, authInfo); 56 | 57 | Assertions.assertNotNull(authInfo.user().getId()); 58 | Assertions.assertNotNull(authInfo.organization().getId()); 59 | } 60 | 61 | @Test 62 | public void testGetAuthInfoWhenUserOrgAlreadyExistsAndInfoChanged() { 63 | AuthInfo expectedAuthInfo = mockAuthenticationContext(); 64 | 65 | // make a new user to change the values that are going to be updated 66 | User user = makeUser(); 67 | user.setUsername(expectedAuthInfo.user().getUsername()); 68 | userRepository.save(user); 69 | 70 | authService.getAuthInfo(); 71 | 72 | // when there are updates on the user info, the user on the database should be sync 73 | Mockito.verify(userRepository, Mockito.times(2)).save(Mockito.any()); 74 | Mockito.verify(organizationRepository, Mockito.times(1)).save(Mockito.any()); 75 | } 76 | 77 | @Test 78 | public void testGetAuthInfoWhenUserOrgAlreadyExistsButInfoNotChanged() { 79 | AuthInfo expectedAuthInfo = mockAuthenticationContext(); 80 | 81 | userRepository.save(expectedAuthInfo.user()); 82 | organizationRepository.save(expectedAuthInfo.organization()); 83 | 84 | authService.getAuthInfo(); 85 | 86 | // the only calls to save should be the previous calls 87 | Mockito.verify(userRepository, Mockito.times(1)).save(Mockito.any()); 88 | Mockito.verify(organizationRepository, Mockito.times(1)).save(Mockito.any()); 89 | } 90 | 91 | @Test 92 | public void testGetAuthInfoWhenUnauthenticated() { 93 | Assertions.assertThrows( 94 | AccessDeniedException.class, 95 | () -> authService.getAuthInfo() 96 | ); 97 | } 98 | 99 | @Test 100 | public void testGetAuthInfoTheIsNoOrganizationInTheToken() { 101 | User user = makeUser(); 102 | 103 | // no organization 104 | Jwt jwt = TestJWTUtils.createJwt(user, null, "ROLE_ADMIN"); 105 | JwtAuthenticationToken jwtAuthToken = new JwtAuthenticationToken(jwt); 106 | SecurityContextHolder.getContext().setAuthentication(jwtAuthToken); 107 | 108 | Assertions.assertThrows( 109 | AccessDeniedException.class, 110 | () -> authService.getAuthInfo() 111 | ); 112 | } 113 | 114 | @Test 115 | public void testGetAuthInfoTheIsMoreThanOrganizationInTheToken() { 116 | User user = makeUser(); 117 | 118 | // more than 1 organization 119 | Jwt jwt = TestJWTUtils.createJwt( 120 | user, 121 | Arrays.asList(makeOrganization().getAlias(), makeOrganization().getAlias()), 122 | "ROLE_ADMIN" 123 | ); 124 | JwtAuthenticationToken jwtAuthToken = new JwtAuthenticationToken(jwt); 125 | 126 | SecurityContextHolder.getContext().setAuthentication(jwtAuthToken); 127 | 128 | Assertions.assertThrows( 129 | AccessDeniedException.class, 130 | () -> authService.getAuthInfo() 131 | ); 132 | } 133 | 134 | private HasOrganization mockImplOfHasOrganization() { 135 | return new HasOrganization() { 136 | private Organization organization; 137 | @Override 138 | public void setOrganization(Organization entity) { 139 | this.organization = entity; 140 | } 141 | @Override 142 | public Organization getOrganization() { 143 | return this.organization; 144 | } 145 | }; 146 | } 147 | 148 | @Test 149 | public void testSetAuthOrganization() { 150 | HasOrganization belongsToOrg = mockImplOfHasOrganization(); 151 | 152 | AuthInfo expectedAuthInfo = mockAuthenticationContext(); 153 | 154 | authService.setAuthOrganization(belongsToOrg); 155 | 156 | Assertions.assertEquals(expectedAuthInfo.organization(), belongsToOrg.getOrganization()); 157 | } 158 | 159 | @Test 160 | public void testValidateOwnership() { 161 | HasOrganization belongsToOrg = mockImplOfHasOrganization(); 162 | belongsToOrg.setOrganization(makeOrganization()); 163 | 164 | mockAuthenticationContext(); 165 | 166 | Assertions.assertThrows( 167 | AccessDeniedException.class, 168 | () -> authService.validateOwnership(belongsToOrg) 169 | ); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /auth-lib/src/test/java/com/company/secureapispring/auth/services/OrganizationServiceIT.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.auth.services; 2 | 3 | import com.company.secureapispring.auth.AbstractIT; 4 | import com.company.secureapispring.auth.AuthLibSpringBootTest; 5 | import com.company.secureapispring.auth.entities.Organization; 6 | import com.company.secureapispring.auth.repositories.OrganizationRepository; 7 | import com.company.secureapispring.auth.repositories.UserRepository; 8 | import org.junit.jupiter.api.Assertions; 9 | import org.junit.jupiter.api.Test; 10 | import org.mockito.Mockito; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.mock.mockito.SpyBean; 13 | import org.springframework.transaction.annotation.Transactional; 14 | 15 | @AuthLibSpringBootTest 16 | public class OrganizationServiceIT extends AbstractIT { 17 | 18 | @Autowired 19 | private OrganizationService organizationService; 20 | 21 | @SpyBean 22 | private OrganizationRepository organizationRepository; 23 | 24 | @SpyBean 25 | private UserRepository userRepository; 26 | 27 | @Test 28 | public void testFindByAlias() { 29 | organizationRepository.save(makeOrganization()); 30 | Organization organization = organizationRepository.save(makeOrganization()); 31 | 32 | Organization found = organizationRepository.findByAlias(organization.getAlias()).orElseThrow(); 33 | 34 | Assertions.assertEquals(organization, found); 35 | } 36 | 37 | @Test 38 | public void testFindByAliasIsCacheable() { 39 | Organization organization = organizationRepository.save(makeOrganization()); 40 | 41 | organizationService.findByAlias(organization.getAlias()); 42 | organizationService.findByAlias(organization.getAlias()); 43 | 44 | Mockito.verify(organizationRepository, Mockito.times(1)).findByAlias(organization.getAlias()); 45 | } 46 | 47 | @Test 48 | @Transactional 49 | public void testSyncOrganization() { 50 | // mock a new organization entity to update the existent one 51 | Organization newOrganizationInfo = makeOrganization(); 52 | 53 | // sync a new organization 54 | Organization syncResponse = organizationService.sync(new Organization(), newOrganizationInfo); 55 | 56 | // verify the persisted organization 57 | Organization persistedOrganization = organizationRepository.getReferenceById(syncResponse.getId()); 58 | 59 | Assertions.assertEquals(syncResponse, persistedOrganization); 60 | 61 | Assertions.assertEquals(newOrganizationInfo, persistedOrganization); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /auth-lib/src/test/java/com/company/secureapispring/auth/services/UserServiceIT.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.auth.services; 2 | 3 | import com.company.secureapispring.auth.AbstractIT; 4 | import com.company.secureapispring.auth.AuthLibSpringBootTest; 5 | import com.company.secureapispring.auth.entities.User; 6 | import com.company.secureapispring.auth.repositories.UserRepository; 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | import org.mockito.Mockito; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.mock.mockito.SpyBean; 12 | import org.springframework.transaction.annotation.Transactional; 13 | 14 | @AuthLibSpringBootTest 15 | public class UserServiceIT extends AbstractIT { 16 | 17 | @Autowired 18 | private UserService userService; 19 | 20 | @SpyBean 21 | private UserRepository userRepository; 22 | 23 | @Test 24 | public void testFindByUsername() { 25 | userRepository.save(makeUser()); 26 | User user = userRepository.save(makeUser()); 27 | 28 | User found = userRepository.findByUsername(user.getUsername()).orElseThrow(); 29 | 30 | Assertions.assertEquals(user, found); 31 | } 32 | 33 | @Test 34 | public void testFindByUsernameIsCacheable() { 35 | User user = userRepository.save(makeUser()); 36 | 37 | userService.findByUsername(user.getUsername()); 38 | userService.findByUsername(user.getUsername()); 39 | 40 | Mockito.verify(userRepository, Mockito.times(1)).findByUsername(user.getUsername()); 41 | } 42 | 43 | @Test 44 | @Transactional 45 | public void testSyncNewUser() { 46 | User detachedUser = makeUser(); 47 | User syncResponse = userService.sync(new User(), detachedUser); 48 | 49 | User persistedUser = userRepository.getReferenceById(syncResponse.getId()); 50 | 51 | Assertions.assertEquals(syncResponse, persistedUser); 52 | 53 | Assertions.assertEquals(detachedUser, persistedUser); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /auth-lib/src/test/java/com/company/secureapispring/auth/utils/TestJWTUtils.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.auth.utils; 2 | 3 | import com.company.secureapispring.auth.entities.Organization; 4 | import com.company.secureapispring.auth.entities.User; 5 | import com.nimbusds.jose.JWSAlgorithm; 6 | import com.nimbusds.jose.jwk.source.ImmutableSecret; 7 | import com.nimbusds.jose.jwk.source.JWKSource; 8 | import com.nimbusds.jose.proc.SecurityContext; 9 | import org.springframework.security.oauth2.jwt.*; 10 | 11 | import javax.crypto.SecretKey; 12 | import javax.crypto.spec.SecretKeySpec; 13 | import java.security.SecureRandom; 14 | import java.util.*; 15 | 16 | public final class TestJWTUtils { 17 | private final static NimbusJwtEncoder JWT_ENCODER; 18 | 19 | static { 20 | String secret = Base64.getEncoder().encodeToString(new SecureRandom().generateSeed(32)); 21 | SecretKey key = new SecretKeySpec(secret.getBytes(), JWSAlgorithm.HS256.getName()); 22 | JWKSource immutableSecret = new ImmutableSecret<>(key); 23 | JWT_ENCODER = new NimbusJwtEncoder(immutableSecret); 24 | } 25 | 26 | private TestJWTUtils() {} 27 | 28 | public static Jwt createJwt(User user, List organizationAliases, String... roles) { 29 | JwtClaimsSet claimsSet = JwtClaimsSet.builder() 30 | .issuer("http://localhost:8080/realms/app") 31 | .claims(claims -> { 32 | Map realmAccess = new HashMap<>(); 33 | realmAccess.put("roles", roles); 34 | claims.put("given_name", user.getGivenName()); 35 | claims.put("family_name", user.getFamilyName()); 36 | claims.put("preferred_username", user.getUsername()); 37 | claims.put("email", user.getEmail()); 38 | claims.put("email_verified", user.isEmailVerified()); 39 | if (organizationAliases != null) { 40 | claims.put("organization", organizationAliases); 41 | } 42 | claims.put("realm_access", realmAccess); 43 | }) 44 | .build(); 45 | JwsHeader jwsHeader = JwsHeader.with(JWSAlgorithm.HS256::getName).build(); 46 | return JWT_ENCODER.encode(JwtEncoderParameters.from(jwsHeader, claimsSet)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /auth-lib/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8083 2 | spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9080/realms/app 3 | cors.allowed-origins=* 4 | 5 | spring.profiles.active=${SPRING_PROFILE:test} 6 | spring.datasource.url=${DATASOURCE_JDBC_URL_TEST} 7 | spring.datasource.username=${DATASOURCE_USERNAME_TEST} 8 | spring.datasource.password=${DATASOURCE_PASSWORD_TEST} 9 | 10 | spring.jpa.open-in-view=false 11 | spring.jpa.hibernate.ddl-auto=none 12 | 13 | spring.liquibase.change-log=classpath:liquibase/main.xml 14 | spring.liquibase.contexts=test 15 | spring.liquibase.enabled=true 16 | 17 | # Enable Hibernate Second Level Cache 18 | spring.jpa.properties.hibernate.cache.use_second_level_cache=false 19 | spring.jpa.properties.hibernate.cache.use_query_cache=false 20 | 21 | spring.main.allow-bean-definition-overriding=true 22 | 23 | logging.level.root=ERROR 24 | logging.level.org.hibernate.SQL=ERROR 25 | logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE 26 | -------------------------------------------------------------------------------- /auth-lib/src/test/resources/liquibase/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /customer-svc/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | .mvn/wrapper/maven-wrapper.jar 11 | .idea 12 | -------------------------------------------------------------------------------- /customer-svc/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar 19 | -------------------------------------------------------------------------------- /customer-svc/README.md: -------------------------------------------------------------------------------- 1 | # Secure API Spring - Customer Service 2 | 3 | ## Running tests 4 | 5 | The tests use the Testcontainers to start a PostgreSQL container, so, you can just run: 6 | 7 | `./mvnw install` 8 | 9 | ### Generate coverage test report 10 | 11 | `./mvnw jacoco:report` 12 | 13 | and see `target/site/jacoco/index.html` 14 | 15 | ### Run the application in dev mode 16 | 17 | `./mvnw spring-boot:run` 18 | 19 | ## Deploy with Jenkins to Kubernetes cluster 20 | 21 | See the [README.md](./src/main/jenkins/README.md) for more details. 22 | -------------------------------------------------------------------------------- /customer-svc/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.1.1 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | # e.g. to debug Maven itself, use 32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | # ---------------------------------------------------------------------------- 35 | 36 | if [ -z "$MAVEN_SKIP_RC" ] ; then 37 | 38 | if [ -f /usr/local/etc/mavenrc ] ; then 39 | . /usr/local/etc/mavenrc 40 | fi 41 | 42 | if [ -f /etc/mavenrc ] ; then 43 | . /etc/mavenrc 44 | fi 45 | 46 | if [ -f "$HOME/.mavenrc" ] ; then 47 | . "$HOME/.mavenrc" 48 | fi 49 | 50 | fi 51 | 52 | # OS specific support. $var _must_ be set to either true or false. 53 | cygwin=false; 54 | darwin=false; 55 | mingw=false 56 | case "`uname`" in 57 | CYGWIN*) cygwin=true ;; 58 | MINGW*) mingw=true;; 59 | Darwin*) darwin=true 60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 62 | if [ -z "$JAVA_HOME" ]; then 63 | if [ -x "/usr/libexec/java_home" ]; then 64 | JAVA_HOME="`/usr/libexec/java_home`"; export JAVA_HOME 65 | else 66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME 67 | fi 68 | fi 69 | ;; 70 | esac 71 | 72 | if [ -z "$JAVA_HOME" ] ; then 73 | if [ -r /etc/gentoo-release ] ; then 74 | JAVA_HOME=`java-config --jre-home` 75 | fi 76 | fi 77 | 78 | # For Cygwin, ensure paths are in UNIX format before anything is touched 79 | if $cygwin ; then 80 | [ -n "$JAVA_HOME" ] && 81 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 82 | [ -n "$CLASSPATH" ] && 83 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 84 | fi 85 | 86 | # For Mingw, ensure paths are in UNIX format before anything is touched 87 | if $mingw ; then 88 | [ -n "$JAVA_HOME" ] && 89 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 90 | fi 91 | 92 | if [ -z "$JAVA_HOME" ]; then 93 | javaExecutable="`which javac`" 94 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 95 | # readlink(1) is not available as standard on Solaris 10. 96 | readLink=`which readlink` 97 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 98 | if $darwin ; then 99 | javaHome="`dirname \"$javaExecutable\"`" 100 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 101 | else 102 | javaExecutable="`readlink -f \"$javaExecutable\"`" 103 | fi 104 | javaHome="`dirname \"$javaExecutable\"`" 105 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 106 | JAVA_HOME="$javaHome" 107 | export JAVA_HOME 108 | fi 109 | fi 110 | fi 111 | 112 | if [ -z "$JAVACMD" ] ; then 113 | if [ -n "$JAVA_HOME" ] ; then 114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 115 | # IBM's JDK on AIX uses strange locations for the executables 116 | JAVACMD="$JAVA_HOME/jre/sh/java" 117 | else 118 | JAVACMD="$JAVA_HOME/bin/java" 119 | fi 120 | else 121 | JAVACMD="`\\unset -f command; \\command -v java`" 122 | fi 123 | fi 124 | 125 | if [ ! -x "$JAVACMD" ] ; then 126 | echo "Error: JAVA_HOME is not defined correctly." >&2 127 | echo " We cannot execute $JAVACMD" >&2 128 | exit 1 129 | fi 130 | 131 | if [ -z "$JAVA_HOME" ] ; then 132 | echo "Warning: JAVA_HOME environment variable is not set." 133 | fi 134 | 135 | # traverses directory structure from process work directory to filesystem root 136 | # first directory with .mvn subdirectory is considered project base directory 137 | find_maven_basedir() { 138 | if [ -z "$1" ] 139 | then 140 | echo "Path not specified to find_maven_basedir" 141 | return 1 142 | fi 143 | 144 | basedir="$1" 145 | wdir="$1" 146 | while [ "$wdir" != '/' ] ; do 147 | if [ -d "$wdir"/.mvn ] ; then 148 | basedir=$wdir 149 | break 150 | fi 151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 152 | if [ -d "${wdir}" ]; then 153 | wdir=`cd "$wdir/.."; pwd` 154 | fi 155 | # end of workaround 156 | done 157 | printf '%s' "$(cd "$basedir"; pwd)" 158 | } 159 | 160 | # concatenates all lines of a file 161 | concat_lines() { 162 | if [ -f "$1" ]; then 163 | echo "$(tr -s '\n' ' ' < "$1")" 164 | fi 165 | } 166 | 167 | BASE_DIR=$(find_maven_basedir "$(dirname $0)") 168 | if [ -z "$BASE_DIR" ]; then 169 | exit 1; 170 | fi 171 | 172 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR 173 | if [ "$MVNW_VERBOSE" = true ]; then 174 | echo $MAVEN_PROJECTBASEDIR 175 | fi 176 | 177 | ########################################################################################## 178 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 179 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 180 | ########################################################################################## 181 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 182 | if [ "$MVNW_VERBOSE" = true ]; then 183 | echo "Found .mvn/wrapper/maven-wrapper.jar" 184 | fi 185 | else 186 | if [ "$MVNW_VERBOSE" = true ]; then 187 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 188 | fi 189 | if [ -n "$MVNW_REPOURL" ]; then 190 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 191 | else 192 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 193 | fi 194 | while IFS="=" read key value; do 195 | case "$key" in (wrapperUrl) wrapperUrl="$value"; break ;; 196 | esac 197 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 198 | if [ "$MVNW_VERBOSE" = true ]; then 199 | echo "Downloading from: $wrapperUrl" 200 | fi 201 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 202 | if $cygwin; then 203 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 204 | fi 205 | 206 | if command -v wget > /dev/null; then 207 | QUIET="--quiet" 208 | if [ "$MVNW_VERBOSE" = true ]; then 209 | echo "Found wget ... using wget" 210 | QUIET="" 211 | fi 212 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 213 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" 214 | else 215 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" 216 | fi 217 | [ $? -eq 0 ] || rm -f "$wrapperJarPath" 218 | elif command -v curl > /dev/null; then 219 | QUIET="--silent" 220 | if [ "$MVNW_VERBOSE" = true ]; then 221 | echo "Found curl ... using curl" 222 | QUIET="" 223 | fi 224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L 226 | else 227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L 228 | fi 229 | [ $? -eq 0 ] || rm -f "$wrapperJarPath" 230 | else 231 | if [ "$MVNW_VERBOSE" = true ]; then 232 | echo "Falling back to using Java to download" 233 | fi 234 | javaSource="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 235 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" 236 | # For Cygwin, switch paths to Windows format before running javac 237 | if $cygwin; then 238 | javaSource=`cygpath --path --windows "$javaSource"` 239 | javaClass=`cygpath --path --windows "$javaClass"` 240 | fi 241 | if [ -e "$javaSource" ]; then 242 | if [ ! -e "$javaClass" ]; then 243 | if [ "$MVNW_VERBOSE" = true ]; then 244 | echo " - Compiling MavenWrapperDownloader.java ..." 245 | fi 246 | # Compiling the Java class 247 | ("$JAVA_HOME/bin/javac" "$javaSource") 248 | fi 249 | if [ -e "$javaClass" ]; then 250 | # Running the downloader 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo " - Running MavenWrapperDownloader.java ..." 253 | fi 254 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 255 | fi 256 | fi 257 | fi 258 | fi 259 | ########################################################################################## 260 | # End of extension 261 | ########################################################################################## 262 | 263 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 264 | 265 | # For Cygwin, switch paths to Windows format before running java 266 | if $cygwin; then 267 | [ -n "$JAVA_HOME" ] && 268 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 269 | [ -n "$CLASSPATH" ] && 270 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 271 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 272 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 273 | fi 274 | 275 | # Provide a "standardized" way to retrieve the CLI args that will 276 | # work with both Windows and non-Windows executions. 277 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 278 | export MAVEN_CMD_LINE_ARGS 279 | 280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 281 | 282 | exec "$JAVACMD" \ 283 | $MAVEN_OPTS \ 284 | $MAVEN_DEBUG_OPTS \ 285 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 286 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 287 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 288 | -------------------------------------------------------------------------------- /customer-svc/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Apache Maven Wrapper startup batch script, version 3.1.1 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 28 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 29 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 30 | @REM e.g. to debug Maven itself, use 31 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 32 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 33 | @REM ---------------------------------------------------------------------------- 34 | 35 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 36 | @echo off 37 | @REM set title of command window 38 | title %0 39 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 40 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 41 | 42 | @REM set %HOME% to equivalent of $HOME 43 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 44 | 45 | @REM Execute a user defined script before this one 46 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 47 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 48 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 49 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 50 | :skipRcPre 51 | 52 | @setlocal 53 | 54 | set ERROR_CODE=0 55 | 56 | @REM To isolate internal variables from possible post scripts, we use another setlocal 57 | @setlocal 58 | 59 | @REM ==== START VALIDATION ==== 60 | if not "%JAVA_HOME%" == "" goto OkJHome 61 | 62 | echo. 63 | echo Error: JAVA_HOME not found in your environment. >&2 64 | echo Please set the JAVA_HOME variable in your environment to match the >&2 65 | echo location of your Java installation. >&2 66 | echo. 67 | goto error 68 | 69 | :OkJHome 70 | if exist "%JAVA_HOME%\bin\java.exe" goto init 71 | 72 | echo. 73 | echo Error: JAVA_HOME is set to an invalid directory. >&2 74 | echo JAVA_HOME = "%JAVA_HOME%" >&2 75 | echo Please set the JAVA_HOME variable in your environment to match the >&2 76 | echo location of your Java installation. >&2 77 | echo. 78 | goto error 79 | 80 | @REM ==== END VALIDATION ==== 81 | 82 | :init 83 | 84 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 85 | @REM Fallback to current working directory if not found. 86 | 87 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 88 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 89 | 90 | set EXEC_DIR=%CD% 91 | set WDIR=%EXEC_DIR% 92 | :findBaseDir 93 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 94 | cd .. 95 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 96 | set WDIR=%CD% 97 | goto findBaseDir 98 | 99 | :baseDirFound 100 | set MAVEN_PROJECTBASEDIR=%WDIR% 101 | cd "%EXEC_DIR%" 102 | goto endDetectBaseDir 103 | 104 | :baseDirNotFound 105 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 106 | cd "%EXEC_DIR%" 107 | 108 | :endDetectBaseDir 109 | 110 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 111 | 112 | @setlocal EnableExtensions EnableDelayedExpansion 113 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 114 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 115 | 116 | :endReadAdditionalConfig 117 | 118 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 123 | 124 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 125 | IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | if "%MVNW_VERBOSE%" == "true" ( 132 | echo Found %WRAPPER_JAR% 133 | ) 134 | ) else ( 135 | if not "%MVNW_REPOURL%" == "" ( 136 | SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 137 | ) 138 | if "%MVNW_VERBOSE%" == "true" ( 139 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 140 | echo Downloading from: %WRAPPER_URL% 141 | ) 142 | 143 | powershell -Command "&{"^ 144 | "$webclient = new-object System.Net.WebClient;"^ 145 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 146 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 147 | "}"^ 148 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ 149 | "}" 150 | if "%MVNW_VERBOSE%" == "true" ( 151 | echo Finished downloading %WRAPPER_JAR% 152 | ) 153 | ) 154 | @REM End of extension 155 | 156 | @REM Provide a "standardized" way to retrieve the CLI args that will 157 | @REM work with both Windows and non-Windows executions. 158 | set MAVEN_CMD_LINE_ARGS=%* 159 | 160 | %MAVEN_JAVA_EXE% ^ 161 | %JVM_CONFIG_MAVEN_PROPS% ^ 162 | %MAVEN_OPTS% ^ 163 | %MAVEN_DEBUG_OPTS% ^ 164 | -classpath %WRAPPER_JAR% ^ 165 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 166 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 167 | if ERRORLEVEL 1 goto error 168 | goto end 169 | 170 | :error 171 | set ERROR_CODE=1 172 | 173 | :end 174 | @endlocal & set ERROR_CODE=%ERROR_CODE% 175 | 176 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 177 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 178 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 179 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 180 | :skipRcPost 181 | 182 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 183 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 184 | 185 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 186 | 187 | cmd /C exit /B %ERROR_CODE% 188 | -------------------------------------------------------------------------------- /customer-svc/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | customer-svc 6 | 0.0.1-SNAPSHOT 7 | 8 | com.company.secureapispring 9 | secure-api-spring-parent 10 | 0.0.1-SNAPSHOT 11 | 12 | 13 | 14 | org.springframework.boot 15 | spring-boot-starter-web 16 | 17 | 18 | org.springframework.boot 19 | spring-boot-starter-data-jpa 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-validation 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-devtools 28 | runtime 29 | true 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-oauth2-resource-server 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-security 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-cache 42 | 43 | 44 | com.company.secureapispring 45 | auth-lib 46 | 47 | 48 | net.datafaker 49 | datafaker 50 | test 51 | 52 | 53 | org.liquibase 54 | liquibase-core 55 | 56 | 57 | org.testcontainers 58 | junit-jupiter 59 | test 60 | 61 | 62 | org.postgresql 63 | postgresql 64 | runtime 65 | 66 | 67 | 68 | 69 | org.testcontainers 70 | postgresql 71 | test 72 | 73 | 74 | org.springdoc 75 | springdoc-openapi-starter-webmvc-ui 76 | 77 | 78 | com.google.code.findbugs 79 | jsr305 80 | 81 | 82 | org.springframework.boot 83 | spring-boot-starter-data-redis 84 | 85 | 86 | org.redisson 87 | redisson-spring-boot-starter 88 | 89 | 90 | 91 | org.redisson 92 | redisson-hibernate-6 93 | 94 | 95 | 96 | 97 | 98 | 99 | org.springframework.boot 100 | spring-boot-maven-plugin 101 | 102 | 103 | 104 | org.projectlombok 105 | lombok 106 | 107 | 108 | 109 | 110 | 111 | build-info 112 | 113 | build-info 114 | 115 | 116 | 117 | 118 | 119 | org.apache.maven.plugins 120 | maven-surefire-plugin 121 | 122 | **/*IT.java 123 | 124 | 125 | 126 | org.jacoco 127 | jacoco-maven-plugin 128 | 129 | ${basedir}/target/coverage-reports/jacoco-unit.exec 130 | ${basedir}/target/coverage-reports/jacoco-unit.exec 131 | file 132 | true 133 | 134 | *MethodAccess 135 | 136 | 137 | 138 | 139 | jacoco-initialize 140 | 141 | prepare-agent 142 | 143 | test-compile 144 | 145 | 146 | jacoco-site 147 | verify 148 | 149 | report 150 | 151 | 152 | 153 | 154 | 155 | maven-resources-plugin 156 | 157 | 158 | ${*} 159 | 160 | 161 | 162 | 163 | copy-resources 164 | package 165 | 166 | copy-resources 167 | 168 | 169 | ${basedir}/target/kubernetes 170 | 171 | 172 | src/main/kubernetes 173 | true 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /customer-svc/src/main/java/com/company/secureapispring/customer/CustomerSvcApp.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.autoconfigure.domain.EntityScan; 6 | import org.springframework.context.annotation.ComponentScan; 7 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 8 | 9 | @SpringBootApplication 10 | @ComponentScan(basePackages = { 11 | "com.company.secureapispring.auth", 12 | "com.company.secureapispring.customer", 13 | }) 14 | @EnableJpaRepositories( 15 | basePackages = { 16 | "com.company.secureapispring.auth", 17 | "com.company.secureapispring.customer", 18 | } 19 | ) 20 | @EntityScan( 21 | basePackages = { 22 | "com.company.secureapispring.auth", 23 | "com.company.secureapispring.customer", 24 | } 25 | ) 26 | public class CustomerSvcApp { 27 | 28 | public static void main(String[] args) { 29 | SpringApplication.run(CustomerSvcApp.class, args); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /customer-svc/src/main/java/com/company/secureapispring/customer/cache/CacheConfig.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.cache; 2 | 3 | import org.redisson.Redisson; 4 | import org.redisson.api.RedissonClient; 5 | import org.redisson.config.Config; 6 | import org.redisson.spring.cache.RedissonSpringCacheManager; 7 | import org.springframework.cache.CacheManager; 8 | import org.springframework.cache.annotation.EnableCaching; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | import java.nio.file.Files; 13 | import java.nio.file.Paths; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | import java.util.Objects; 17 | 18 | @Configuration 19 | @EnableCaching 20 | public class CacheConfig { 21 | 22 | @Bean(destroyMethod="shutdown") 23 | RedissonClient redisson() throws Exception { 24 | String yamlConfig = Files.readString( 25 | Paths.get(Objects.requireNonNull( 26 | CacheConfig.class.getClassLoader().getResource("redisson.yaml")).toURI() 27 | )); 28 | Config config = Config.fromYAML(yamlConfig); 29 | return Redisson.create(config); 30 | } 31 | 32 | @Bean 33 | CacheManager cacheManager(RedissonClient redissonClient) { 34 | Map config = new HashMap<>(); 35 | int oneHour = 3600 * 1000; 36 | config.put( 37 | com.company.secureapispring.auth.cache.CacheName.USER_BY_USERNAME, 38 | new org.redisson.spring.cache.CacheConfig(oneHour, oneHour) 39 | ); 40 | config.put( 41 | com.company.secureapispring.auth.cache.CacheName.ORG_BY_ALIAS, 42 | new org.redisson.spring.cache.CacheConfig(oneHour, oneHour) 43 | ); 44 | config.put(CacheName.ALL_COUNTRIES, new org.redisson.spring.cache.CacheConfig(oneHour, oneHour)); 45 | config.put(CacheName.ALL_STATE_PROVINCES, new org.redisson.spring.cache.CacheConfig(oneHour, oneHour)); 46 | return new RedissonSpringCacheManager(redissonClient, config); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /customer-svc/src/main/java/com/company/secureapispring/customer/cache/CacheName.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.cache; 2 | 3 | public final class CacheName { 4 | public static final String ALL_COUNTRIES = "cs.allCountries"; 5 | public static final String ALL_STATE_PROVINCES = "cs.allStateProvinces"; 6 | private CacheName() {} 7 | } 8 | -------------------------------------------------------------------------------- /customer-svc/src/main/java/com/company/secureapispring/customer/controllers/AppController.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.controllers; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.boot.info.BuildProperties; 5 | import org.springframework.cache.Cache; 6 | import org.springframework.cache.CacheManager; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.security.access.annotation.Secured; 9 | import org.springframework.security.access.prepost.PreAuthorize; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.ResponseStatus; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import java.util.Optional; 16 | 17 | @RestController 18 | @RequestMapping("/app") 19 | @RequiredArgsConstructor 20 | public class AppController { 21 | private final BuildProperties buildProperties; 22 | private final CacheManager cacheManager; 23 | 24 | @GetMapping(value = "/build") 25 | @PreAuthorize("isAuthenticated()") 26 | public BuildProperties getAppVersion() { 27 | return buildProperties; 28 | } 29 | 30 | @ResponseStatus(HttpStatus.NO_CONTENT) 31 | @GetMapping("/clear-cache") 32 | @Secured(value = {"ROLE_ADMIN"}) 33 | public void clearCache() { 34 | cacheManager.getCacheNames().forEach(s -> { 35 | Optional.ofNullable(cacheManager.getCache(s)).ifPresent(Cache::clear); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /customer-svc/src/main/java/com/company/secureapispring/customer/controllers/CountryController.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.controllers; 2 | 3 | import com.company.secureapispring.customer.entities.Country; 4 | import com.company.secureapispring.customer.entities.StateProvince; 5 | import com.company.secureapispring.customer.services.CountryService; 6 | import com.company.secureapispring.customer.services.StateProvinceService; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.security.access.prepost.PreAuthorize; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import java.util.List; 15 | 16 | @RestController 17 | @RequestMapping("/countries") 18 | @RequiredArgsConstructor 19 | public class CountryController { 20 | private final CountryService countryService; 21 | private final StateProvinceService stateProvinceService; 22 | 23 | @PreAuthorize("isAuthenticated()") 24 | @GetMapping 25 | public List findAll() { 26 | return countryService.findAll(); 27 | } 28 | 29 | @PreAuthorize("isAuthenticated()") 30 | @GetMapping("/{countryId}/state-provinces") 31 | public List findStateProvinces(@PathVariable Integer countryId) { 32 | return stateProvinceService.findAll(countryId); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /customer-svc/src/main/java/com/company/secureapispring/customer/controllers/CustomerController.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.controllers; 2 | 3 | import com.company.secureapispring.customer.entities.Customer; 4 | import com.company.secureapispring.customer.exceptions.BadRequestException; 5 | import com.company.secureapispring.customer.services.CustomerService; 6 | import com.fasterxml.jackson.annotation.JsonView; 7 | import jakarta.validation.Valid; 8 | import jakarta.validation.constraints.NotNull; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.security.access.annotation.Secured; 13 | import org.springframework.web.bind.annotation.*; 14 | 15 | import java.util.List; 16 | import java.util.Objects; 17 | 18 | @RestController 19 | @RequestMapping("/customers") 20 | @RequiredArgsConstructor 21 | public class CustomerController { 22 | private final CustomerService customerService; 23 | 24 | @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) 25 | @ResponseStatus(HttpStatus.CREATED) 26 | @Secured("ROLE_ADMIN") 27 | public Customer create(@Valid @RequestBody Customer input) { 28 | if (input.getId() != null) { 29 | throw (new BadRequestException()).withError("id", "must be null"); 30 | } 31 | return customerService.create(input); 32 | } 33 | 34 | @GetMapping("/{id}") 35 | @Secured(value = {"ROLE_ADMIN", "ROLE_ANALYST"}) 36 | public Customer get(@NotNull @PathVariable Long id) { 37 | return customerService.get(id); 38 | } 39 | 40 | @JsonView(Customer.ListJsonView.class) 41 | @GetMapping 42 | @Secured(value = {"ROLE_ADMIN", "ROLE_ANALYST"}) 43 | public List listAll() { 44 | return customerService.listAll(); 45 | } 46 | 47 | @PutMapping(path = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) 48 | @Secured("ROLE_ADMIN") 49 | public Customer update(@PathVariable Long id, @Valid @RequestBody Customer input) { 50 | if (!Objects.equals(id, input.getId())) { 51 | throw (new BadRequestException()).withError("id", "must be equals id from url"); 52 | } 53 | return customerService.update(input); 54 | } 55 | 56 | @DeleteMapping(path = "/{id}") 57 | @Secured("ROLE_ADMIN") 58 | public Customer delete(@NotNull @PathVariable Long id) { 59 | return customerService.delete(id); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /customer-svc/src/main/java/com/company/secureapispring/customer/entities/AbstractAuditingEntity.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.entities; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.EntityListeners; 6 | import jakarta.persistence.MappedSuperclass; 7 | import lombok.Data; 8 | import org.springframework.data.annotation.CreatedBy; 9 | import org.springframework.data.annotation.CreatedDate; 10 | import org.springframework.data.annotation.LastModifiedBy; 11 | import org.springframework.data.annotation.LastModifiedDate; 12 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 13 | 14 | import java.io.Serial; 15 | import java.io.Serializable; 16 | import java.time.Instant; 17 | 18 | @Data 19 | @MappedSuperclass 20 | @EntityListeners(AuditingEntityListener.class) 21 | @JsonIgnoreProperties(value = { "createdBy", "createdDate", "lastModifiedBy", "lastModifiedDate" }, allowGetters = true) 22 | public abstract class AbstractAuditingEntity implements Serializable { 23 | 24 | @Serial 25 | private static final long serialVersionUID = 1L; 26 | 27 | @CreatedBy 28 | @Column(name = "created_by", nullable = false, updatable = false) 29 | private String createdBy; 30 | 31 | @CreatedDate 32 | @Column(name = "created_date", nullable = false, updatable = false) 33 | private Instant createdDate = Instant.now(); 34 | 35 | @LastModifiedBy 36 | @Column(name = "last_modified_by") 37 | private String lastModifiedBy; 38 | 39 | @LastModifiedDate 40 | @Column(name = "last_modified_date") 41 | private Instant lastModifiedDate; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /customer-svc/src/main/java/com/company/secureapispring/customer/entities/Country.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.entities; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | import org.hibernate.annotations.Cache; 7 | import org.hibernate.annotations.CacheConcurrencyStrategy; 8 | 9 | import java.io.Serializable; 10 | 11 | @Entity 12 | @Table(name = "countries") 13 | @EqualsAndHashCode(onlyExplicitlyIncluded = true) 14 | @Data 15 | @Cache(region = "countries", usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) 16 | public class Country implements Serializable { 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | @EqualsAndHashCode.Include 20 | private Integer id; 21 | 22 | @Column 23 | private String abbreviation; 24 | 25 | @Column 26 | private String name; 27 | } 28 | -------------------------------------------------------------------------------- /customer-svc/src/main/java/com/company/secureapispring/customer/entities/Customer.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.entities; 2 | 3 | import com.company.secureapispring.auth.entities.Organization; 4 | import com.company.secureapispring.auth.interfaces.HasOrganization; 5 | import com.fasterxml.jackson.annotation.JsonIgnore; 6 | import com.fasterxml.jackson.annotation.JsonView; 7 | import io.swagger.v3.oas.annotations.media.Schema; 8 | import jakarta.persistence.*; 9 | import jakarta.validation.constraints.Email; 10 | import jakarta.validation.constraints.NotBlank; 11 | import jakarta.validation.constraints.NotNull; 12 | import jakarta.validation.constraints.Size; 13 | import lombok.Data; 14 | import lombok.EqualsAndHashCode; 15 | 16 | @EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) 17 | @Data 18 | @Entity 19 | @Table(name = "customers") 20 | public class Customer extends AbstractAuditingEntity implements HasOrganization { 21 | 22 | @Schema(accessMode = Schema.AccessMode.READ_ONLY) 23 | @JsonView(Customer.ListJsonView.class) 24 | @EqualsAndHashCode.Include 25 | @Id 26 | @GeneratedValue(strategy = GenerationType.IDENTITY) 27 | private Long id; 28 | 29 | @JsonView(Customer.ListJsonView.class) 30 | @NotBlank 31 | @Size(min = 1, max = 255) 32 | @Column(name = "first_name", nullable = false) 33 | private String firstName; 34 | 35 | @JsonView(Customer.ListJsonView.class) 36 | @NotBlank 37 | @Size(min = 1, max = 255) 38 | @Column(name = "last_name", nullable = false) 39 | private String lastName; 40 | 41 | @JsonView(Customer.ListJsonView.class) 42 | @NotBlank 43 | @Email 44 | @Size(min = 5, max = 255) 45 | @Column(name = "email", nullable = false, unique = true) 46 | private String email; 47 | 48 | @NotBlank 49 | @Size(min = 1, max = 255) 50 | @Column(name = "address", nullable = false) 51 | private String address; 52 | 53 | @Column(name = "address2") 54 | private String address2; 55 | 56 | @Size(min = 1, max = 30) 57 | @NotBlank 58 | @Column(name = "postal_code", nullable = false, length = 30) 59 | private String postalCode; 60 | 61 | @NotNull 62 | @ManyToOne(optional = false, fetch = FetchType.LAZY) 63 | @JoinColumn(name = "state_province_id") 64 | private StateProvince stateProvince; 65 | 66 | @JsonIgnore 67 | @Schema(accessMode = Schema.AccessMode.READ_ONLY) 68 | @ManyToOne(optional = false, fetch = FetchType.LAZY) 69 | @JoinColumn(name = "organization_id", updatable = false) 70 | private Organization organization; 71 | 72 | public static class ListJsonView { 73 | private ListJsonView() { 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /customer-svc/src/main/java/com/company/secureapispring/customer/entities/StateProvince.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.entities; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | 7 | import java.io.Serializable; 8 | 9 | @Entity 10 | @Table(name = "state_provinces") 11 | @EqualsAndHashCode(onlyExplicitlyIncluded = true) 12 | @Data 13 | public class StateProvince implements Serializable { 14 | @Id 15 | @GeneratedValue(strategy = GenerationType.IDENTITY) 16 | @EqualsAndHashCode.Include 17 | private Integer id; 18 | 19 | @Column 20 | private String abbreviation; 21 | 22 | @Column 23 | private String name; 24 | 25 | @ManyToOne(optional = false) 26 | @JoinColumn(name = "country_id") 27 | private Country country; 28 | } -------------------------------------------------------------------------------- /customer-svc/src/main/java/com/company/secureapispring/customer/exceptions/BadRequestException.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.http.HttpStatusCode; 5 | import org.springframework.http.ProblemDetail; 6 | import org.springframework.web.ErrorResponse; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | public class BadRequestException extends RuntimeException implements ErrorResponse { 12 | private final Map errors = new HashMap<>(); 13 | private final ProblemDetail body; 14 | 15 | public BadRequestException() { 16 | super("Invalid request content."); 17 | this.body = ProblemDetail.forStatusAndDetail(this.getStatusCode(), "Invalid request content."); 18 | this.body.setProperty("errors", this.errors); 19 | } 20 | 21 | public BadRequestException withError(String propertyName, String message) { 22 | this.errors.put(propertyName, message); 23 | return this; 24 | } 25 | 26 | @Override 27 | public HttpStatusCode getStatusCode() { 28 | return HttpStatus.BAD_REQUEST; 29 | } 30 | 31 | @Override 32 | public ProblemDetail getBody() { 33 | return this.body; 34 | } 35 | 36 | 37 | } 38 | -------------------------------------------------------------------------------- /customer-svc/src/main/java/com/company/secureapispring/customer/exceptions/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.exceptions; 2 | 3 | import jakarta.persistence.EntityNotFoundException; 4 | import lombok.extern.log4j.Log4j2; 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.springframework.dao.DataIntegrityViolationException; 7 | import org.springframework.http.*; 8 | import org.springframework.lang.Nullable; 9 | import org.springframework.validation.FieldError; 10 | import org.springframework.web.bind.MethodArgumentNotValidException; 11 | import org.springframework.web.bind.annotation.ExceptionHandler; 12 | import org.springframework.web.bind.annotation.RestControllerAdvice; 13 | import org.springframework.web.context.request.WebRequest; 14 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 15 | 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | import java.util.regex.Matcher; 19 | import java.util.regex.Pattern; 20 | 21 | @Log4j2 22 | @RestControllerAdvice 23 | public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { 24 | @ExceptionHandler(EntityNotFoundException.class) 25 | protected ProblemDetail handleEntityNotFoundException(EntityNotFoundException ex) { 26 | return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage()); 27 | } 28 | 29 | @ExceptionHandler(BadRequestException.class) 30 | protected ProblemDetail handleEntityNotFoundException(BadRequestException ex) { 31 | return ex.getBody(); 32 | } 33 | 34 | /** 35 | * Maps DataIntegrityViolationException to a 409 Conflict HTTP status code. 36 | */ 37 | @ExceptionHandler({ DataIntegrityViolationException.class }) 38 | public ProblemDetail handleDataIntegrityViolationException(DataIntegrityViolationException ex) { 39 | String regex = "(\\bviolates foreign key constraint\\b|\\bviolates unique constraint\\b) \"([^\"]+)\""; 40 | Matcher matcher = Pattern.compile(regex).matcher(ex.getMessage()); 41 | String constraintName = matcher.find() ? matcher.group(2) : StringUtils.EMPTY; 42 | String message = getMessage(constraintName, ex); 43 | return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, message); 44 | } 45 | 46 | @Override 47 | @Nullable 48 | protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { 49 | Map errors = new HashMap<>(); 50 | ex.getBindingResult().getAllErrors().forEach((error) -> { 51 | String fieldName = ((FieldError) error).getField(); 52 | String errorMessage = error.getDefaultMessage(); 53 | errors.put(fieldName, errorMessage); 54 | }); 55 | ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getBody().getDetail()); 56 | problemDetail.setProperty("errors", errors); 57 | return this.handleExceptionInternal(ex, problemDetail, headers, status, request); 58 | } 59 | 60 | protected String getMessage(String constraintName, DataIntegrityViolationException ex) { 61 | String message; 62 | switch (constraintName) { 63 | case "fk_state_province_id" -> message = "Please enter a valid State/Province."; 64 | case "customers_email_key" -> message = "There is already a customer registered with this email address. Please use a different email."; 65 | case "fk_organization_id" -> message = "Organization was not found."; 66 | default -> { 67 | log.error("The constraint name '{}' is unknown. Detail message: {}", constraintName, ex.getMessage()); 68 | message = "One or more relationship of the object is not valid."; 69 | } 70 | } 71 | return message; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /customer-svc/src/main/java/com/company/secureapispring/customer/repositories/CountryRepository.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.repositories; 2 | 3 | import com.company.secureapispring.customer.entities.Country; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface CountryRepository extends JpaRepository { 9 | } 10 | -------------------------------------------------------------------------------- /customer-svc/src/main/java/com/company/secureapispring/customer/repositories/CustomerRepository.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.repositories; 2 | 3 | import com.company.secureapispring.customer.entities.Customer; 4 | import org.springframework.data.domain.Sort; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.data.jpa.repository.Query; 7 | import org.springframework.data.repository.query.Param; 8 | import org.springframework.stereotype.Repository; 9 | 10 | import java.util.List; 11 | import java.util.Optional; 12 | 13 | @Repository 14 | public interface CustomerRepository extends JpaRepository { 15 | @Query("SELECT c FROM Customer c JOIN FETCH c.stateProvince WHERE c.id = (:id)") 16 | Optional findByIdWithStateProvince(@Param("id") Long id); 17 | 18 | @Query("SELECT c FROM Customer c WHERE c.organization.id = (:organizationId)") 19 | List findAllByOrganizationId(@Param("organizationId") Long organizationId, Sort sort); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /customer-svc/src/main/java/com/company/secureapispring/customer/repositories/StateProvinceRepository.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.repositories; 2 | 3 | import com.company.secureapispring.customer.entities.StateProvince; 4 | import org.springframework.data.domain.Sort; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.util.List; 9 | 10 | @Repository 11 | public interface StateProvinceRepository extends JpaRepository { 12 | List findAllByCountryId(Integer countryId, Sort sortable); 13 | } 14 | -------------------------------------------------------------------------------- /customer-svc/src/main/java/com/company/secureapispring/customer/security/OpenAPISecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.security; 2 | 3 | import io.swagger.v3.oas.models.Components; 4 | import io.swagger.v3.oas.models.OpenAPI; 5 | import io.swagger.v3.oas.models.info.Info; 6 | import io.swagger.v3.oas.models.security.*; 7 | import org.springframework.boot.info.BuildProperties; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.core.env.Environment; 11 | 12 | @Configuration 13 | public class OpenAPISecurityConfig { 14 | 15 | private final Environment env; 16 | private final BuildProperties buildProperties; 17 | 18 | private static final String OAUTH_SCHEME_NAME = "OAuthSecuritySchema"; 19 | 20 | public OpenAPISecurityConfig(Environment env, BuildProperties buildProperties) { 21 | this.env = env; 22 | this.buildProperties = buildProperties; 23 | } 24 | 25 | @Bean 26 | public OpenAPI openAPI() { 27 | return new OpenAPI().components(new Components() 28 | .addSecuritySchemes(OAUTH_SCHEME_NAME, createOAuthScheme())) 29 | .addSecurityItem(new SecurityRequirement().addList(OAUTH_SCHEME_NAME)) 30 | .info(new Info().title(env.getRequiredProperty("app.name")) 31 | .description(env.getRequiredProperty("app.description")) 32 | .version(buildProperties.getVersion())) 33 | ; 34 | } 35 | 36 | private SecurityScheme createOAuthScheme() { 37 | OAuthFlows flows = createOAuthFlows(); 38 | return new SecurityScheme().type(SecurityScheme.Type.OAUTH2) 39 | .flows(flows); 40 | } 41 | 42 | private OAuthFlows createOAuthFlows() { 43 | OAuthFlow flow = createAuthorizationCodeFlow(); 44 | return new OAuthFlows().implicit(flow); 45 | } 46 | 47 | private OAuthFlow createAuthorizationCodeFlow() { 48 | String authorizationUrl = env.getRequiredProperty("spring.security.oauth2.resourceserver.jwt.issuer-uri"); 49 | return new OAuthFlow() 50 | .authorizationUrl(authorizationUrl + "/protocol/openid-connect/auth") 51 | .scopes(new Scopes().addString("read_access", "read data") 52 | .addString("write_access", "modify data") 53 | ) 54 | ; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /customer-svc/src/main/java/com/company/secureapispring/customer/security/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.security; 2 | 3 | import com.company.secureapispring.auth.domain.AuditAwareImpl; 4 | import lombok.extern.log4j.Log4j2; 5 | import org.springframework.cache.annotation.EnableCaching; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.context.annotation.Profile; 9 | import org.springframework.core.convert.converter.Converter; 10 | import org.springframework.core.env.Environment; 11 | import org.springframework.data.domain.AuditorAware; 12 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 13 | import org.springframework.security.authentication.AbstractAuthenticationToken; 14 | import org.springframework.security.config.Customizer; 15 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 16 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 17 | import org.springframework.security.config.http.SessionCreationPolicy; 18 | import org.springframework.security.core.GrantedAuthority; 19 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 20 | import org.springframework.security.oauth2.jwt.Jwt; 21 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; 22 | import org.springframework.security.web.SecurityFilterChain; 23 | import org.springframework.web.cors.CorsConfiguration; 24 | import org.springframework.web.cors.CorsConfigurationSource; 25 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 26 | 27 | import java.util.Arrays; 28 | import java.util.Collection; 29 | import java.util.Collections; 30 | import java.util.Map; 31 | import java.util.stream.Collectors; 32 | 33 | @Log4j2 34 | @Configuration 35 | @EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true) 36 | @EnableJpaAuditing 37 | @EnableCaching 38 | public class SecurityConfig { 39 | 40 | private final String[] allowedOrigins; 41 | 42 | public SecurityConfig(Environment environment) { 43 | this.allowedOrigins = environment.getRequiredProperty("cors.allowed-origins", String[].class); 44 | } 45 | 46 | @Bean 47 | public AuditorAware auditorProvider() { 48 | return new AuditAwareImpl(); 49 | } 50 | 51 | // https://docs.spring.io/spring-security/reference/6.1/servlet/integrations/cors.html#page-title 52 | @Bean 53 | public CorsConfigurationSource corsConfigurationSource() { 54 | log.info("CORS Allowed Origins: {}", Arrays.toString(allowedOrigins)); 55 | CorsConfiguration configuration = new CorsConfiguration(); 56 | configuration.setAllowedHeaders(Arrays.asList("Access-Control-Allow-Headers", "Origin", "Content-Type", "Accept", "Authorization")); 57 | configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "OPTIONS", "DELETE", "PATCH")); 58 | configuration.setAllowedOrigins(Arrays.asList(this.allowedOrigins)); 59 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 60 | source.registerCorsConfiguration("/**", configuration); 61 | return source; 62 | } 63 | 64 | // https://docs.spring.io/spring-security/reference/reactive/oauth2/resource-server/jwt.html 65 | @Bean 66 | @Profile("open-api") 67 | public SecurityFilterChain devFilterChain(HttpSecurity http) throws Exception { 68 | return http.authorizeHttpRequests(auth -> auth 69 | .requestMatchers("/docs/**").permitAll() 70 | .requestMatchers("/swagger-ui/**").permitAll() 71 | .anyRequest() 72 | .authenticated() 73 | ) 74 | .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) 75 | .oauth2ResourceServer(oauth2 -> oauth2 76 | .jwt(jwt -> jwt.jwtAuthenticationConverter(grantedAuthoritiesExtractor()))) 77 | .cors(Customizer.withDefaults()) 78 | .build(); 79 | } 80 | 81 | @Bean 82 | @Profile("!open-api") 83 | public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { 84 | return http.authorizeHttpRequests(auth -> auth 85 | .anyRequest() 86 | .authenticated() 87 | ) 88 | .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) 89 | .oauth2ResourceServer(oauth2 -> oauth2 90 | .jwt(jwt -> jwt.jwtAuthenticationConverter(grantedAuthoritiesExtractor()))) 91 | .cors(Customizer.withDefaults()) 92 | .build(); 93 | } 94 | 95 | private Converter grantedAuthoritiesExtractor() { 96 | JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); 97 | jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new GrantedAuthoritiesExtractor()); 98 | return jwtAuthenticationConverter; 99 | } 100 | 101 | static class GrantedAuthoritiesExtractor implements Converter> { 102 | public Collection convert(Jwt jwt) { 103 | return ( 104 | (Map>) jwt.getClaims().getOrDefault("realm_access", Collections.emptyMap()) 105 | ).getOrDefault("roles", Collections.emptyList()) 106 | .stream() 107 | .map(Object::toString) 108 | .map(SimpleGrantedAuthority::new) 109 | .collect(Collectors.toList()); 110 | } 111 | } 112 | 113 | } -------------------------------------------------------------------------------- /customer-svc/src/main/java/com/company/secureapispring/customer/services/CountryService.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.services; 2 | 3 | import com.company.secureapispring.customer.cache.CacheName; 4 | import com.company.secureapispring.customer.entities.Country; 5 | import com.company.secureapispring.customer.repositories.CountryRepository; 6 | import jakarta.transaction.Transactional; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.cache.annotation.Cacheable; 9 | import org.springframework.data.domain.Sort; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.List; 13 | 14 | @Service 15 | @Transactional 16 | @RequiredArgsConstructor 17 | public class CountryService { 18 | private final CountryRepository countryRepository; 19 | 20 | @Cacheable(cacheNames = CacheName.ALL_COUNTRIES, unless = "#result == null") 21 | public List findAll() { 22 | return countryRepository.findAll(Sort.by("name")); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /customer-svc/src/main/java/com/company/secureapispring/customer/services/CustomerService.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.services; 2 | 3 | import com.company.secureapispring.auth.entities.Organization; 4 | import com.company.secureapispring.auth.services.AuthService; 5 | import com.company.secureapispring.customer.entities.Customer; 6 | import com.company.secureapispring.customer.repositories.CustomerRepository; 7 | import jakarta.persistence.EntityNotFoundException; 8 | import jakarta.transaction.Transactional; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.beans.BeanUtils; 11 | import org.springframework.data.domain.Sort; 12 | import org.springframework.stereotype.Service; 13 | 14 | import java.util.List; 15 | 16 | @Service 17 | @Transactional 18 | @RequiredArgsConstructor 19 | public class CustomerService { 20 | private final AuthService authService; 21 | private final CustomerRepository customerRepository; 22 | 23 | public Customer create(Customer input) { 24 | Customer customer = new Customer(); 25 | BeanUtils.copyProperties(input, customer); 26 | authService.setAuthOrganization(customer); 27 | return customerRepository.save(customer); 28 | } 29 | 30 | public Customer update(Customer input) { 31 | Customer customer = customerRepository.findById(input.getId()).orElseThrow(EntityNotFoundException::new); 32 | authService.validateOwnership(customer); 33 | BeanUtils.copyProperties(input, customer); 34 | return customerRepository.save(customer); 35 | } 36 | 37 | public Customer delete(Long id) { 38 | Customer customer = customerRepository.findByIdWithStateProvince(id).orElseThrow(EntityNotFoundException::new); 39 | authService.validateOwnership(customer); 40 | customerRepository.delete(customer); 41 | return customer; 42 | } 43 | 44 | public Customer get(Long id) { 45 | Customer customer = customerRepository.findByIdWithStateProvince(id) 46 | .orElseThrow(EntityNotFoundException::new); 47 | authService.validateOwnership(customer); 48 | return customer; 49 | } 50 | 51 | public List listAll() { 52 | Organization organization = authService.getAuthInfo().organization(); 53 | return customerRepository.findAllByOrganizationId(organization.getId(), Sort.by("lastName", "firstName")); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /customer-svc/src/main/java/com/company/secureapispring/customer/services/StateProvinceService.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.services; 2 | 3 | import com.company.secureapispring.customer.cache.CacheName; 4 | import com.company.secureapispring.customer.entities.StateProvince; 5 | import com.company.secureapispring.customer.repositories.StateProvinceRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.cache.annotation.Cacheable; 8 | import org.springframework.data.domain.Sort; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.util.List; 12 | 13 | @Service 14 | @RequiredArgsConstructor 15 | public class StateProvinceService { 16 | private final StateProvinceRepository stateProvinceRepository; 17 | 18 | @Cacheable(cacheNames = CacheName.ALL_STATE_PROVINCES, unless = "#result == null") 19 | public List findAll(Integer countryId) { 20 | return stateProvinceRepository.findAllByCountryId(countryId, Sort.by("name")); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /customer-svc/src/main/jenkins/Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | options { 3 | timeout(time: 5, unit: 'MINUTES') 4 | disableConcurrentBuilds() 5 | } 6 | agent { 7 | // Must have Docker Pipeline and Docker Commons Plugin Installed 8 | // The image name could be used but with the Dockerfile it's possible customize the image if needed 9 | dockerfile { 10 | filename 'Dockerfile' 11 | dir './docker/jenkins' 12 | args """ 13 | -u root:root 14 | -v /home/jenkins/.m2:/root/.m2 15 | -v /home/jenkins/.customer-svc-deploys:/root/.customer-svc-deploys 16 | -v /var/run/docker.sock:/var/run/docker.sock 17 | """ 18 | } 19 | } 20 | stages { 21 | stage('Setup') { 22 | steps { 23 | script { 24 | env.PROJECT_ARTIFACT = "customer-svc" 25 | env.APP_VERSION = sh( 26 | script: "./mvnw -pl ${env.PROJECT_ARTIFACT} help:evaluate -Dexpression=project.version -q -DforceStdout", 27 | returnStdout: true 28 | ).trim() 29 | env.ENV_NAME = "${env.BRANCH_NAME}" 30 | env.SPRING_APP_PORT = 8081 31 | env.NAMESPACE = "default" 32 | env.REPLICAS = "1" 33 | env.LOCAL_IMAGE_TAG = "${env.PROJECT_ARTIFACT}:${env.APP_VERSION}" 34 | env.REMOTE_IMAGE_TAG = "${DOCKER_REGISTRY_NAMESPACE}/${env.PROJECT_ARTIFACT}:${env.APP_VERSION}-${BUILD_NUMBER}" 35 | env.CHANGE_CAUSE = "App version ${env.APP_VERSION}, build #${BUILD_NUMBER}" 36 | } 37 | } 38 | } 39 | stage('Test') { 40 | steps { 41 | sh """ 42 | ./mvnw test -P${PROJECT_ARTIFACT} 43 | """ 44 | } 45 | // Must have JaCoCo plugin installed 46 | post { 47 | always { 48 | junit "${env.PROJECT_ARTIFACT}/target/surefire-reports/*.xml" 49 | step( [ $class: 'JacocoPublisher' ] ) 50 | } 51 | } 52 | } 53 | stage('Package') { 54 | steps { 55 | sh """ 56 | ./mvnw install -P${env.PROJECT_ARTIFACT} -DskipTests 57 | """ 58 | } 59 | } 60 | // This stage push the docker image of the project to PUBLIC repository on Docker Hub. 61 | // MAKE SURE TO CHANGE THIS PIPELINE IT IF YOU WANT IT PRIVATE. 62 | stage('Build and Push the Docker Image') { 63 | steps { 64 | withCredentials([ 65 | usernamePassword(credentialsId: 'docker-credentials', usernameVariable: 'REGISTRY_USERNAME', passwordVariable: 'REGISTRY_PASSWORD'), 66 | ]) { 67 | sh """ 68 | ./mvnw spring-boot:build-image -f ${env.PROJECT_ARTIFACT} -DskipTests 69 | docker login --username ${REGISTRY_USERNAME} --password ${REGISTRY_PASSWORD} 70 | docker tag ${env.LOCAL_IMAGE_TAG} ${env.REMOTE_IMAGE_TAG} 71 | docker push ${env.REMOTE_IMAGE_TAG} 72 | """ 73 | } 74 | } 75 | } 76 | stage('Deploy') { 77 | steps { 78 | withCredentials([file(credentialsId: 'kube-config', variable: 'KUBE_CONFIG_FILE')]) { 79 | script { 80 | def DEPLOYMENT_FILE_NAME="deploy-${env.PROJECT_ARTIFACT}-${env.APP_VERSION}.yaml"; 81 | sh """ 82 | mkdir -p /root/.kube/ 83 | cp $KUBE_CONFIG_FILE /root/.kube/config 84 | envsubst < ${env.PROJECT_ARTIFACT}/src/main/kubernetes/templates/deploy.yaml > "${env.PROJECT_ARTIFACT}/target/${DEPLOYMENT_FILE_NAME}" 85 | cp ${env.PROJECT_ARTIFACT}/target/${DEPLOYMENT_FILE_NAME} /root/.customer-svc-deploys/${DEPLOYMENT_FILE_NAME} 86 | kubectl apply -f ${env.PROJECT_ARTIFACT}/target/${DEPLOYMENT_FILE_NAME} 87 | """ 88 | } 89 | } 90 | } 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /customer-svc/src/main/jenkins/README.md: -------------------------------------------------------------------------------- 1 | ## Jenkins 2 | 3 | ### Required plugins 4 | 5 | - Docker Pipeline. 6 | - Docker Commons. 7 | - JaCoCo. 8 | - SSH Agent. 9 | 10 | ### Required credentials (Manage Jenkins -> Credentials) 11 | 12 | - Create a credential of type `Username with password` named `docker-credentials` using your Docker Hub login details. 13 | - Create a credential of type `Secret file` named `kube-config`, using your kube config file. 14 | 15 | Note: You can retrieve the kube config file by running the command `microk8s config`. 16 | 17 | ### Required environment variables (Manage Jenkins -> System -> Global properties) 18 | 19 | - DOCKER_REGISTRY_NAMESPACE: When deploying the built image to Docker Hub (publicly accessible), create this environment variable and set it to your Docker username. 20 | 21 | ### Pipeline 22 | 23 | Create a New Item of type `Pipeline Multibranch`. 24 | 25 | - In the `Branch Sources` section, select GitHub as the source. 26 | - In the Behaviors section, add the option `Filter by name (with regular expression)` and use `^(main|develop)` as the regular expression. 27 | - Set the `Script Path` to `customer-svc/src/main/jenkins/Jenkinsfile`. 28 | 29 | See the [Jenkinsfile](./Jenkinsfile) for more details. 30 | 31 | ### How to debug/test the Jenkins agent image 32 | 33 | In the root directory of the repository, run the following command: 34 | 35 | `./task.sh docker:jenkins:up`. 36 | 37 | This will build the Jenkins Agent image and run it as a container. To access the container, use the following command: 38 | 39 | `./task.sh docker:jenkins:bash`. 40 | 41 | ### Troubleshooting - Error can not connect to Ryuk at 172.17.0.1:32768 42 | 43 | When building the project on the Jenkins server, if you encounter the following error during the Jenkins build: 44 | 45 | ```bash 46 | 04:44:03.434 [main] INFO tc.testcontainers/ryuk:0.7.0 -- Container testcontainers/ryuk:0.7.0 started in PT0.718419967S 47 | 04:44:08.444 [testcontainers-ryuk] WARN org.testcontainers.utility.RyukResourceReaper -- Can not connect to Ryuk at 172.17.0.1:32768 48 | java.net.SocketTimeoutException: Connect timed out 49 | ... 50 | ``` 51 | 52 | Make sure to allow the IP (Docker network) 172.17.0.1 into your firewall, e.g.: 53 | 54 | `ufw allow from 172.17.0.0/16` 55 | -------------------------------------------------------------------------------- /customer-svc/src/main/kubernetes/templates/config-map.yaml: -------------------------------------------------------------------------------- 1 | # Example of config map for K8s 2 | kind: ConfigMap 3 | apiVersion: v1 4 | metadata: 5 | name: "customer-svc-main" 6 | namespace: default 7 | data: 8 | LOG_LEVEL: "ERROR" 9 | OIDC_AUTH_SERVER_URL: "https://auth.example.com/realms/secure-api-spring" 10 | DATASOURCE_JDBC_URL: "jdbc:postgresql://postgresql.default.svc.cluster.local:5432/customer-svc" 11 | SPRING_APP_PORT: "8081" 12 | SPRING_APP_CONTEXT_PATH: "/customer-svc" 13 | CORS_ORIGINS: "*" 14 | SPRING_PROFILE_ACTIVE: "dev" 15 | REDIS_HOST: "redis.default.svc.cluster.local" 16 | REDIS_PORT: "6379" 17 | REDIS_DATABASE: "0" 18 | -------------------------------------------------------------------------------- /customer-svc/src/main/kubernetes/templates/deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: ${PROJECT_ARTIFACT}-${ENV_NAME} 6 | name: ${PROJECT_ARTIFACT}-${ENV_NAME} 7 | namespace: ${NAMESPACE} 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app.kubernetes.io/name: ${PROJECT_ARTIFACT}-${ENV_NAME} 13 | template: 14 | metadata: 15 | labels: 16 | app.kubernetes.io/name: ${PROJECT_ARTIFACT}-${ENV_NAME} 17 | name: ${PROJECT_ARTIFACT}-${ENV_NAME} 18 | namespace: ${NAMESPACE} 19 | annotations: 20 | kubernetes.io/change-cause: "${CHANGE_CAUSE}" 21 | spec: 22 | containers: 23 | - env: 24 | - name: KUBERNETES_NAMESPACE 25 | value: ${NAMESPACE} 26 | - name: APP_VERSION 27 | value: "${APP_VERSION}" 28 | - name: BUILD_NUMBER 29 | value: "${BUILD_NUMBER}" 30 | envFrom: 31 | - configMapRef: 32 | name: ${PROJECT_ARTIFACT}-${ENV_NAME} 33 | optional: false 34 | - secretRef: 35 | name: ${PROJECT_ARTIFACT}-${ENV_NAME} 36 | optional: false 37 | image: ${REMOTE_IMAGE_TAG} 38 | imagePullPolicy: IfNotPresent 39 | name: ${PROJECT_ARTIFACT}-${ENV_NAME} 40 | ports: 41 | - containerPort: ${SPRING_APP_PORT} 42 | name: http 43 | protocol: TCP 44 | resources: 45 | limits: 46 | cpu: 1 47 | memory: 768Mi 48 | requests: 49 | cpu: 500m 50 | memory: 128Mi 51 | imagePullSecrets: 52 | - name: registry-credentials 53 | -------------------------------------------------------------------------------- /customer-svc/src/main/kubernetes/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: "customer-svc-main" 5 | type: Opaque 6 | stringData: 7 | DATASOURCE_USERNAME: "" 8 | DATASOURCE_PASSWORD: "" 9 | 10 | -------------------------------------------------------------------------------- /customer-svc/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Properties reference 2 | # https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#appendix.application-properties.json 3 | app.name="Secure API with Spring Boot 3" 4 | app.description="This git mono repository is an example Java REST API application that was configured to use Keycloak as access management." 5 | 6 | server.port=${SPRING_APP_PORT:8081} 7 | server.servlet.context-path=${SPRING_APP_CONTEXT_PATH:} 8 | 9 | spring.profiles.active=${SPRING_PROFILE_ACTIVE:dev,open-api} 10 | 11 | spring.datasource.url=${DATASOURCE_JDBC_URL:jdbc:postgresql://localhost:5432/customers} 12 | spring.datasource.username=${DATASOURCE_USERNAME:customers} 13 | spring.datasource.password=${DATASOURCE_PASSWORD:password} 14 | 15 | spring.jpa.open-in-view=false 16 | spring.jpa.hibernate.ddl-auto=none 17 | 18 | spring.liquibase.change-log=classpath:liquibase/main.xml 19 | spring.liquibase.contexts=dev 20 | spring.liquibase.enabled=true 21 | 22 | springdoc.api-docs.path=/docs 23 | 24 | # https://docs.spring.io/spring-security/reference/reactive/oauth2/resource-server/jwt.html 25 | spring.security.oauth2.resourceserver.jwt.issuer-uri=${OIDC_AUTH_SERVER_URL:http://localhost:9080/realms/app} 26 | 27 | cors.allowed-origins=${CORS_ORIGINS:http://localhost:4200} 28 | 29 | logging.level.root=${LOG_LEVEL:INFO} 30 | logging.level.org.hibernate.SQL=DEBUG 31 | #logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE 32 | #logging.level.org.springframework.security=DEBUG 33 | #logging.level.springdoc.api-docs.path=DEBUG 34 | 35 | # Enable Hibernate Second Level Cache 36 | spring.jpa.properties.hibernate.cache.use_second_level_cache=true 37 | spring.jpa.properties.hibernate.cache.use_query_cache=false 38 | spring.jpa.properties.hibernate.cache.region.factory_class=org.redisson.hibernate.RedissonRegionFactory 39 | spring.jpa.properties.hibernate.cache.region_prefix=hbn 40 | spring.jpa.properties.hibernate.cache.redisson.config=redisson.yaml 41 | -------------------------------------------------------------------------------- /customer-svc/src/main/resources/liquibase/changelog/000000_initial_schema.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 133 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /customer-svc/src/main/resources/liquibase/data/countries.csv: -------------------------------------------------------------------------------- 1 | id,abbreviation,name 2 | 1,CAN,Canada 3 | 2,USA,United States 4 | -------------------------------------------------------------------------------- /customer-svc/src/main/resources/liquibase/data/state-provinces.csv: -------------------------------------------------------------------------------- 1 | id,abbreviation,name,country_id 2 | 1,AB,Alberta,1 3 | 2,BC,British Columbia,1 4 | 3,MB,Manitoba,1 5 | 4,NB,New Brunswick,1 6 | 5,NL,Newfoundland and Labrador,1 7 | 6,NS,Nova Scotia,1 8 | 7,ON,Ontario,1 9 | 8,PE,Prince Edward Island,1 10 | 9,QC,Quebec,1 11 | 10,SK,Saskatchewan,1 12 | 11,NT,Northwest Territories,1 13 | 12,NU,Nunavut,1 14 | 13,YT,Yukon,1 15 | 14,AL,Alabama,2 16 | 15,AK,Alaska,2 17 | 16,AZ,Arizona,2 18 | 17,AR,Arkansas,2 19 | 18,CA,California,2 20 | 19,CO,Colorado,2 21 | 20,CT,Connecticut,2 22 | 21,DE,Delaware,2 23 | 22,FL,Florida,2 24 | 23,GA,Georgia,2 25 | 24,HI,Hawaii,2 26 | 25,ID,Idaho,2 27 | 26,IL,Illinois,2 28 | 27,IN,Indiana,2 29 | 28,IA,Iowa,2 30 | 29,KS,Kansas,2 31 | 30,KY,Kentucky,2 32 | 31,LA,Louisiana,2 33 | 32,ME,Maine,2 34 | 33,MD,Maryland,2 35 | 34,MA,Massachusetts,2 36 | 35,MI,Michigan,2 37 | 36,MN,Minnesota,2 38 | 37,MS,Mississippi,2 39 | 38,MO,Missouri,2 40 | 39,MT,Montana,2 41 | 40,NE,Nebraska,2 42 | 41,NV,Nevada,2 43 | 42,NH,New Hampshire,2 44 | 43,NJ,New Jersey,2 45 | 44,NM,New Mexico,2 46 | 45,NY,New York,2 47 | 46,NC,North Carolina,2 48 | 47,ND,North Dakota,2 49 | 48,OH,Ohio,2 50 | 49,OK,Oklahoma,2 51 | 50,OR,Oregon,2 52 | 51,PA,Pennsylvania,2 53 | 52,RI,Rhode Island,2 54 | 53,SC,South Carolina,2 55 | 54,SD,South Dakota,2 56 | 55,TN,Tennessee,2 57 | 56,TX,Texas,2 58 | 57,UT,Utah,2 59 | 58,VT,Vermont,2 60 | 59,VA,Virginia,2 61 | 60,WA,Washington,2 62 | 61,WV,West Virginia,2 63 | 62,WI,Wisconsin,2 64 | 63,WY,Wyoming,2 65 | -------------------------------------------------------------------------------- /customer-svc/src/main/resources/liquibase/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /customer-svc/src/main/resources/redisson.yaml: -------------------------------------------------------------------------------- 1 | # Config reference 2 | # https://redisson.org/docs/configuration/ 3 | singleServerConfig: 4 | idleConnectionTimeout: 10000 5 | connectTimeout: 10000 6 | timeout: 3000 7 | retryAttempts: 3 8 | retryInterval: 1500 9 | password: null 10 | subscriptionsPerConnection: 5 11 | clientName: null 12 | address: "redis://${REDIS_HOST:-localhost}:${REDIS_PORT:-6379}" 13 | subscriptionConnectionMinimumIdleSize: 1 14 | subscriptionConnectionPoolSize: ${REDIS_CONNECTION_POOL_SIZE:-5} 15 | connectionMinimumIdleSize: ${REDIS_CONNECTION_MINIMUM_IDLE_SIZE:-2} 16 | connectionPoolSize: ${REDIS_CONNECTION_POOL_SIZE:-10} 17 | database: ${REDIS_DATABASE:-0} 18 | dnsMonitoringInterval: 5000 19 | threads: ${REDIS_THREADS:-4} 20 | nettyThreads: ${REDIS_NETTY_THREADS:-8} 21 | # https://redisson.org/docs/data-and-services/data-serialization/ 22 | codec: ! {} 23 | # codec: ! {} 24 | transportMode: "NIO" 25 | 26 | -------------------------------------------------------------------------------- /customer-svc/src/test/java/com/company/secureapispring/customer/AbstractIT.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer; 2 | 3 | import com.company.secureapispring.auth.entities.Organization; 4 | import com.company.secureapispring.auth.entities.User; 5 | import com.company.secureapispring.customer.factory.EntityFactory; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.cache.CacheManager; 9 | import org.springframework.test.context.event.annotation.AfterTestClass; 10 | import org.testcontainers.containers.GenericContainer; 11 | import org.testcontainers.containers.PostgreSQLContainer; 12 | import org.testcontainers.utility.DockerImageName; 13 | 14 | import java.util.Objects; 15 | 16 | public abstract class AbstractIT { 17 | 18 | private User dechatedUser; 19 | private Organization detachedOrganization; 20 | 21 | @Autowired 22 | protected CacheManager cacheManager; 23 | 24 | private static final PostgreSQLContainer dbContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:15.6-alpine")) 25 | .withDatabaseName("customers_test") 26 | .withUsername("customers_test") 27 | .withPassword("password"); 28 | 29 | private static final GenericContainer redis = new GenericContainer<>(DockerImageName.parse("redis:alpine")) 30 | .withExposedPorts(6379); 31 | 32 | static { 33 | dbContainer.start(); 34 | System.setProperty("DATASOURCE_JDBC_URL_TEST", dbContainer.getJdbcUrl()); 35 | System.setProperty("DATASOURCE_USERNAME_TEST", dbContainer.getUsername()); 36 | System.setProperty("DATASOURCE_PASSWORD_TEST", dbContainer.getPassword()); 37 | 38 | redis.start(); 39 | System.setProperty("REDIS_HOST", redis.getHost()); 40 | System.setProperty("REDIS_PORT", redis.getMappedPort(6379).toString()); 41 | } 42 | 43 | @BeforeEach 44 | public void setup() { 45 | this.dechatedUser = EntityFactory.user().make(); 46 | this.detachedOrganization = EntityFactory.organization().make(); 47 | cacheManager.getCacheNames().forEach(s -> Objects.requireNonNull(cacheManager.getCache(s)).clear()); 48 | } 49 | 50 | @AfterTestClass 51 | public static void stopContainer() { 52 | dbContainer.stop(); 53 | } 54 | 55 | protected User getDechatedUser() { 56 | return this.dechatedUser; 57 | } 58 | 59 | protected Organization getDetachedOrganization() { 60 | return this.detachedOrganization; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /customer-svc/src/test/java/com/company/secureapispring/customer/CustomerSvcSpringBootAppTest.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer; 2 | 3 | import org.springframework.boot.test.context.SpringBootTest; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Target(ElementType.TYPE) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @SpringBootTest(classes = {TestConfiguration.class, CustomerSvcApp.class}) 13 | public @interface CustomerSvcSpringBootAppTest { 14 | } 15 | -------------------------------------------------------------------------------- /customer-svc/src/test/java/com/company/secureapispring/customer/TestConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.security.oauth2.jwt.JwtDecoder; 5 | 6 | class TestConfiguration { 7 | @Bean 8 | public JwtDecoder jwtDecoder() { 9 | return TestJWTUtils::decode; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /customer-svc/src/test/java/com/company/secureapispring/customer/TestJWTUtils.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer; 2 | 3 | import com.company.secureapispring.auth.entities.Organization; 4 | import com.company.secureapispring.auth.entities.User; 5 | import com.nimbusds.jose.JWSAlgorithm; 6 | import com.nimbusds.jose.jwk.source.ImmutableSecret; 7 | import com.nimbusds.jose.jwk.source.JWKSource; 8 | import com.nimbusds.jose.proc.SecurityContext; 9 | import org.springframework.security.oauth2.jwt.*; 10 | 11 | import javax.crypto.SecretKey; 12 | import javax.crypto.spec.SecretKeySpec; 13 | import java.util.Collections; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | public final class TestJWTUtils { 18 | private final static String SECRET = "q3t6w9z$C&F)J@NcRfUjXnZr4u7x!A%D"; 19 | private final static NimbusJwtEncoder JWT_ENCODER; 20 | private final static NimbusJwtDecoder JWT_DECODER; 21 | 22 | static { 23 | SecretKey key = new SecretKeySpec(SECRET.getBytes(), JWSAlgorithm.HS256.getName()); 24 | JWKSource immutableSecret = new ImmutableSecret<>(key); 25 | JWT_ENCODER = new NimbusJwtEncoder(immutableSecret); 26 | JWT_DECODER = NimbusJwtDecoder.withSecretKey(key).build(); 27 | } 28 | 29 | private TestJWTUtils() {} 30 | 31 | private static Jwt createJwt(User user, Organization organization, String... roles) { 32 | JwtClaimsSet claimsSet = JwtClaimsSet.builder() 33 | .issuer("http://localhost:8080/realms/app") 34 | .claims(claims -> { 35 | Map realmAccess = new HashMap<>(); 36 | realmAccess.put("roles", roles); 37 | claims.put("given_name", user.getGivenName()); 38 | claims.put("family_name", user.getFamilyName()); 39 | claims.put("preferred_username", user.getUsername()); 40 | claims.put("email", user.getEmail()); 41 | claims.put("email_verified", user.isEmailVerified()); 42 | claims.put("organization", Collections.singletonList(organization.getAlias())); 43 | claims.put("realm_access", realmAccess); 44 | }) 45 | .build(); 46 | JwsHeader jwsHeader = JwsHeader.with(JWSAlgorithm.HS256::getName).build(); 47 | return JWT_ENCODER.encode(JwtEncoderParameters.from(jwsHeader, claimsSet)); 48 | } 49 | 50 | public static String encode(User user, Organization organization, String... roles) { 51 | return createJwt(user, organization, roles).getTokenValue(); 52 | } 53 | 54 | public static Jwt decode(String jwtToken) { 55 | return JWT_DECODER.decode(jwtToken); 56 | } 57 | 58 | public static String getAuthHeader(User user, Organization organization, String... roles) { 59 | return "Bearer " + TestJWTUtils.encode(user, organization, roles); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /customer-svc/src/test/java/com/company/secureapispring/customer/controllers/AppControllerIT.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.controllers; 2 | 3 | import com.company.secureapispring.customer.AbstractIT; 4 | import com.company.secureapispring.customer.CustomerSvcSpringBootAppTest; 5 | import com.company.secureapispring.customer.TestJWTUtils; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 9 | import org.springframework.http.HttpHeaders; 10 | import org.springframework.test.web.servlet.MockMvc; 11 | 12 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 13 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 14 | 15 | @AutoConfigureMockMvc 16 | @CustomerSvcSpringBootAppTest 17 | public class AppControllerIT extends AbstractIT { 18 | private static final String ENDPOINT = "/app"; 19 | 20 | @Autowired 21 | private MockMvc mockMvc; 22 | 23 | @Test 24 | public void testGetAppBuild() throws Exception { 25 | mockMvc.perform(get(AppControllerIT.ENDPOINT + "/build") 26 | .header(HttpHeaders.AUTHORIZATION, TestJWTUtils.getAuthHeader( 27 | this.getDechatedUser(), 28 | this.getDetachedOrganization(), 29 | "any" 30 | ))) 31 | .andExpect(status().isOk()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /customer-svc/src/test/java/com/company/secureapispring/customer/controllers/CountryControllerIT.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.controllers; 2 | 3 | 4 | import com.company.secureapispring.customer.AbstractIT; 5 | import com.company.secureapispring.customer.CustomerSvcSpringBootAppTest; 6 | import com.company.secureapispring.customer.TestJWTUtils; 7 | import com.company.secureapispring.customer.entities.Country; 8 | import com.company.secureapispring.customer.entities.StateProvince; 9 | import com.company.secureapispring.customer.factory.EntityFactory; 10 | import com.company.secureapispring.customer.repositories.CountryRepository; 11 | import com.company.secureapispring.customer.repositories.StateProvinceRepository; 12 | import org.junit.jupiter.api.Test; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 15 | import org.springframework.http.HttpHeaders; 16 | import org.springframework.test.web.servlet.MockMvc; 17 | import org.springframework.transaction.annotation.Transactional; 18 | 19 | import java.util.ArrayList; 20 | import java.util.Comparator; 21 | import java.util.List; 22 | 23 | import static org.hamcrest.Matchers.*; 24 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 25 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 26 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 27 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 28 | 29 | @AutoConfigureMockMvc 30 | @CustomerSvcSpringBootAppTest 31 | @Transactional 32 | public class CountryControllerIT extends AbstractIT { 33 | 34 | private static final String ENDPOINT = "/countries"; 35 | private static final String STATE_PROVINCES_ENDPOINT = "/countries/%d/state-provinces"; 36 | 37 | @Autowired 38 | private MockMvc mockMvc; 39 | 40 | @Autowired 41 | private CountryRepository countryRepository; 42 | 43 | @Autowired 44 | private StateProvinceRepository stateProvinceRepository; 45 | 46 | @Test 47 | public void testFindAllCountriesWhenAuthenticated() throws Exception { 48 | List expectedCountries = new ArrayList<>(); 49 | expectedCountries.add(EntityFactory 50 | .country() 51 | .with(Country::setAbbreviation, "USA") 52 | .persist(countryRepository)); 53 | expectedCountries.add(EntityFactory 54 | .country() 55 | .with(Country::setAbbreviation, "BRA") 56 | .persist(countryRepository)); 57 | expectedCountries.add(EntityFactory 58 | .country() 59 | .with(Country::setAbbreviation, "CAN") 60 | .persist(countryRepository)); 61 | expectedCountries.sort(Comparator.comparing(Country::getName)); 62 | 63 | mockMvc.perform(get(CountryControllerIT.ENDPOINT) 64 | .header(HttpHeaders.AUTHORIZATION, TestJWTUtils.getAuthHeader( 65 | this.getDechatedUser(), 66 | this.getDetachedOrganization(), 67 | "any" 68 | ))) 69 | .andExpect(status().isOk()) 70 | .andExpect(jsonPath("$", hasSize(expectedCountries.size()))) 71 | .andExpect(jsonPath("[0].name", equalTo(expectedCountries.getFirst().getName()))) 72 | .andExpect(jsonPath(String.format( 73 | "[%d].name", 74 | expectedCountries.size()-1), 75 | equalTo(expectedCountries.getLast().getName()) 76 | )); 77 | } 78 | 79 | @Test 80 | public void testFindAllCountriesWithoutAuthentication() throws Exception { 81 | this.mockMvc.perform( 82 | get(CountryControllerIT.ENDPOINT) 83 | ) 84 | .andDo(print()) 85 | .andExpect(status().isUnauthorized()); 86 | } 87 | 88 | private String getBaseEndpointForStateProvinces(Integer countryId) { 89 | return String.format(CountryControllerIT.STATE_PROVINCES_ENDPOINT, countryId); 90 | } 91 | 92 | @Test 93 | public void testFindAllStateProvincesWhenAuthenticated() throws Exception { 94 | Country country = EntityFactory 95 | .country() 96 | .persist(countryRepository); 97 | 98 | List expectedStateProvinces = new ArrayList<>(); 99 | expectedStateProvinces.add(EntityFactory 100 | .stateProvince() 101 | .with(StateProvince::setAbbreviation, "BC") 102 | .with(StateProvince::setCountry, country) 103 | .persist(stateProvinceRepository)); 104 | expectedStateProvinces.add(EntityFactory 105 | .stateProvince() 106 | .with(StateProvince::setAbbreviation, "AB") 107 | .with(StateProvince::setCountry, country) 108 | .persist(stateProvinceRepository)); 109 | expectedStateProvinces.add(EntityFactory 110 | .stateProvince() 111 | .with(StateProvince::setAbbreviation, "ON") 112 | .with(StateProvince::setCountry, country) 113 | .persist(stateProvinceRepository)); 114 | expectedStateProvinces.sort(Comparator.comparing(StateProvince::getName)); 115 | 116 | int lastIndex = expectedStateProvinces.size()-1; 117 | mockMvc.perform(get(getBaseEndpointForStateProvinces(country.getId())) 118 | .header(HttpHeaders.AUTHORIZATION, TestJWTUtils.getAuthHeader( 119 | this.getDechatedUser(), 120 | this.getDetachedOrganization(), 121 | "any" 122 | ))) 123 | .andExpect(status().isOk()) 124 | .andExpect(jsonPath("$", hasSize(expectedStateProvinces.size()))) 125 | .andExpect(jsonPath("[0].name", equalTo(expectedStateProvinces.getFirst().getName()))) 126 | .andExpect(jsonPath("[0].country.id", is(country.getId()))) 127 | .andExpect(jsonPath( 128 | String.format("[%d].name", lastIndex), 129 | equalTo(expectedStateProvinces.getLast().getName()) 130 | )) 131 | .andExpect(jsonPath( 132 | String.format("[%d].country.id", lastIndex), 133 | is(country.getId()) 134 | )); 135 | } 136 | 137 | @Test 138 | public void testFindAllStateProvincesWithoutAuthentication() throws Exception { 139 | Country country = EntityFactory 140 | .country() 141 | .persist(countryRepository); 142 | this.mockMvc.perform( 143 | get(getBaseEndpointForStateProvinces(country.getId())) 144 | ) 145 | .andDo(print()) 146 | .andExpect(status().isUnauthorized()); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /customer-svc/src/test/java/com/company/secureapispring/customer/factory/EntityBuilder.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.factory; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.function.BiConsumer; 8 | import java.util.function.Consumer; 9 | import java.util.function.Supplier; 10 | 11 | public final class EntityBuilder { 12 | private final Supplier instantiation; 13 | 14 | private final List> instanceModifiers = new ArrayList<>(); 15 | 16 | private EntityBuilder(Supplier instantiation) { 17 | this.instantiation = instantiation; 18 | } 19 | 20 | public static EntityBuilder of(Supplier instantiator) { 21 | return new EntityBuilder(instantiator); 22 | } 23 | 24 | public EntityBuilder with(BiConsumer consumer, U value) { 25 | Consumer modifier = instance -> consumer.accept(instance, value); 26 | instanceModifiers.add(modifier); 27 | return this; 28 | } 29 | 30 | public T make() { 31 | T instance = instantiation.get(); 32 | instanceModifiers.forEach(modifier -> modifier.accept(instance)); 33 | instanceModifiers.clear(); 34 | return instance; 35 | } 36 | 37 | public T persist(JpaRepository repo) { 38 | T value = make(); 39 | repo.save(value); 40 | return value; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /customer-svc/src/test/java/com/company/secureapispring/customer/factory/EntityFactory.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.factory; 2 | 3 | import com.company.secureapispring.auth.entities.Organization; 4 | import com.company.secureapispring.auth.entities.User; 5 | import com.company.secureapispring.customer.entities.Country; 6 | import com.company.secureapispring.customer.entities.Customer; 7 | import com.company.secureapispring.customer.entities.StateProvince; 8 | import lombok.Getter; 9 | import net.datafaker.Faker; 10 | 11 | import java.time.Instant; 12 | 13 | public final class EntityFactory { 14 | @Getter 15 | private static final Faker faker = new Faker(); 16 | private static int counter; 17 | 18 | private EntityFactory() { 19 | } 20 | 21 | public static EntityBuilder country() { 22 | return EntityBuilder 23 | .of(Country::new) 24 | .with(Country::setAbbreviation, faker.country().countryCode3()) 25 | .with(Country::setName, faker.country().name()); 26 | } 27 | 28 | public static EntityBuilder stateProvince() { 29 | return EntityBuilder 30 | .of(StateProvince::new) 31 | .with(StateProvince::setAbbreviation, faker.address().stateAbbr()) 32 | .with(StateProvince::setName, faker.address().state()); 33 | } 34 | 35 | // Safe generate unique email during tests 36 | public static String generateUniqueEmail() { 37 | return faker.internet().emailAddress().replace("@", "+" + (counter++) + "@"); 38 | } 39 | 40 | public static EntityBuilder customer() { 41 | return EntityBuilder 42 | .of(Customer::new) 43 | .with(Customer::setFirstName, faker.name().firstName()) 44 | .with(Customer::setLastName, faker.name().lastName()) 45 | .with(Customer::setEmail, generateUniqueEmail()) 46 | .with(Customer::setAddress, faker.address().streetAddressNumber()) 47 | .with(Customer::setAddress2, faker.address().secondaryAddress()) 48 | .with(Customer::setPostalCode, faker.address().zipCode()); 49 | } 50 | 51 | public static EntityBuilder user() { 52 | String email = generateUniqueEmail(); 53 | String username = email.substring(0, email.indexOf("@")); 54 | return EntityBuilder 55 | .of(User::new) 56 | .with(User::setGivenName, faker.name().firstName()) 57 | .with(User::setFamilyName, faker.name().lastName()) 58 | .with(User::setEmail, email) 59 | .with(User::setEmailVerified, true) 60 | .with(User::setUsername, username); 61 | } 62 | 63 | public static EntityBuilder organization() { 64 | String email = generateUniqueEmail(); 65 | String username = email.substring(0, email.indexOf("@")); 66 | return EntityBuilder 67 | .of(Organization::new) 68 | .with(Organization::setAlias, faker.internet().uuid()) 69 | .with(Organization::setCreatedBy, username) 70 | .with(Organization::setCreatedDate, Instant.now()); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /customer-svc/src/test/java/com/company/secureapispring/customer/services/CountryServiceIT.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.services; 2 | 3 | import com.company.secureapispring.customer.AbstractIT; 4 | import com.company.secureapispring.customer.CustomerSvcSpringBootAppTest; 5 | import com.company.secureapispring.customer.entities.Country; 6 | import com.company.secureapispring.customer.factory.EntityFactory; 7 | import com.company.secureapispring.customer.repositories.CountryRepository; 8 | import org.junit.jupiter.api.Test; 9 | import org.mockito.Mockito; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.mock.mockito.SpyBean; 12 | import org.springframework.data.domain.Sort; 13 | import org.springframework.transaction.annotation.Transactional; 14 | 15 | @CustomerSvcSpringBootAppTest 16 | @Transactional 17 | public class CountryServiceIT extends AbstractIT { 18 | @Autowired 19 | private CountryService countryService; 20 | 21 | @SpyBean 22 | private CountryRepository countryRepository; 23 | 24 | @Test 25 | public void testFindAllIsCacheable() { 26 | EntityFactory 27 | .country() 28 | .with(Country::setAbbreviation, "USA") 29 | .persist(countryRepository); 30 | 31 | countryService.findAll(); 32 | countryService.findAll(); 33 | 34 | Mockito.verify(countryRepository, Mockito.times(1)).findAll(Sort.by("name")); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /customer-svc/src/test/java/com/company/secureapispring/customer/services/StateProvinceServiceIT.java: -------------------------------------------------------------------------------- 1 | package com.company.secureapispring.customer.services; 2 | 3 | import com.company.secureapispring.customer.AbstractIT; 4 | import com.company.secureapispring.customer.CustomerSvcSpringBootAppTest; 5 | import com.company.secureapispring.customer.entities.Country; 6 | import com.company.secureapispring.customer.entities.StateProvince; 7 | import com.company.secureapispring.customer.factory.EntityFactory; 8 | import com.company.secureapispring.customer.repositories.CountryRepository; 9 | import com.company.secureapispring.customer.repositories.StateProvinceRepository; 10 | import org.junit.jupiter.api.Test; 11 | import org.mockito.Mockito; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.mock.mockito.SpyBean; 14 | import org.springframework.data.domain.Sort; 15 | import org.springframework.transaction.annotation.Transactional; 16 | 17 | @CustomerSvcSpringBootAppTest 18 | @Transactional 19 | public class StateProvinceServiceIT extends AbstractIT { 20 | @Autowired 21 | private StateProvinceService stateProvinceService; 22 | 23 | @Autowired 24 | private CountryRepository countryRepository; 25 | 26 | @SpyBean 27 | private StateProvinceRepository stateProvinceRepository; 28 | 29 | @Test 30 | public void testFindAllIsCacheable() { 31 | Country country = EntityFactory 32 | .country() 33 | .with(Country::setAbbreviation, "CAN") 34 | .persist(countryRepository); 35 | 36 | EntityFactory 37 | .stateProvince() 38 | .with(StateProvince::setAbbreviation, "BC") 39 | .with(StateProvince::setCountry, country) 40 | .persist(stateProvinceRepository); 41 | 42 | stateProvinceService.findAll(country.getId()); 43 | stateProvinceService.findAll(country.getId()); 44 | 45 | Mockito.verify(stateProvinceRepository, Mockito.times(1)).findAllByCountryId(country.getId(), Sort.by("name")); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /customer-svc/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | app.name="Secure API with Spring Boot 3" 2 | app.description="This git mono repository is an example Java REST API application that was configured to use Keycloak as access management." 3 | 4 | server.port=8082 5 | 6 | spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9080/realms/app 7 | 8 | cors.allowed-origins=* 9 | 10 | spring.profiles.active=${SPRING_PROFILE:test} 11 | spring.datasource.url=${DATASOURCE_JDBC_URL_TEST} 12 | spring.datasource.username=${DATASOURCE_USERNAME_TEST} 13 | spring.datasource.password=${DATASOURCE_PASSWORD_TEST} 14 | 15 | spring.jpa.open-in-view=false 16 | spring.jpa.hibernate.ddl-auto=none 17 | 18 | spring.liquibase.change-log=classpath:liquibase/main.xml 19 | spring.liquibase.contexts=test 20 | spring.liquibase.enabled=true 21 | 22 | # Enable Hibernate Second Level Cache 23 | spring.jpa.properties.hibernate.cache.use_second_level_cache=false 24 | spring.jpa.properties.hibernate.cache.use_query_cache=false 25 | 26 | spring.redis.database=0 27 | 28 | spring.main.allow-bean-definition-overriding=true 29 | 30 | logging.level.root=ERROR 31 | logging.level.org.hibernate.SQL=ERROR 32 | logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE 33 | -------------------------------------------------------------------------------- /docker/.env.example: -------------------------------------------------------------------------------- 1 | # Make sure to use a strong password and harden the configuration in case you expose image outside your local machine 2 | 3 | POSTGRES_DATA_DIR=/var/lib/secure-api-spring/postgresql 4 | POSTGRES_PASSWORD=postgres 5 | POSTGRES_PORT=127.0.0.1:5432:5432 6 | 7 | KC_PORT=127.0.0.1:9080:8080 8 | KC_DB_URL_DATABASE=keycloak 9 | KC_DB_USERNAME=keycloak 10 | KC_DB_PASSWORD=password 11 | KC_LOG_LEVEL=INFO 12 | KEYCLOAK_ADMIN_PASSWORD=admin 13 | 14 | CUSTOMER_SERVICE_DATABASE=customers 15 | CUSTOMER_SERVICE_DB_USERNAME=customers 16 | CUSTOMER_SERVICE_DB_PASSWORD=password 17 | 18 | REDIS_PORT=127.0.0.1:6379:6379 19 | -------------------------------------------------------------------------------- /docker/jenkins/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stable-slim 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | 5 | # https://docs.docker.com/engine/install/debian/ 6 | # Add Docker's official GPG key: 7 | RUN apt update 8 | RUN apt install -y ca-certificates curl 9 | RUN install -m 0755 -d /etc/apt/keyrings 10 | RUN curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc 11 | Run chmod a+r /etc/apt/keyrings/docker.asc 12 | # Add the repository to Apt sources: 13 | RUN echo \ 14 | "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \ 15 | $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ 16 | tee /etc/apt/sources.list.d/docker.list > /dev/null 17 | RUN apt update 18 | RUN apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 19 | 20 | ENV DOCKER_BUILDKIT=1 21 | 22 | # JVM (GraalVM to use native image in the future) 23 | RUN cd /opt && \ 24 | curl -L -O "https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-23.0.0/graalvm-community-jdk-23.0.0_linux-x64_bin.tar.gz" && \ 25 | tar -xvf graalvm-community-jdk-23.0.0_linux-x64_bin.tar.gz && \ 26 | rm -rf graalvm-community-jdk-23.0.0_linux-x64_bin.tar.gz 27 | 28 | ENV PATH="${PATH}:/opt/graalvm-community-openjdk-23+37.1/bin" 29 | ENV JAVA_HOME="/opt/graalvm-community-openjdk-23+37.1" 30 | 31 | # kubectl to deploy on K8s 32 | RUN curl -L -O "https://s3.us-west-2.amazonaws.com/amazon-eks/1.29.8/2024-09-11/bin/linux/amd64/kubectl" && \ 33 | chmod ugo+x ./kubectl && \ 34 | mkdir -p /usr/local/bin && cp ./kubectl /usr/local/bin/kubectl 35 | 36 | # install envsubst command of the gettext package 37 | RUN apt install -y gettext 38 | 39 | # Clean up, try to reduce image size 40 | RUN apt autoremove -y \ 41 | && apt clean all \ 42 | && rm -rf /var/lib/apt/lists/* \ 43 | && rm -rf /usr/share/doc /usr/share/man /usr/share/locale \ 44 | && rm -f /usr/local/etc/php-fpm.d/*.conf \ 45 | && rm -rf /usr/src/php \ 46 | && rm -rf /var/tmp/* /usr/share/doc/* 47 | 48 | WORKDIR /opt/secure-api-spring 49 | -------------------------------------------------------------------------------- /docker/jenkins/jenkins-agent.yml: -------------------------------------------------------------------------------- 1 | # used to test the Docker image that will be used as Jenkins agent 2 | services: 3 | jenkins_agent: 4 | container_name: jenkins_agent 5 | build: 6 | context: '../..' 7 | dockerfile: docker/jenkins/Dockerfile 8 | tty: true 9 | volumes: 10 | - /var/run/docker.sock:/var/run/docker.sock 11 | - ../..:/opt/secure-api-spring 12 | -------------------------------------------------------------------------------- /docker/keycloak/keycloak.yml: -------------------------------------------------------------------------------- 1 | # Make sure to use a strong password and harden the configuration in case you expose image outside your local machine 2 | services: 3 | keycloak: 4 | container_name: secure-api-spring-keycloak 5 | image: quay.io/keycloak/keycloak:26.0.2 6 | command: 'start-dev --import-realm --features organization' 7 | env_file: 8 | - ../.env # copy the .env.example as change the password as you wish 9 | environment: 10 | - KC_DB=postgres 11 | - KC_DB_URL_HOST=postgresql 12 | - KC_DB_URL_PORT=5432 13 | - KC_DB_SCHEMA=public 14 | - KC_LOG_LEVEL=ERROR 15 | - KEYCLOAK_ADMIN=admin 16 | ports: 17 | - ${KC_PORT} 18 | extra_hosts: 19 | - 'host.docker.internal:host-gateway' 20 | volumes: 21 | - ./data:/opt/keycloak/data/import 22 | -------------------------------------------------------------------------------- /docker/postgresql/PostgreSQL.dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:15.6-alpine 2 | COPY create-db.sh /create-db.sh 3 | RUN chmod +x /create-db.sh 4 | COPY init-user-db.sh /docker-entrypoint-initdb.d/init-user-db.sh 5 | -------------------------------------------------------------------------------- /docker/postgresql/create-db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set +x 3 | 4 | if [ "$#" -ne 3 ]; then 5 | echo "Usage: $0 " 6 | exit 1 7 | fi 8 | 9 | DB_NAME=$1 10 | DB_USERNAME=$2 11 | DB_PASSWORD=$3 12 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -c " 13 | CREATE ROLE ${DB_USERNAME} 14 | WITH NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT LOGIN NOREPLICATION NOBYPASSRLS 15 | PASSWORD '${DB_PASSWORD}'" 16 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -c " 17 | CREATE DATABASE ${DB_NAME} 18 | WITH OWNER = ${DB_USERNAME} ENCODING = 'UTF8'" 19 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$DB_USERNAME" -c " 20 | GRANT ALL ON SCHEMA public to ${DB_NAME}" 21 | -------------------------------------------------------------------------------- /docker/postgresql/init-user-db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set +x 4 | 5 | # Create the database used by Keycloak 6 | /create-db.sh "$KC_DB_URL_DATABASE" "$KC_DB_USERNAME" "$KC_DB_PASSWORD" 7 | 8 | # Create the database used by Customer Service 9 | /create-db.sh "$CUSTOMER_SERVICE_DATABASE" "$CUSTOMER_SERVICE_DB_USERNAME" "$CUSTOMER_SERVICE_DB_PASSWORD" 10 | -------------------------------------------------------------------------------- /docker/postgresql/postgresql.yml: -------------------------------------------------------------------------------- 1 | # Make sure to use a strong password and harden the configuration in case you expose image outside your local machine 2 | services: 3 | postgresql: 4 | container_name: secure-api-spring-postgresql 5 | build: 6 | context: . 7 | dockerfile: PostgreSQL.dockerfile 8 | volumes: 9 | - ${POSTGRES_DATA_DIR}:/var/lib/postgresql/data/ 10 | env_file: 11 | - ../.env # copy the .env.example as change the password as you wish 12 | environment: 13 | - POSTGRES_USER=postgres 14 | - POSTGRES_HOST_AUTH_METHOD=password 15 | ports: 16 | - ${POSTGRES_PORT} 17 | -------------------------------------------------------------------------------- /docker/redis.yml: -------------------------------------------------------------------------------- 1 | # Make sure to use a strong password and harden the configuration in case you expose image outside your local machine 2 | services: 3 | redis: 4 | container_name: secure-api-spring-redis 5 | image: redis:bookworm 6 | env_file: 7 | - ./.env 8 | ports: 9 | - "${REDIS_PORT:-127.0.0.1:6379:6379}" 10 | -------------------------------------------------------------------------------- /docker/services.yml: -------------------------------------------------------------------------------- 1 | # Make sure to use a strong password and harden the configuration in case you expose image outside your local machine 2 | services: 3 | redis: 4 | extends: 5 | file: ./redis.yml 6 | service: redis 7 | postgresql: 8 | extends: 9 | file: ./postgresql/postgresql.yml 10 | service: postgresql 11 | keycloak: 12 | extends: 13 | file: ./keycloak/keycloak.yml 14 | service: keycloak 15 | depends_on: 16 | - postgresql 17 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.1.1 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | # e.g. to debug Maven itself, use 32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | # ---------------------------------------------------------------------------- 35 | 36 | if [ -z "$MAVEN_SKIP_RC" ] ; then 37 | 38 | if [ -f /usr/local/etc/mavenrc ] ; then 39 | . /usr/local/etc/mavenrc 40 | fi 41 | 42 | if [ -f /etc/mavenrc ] ; then 43 | . /etc/mavenrc 44 | fi 45 | 46 | if [ -f "$HOME/.mavenrc" ] ; then 47 | . "$HOME/.mavenrc" 48 | fi 49 | 50 | fi 51 | 52 | # OS specific support. $var _must_ be set to either true or false. 53 | cygwin=false; 54 | darwin=false; 55 | mingw=false 56 | case "`uname`" in 57 | CYGWIN*) cygwin=true ;; 58 | MINGW*) mingw=true;; 59 | Darwin*) darwin=true 60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 62 | if [ -z "$JAVA_HOME" ]; then 63 | if [ -x "/usr/libexec/java_home" ]; then 64 | JAVA_HOME="`/usr/libexec/java_home`"; export JAVA_HOME 65 | else 66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME 67 | fi 68 | fi 69 | ;; 70 | esac 71 | 72 | if [ -z "$JAVA_HOME" ] ; then 73 | if [ -r /etc/gentoo-release ] ; then 74 | JAVA_HOME=`java-config --jre-home` 75 | fi 76 | fi 77 | 78 | # For Cygwin, ensure paths are in UNIX format before anything is touched 79 | if $cygwin ; then 80 | [ -n "$JAVA_HOME" ] && 81 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 82 | [ -n "$CLASSPATH" ] && 83 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 84 | fi 85 | 86 | # For Mingw, ensure paths are in UNIX format before anything is touched 87 | if $mingw ; then 88 | [ -n "$JAVA_HOME" ] && 89 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 90 | fi 91 | 92 | if [ -z "$JAVA_HOME" ]; then 93 | javaExecutable="`which javac`" 94 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 95 | # readlink(1) is not available as standard on Solaris 10. 96 | readLink=`which readlink` 97 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 98 | if $darwin ; then 99 | javaHome="`dirname \"$javaExecutable\"`" 100 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 101 | else 102 | javaExecutable="`readlink -f \"$javaExecutable\"`" 103 | fi 104 | javaHome="`dirname \"$javaExecutable\"`" 105 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 106 | JAVA_HOME="$javaHome" 107 | export JAVA_HOME 108 | fi 109 | fi 110 | fi 111 | 112 | if [ -z "$JAVACMD" ] ; then 113 | if [ -n "$JAVA_HOME" ] ; then 114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 115 | # IBM's JDK on AIX uses strange locations for the executables 116 | JAVACMD="$JAVA_HOME/jre/sh/java" 117 | else 118 | JAVACMD="$JAVA_HOME/bin/java" 119 | fi 120 | else 121 | JAVACMD="`\\unset -f command; \\command -v java`" 122 | fi 123 | fi 124 | 125 | if [ ! -x "$JAVACMD" ] ; then 126 | echo "Error: JAVA_HOME is not defined correctly." >&2 127 | echo " We cannot execute $JAVACMD" >&2 128 | exit 1 129 | fi 130 | 131 | if [ -z "$JAVA_HOME" ] ; then 132 | echo "Warning: JAVA_HOME environment variable is not set." 133 | fi 134 | 135 | # traverses directory structure from process work directory to filesystem root 136 | # first directory with .mvn subdirectory is considered project base directory 137 | find_maven_basedir() { 138 | if [ -z "$1" ] 139 | then 140 | echo "Path not specified to find_maven_basedir" 141 | return 1 142 | fi 143 | 144 | basedir="$1" 145 | wdir="$1" 146 | while [ "$wdir" != '/' ] ; do 147 | if [ -d "$wdir"/.mvn ] ; then 148 | basedir=$wdir 149 | break 150 | fi 151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 152 | if [ -d "${wdir}" ]; then 153 | wdir=`cd "$wdir/.."; pwd` 154 | fi 155 | # end of workaround 156 | done 157 | printf '%s' "$(cd "$basedir"; pwd)" 158 | } 159 | 160 | # concatenates all lines of a file 161 | concat_lines() { 162 | if [ -f "$1" ]; then 163 | echo "$(tr -s '\n' ' ' < "$1")" 164 | fi 165 | } 166 | 167 | BASE_DIR=$(find_maven_basedir "$(dirname $0)") 168 | if [ -z "$BASE_DIR" ]; then 169 | exit 1; 170 | fi 171 | 172 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR 173 | if [ "$MVNW_VERBOSE" = true ]; then 174 | echo $MAVEN_PROJECTBASEDIR 175 | fi 176 | 177 | ########################################################################################## 178 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 179 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 180 | ########################################################################################## 181 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 182 | if [ "$MVNW_VERBOSE" = true ]; then 183 | echo "Found .mvn/wrapper/maven-wrapper.jar" 184 | fi 185 | else 186 | if [ "$MVNW_VERBOSE" = true ]; then 187 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 188 | fi 189 | if [ -n "$MVNW_REPOURL" ]; then 190 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 191 | else 192 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 193 | fi 194 | while IFS="=" read key value; do 195 | case "$key" in (wrapperUrl) wrapperUrl="$value"; break ;; 196 | esac 197 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 198 | if [ "$MVNW_VERBOSE" = true ]; then 199 | echo "Downloading from: $wrapperUrl" 200 | fi 201 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 202 | if $cygwin; then 203 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 204 | fi 205 | 206 | if command -v wget > /dev/null; then 207 | QUIET="--quiet" 208 | if [ "$MVNW_VERBOSE" = true ]; then 209 | echo "Found wget ... using wget" 210 | QUIET="" 211 | fi 212 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 213 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" 214 | else 215 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" 216 | fi 217 | [ $? -eq 0 ] || rm -f "$wrapperJarPath" 218 | elif command -v curl > /dev/null; then 219 | QUIET="--silent" 220 | if [ "$MVNW_VERBOSE" = true ]; then 221 | echo "Found curl ... using curl" 222 | QUIET="" 223 | fi 224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L 226 | else 227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L 228 | fi 229 | [ $? -eq 0 ] || rm -f "$wrapperJarPath" 230 | else 231 | if [ "$MVNW_VERBOSE" = true ]; then 232 | echo "Falling back to using Java to download" 233 | fi 234 | javaSource="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 235 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" 236 | # For Cygwin, switch paths to Windows format before running javac 237 | if $cygwin; then 238 | javaSource=`cygpath --path --windows "$javaSource"` 239 | javaClass=`cygpath --path --windows "$javaClass"` 240 | fi 241 | if [ -e "$javaSource" ]; then 242 | if [ ! -e "$javaClass" ]; then 243 | if [ "$MVNW_VERBOSE" = true ]; then 244 | echo " - Compiling MavenWrapperDownloader.java ..." 245 | fi 246 | # Compiling the Java class 247 | ("$JAVA_HOME/bin/javac" "$javaSource") 248 | fi 249 | if [ -e "$javaClass" ]; then 250 | # Running the downloader 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo " - Running MavenWrapperDownloader.java ..." 253 | fi 254 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 255 | fi 256 | fi 257 | fi 258 | fi 259 | ########################################################################################## 260 | # End of extension 261 | ########################################################################################## 262 | 263 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 264 | 265 | # For Cygwin, switch paths to Windows format before running java 266 | if $cygwin; then 267 | [ -n "$JAVA_HOME" ] && 268 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 269 | [ -n "$CLASSPATH" ] && 270 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 271 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 272 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 273 | fi 274 | 275 | # Provide a "standardized" way to retrieve the CLI args that will 276 | # work with both Windows and non-Windows executions. 277 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 278 | export MAVEN_CMD_LINE_ARGS 279 | 280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 281 | 282 | exec "$JAVACMD" \ 283 | $MAVEN_OPTS \ 284 | $MAVEN_DEBUG_OPTS \ 285 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 286 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 287 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 288 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Apache Maven Wrapper startup batch script, version 3.1.1 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 28 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 29 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 30 | @REM e.g. to debug Maven itself, use 31 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 32 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 33 | @REM ---------------------------------------------------------------------------- 34 | 35 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 36 | @echo off 37 | @REM set title of command window 38 | title %0 39 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 40 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 41 | 42 | @REM set %HOME% to equivalent of $HOME 43 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 44 | 45 | @REM Execute a user defined script before this one 46 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 47 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 48 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 49 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 50 | :skipRcPre 51 | 52 | @setlocal 53 | 54 | set ERROR_CODE=0 55 | 56 | @REM To isolate internal variables from possible post scripts, we use another setlocal 57 | @setlocal 58 | 59 | @REM ==== START VALIDATION ==== 60 | if not "%JAVA_HOME%" == "" goto OkJHome 61 | 62 | echo. 63 | echo Error: JAVA_HOME not found in your environment. >&2 64 | echo Please set the JAVA_HOME variable in your environment to match the >&2 65 | echo location of your Java installation. >&2 66 | echo. 67 | goto error 68 | 69 | :OkJHome 70 | if exist "%JAVA_HOME%\bin\java.exe" goto init 71 | 72 | echo. 73 | echo Error: JAVA_HOME is set to an invalid directory. >&2 74 | echo JAVA_HOME = "%JAVA_HOME%" >&2 75 | echo Please set the JAVA_HOME variable in your environment to match the >&2 76 | echo location of your Java installation. >&2 77 | echo. 78 | goto error 79 | 80 | @REM ==== END VALIDATION ==== 81 | 82 | :init 83 | 84 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 85 | @REM Fallback to current working directory if not found. 86 | 87 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 88 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 89 | 90 | set EXEC_DIR=%CD% 91 | set WDIR=%EXEC_DIR% 92 | :findBaseDir 93 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 94 | cd .. 95 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 96 | set WDIR=%CD% 97 | goto findBaseDir 98 | 99 | :baseDirFound 100 | set MAVEN_PROJECTBASEDIR=%WDIR% 101 | cd "%EXEC_DIR%" 102 | goto endDetectBaseDir 103 | 104 | :baseDirNotFound 105 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 106 | cd "%EXEC_DIR%" 107 | 108 | :endDetectBaseDir 109 | 110 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 111 | 112 | @setlocal EnableExtensions EnableDelayedExpansion 113 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 114 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 115 | 116 | :endReadAdditionalConfig 117 | 118 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 123 | 124 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 125 | IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | if "%MVNW_VERBOSE%" == "true" ( 132 | echo Found %WRAPPER_JAR% 133 | ) 134 | ) else ( 135 | if not "%MVNW_REPOURL%" == "" ( 136 | SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 137 | ) 138 | if "%MVNW_VERBOSE%" == "true" ( 139 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 140 | echo Downloading from: %WRAPPER_URL% 141 | ) 142 | 143 | powershell -Command "&{"^ 144 | "$webclient = new-object System.Net.WebClient;"^ 145 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 146 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 147 | "}"^ 148 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ 149 | "}" 150 | if "%MVNW_VERBOSE%" == "true" ( 151 | echo Finished downloading %WRAPPER_JAR% 152 | ) 153 | ) 154 | @REM End of extension 155 | 156 | @REM Provide a "standardized" way to retrieve the CLI args that will 157 | @REM work with both Windows and non-Windows executions. 158 | set MAVEN_CMD_LINE_ARGS=%* 159 | 160 | %MAVEN_JAVA_EXE% ^ 161 | %JVM_CONFIG_MAVEN_PROPS% ^ 162 | %MAVEN_OPTS% ^ 163 | %MAVEN_DEBUG_OPTS% ^ 164 | -classpath %WRAPPER_JAR% ^ 165 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 166 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 167 | if ERRORLEVEL 1 goto error 168 | goto end 169 | 170 | :error 171 | set ERROR_CODE=1 172 | 173 | :end 174 | @endlocal & set ERROR_CODE=%ERROR_CODE% 175 | 176 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 177 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 178 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 179 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 180 | :skipRcPost 181 | 182 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 183 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 184 | 185 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 186 | 187 | cmd /C exit /B %ERROR_CODE% 188 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | com.company.secureapispring 6 | secure-api-spring-parent 7 | 0.0.1-SNAPSHOT 8 | pom 9 | 10 | org.springframework.boot 11 | spring-boot-starter-parent 12 | 3.3.4 13 | 14 | 15 | 21 16 | 1.19.8 17 | ${java.version} 18 | ${java.version} 19 | 0.0.1-SNAPSHOT 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-test 29 | test 30 | 31 | 32 | org.projectlombok 33 | lombok 34 | true 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | org.testcontainers 43 | testcontainers-bom 44 | ${testcontainers.version} 45 | pom 46 | import 47 | 48 | 49 | com.company.secureapispring 50 | auth-lib 51 | ${auth-lib.version} 52 | 53 | 54 | com.company.secureapispring 55 | auth-lib 56 | ${auth-lib.version} 57 | test-jar 58 | test 59 | 60 | 61 | net.datafaker 62 | datafaker 63 | 2.4.0 64 | test 65 | 66 | 67 | org.springdoc 68 | springdoc-openapi-starter-webmvc-ui 69 | 2.5.0 70 | 71 | 72 | org.redisson 73 | redisson-spring-boot-starter 74 | 3.37.0 75 | 76 | 77 | 78 | org.redisson 79 | redisson-hibernate-6 80 | 3.37.0 81 | 82 | 83 | com.google.code.findbugs 84 | jsr305 85 | 3.0.2 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | org.apache.maven.plugins 95 | maven-compiler-plugin 96 | 97 | ${java.version} 98 | ${java.version} 99 | 100 | 101 | org.projectlombok 102 | lombok 103 | ${lombok.version} 104 | 105 | 106 | 107 | 108 | 109 | org.jacoco 110 | jacoco-maven-plugin 111 | 0.8.12 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | customer-svc 120 | 121 | true 122 | 123 | 124 | auth-lib 125 | customer-svc 126 | 127 | 128 | 129 | auth-lib 130 | 131 | false 132 | 133 | 134 | auth-lib 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /task.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | case $1 in 4 | "services:up") 5 | cp -n docker/.env.example docker/.env && docker compose --env-file=docker/.env -f docker/services.yml up --build 6 | ;; 7 | "services:up:bg") 8 | cp -n docker/.env.example docker/.env 9 | docker compose --env-file=docker/.env -f docker/services.yml up --build -d 10 | ;; 11 | "services:down") 12 | docker compose --env-file=docker/.env -f docker/services.yml down -v 13 | ;; 14 | "customer-svc:run") 15 | ./mvnw -pl customer-svc spring-boot:run 16 | ;; 17 | "customer-svc:install") 18 | ./mvnw clean install -Pcustomer-svc 19 | ;; 20 | "customer-svc:test-report") 21 | ./mvnw -pl customer-svc jacoco:report 22 | echo -e "\nClick in the link below to open the report on the default browser:\n" 23 | echo -e "file://$(pwd)/customer-svc/target/site/jacoco/index.html\n" 24 | ;; 25 | "services:db:ssh") 26 | docker exec -it secure-api-spring-postgresql bash 27 | ;; 28 | "docker:jenkins:up") 29 | docker compose -f docker/jenkins/jenkins-agent.yml up --build 30 | ;; 31 | "docker:jenkins:bash") 32 | docker exec -it jenkins_agent bash 33 | ;; 34 | "auth-lib:test-report") 35 | ./mvnw -pl auth-lib jacoco:report 36 | echo -e "\nClick in the link below to open the report on the default browser:\n" 37 | echo -e "file://$(pwd)/auth-lib/target/site/jacoco/index.html\n" 38 | ;; 39 | *) 40 | echo "Invalid task name: $1. Please provide a valid task name." 41 | ;; 42 | esac 43 | --------------------------------------------------------------------------------