├── .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 |
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 |
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