diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7674504..15f3631 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ name: Build # - REGISTRY_HOST: Container registry host (e.g. mtr.devops.telekom.de) # - REGISTRY_REPO: Repository path (e.g. /tardis-internal/gateway/jumper-sse) # - REGISTRY_USER: Container registry username -# - JAVA_VERSION: Java version to use (defaults to '17' if not specified) +# - JAVA_VERSION: Java version to use (defaults to '21' if not specified) # - JAVA_DISTRIBUTION: Java distribution to use (defaults to 'temurin' if not specified) # - BASE_IMAGE: The base image used for the Docker build # @@ -34,7 +34,7 @@ permissions: contents: read env: - JAVA_VERSION: ${{ vars.JAVA_VERSION || '17' }} + JAVA_VERSION: ${{ vars.JAVA_VERSION || '21' }} JAVA_DISTRIBUTION: ${{ vars.JAVA_DISTRIBUTION || 'zulu' }} jobs: @@ -43,9 +43,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: ${{ env.JAVA_DISTRIBUTION }} java-version: ${{ env.JAVA_VERSION }} @@ -60,23 +60,16 @@ jobs: version: ${{ steps.extract-version.outputs.version }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: ${{ env.JAVA_DISTRIBUTION }} java-version: ${{ env.JAVA_VERSION }} cache: 'maven' - - name: Cache compiled classes - uses: actions/cache@v4 - with: - path: target - key: ${{ runner.os }}-maven-target-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-maven-target- - name: Build with Maven run: | - mvn -B package -DskipTests -U + mvn -B clean package -DskipTests -U - name: Extract version id: extract-version run: | @@ -94,20 +87,13 @@ jobs: needs: [build-maven] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: ${{ env.JAVA_DISTRIBUTION }} java-version: ${{ env.JAVA_VERSION }} cache: 'maven' - - name: Cache compiled classes - uses: actions/cache@v4 - with: - path: target - key: ${{ runner.os }}-maven-target-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-maven-target- - name: Run tests run: mvn -B test - name: Upload test results @@ -127,12 +113,12 @@ jobs: image-tag: ${{ steps.tag.outputs.image-tag }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Download Maven build artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: maven-build - path: "target" + path: "target" # check of breaking change - name: Inject slug vars uses: rlespinasse/github-slug-action@v5 - name: Determine tag @@ -184,7 +170,7 @@ jobs: contents: write steps: - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@0.32.0 + uses: aquasecurity/trivy-action@0.33.1 env: TRIVY_USERNAME: ${{ vars.REGISTRY_USER }} TRIVY_PASSWORD: ${{ secrets.REGISTRY_TOKEN }} diff --git a/.github/workflows/ort.yml b/.github/workflows/ort.yml index 96903de..8303ee8 100644 --- a/.github/workflows/ort.yml +++ b/.github/workflows/ort.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout project if: github.event_name != 'pull_request' # Checkout not necessary for PRs - uses: actions/checkout@v4 + uses: actions/checkout@v5 - uses: dorny/paths-filter@v3 id: filter with: @@ -53,7 +53,7 @@ jobs: git config --global url.https://github.com/.insteadOf ssh://git@github.com/ git config --global url.https://github.com/.insteadOf git://github.com/ - name: Checkout project - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Prepare ORT config run: | # Move into default config dir diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f8fa12a..9eed152 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: new-release-version: ${{ steps.semantic-release.outputs.new-release-version }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 fetch-tags: 'true' @@ -48,7 +48,7 @@ jobs: EOF - name: Semantic Release id: semantic-release - uses: cycjimmy/semantic-release-action@v4 + uses: cycjimmy/semantic-release-action@v5 with: branches: | ['main'] diff --git a/.github/workflows/reuse.yml b/.github/workflows/reuse.yml index b9f9e64..52a1d2c 100644 --- a/.github/workflows/reuse.yml +++ b/.github/workflows/reuse.yml @@ -16,6 +16,6 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: REUSE Compliance Check uses: fsfe/reuse-action@v5 diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..370ca33 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2023 Apache Software Foundation +# +# SPDX-License-Identifier: Apache-2.0 + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip diff --git a/Dockerfile b/Dockerfile index e55684b..e8eb1f9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 -ARG BASE_IMAGE=gcr.io/distroless/java17-debian12:nonroot +ARG BASE_IMAGE=gcr.io/distroless/java21-debian12:nonroot FROM ${BASE_IMAGE} EXPOSE 8080 @@ -10,4 +10,4 @@ EXPOSE 8080 COPY target/*.jar /usr/share/app.jar ENTRYPOINT ["/usr/bin/java", "-jar"] -CMD ["/usr/share/app.jar"] +CMD ["/usr/share/app.jar"] \ No newline at end of file diff --git a/Dockerfile.multi-stage b/Dockerfile.multi-stage index b4ffb67..5fcb50e 100644 --- a/Dockerfile.multi-stage +++ b/Dockerfile.multi-stage @@ -8,7 +8,7 @@ # The final container is started with a non-root user. -FROM maven:3.9-eclipse-temurin-17 AS build +FROM maven:3.9-eclipse-temurin-21 AS build RUN mkdir -p /usr/app WORKDIR /usr/app ADD . /usr/app @@ -16,18 +16,18 @@ ADD . /usr/app RUN mvn -f /usr/app/pom.xml clean package -DskipTests -FROM eclipse-temurin:17-jre +FROM eclipse-temurin:21-jre RUN apt-get install -y \ curl \ && rm -rf /var/lib/apt/lists/* -USER 1000:1000 +USER 1001:1001 -EXPOSE 8080 8082 +EXPOSE 8080 -COPY --from=build /usr/app/target/*.jar /usr/share/app.jar +COPY target/*.jar /home/cloud/app.jar -WORKDIR /usr/share/ +WORKDIR /home/cloud -CMD java $JVM_OPTS -jar /usr/share/app.jar \ No newline at end of file +CMD java $JVM_OPTS -jar /home/cloud/app.jar \ No newline at end of file diff --git a/README.md b/README.md index e570491..d396fca 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ The Dockerfile supports customization via build arguments to specify a custom ba docker build --platform linux/amd64 -t jumper --build-arg BASE_IMAGE= . ``` -By default, the Dockerfile uses `eclipse-temurin:17-jre-alpine`. +By default, the Dockerfile uses `eclipse-temurin:21-jre-alpine`. #### One-Step Multi-Stage Build @@ -266,7 +266,7 @@ When a consumer sets the `X-Token-Exchange` header containing an external provid Spectre allows a third-party listener application to monitor communication between consumer and provider for specific APIs. **Prerequisites:** -- Configured `horizon.publishEventUrl` in application properties +- Configured `jumper.horizon.publishEventUrl` in application properties - Properly configured `jumper_config` header with listener settings ```json diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8e3c91 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: 2023 Deutsche Telekom AG +# +# SPDX-License-Identifier: Apache-2.0 + +# file only for local debugging of tracing etc + +services: + jaeger: + image: jaegertracing/jaeger:2.3.0 + ports: + - "16686:16686" + - "4317:4317" + - "4318:4318" + - "9411:9411" + volumes: + - "./docker-support/jaeger.yml:/etc/jaeger/config.yml" + command: [ "--config", "/etc/jaeger/config.yml" ] + networks: + - jaeger + links: + - prometheus + + prometheus: + image: prom/prometheus:v3.1.0 + volumes: + - "./docker-support/prometheus.yml:/etc/prometheus/prometheus.yml" + ports: + - "9090:9090" + networks: + - jaeger + + echo: + image: mendhak/http-https-echo:latest + platform: linux/amd64 + ports: + - "8081:8080" + networks: + - jaeger + + redis: + image: 'bitnami/redis:latest@sha256:65f55fefc0acd7f1a1da44b39be3044bcfbc03f4a49c4689453097f929f07132' + environment: + - REDIS_PASSWORD=foobar + ports: + - '6379:6379' + networks: + - jaeger + +networks: + jaeger: diff --git a/docker-support/jaeger.yml b/docker-support/jaeger.yml new file mode 100644 index 0000000..8328956 --- /dev/null +++ b/docker-support/jaeger.yml @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: 2023 Deutsche Telekom AG +# +# SPDX-License-Identifier: Apache-2.0 + +service: + extensions: [jaeger_storage, jaeger_query] + pipelines: + traces: + receivers: [otlp,zipkin] + processors: [batch] + exporters: [jaeger_storage_exporter, spanmetrics] + metrics/spanmetrics: + receivers: [spanmetrics] + exporters: [prometheus] + telemetry: + resource: + service.name: jaeger + metrics: + level: detailed + readers: + - pull: + exporter: + prometheus: + host: 0.0.0.0 + port: 8888 + +extensions: + jaeger_query: + storage: + traces: some_storage + metrics: some_metrics_storage + jaeger_storage: + backends: + some_storage: + memory: + max_traces: 100000 + metric_backends: + some_metrics_storage: + prometheus: + endpoint: "http://prometheus:9090" + normalize_calls: true + normalize_duration: true + +connectors: + spanmetrics: + +receivers: + otlp: + protocols: + grpc: + endpoint: "0.0.0.0:4317" + http: + endpoint: "0.0.0.0:4318" + zipkin: + endpoint: "0.0.0.0:9411" + +processors: + batch: + +exporters: + jaeger_storage_exporter: + trace_storage: some_storage + prometheus: + endpoint: "0.0.0.0:8889" \ No newline at end of file diff --git a/docker-support/prometheus.yml b/docker-support/prometheus.yml new file mode 100644 index 0000000..3185341 --- /dev/null +++ b/docker-support/prometheus.yml @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2023 Deutsche Telekom AG +# +# SPDX-License-Identifier: Apache-2.0 + +global: + scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. + evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. + # scrape_timeout is set to the global default (10s). + +scrape_configs: + - job_name: aggregated-trace-metrics + static_configs: + - targets: ['jaeger:8889'] \ No newline at end of file diff --git a/mvnw b/mvnw index ab7c23d..dd35281 100755 --- a/mvnw +++ b/mvnw @@ -13,7 +13,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# https://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -24,215 +24,241 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.2 # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # - # Look for the Apple JDKs first to preserve the existing behaviour, and then look - # for the new JDKs provided by Oracle. - # - if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then - # - # Apple JDKs - # - export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home - fi - - if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then - # - # Apple JDKs - # - export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home - fi - - if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then - # - # Oracle JDKs - # - export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home - fi - - if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then - # - # Apple JDKs - # - export JAVA_HOME=`/usr/libexec/java_home` - fi - ;; +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; esac -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" else - PRG="`dirname "$PRG"`/$link" + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" done + printf %x\\n $h +} - saveddir=`pwd` +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - M2_HOME=`dirname "$PRG"`/.. +die() { + printf %s\\n "$1" >&2 + exit 1 +} - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} -# For Migwn, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" fi -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi +mkdir -p -- "${MAVEN_HOME%/*}" -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" fi -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - local basedir=$(pwd) - local wdir=$(pwd) - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true fi - wdir=$(cd "$wdir/.."; pwd) - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 fi -} - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} "$@" +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd index 8e2900a..042216a 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -2,6 +2,7 @@ REM SPDX-FileCopyrightText: 2023 Apache Software Foundation REM REM SPDX-License-Identifier: Apache-2.0 +<# : batch portion @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @@ -11,7 +12,7 @@ REM SPDX-License-Identifier: Apache-2.0 @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM http://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -22,128 +23,131 @@ REM SPDX-License-Identifier: Apache-2.0 @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir +@REM Apache Maven Wrapper startup batch script, version 3.3.2 @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output @REM ---------------------------------------------------------------------------- -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -set MAVEN_CMD_LINE_ARGS=%* - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - -set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar"" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% \ No newline at end of file +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml index dc3e12f..20296f0 100644 --- a/pom.xml +++ b/pom.xml @@ -19,19 +19,16 @@ SPDX-License-Identifier: Apache-2.0 org.springframework.boot spring-boot-starter-parent - 2.7.18 + 3.5.5 - 17 - 2021.0.8 - 6.3.2.RELEASE + 21 + 2025.0.0 + 7.14.0 - 2020.0.47 - 4.1.112.Final - 5.9.3 - 1.10.0 - 1.21.3 + 5.15.0 + 0.11.5 @@ -43,49 +40,13 @@ SPDX-License-Identifier: Apache-2.0 pom import - - - org.junit - junit-bom - ${junit.jupiter.version} - import - pom - - - io.projectreactor.netty - reactor-netty - compile - - - io.projectreactor.netty - reactor-netty-core - compile - - - io.projectreactor.netty - reactor-netty-http - compile - - - io.projectreactor.netty - reactor-netty-http-brave - runtime - - - io.projectreactor - reactor-core - compile - - - - io.micrometer - micrometer-registry-prometheus - 1.10.4 + org.springframework.boot + spring-boot-starter @@ -94,93 +55,51 @@ SPDX-License-Identifier: Apache-2.0 - org.apache.commons - commons-lang3 - 3.18.0 - + org.springframework.cloud + spring-cloud-starter-gateway-server-webflux + - org.springframework.cloud - spring-cloud-starter-gateway + org.springframework.boot + spring-boot-starter-data-redis-reactive + - org.springframework.cloud - spring-cloud-starter-contract-stub-runner - - - spring-boot-starter-web - org.springframework.boot - - + io.micrometer + micrometer-registry-prometheus io.jsonwebtoken jjwt-api - 0.11.5 + ${jsonwebtoken.version} io.jsonwebtoken jjwt-impl - 0.11.5 + ${jsonwebtoken.version} runtime io.jsonwebtoken jjwt-jackson - 0.11.5 + ${jsonwebtoken.version} runtime - - org.slf4j - slf4j-api - - - - ch.qos.logback - logback-classic - - - net.logstash.logback - logstash-logback-encoder - 7.2 - - - - org.projectlombok - lombok - 1.18.26 - - - - org.springframework.boot - spring-boot-starter-webflux - - org.springframework.boot spring-boot-starter-oauth2-client - org.springframework.cloud - spring-cloud-starter-sleuth - - - org.springframework.cloud - spring-cloud-sleuth-zipkin - - - - org.springframework.boot - spring-boot-starter-test - test + io.micrometer + micrometer-tracing-bridge-brave - org.springframework.boot - spring-boot-starter-data-redis + io.zipkin.reporter2 + zipkin-reporter-brave @@ -193,43 +112,48 @@ SPDX-License-Identifier: Apache-2.0 - org.springframework.data - spring-data-redis + org.springframework.retry + spring-retry - io.lettuce - lettuce-core - ${redis-lettuce.version} + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + - org.springframework.retry - spring-retry + org.apache.commons + commons-lang3 - org.junit.jupiter - junit-jupiter - ${junit.jupiter.version} + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test test + - org.junit.platform - junit-platform-suite - ${junit.platform.version} + org.springframework.cloud + spring-cloud-starter-contract-stub-runner test org.mock-server mockserver-netty - 5.15.0 + ${mockserver.version} test org.mock-server mockserver-client-java - 5.15.0 + ${mockserver.version} test @@ -255,34 +179,42 @@ SPDX-License-Identifier: Apache-2.0 - org.testcontainers - testcontainers - ${testcontainers.version} + org.awaitility + awaitility test + - org.testcontainers - junit-jupiter - ${testcontainers.version} + org.springframework.boot + spring-boot-testcontainers test + - org.awaitility - awaitility + io.projectreactor + reactor-test test - - com.diffplug.spotless - spotless-maven-plugin - 2.39.0 + org.junit.platform + junit-platform-suite + test + - com.google.googlejavaformat - google-java-format - 1.18.1 + org.testcontainers + junit-jupiter + test + + + + io.projectreactor.tools + blockhound + 1.0.10.RELEASE + test + @@ -294,7 +226,7 @@ SPDX-License-Identifier: Apache-2.0 com.diffplug.spotless spotless-maven-plugin - 2.39.0 + 2.46.1 @@ -319,7 +251,7 @@ SPDX-License-Identifier: Apache-2.0 4 - 1.18.1 + 1.19.2 @@ -330,6 +262,7 @@ SPDX-License-Identifier: Apache-2.0 ignore true + -XX:+AllowRedefinitionToAddDeleteMethods diff --git a/src/main/java/jumper/Application.java b/src/main/java/jumper/Application.java index b8aa9a8..554b461 100644 --- a/src/main/java/jumper/Application.java +++ b/src/main/java/jumper/Application.java @@ -4,131 +4,13 @@ package jumper; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import jumper.filter.RemoveRequestHeaderFilter; -import jumper.filter.RequestFilter; -import jumper.filter.RequestTransformationFilter; -import jumper.filter.ResponseFilter; -import jumper.filter.ResponseTransformationFilter; -import jumper.filter.SpectreRequestFilter; -import jumper.filter.SpectreResponseFilter; -import jumper.filter.SpectreRoutingFilter; -import jumper.filter.rewrite.SpectreBodyRewrite; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.gateway.route.RouteLocator; -import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.http.HttpMethod; @SpringBootApplication public class Application { - @Value("${horizon.publishEventUrl}") - private String publishEventUrl; - public static void main(String[] args) { SpringApplication.run(Application.class, args); } - - @Bean - public RouteLocator proxyRoute( - RouteLocatorBuilder builder, - RequestFilter requestFilter, - RemoveRequestHeaderFilter removeRequestHeader, - ResponseFilter responseFilter, - SpectreRequestFilter spectreRequestFilter, - SpectreResponseFilter spectreResponseFilter, - RequestTransformationFilter requestTransformationFilter, - ResponseTransformationFilter responseTransformationFilter, - SpectreRoutingFilter spectreRoutingFilter, - SpectreBodyRewrite spectreBodyRewrite) { - - Set headerRemovalList = - new HashSet<>( - Arrays.asList( - Constants.HEADER_JUMPER_CONFIG, - Constants.HEADER_ROUTING_CONFIG, - Constants.HEADER_TOKEN_ENDPOINT, - Constants.HEADER_REMOTE_API_URL, - Constants.HEADER_ISSUER, - Constants.HEADER_CLIENT_ID, - Constants.HEADER_CLIENT_SECRET, - Constants.HEADER_API_BASE_PATH, - "x-consumer-id", - "x-consumer-custom-id", - "x-consumer-groups", - "x-consumer-username", - "x-anonymous-consumer", - "x-anonymous-groups", - "x-forwarded-prefix", - Constants.HEADER_ACCESS_TOKEN_FORWARDING)); - - return builder - .routes() - .route( - "jumper_route", - p -> - p.path(Constants.PROXY_ROOT_PATH_PREFIX + "/**") - .filters( - filterSpec -> - filterSpec - .filter( - requestFilter.apply( - new RequestFilter.Config(Constants.PROXY_ROOT_PATH_PREFIX))) - .filter( - removeRequestHeader.apply( - config -> config.setHeaders(headerRemovalList))) - .filter(responseFilter.apply(config -> {}))) - .uri("no://op")) - .route( - "listener_route", - p -> - p.path(Constants.LISTENER_ROOT_PATH_PREFIX + "/**") - .filters( - filterSpec -> - filterSpec - .filter( - requestFilter.apply( - new RequestFilter.Config( - Constants.LISTENER_ROOT_PATH_PREFIX))) - .filter( - removeRequestHeader.apply( - config -> config.setHeaders(headerRemovalList))) - .filter(requestTransformationFilter) - .filter(spectreRequestFilter.apply(config -> {})) - .filter(responseFilter.apply(config -> {})) - .filter(responseTransformationFilter) - .filter(spectreResponseFilter.apply(config -> {}))) - .uri("no://op")) - .route( - "auto_event_route_post", - p -> - p.path(Constants.AUTOEVENT_ROOT_PATH_PREFIX + "/**") - .and() - .method(HttpMethod.POST) - .filters( - filterSpec -> - filterSpec - .modifyRequestBody(String.class, String.class, spectreBodyRewrite) - .removeRequestParameter(Constants.QUERY_PARAM_LISTENER) - .filter(spectreRoutingFilter.apply())) - .uri(publishEventUrl)) - .route( - "auto_event_route_head", - p -> - p.path(Constants.AUTOEVENT_ROOT_PATH_PREFIX + "/**") - .and() - .method(HttpMethod.HEAD) - .filters( - filterSpec -> - filterSpec - .removeRequestParameter(Constants.QUERY_PARAM_LISTENER) - .filter(spectreRoutingFilter.apply())) - .uri(publishEventUrl)) - .build(); - } } diff --git a/src/main/java/jumper/Constants.java b/src/main/java/jumper/Constants.java index bcdb4dc..5ead550 100644 --- a/src/main/java/jumper/Constants.java +++ b/src/main/java/jumper/Constants.java @@ -15,6 +15,7 @@ public class Constants { public static final String HEADER_X_SPACEGATE_SCOPE = "X-Spacegate-Scope"; public static final String HEADER_JUMPER_CONFIG = "jumper_config"; public static final String HEADER_ROUTING_CONFIG = "routing_config"; + public static final String GATEWAY_ATTRIBUTE_OAUTH_FILTER_NEEDED = "oauth_filter_needed"; public static final String HEADER_ISSUER = "issuer"; public static final String HEADER_TOKEN_ENDPOINT = "token_endpoint"; diff --git a/src/main/java/jumper/config/CachingConfig.java b/src/main/java/jumper/config/CachingConfig.java index 0f833ff..0febea6 100644 --- a/src/main/java/jumper/config/CachingConfig.java +++ b/src/main/java/jumper/config/CachingConfig.java @@ -23,15 +23,18 @@ public class CachingConfig { @Bean @Qualifier("caffeineCacheManager") - public CacheManager cacheManager() { + public CacheManager caffeineCacheManager() { CaffeineCacheManager manager = new CaffeineCacheManager(); + manager.setAllowNullValues(false); cacheConfigProperties.getCaffeineCaches().forEach(cc -> createCaffeineCache(manager, cc)); return manager; } private void createCaffeineCache( - CaffeineCacheManager manager, CacheConfigProperties.CaffeineCache caffeineCache) { - Cache cache = Caffeine.from(caffeineCache.getSpec()).build(); - caffeineCache.getCacheNames().forEach(cn -> manager.registerCustomCache(cn, cache)); + CaffeineCacheManager manager, CacheConfigProperties.CaffeineCache cfg) { + for (String cacheName : cfg.getCacheNames()) { + Cache cache = Caffeine.from(cfg.getSpec()).recordStats().build(); + manager.registerCustomCache(cacheName, cache); + } } } diff --git a/src/main/java/jumper/config/CloudGatewayPrefixedGatewayObservationConvention.java b/src/main/java/jumper/config/CloudGatewayPrefixedGatewayObservationConvention.java new file mode 100644 index 0000000..5b10687 --- /dev/null +++ b/src/main/java/jumper/config/CloudGatewayPrefixedGatewayObservationConvention.java @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2023 Deutsche Telekom AG +// +// SPDX-License-Identifier: Apache-2.0 + +package jumper.config; + +import org.springframework.cloud.gateway.filter.headers.observation.DefaultGatewayObservationConvention; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +@Component +public class CloudGatewayPrefixedGatewayObservationConvention + extends DefaultGatewayObservationConvention { + + public static final String NAME = "spring.cloud.gateway.http.client.requests"; + + @Override + @NonNull + public String getName() { + return NAME; + } +} diff --git a/src/main/java/jumper/config/CustomErrorWebFluxAutoConfiguration.java b/src/main/java/jumper/config/CustomErrorWebFluxAutoConfiguration.java index 04edd79..3cb6638 100644 --- a/src/main/java/jumper/config/CustomErrorWebFluxAutoConfiguration.java +++ b/src/main/java/jumper/config/CustomErrorWebFluxAutoConfiguration.java @@ -6,6 +6,7 @@ import static org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type.REACTIVE; +import io.micrometer.tracing.Tracer; import java.util.stream.Collectors; import jumper.exception.JsonErrorWebExceptionHandler; import org.springframework.beans.factory.ObjectProvider; @@ -19,8 +20,6 @@ import org.springframework.boot.web.reactive.error.DefaultErrorAttributes; import org.springframework.boot.web.reactive.error.ErrorAttributes; import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler; -import org.springframework.cloud.sleuth.CurrentTraceContext; -import org.springframework.cloud.sleuth.Tracer; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -45,8 +44,7 @@ public ErrorWebExceptionHandler errorWebExceptionHandler( ServerCodecConfigurer serverCodecConfigurer, ApplicationContext applicationContext, ServerProperties serverProperties, - Tracer tracer, - CurrentTraceContext currentTraceContext) { + Tracer tracer) { JsonErrorWebExceptionHandler exceptionHandler = new JsonErrorWebExceptionHandler( @@ -54,8 +52,7 @@ public ErrorWebExceptionHandler errorWebExceptionHandler( webProperties.getResources(), serverProperties.getError(), applicationContext, - tracer, - currentTraceContext); + tracer); exceptionHandler.setViewResolvers(viewResolvers.orderedStream().collect(Collectors.toList())); exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters()); diff --git a/src/main/java/jumper/config/HttpClientConfiguration.java b/src/main/java/jumper/config/HttpClientConfiguration.java index 11a808a..9a558b7 100644 --- a/src/main/java/jumper/config/HttpClientConfiguration.java +++ b/src/main/java/jumper/config/HttpClientConfiguration.java @@ -14,6 +14,7 @@ import java.util.stream.Stream; import javax.net.ssl.SSLException; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.PropertyMapper; @@ -29,6 +30,7 @@ @Configuration @RequiredArgsConstructor +@Slf4j public class HttpClientConfiguration { @Value("${CUSTOM_CIPHERS:}") @@ -55,12 +57,13 @@ public HttpClientCustomizer httpClientCustomizer() throws SSLException { } @Bean("spectreServiceWebClient") - public WebClient createWebClientForSpectreService() { - return WebClient.create(); + public WebClient createWebClientForSpectreService(WebClient.Builder webClientBuilder) { + return webClientBuilder.build(); } @Bean("oauthTokenUtilWebClient") - public WebClient createWebClientForOauthTokenUtil() throws SSLException { + public WebClient createWebClientForOauthTokenUtil(WebClient.Builder webClientBuilder) + throws SSLException { SslContext sslContext = createSslContextWithCustomizedCiphers(); HttpClient httpClient = HttpClient.create(getProvider()) @@ -68,7 +71,7 @@ public WebClient createWebClientForOauthTokenUtil() throws SSLException { .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, oauthConnectTimeout); httpClient = configureProxy(httpClient); - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)).build(); + return webClientBuilder.clientConnector(new ReactorClientHttpConnector(httpClient)).build(); } private SslContext createSslContextWithCustomizedCiphers() throws SSLException { @@ -118,6 +121,7 @@ private HttpClient configureProxy(HttpClient httpClient) { // configure proxy only if proxy host is set. if (StringUtils.isNotBlank((properties.getProxy().getHost()))) { + log.info("Configuring Proxy: {}", properties.getProxy()); HttpClientProperties.Proxy proxyProperties = properties.getProxy(); httpClient = httpClient.proxy(proxySpec -> configureProxyProvider(proxyProperties, proxySpec)); diff --git a/src/main/java/jumper/config/RoutingConfiguration.java b/src/main/java/jumper/config/RoutingConfiguration.java new file mode 100644 index 0000000..b8be4e5 --- /dev/null +++ b/src/main/java/jumper/config/RoutingConfiguration.java @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: 2023 Deutsche Telekom AG +// +// SPDX-License-Identifier: Apache-2.0 + +package jumper.config; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import jumper.Constants; +import jumper.filter.*; +import jumper.filter.rewrite.SpectreBodyRewrite; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; + +@Configuration +public class RoutingConfiguration { + + @Value("${jumper.horizon.publishEventUrl}") + private String publishEventUrl; + + @Bean + public RouteLocator proxyRoute( + RouteLocatorBuilder builder, + RequestFilter requestFilter, + UpstreamOAuthFilter upstreamOauthFilter, + RemoveRequestHeaderFilter removeRequestHeader, + ResponseFilter responseFilter, + SpectreRequestFilter spectreRequestFilter, + SpectreResponseFilter spectreResponseFilter, + RequestTransformationFilter requestTransformationFilter, + ResponseTransformationFilter responseTransformationFilter, + SpectreRoutingFilter spectreRoutingFilter, + SpectreBodyRewrite spectreBodyRewrite) { + + Set headerRemovalList = + new HashSet<>( + Arrays.asList( + Constants.HEADER_JUMPER_CONFIG, + Constants.HEADER_ROUTING_CONFIG, + Constants.HEADER_TOKEN_ENDPOINT, + Constants.HEADER_REMOTE_API_URL, + Constants.HEADER_ISSUER, + Constants.HEADER_CLIENT_ID, + Constants.HEADER_CLIENT_SECRET, + Constants.HEADER_API_BASE_PATH, + "x-consumer-id", + "x-consumer-custom-id", + "x-consumer-groups", + "x-consumer-username", + "x-anonymous-consumer", + "x-anonymous-groups", + "x-forwarded-prefix", + Constants.HEADER_ACCESS_TOKEN_FORWARDING)); + + return builder + .routes() + .route( + "jumper_route", + p -> + p.path(Constants.PROXY_ROOT_PATH_PREFIX + "/**") + .filters( + filterSpec -> + filterSpec + .filter( + requestFilter.apply( + new RequestFilter.Config(Constants.PROXY_ROOT_PATH_PREFIX))) + .filter(upstreamOauthFilter.apply(config -> {})) + .filter( + removeRequestHeader.apply( + config -> config.setHeaders(headerRemovalList))) + .filter(responseFilter.apply(config -> {}))) + .uri("no://op")) + .route( + "listener_route", + p -> + p.path(Constants.LISTENER_ROOT_PATH_PREFIX + "/**") + .filters( + filterSpec -> + filterSpec + .filter( + requestFilter.apply( + new RequestFilter.Config( + Constants.LISTENER_ROOT_PATH_PREFIX))) + .filter( + removeRequestHeader.apply( + config -> config.setHeaders(headerRemovalList))) + .filter(requestTransformationFilter) + .filter(spectreRequestFilter.apply(config -> {})) + .filter(responseFilter.apply(config -> {})) + .filter(responseTransformationFilter) + .filter(spectreResponseFilter.apply(config -> {}))) + .uri("no://op")) + .route( + "auto_event_route_post", + p -> + p.path(Constants.AUTOEVENT_ROOT_PATH_PREFIX + "/**") + .and() + .method(HttpMethod.POST) + .filters( + filterSpec -> + filterSpec + .modifyRequestBody(String.class, String.class, spectreBodyRewrite) + .removeRequestParameter(Constants.QUERY_PARAM_LISTENER) + .filter(spectreRoutingFilter.apply())) + .uri(publishEventUrl)) + .route( + "auto_event_route_head", + p -> + p.path(Constants.AUTOEVENT_ROOT_PATH_PREFIX + "/**") + .and() + .method(HttpMethod.HEAD) + .filters( + filterSpec -> + filterSpec + .removeRequestParameter(Constants.QUERY_PARAM_LISTENER) + .filter(spectreRoutingFilter.apply())) + .uri(publishEventUrl)) + .build(); + } +} diff --git a/src/main/java/jumper/config/SecurityConfiguration.java b/src/main/java/jumper/config/SecurityConfiguration.java index 29844a0..5b8af6e 100644 --- a/src/main/java/jumper/config/SecurityConfiguration.java +++ b/src/main/java/jumper/config/SecurityConfiguration.java @@ -8,21 +8,27 @@ import org.springframework.context.annotation.Configuration; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository; +import org.springframework.web.server.session.WebSessionManager; +import reactor.core.publisher.Mono; @Configuration public class SecurityConfiguration { @Bean - public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { - return http.httpBasic() - .disable() - .formLogin() - .disable() - .csrf() - .disable() - .logout() - .disable() + return http.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) + .formLogin(ServerHttpSecurity.FormLoginSpec::disable) + .csrf(ServerHttpSecurity.CsrfSpec::disable) + .logout(ServerHttpSecurity.LogoutSpec::disable) + .securityContextRepository(NoOpServerSecurityContextRepository.getInstance()) .build(); } + + @Bean + public WebSessionManager webSessionManager() { + // Return a WebSessionManager that does nothing + return exchange -> Mono.empty(); + } } diff --git a/src/main/java/jumper/config/SleuthConfiguration.java b/src/main/java/jumper/config/SleuthConfiguration.java deleted file mode 100644 index 527fff2..0000000 --- a/src/main/java/jumper/config/SleuthConfiguration.java +++ /dev/null @@ -1,93 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Deutsche Telekom AG -// -// SPDX-License-Identifier: Apache-2.0 - -package jumper.config; - -import brave.http.HttpRequestParser; -import brave.http.HttpResponseParser; -import java.util.List; -import java.util.regex.Pattern; -import jumper.Constants; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.cloud.sleuth.instrument.web.HttpClientRequestParser; -import org.springframework.cloud.sleuth.instrument.web.HttpClientResponseParser; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.util.UriComponentsBuilder; - -@Configuration(proxyBeanMethods = false) -@Slf4j -public class SleuthConfiguration { - - @Value("${spring.sleuth.filter-param-list:}") - List queryFilterList; - - // see - // https://docs.spring.io/spring-cloud-sleuth/docs/current-SNAPSHOT/reference/html/howto.html#how-to-cutomize-http-client-spans - - @Bean(name = HttpClientResponseParser.NAME) - HttpResponseParser httpResponseParser() { - return ((response, context, span) -> - span.tag("http.status_code", String.valueOf(response.statusCode()))); - } - - @Bean(name = HttpClientRequestParser.NAME) - HttpRequestParser httpRequestParser() { - return (request, context, span) -> { - String xTardisTraceId = request.header(Constants.HEADER_X_TARDIS_TRACE_ID); - - String spanName; - if (request.path().contains("token")) { - spanName = "Idp"; - } else if (request.header(Constants.HEADER_CONSUMER_TOKEN) != null) { - spanName = "Gateway"; - } else { - spanName = "Provider"; - } - - span.name("Outgoing Request: " + spanName); - - span.tag("http.url", filterQueryParams(request.url(), queryFilterList)); - - if (xTardisTraceId != null) { - span.tag(Constants.HEADER_X_TARDIS_TRACE_ID, xTardisTraceId); - } - }; - } - - protected static String filterQueryParams(String urlString, List patterns) { - // first check, if there is something to do - if (!urlString.contains("?") || patterns.isEmpty()) { - return urlString; - } - - List compiledPatterns = patterns.stream().map(Pattern::compile).toList(); - - var uriComponents = UriComponentsBuilder.fromHttpUrl(urlString).build(urlString.contains("%")); - - MultiValueMap filteredParams = new LinkedMultiValueMap<>(); - - uriComponents - .getQueryParams() - .forEach( - (key, values) -> { - if (compiledPatterns.stream().noneMatch(p -> p.matcher(key).matches())) { - filteredParams.put(key, values); - } - }); - - return UriComponentsBuilder.newInstance() - .scheme(uriComponents.getScheme()) - .host(uriComponents.getHost()) - .port(uriComponents.getPort()) - .path(uriComponents.getPath()) - .queryParams(filteredParams) - .fragment(uriComponents.getFragment()) - .build() - .toUriString(); - } -} diff --git a/src/main/java/jumper/config/TracingConfiguration.java b/src/main/java/jumper/config/TracingConfiguration.java new file mode 100644 index 0000000..9c3a40f --- /dev/null +++ b/src/main/java/jumper/config/TracingConfiguration.java @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: 2023 Deutsche Telekom AG +// +// SPDX-License-Identifier: Apache-2.0 + +package jumper.config; + +import io.micrometer.common.KeyValue; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationFilter; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; +import jumper.Constants; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.gateway.filter.headers.observation.GatewayContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientRequestObservationContext; +import org.springframework.web.util.UriComponentsBuilder; + +@Configuration(proxyBeanMethods = false) +@Slf4j +public class TracingConfiguration { + + @Value("${jumper.tracing.filter-param-list:}") + List queryFilterList; + + private List compiledQueryFilterPatterns; + + @Bean + public ObservationFilter customSpanNameFilter() { + // Pre-compile regex patterns for better performance + compiledQueryFilterPatterns = queryFilterList.stream().map(Pattern::compile).toList(); + + return (observationContext) -> { + // Modify the span name + switch (observationContext.getName()) { + case "http.client.requests": + observationContext.setContextualName("outgoing request: webclient"); + if (observationContext instanceof ClientRequestObservationContext clientRequestContext) { + ClientRequest request = clientRequestContext.getRequest(); + + String spanName; + if (request != null && request.url().getPath().contains("token")) { + spanName = "idp"; + } else if (request != null + && request.headers().getFirst(Constants.HEADER_CONSUMER_TOKEN) != null) { + spanName = "gateway"; + } else { + spanName = "unknown"; + } + + clientRequestContext.setContextualName("outgoing request: " + spanName); + + String xTardisTraceId = request.headers().getFirst(Constants.HEADER_X_TARDIS_TRACE_ID); + appendXTardisTraceIdHeader(clientRequestContext, xTardisTraceId); + } + break; + case "http.server.requests": + observationContext.setContextualName("incoming request"); + break; + case CloudGatewayPrefixedGatewayObservationConvention.NAME: + if (observationContext instanceof GatewayContext gatewayContext) { + ServerHttpRequest request = gatewayContext.getRequest(); + + gatewayContext.setContextualName("outgoing request: provider"); + gatewayContext.addHighCardinalityKeyValue( + KeyValue.of( + "http.uri", + filterQueryParams(request.getURI().toString(), compiledQueryFilterPatterns))); + + gatewayContext.removeLowCardinalityKeyValue("spring.cloud.gateway.route.id"); + gatewayContext.removeLowCardinalityKeyValue("spring.cloud.gateway.route.uri"); + + String xTardisTraceId = + request.getHeaders().getFirst(Constants.HEADER_X_TARDIS_TRACE_ID); + appendXTardisTraceIdHeader(gatewayContext, xTardisTraceId); + } + break; + } + return observationContext; + }; + } + + private static void appendXTardisTraceIdHeader( + Observation.Context gatewayContext, String xTardisTraceId) { + if (xTardisTraceId != null) { + gatewayContext.addHighCardinalityKeyValue( + KeyValue.of(Constants.HEADER_X_TARDIS_TRACE_ID, xTardisTraceId)); + } + } + + protected String filterQueryParams(String urlString, List compiledPatterns) { + // first check, if there is something to do + if (!urlString.contains("?") || compiledPatterns.isEmpty()) { + return urlString; + } + + var uriComponents = + UriComponentsBuilder.fromUriString(urlString).build(urlString.contains("%")); + + MultiValueMap filteredParams = new LinkedMultiValueMap<>(); + + uriComponents + .getQueryParams() + .forEach( + (key, values) -> { + if (compiledPatterns.stream().noneMatch(p -> p.matcher(key).matches())) { + filteredParams.put(key, values); + } + }); + + return UriComponentsBuilder.newInstance() + .scheme(uriComponents.getScheme()) + .host(uriComponents.getHost()) + .port(uriComponents.getPort()) + .path(Optional.ofNullable(uriComponents.getPath()).orElse("")) + .queryParams(filteredParams) + .fragment(uriComponents.getFragment()) + .build() + .toUriString(); + } +} diff --git a/src/main/java/jumper/exception/JsonErrorWebExceptionHandler.java b/src/main/java/jumper/exception/JsonErrorWebExceptionHandler.java index c249fad..f920dfc 100644 --- a/src/main/java/jumper/exception/JsonErrorWebExceptionHandler.java +++ b/src/main/java/jumper/exception/JsonErrorWebExceptionHandler.java @@ -6,6 +6,8 @@ import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.Tracer; import io.netty.handler.ssl.SslHandshakeTimeoutException; import java.util.Date; import java.util.HashMap; @@ -19,10 +21,6 @@ import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler; import org.springframework.boot.web.error.ErrorAttributeOptions; import org.springframework.boot.web.reactive.error.ErrorAttributes; -import org.springframework.cloud.sleuth.CurrentTraceContext; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.instrument.web.WebFluxSleuthOperators; import org.springframework.context.ApplicationContext; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; @@ -43,7 +41,6 @@ public class JsonErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler { private final Tracer tracer; - private final CurrentTraceContext currentTraceContext; @Value("${spring.application.name}") private String applicationName; @@ -55,12 +52,10 @@ public JsonErrorWebExceptionHandler( Resources resources, ErrorProperties errorProperties, ApplicationContext applicationContext, - Tracer tracer, - CurrentTraceContext currentTraceContext) { + Tracer tracer) { super(errorAttributes, resources, errorProperties, applicationContext); this.tracer = tracer; - this.currentTraceContext = currentTraceContext; } @Override @@ -82,7 +77,7 @@ protected Map getErrorAttributes( errorAttributes.put("message", determineMessage(error, responseStatusAnnotation)); errorAttributes.put("error", errorStatus.getReasonPhrase()); errorAttributes.put("status", errorStatus.value()); - errorAttributes.put("method", request.methodName()); + errorAttributes.put("method", request.method().name()); errorAttributes.put( "traceId", @@ -96,11 +91,7 @@ protected Map getErrorAttributes( ? request.headers().firstHeader(Constants.HEADER_X_TARDIS_TRACE_ID) : ""); - WebFluxSleuthOperators.withSpanInScope( - tracer, - currentTraceContext, - request.exchange(), - () -> writeErrorSpan(error, errorAttributes)); + writeErrorSpan(error, errorAttributes); // should also evaluate include options (stacktrace, message, bindingErrors) return errorAttributes; @@ -144,7 +135,7 @@ private HttpStatus findHttpStatus( customResponseHeaders = new HashMap<>(); if (error instanceof ResponseStatusException) { - return ((ResponseStatusException) error).getStatus(); + return HttpStatus.valueOf(((ResponseStatusException) error).getStatusCode().value()); } /* @@ -169,21 +160,8 @@ private HttpStatus findHttpStatus( .orElse(INTERNAL_SERVER_ERROR); } - @Override - protected void logError(ServerRequest request, ServerResponse response, Throwable throwable) { - WebFluxSleuthOperators.withSpanInScope( - tracer, - currentTraceContext, - request.exchange(), - () -> super.logError(request, response, throwable)); - } - private void logError(ServerRequest request, Throwable throwable) { - WebFluxSleuthOperators.withSpanInScope( - tracer, - currentTraceContext, - request.exchange(), - () -> log.error(request.exchange().getLogPrefix() + this.formatError(throwable, request))); + log.error(request.exchange().getLogPrefix() + this.formatError(throwable, request)); } private String formatError(Throwable ex, ServerRequest request) { @@ -210,7 +188,6 @@ private String determineMessage( private void writeErrorSpan(Throwable error, Map errorAttributes) { Span errorSpan = this.tracer.nextSpan().name("error").start(); - tracer.withSpan(errorSpan); errorSpan.tag("message", (String) errorAttributes.get("message")); errorSpan.tag("http.status_code", errorAttributes.get("status").toString()); diff --git a/src/main/java/jumper/filter/RemoveRequestHeaderFilter.java b/src/main/java/jumper/filter/RemoveRequestHeaderFilter.java index bae5e03..4090cf9 100644 --- a/src/main/java/jumper/filter/RemoveRequestHeaderFilter.java +++ b/src/main/java/jumper/filter/RemoveRequestHeaderFilter.java @@ -10,7 +10,6 @@ import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.OrderedGatewayFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; -import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Component; @Component @@ -27,16 +26,13 @@ public RemoveRequestHeaderFilter() { @Override public GatewayFilter apply(Config config) { return new OrderedGatewayFilter( - (exchange, chain) -> { - ServerHttpRequest request = - exchange - .getRequest() - .mutate() - .headers(httpHeaders -> config.getHeaders().forEach(httpHeaders::remove)) - .build(); - - return chain.filter(exchange.mutate().request(request).build()); - }, + (exchange, chain) -> + chain.filter( + exchange + .mutate() + .request( + req -> req.headers(headers -> config.getHeaders().forEach(headers::remove))) + .build()), REMOVE_REQUEST_HEADER_FILTER_ORDER); } diff --git a/src/main/java/jumper/filter/RequestFilter.java b/src/main/java/jumper/filter/RequestFilter.java index f5de1e6..ac6a2fd 100644 --- a/src/main/java/jumper/filter/RequestFilter.java +++ b/src/main/java/jumper/filter/RequestFilter.java @@ -4,21 +4,22 @@ package jumper.filter; -import static net.logstash.logback.argument.StructuredArguments.value; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.Tracer; import java.net.URI; import java.net.URISyntaxException; import java.util.List; import java.util.Objects; import java.util.Optional; import jumper.Constants; -import jumper.model.TokenInfo; -import jumper.model.config.BasicAuthCredentials; +import jumper.filter.strategy.AuthenticationStrategy; import jumper.model.config.JumperConfig; -import jumper.model.config.OauthCredentials; import jumper.model.request.IncomingRequest; import jumper.model.request.JumperInfoRequest; import jumper.service.*; +import jumper.util.HeaderUtil; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; @@ -30,10 +31,6 @@ import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; -import org.springframework.cloud.sleuth.CurrentTraceContext; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.instrument.web.WebFluxSleuthOperators; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Component; @@ -45,11 +42,9 @@ @Setter public class RequestFilter extends AbstractGatewayFilterFactory { - private final CurrentTraceContext currentTraceContext; private final Tracer tracer; - private final OauthTokenUtil oauthTokenUtil; - private final BasicAuthUtil basicAuthUtil; private final ZoneHealthCheckService zoneHealthCheckService; + private final List authenticationStrategies; @Value("${jumper.issuer.url}") private String localIssuerUrl; @@ -60,408 +55,238 @@ public class RequestFilter extends AbstractGatewayFilterFactory internetFacingZones; - @Value("${spring.application.name}") - private String applicationName; - public static final int REQUEST_FILTER_ORDER = RouteToRequestUrlFilter.ROUTE_TO_URL_FILTER_ORDER + 1; public RequestFilter( - CurrentTraceContext currentTraceContext, Tracer tracer, - OauthTokenUtil oauthTokenUtil, - BasicAuthUtil basicAuthUtil, - ZoneHealthCheckService zoneHealthCheckService) { + ZoneHealthCheckService zoneHealthCheckService, + List authenticationStrategies) { super(Config.class); - this.currentTraceContext = currentTraceContext; this.tracer = tracer; - this.oauthTokenUtil = oauthTokenUtil; - this.basicAuthUtil = basicAuthUtil; this.zoneHealthCheckService = zoneHealthCheckService; + this.authenticationStrategies = authenticationStrategies; } @Override public GatewayFilter apply(Config config) { return new OrderedGatewayFilter( (exchange, chain) -> { - WebFluxSleuthOperators.withSpanInScope( - tracer, - currentTraceContext, - exchange, - () -> { - ServerHttpRequest request = exchange.getRequest(); - addTracingInfo(request); - - JumperConfig jumperConfig; - // failover logic if routing_config header present - if (request.getHeaders().containsKey(Constants.HEADER_ROUTING_CONFIG)) { - // evaluate routingConfig for failover scenario - List jumperConfigList = - JumperConfig.parseJumperConfigListFrom(request); - log.debug("failover case, routing_config: {}", jumperConfigList); - jumperConfig = - evaluateTargetZone( - jumperConfigList, - request.getHeaders().getFirst(Constants.HEADER_X_FAILOVER_SKIP_ZONE)); - jumperConfig.fillProcessingInfo(request); - log.debug("failover case, enhanced jumper_config: {}", jumperConfig); - - } - - // no failover - else { - // Prepare and extract JumperConfigValues - jumperConfig = JumperConfig.parseAndFillJumperConfigFrom(request); - log.debug("JumperConfig decoded: {}", jumperConfig); - } - - // calculate routing stuff and add it to exchange and JumperConfig - calculateRoutingStuff(request, exchange, config.getRoutePathPrefix(), jumperConfig); - - if (config.getRoutePathPrefix().equals(Constants.LISTENER_ROOT_PATH_PREFIX)) { - // ListenerRoute was called, jumperConfig is stored in exchange for later usage - // within Spectre - exchange - .getAttributes() - .put(Constants.HEADER_JUMPER_CONFIG, JumperConfig.toBase64(jumperConfig)); - } - - if (jumperConfig.getSecondaryFailover()) { - // write audit log if needed - AuditLogService.writeFailoverAuditLog(jumperConfig); - - // pass headers from config to provider - HeaderUtil.addHeader( - exchange, Constants.HEADER_REALM, jumperConfig.getRealmName()); - HeaderUtil.addHeader( - exchange, Constants.HEADER_ENVIRONMENT, jumperConfig.getEnvName()); - } - - // handle request - Optional jumperInfoRequest = - initializeJumperInfoRequest(jumperConfig); - - if (!jumperConfig - .getRemoteApiUrl() - .startsWith(Constants.LOCALHOST_ISSUER_SERVICE)) { - - if (Objects.nonNull(jumperConfig.getInternalTokenEndpoint())) { - // GW-2-GW MESH TOKEN GENERATION - log.debug("----------------GATEWAY MESH-------------"); - jumperInfoRequest.ifPresent( - i -> i.setInfoScenario(false, false, true, false, false, false)); - - TokenInfo meshTokenInfo = - oauthTokenUtil.getInternalMeshAccessToken(jumperConfig); - - // set gw and consumer tokens correctly - HeaderUtil.addHeader( - exchange, - Constants.HEADER_AUTHORIZATION, - "Bearer " + meshTokenInfo.getAccessToken()); - HeaderUtil.addHeader( - exchange, Constants.HEADER_CONSUMER_TOKEN, jumperConfig.getConsumerToken()); - - checkForInternetFacingZone( - exchange, - jumperConfig.getConsumerOriginZone(), - jumperConfig.getConsumerToken()); - - } else { - // ALL NON MESH SCENARIOS - - if (request.getHeaders().containsKey(Constants.HEADER_X_TOKEN_EXCHANGE) - && isInternetFacingZone(currentZone)) { - - log.debug("----------------X-TOKEN-EXCHANGE HEADER-------------"); - jumperInfoRequest.ifPresent( - i -> i.setInfoScenario(false, false, false, false, false, true)); - - addXtokenExchange(exchange); - - } else { - - Optional basicAuthCredentials = - jumperConfig.getBasicAuthCredentials(); - if (basicAuthCredentials.isPresent()) { - // External Authorization with BasicAuth - log.debug("----------------BASIC AUTH HEADER-------------"); - jumperInfoRequest.ifPresent( - i -> i.setInfoScenario(false, false, false, false, true, false)); - - String encodedBasicAuth = - basicAuthUtil.encodeBasicAuth( - basicAuthCredentials.get().getUsername(), - basicAuthCredentials.get().getPassword()); - - HeaderUtil.addHeader( - exchange, - Constants.HEADER_AUTHORIZATION, - Constants.BASIC + " " + encodedBasicAuth); - - } else { - - if (Objects.nonNull(jumperConfig.getExternalTokenEndpoint())) { - // External Authorization with OAuth - log.debug("----------------EXTERNAL AUTHORIZATION-------------"); - log.debug( - "Remote TokenEndpoint is set to: {}", - jumperConfig.getExternalTokenEndpoint()); - jumperInfoRequest.ifPresent( - i -> i.setInfoScenario(false, false, false, true, false, false)); - - Optional oauthCredentials = - jumperConfig.getOauthCredentials(); - if (oauthCredentials.isPresent() - && StringUtils.isNotBlank(oauthCredentials.get().getGrantType())) { - - TokenInfo tokenInfo = - oauthTokenUtil.getAccessTokenWithOauthCredentialsObject( - jumperConfig.getExternalTokenEndpoint(), - oauthCredentials.get()); - - HeaderUtil.addHeader( - exchange, - Constants.HEADER_AUTHORIZATION, - Constants.BEARER + " " + tokenInfo.getAccessToken()); - - } else { - getAccessTokenFromExternalIdpLegacy(exchange, jumperConfig); - } - - } else if (Boolean.FALSE.equals(jumperConfig.getAccessTokenForwarding())) { - // Enhanced Last Mile Security Token scenario - log.debug("----------------LAST MILE SECURITY (ONE TOKEN)-------------"); - jumperInfoRequest.ifPresent( - i -> i.setInfoScenario(true, true, false, false, false, false)); - - String enhancedLastmileSecurityToken = - oauthTokenUtil.generateEnhancedLastMileGatewayToken( - jumperConfig, - String.valueOf(request.getMethod()), - localIssuerUrl + "/" + jumperConfig.getRealmName(), - HeaderUtil.getLastValueFromHeaderField( - request, Constants.HEADER_X_PUBSUB_PUBLISHER_ID), - HeaderUtil.getLastValueFromHeaderField( - request, Constants.HEADER_X_PUBSUB_SUBSCRIBER_ID), - false); - - HeaderUtil.addHeader( - exchange, - Constants.HEADER_AUTHORIZATION, - Constants.BEARER + " " + enhancedLastmileSecurityToken); - log.debug("lastMileSecurityToken: " + enhancedLastmileSecurityToken); - - } else { - // (Legacy) Last Mile Security Token scenario - log.debug("----------------LAST MILE SECURITY (LEGACY)-------------"); - jumperInfoRequest.ifPresent( - i -> i.setInfoScenario(true, false, false, false, false, false)); - - String legacyLastmileSecurityToken = - oauthTokenUtil.generateEnhancedLastMileGatewayToken( - jumperConfig, - String.valueOf(request.getMethod()), - localIssuerUrl + "/" + jumperConfig.getRealmName(), - HeaderUtil.getLastValueFromHeaderField( - request, Constants.HEADER_X_PUBSUB_PUBLISHER_ID), - HeaderUtil.getLastValueFromHeaderField( - request, Constants.HEADER_X_PUBSUB_SUBSCRIBER_ID), - true); - - HeaderUtil.addHeader( - exchange, - Constants.HEADER_LASTMILE_SECURITY_TOKEN, - Constants.BEARER + " " + legacyLastmileSecurityToken); - log.debug("lastMileSecurityToken: " + legacyLastmileSecurityToken); - } - } - } - } - } - - HeaderUtil.addHeader( - exchange, - Constants.HEADER_X_ORIGIN_STARGATE, - jumperConfig.getConsumerOriginStargate()); - HeaderUtil.addHeader( - exchange, Constants.HEADER_X_ORIGIN_ZONE, jumperConfig.getConsumerOriginZone()); - HeaderUtil.rewriteXForwardedHeader(exchange, jumperConfig); - - jumperInfoRequest.ifPresent( - infoRequest -> { - IncomingRequest incReq = createIncomingRequest(jumperConfig, request); - infoRequest.setIncomingRequest(incReq); - log.info("logging request: {}", value("jumperInfo", infoRequest)); - }); - - HeaderUtil.removeHeaders(exchange, jumperConfig.getRemoveHeaders()); - tracer.currentSpan().event("jrqf"); - }); - - return chain.filter(exchange); + var context = createRequestContext(exchange, config); + + handleListenerRoute(context); + handleFailoverAuditLog(context); + applyAuthentication(context); + addStandardHeaders(context); + logRequest(context); + finalizeRequest(context); + + return chain.filter(context.getFinalExchange()); }, - RouteToRequestUrlFilter.ROUTE_TO_URL_FILTER_ORDER + 1); + REQUEST_FILTER_ORDER); } - private Optional initializeJumperInfoRequest(JumperConfig jumperConfig) { + private RequestProcessingContext createRequestContext(ServerWebExchange exchange, Config config) { + ServerHttpRequest readOnlyRequest = exchange.getRequest(); + addOriginalRequestUrl(exchange, readOnlyRequest.getURI()); - if (log.isInfoEnabled()) { - JumperInfoRequest jumperInfoRequest = new JumperInfoRequest(); - return Optional.of(jumperInfoRequest); - } + ServerHttpRequest.Builder requestMutationBuilder = readOnlyRequest.mutate(); + enrichTracingWithDataFrom(readOnlyRequest); - return Optional.empty(); - } + JumperConfig jumperConfig = extractJumperConfig(readOnlyRequest); + URI finalApiUri = + calculateFinalApiUri(readOnlyRequest, config.getRoutePathPrefix(), jumperConfig); + Optional jumperInfoRequest = initializeJumperInfoRequest(); - private IncomingRequest createIncomingRequest( - JumperConfig jumperConfig, ServerHttpRequest request) { - IncomingRequest incReq = new IncomingRequest(); - incReq.setConsumer(jumperConfig.getConsumer()); - incReq.setBasePath(jumperConfig.getApiBasePath()); - incReq.setFinalApiUrl(jumperConfig.getFinalApiUrl()); - incReq.setMethod((request.getMethodValue())); - incReq.setRequestPath(jumperConfig.getRequestPath()); + return new RequestProcessingContext( + exchange, + config, + readOnlyRequest, + requestMutationBuilder, + jumperConfig, + jumperInfoRequest, + finalApiUri, + currentZone, + internetFacingZones, + localIssuerUrl); + } - return incReq; + private void handleListenerRoute(RequestProcessingContext context) { + if (context.getConfig().getRoutePathPrefix().equals(Constants.LISTENER_ROOT_PATH_PREFIX)) { + context + .getExchange() + .getAttributes() + .put( + Constants.HEADER_JUMPER_CONFIG, JumperConfig.toJsonBase64(context.getJumperConfig())); + } } - private void calculateRoutingStuff( - ServerHttpRequest request, - ServerWebExchange exchange, - String routePathPrefix, - JumperConfig jumperConfig) { + private void handleFailoverAuditLog(RequestProcessingContext context) { + if (context.getJumperConfig().getSecondaryFailover()) { + AuditLogService.writeFailoverAuditLog(context.getJumperConfig()); - try { - URI uri = request.getURI(); - String queryParameterPart = uri.getRawQuery(); - String fragmentPart = uri.getFragment(); - String routingPath = uri.getRawPath().replaceFirst("^" + routePathPrefix, ""); + HeaderUtil.addHeader( + context.getRequestBuilder(), + Constants.HEADER_REALM, + context.getJumperConfig().getRealmName()); + HeaderUtil.addHeader( + context.getRequestBuilder(), + Constants.HEADER_ENVIRONMENT, + context.getJumperConfig().getEnvName()); + } + } - String requestPath = jumperConfig.getApiBasePath() + routingPath; + private void applyAuthentication(RequestProcessingContext context) { + authenticationStrategies.stream() + .filter(strategy -> strategy.canHandle(context)) + .findFirst() + .ifPresent( + strategy -> { + log.debug("Applying authentication strategy: {}", strategy.getStrategyName()); + strategy.authenticate(context); + }); + } - if (Objects.nonNull(queryParameterPart)) { - routingPath += "?" + queryParameterPart; - } + private void addStandardHeaders(RequestProcessingContext context) { + HeaderUtil.addHeader( + context.getRequestBuilder(), + Constants.HEADER_X_ORIGIN_STARGATE, + context.getJumperConfig().getConsumerOriginStargate()); + HeaderUtil.addHeader( + context.getRequestBuilder(), + Constants.HEADER_X_ORIGIN_ZONE, + context.getJumperConfig().getConsumerOriginZone()); + HeaderUtil.rewriteXForwardedHeader(context.getRequestBuilder(), context.getJumperConfig()); + } - if (Objects.nonNull(fragmentPart)) { - routingPath += "#" + fragmentPart; - } + private void logRequest(RequestProcessingContext context) { + context + .getJumperInfoRequest() + .ifPresent( + infoRequest -> { + IncomingRequest incReq = + createIncomingRequest(context.getJumperConfig(), context.getReadOnlyRequest()); + infoRequest.setIncomingRequest(incReq); + log.atInfo() + .setMessage("logging request:") + .addKeyValue("jumperInfo", infoRequest) + .log(); + }); + } - String finalApiUrl = jumperConfig.getRemoteApiUrl().replaceAll("/$", "") + routingPath; + private void finalizeRequest(RequestProcessingContext context) { + HeaderUtil.removeHeaders( + context.getRequestBuilder(), context.getJumperConfig().getRemoveHeaders()); + tracer.currentSpan().event("jrqf"); - // store final destination url to exchange - log.debug("Routing set to: " + finalApiUrl); - exchange - .getAttributes() - .put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, new URI(finalApiUrl)); + log.debug("Routing set to: " + context.getFinalApiUri()); + context.getRequestBuilder().uri(context.getFinalApiUri()); + ServerHttpRequest finalRequest = context.getRequestBuilder().build(); + context + .getExchange() + .getAttributes() + .put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, context.getFinalApiUri()); - // add calculated stuff to jumperConfig - jumperConfig.setRequestPath(requestPath); - jumperConfig.setRoutingPath(routingPath); - jumperConfig.setFinalApiUrl(finalApiUrl); + ServerWebExchange finalExchange = context.getExchange().mutate().request(finalRequest).build(); + context.setFinalExchange(finalExchange); - } catch (URISyntaxException e) { - throw new RuntimeException("can not construct URL from " + request.getURI(), e); - } + log.debug("final RequestFilter uri: {}", finalExchange.getRequest().getURI()); + log.debug( + "final exchange attribute GatewayRequestUrlAttr: {}", + finalExchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR).toString()); } - private void getAccessTokenFromExternalIdpLegacy(ServerWebExchange exchange, JumperConfig jc) { - - String consumer = jc.getConsumer(); - String tokenEndpoint = jc.getExternalTokenEndpoint(); + private void setOAuthFilterNeeded(ServerWebExchange exchange, boolean isNeeded) { + exchange.getAttributes().put(Constants.GATEWAY_ATTRIBUTE_OAUTH_FILTER_NEEDED, isNeeded); + } - Optional oauthCredentials = jc.getOauthCredentials(); + private JumperConfig extractJumperConfig(ServerHttpRequest readOnlyRequest) { + JumperConfig jumperConfig; + // failover logic if routing_config header present + if (readOnlyRequest.getHeaders().containsKey(Constants.HEADER_ROUTING_CONFIG)) { + // evaluate routingConfig for failover scenario + List jumperConfigList = JumperConfig.parseJumperConfigListFrom(readOnlyRequest); + log.debug("failover case, routing_config: {}", jumperConfigList); + jumperConfig = + evaluateTargetZone( + jumperConfigList, + readOnlyRequest.getHeaders().getFirst(Constants.HEADER_X_FAILOVER_SKIP_ZONE)); + jumperConfig.fillProcessingInfo(readOnlyRequest); + log.debug("failover case, enhanced jumper_config: {}", jumperConfig); - String clientId = determineClientId(exchange, jc, oauthCredentials); - String clientSecret = determineClientSecret(exchange, jc, oauthCredentials); - String clientScope = determineClientScope(exchange, jc, oauthCredentials); + } - log.debug("Get token for consumer: {} with clientId: {}", consumer, clientId); - if (Objects.nonNull(clientId) && Objects.nonNull(clientSecret)) { - TokenInfo tokenInfo = - oauthTokenUtil.getAccessTokenWithClientCredentials( - tokenEndpoint, clientId, clientSecret, clientScope); - HeaderUtil.addHeader( - exchange, - Constants.HEADER_AUTHORIZATION, - Constants.BEARER + " " + tokenInfo.getAccessToken()); - - } else { - log.warn("not specified oauth config credentials for consumer: {}", consumer); - throw new ResponseStatusException( - HttpStatus.UNAUTHORIZED, "Missing oauth config credentials for consumer " + consumer); + // no failover + else { + // Prepare and extract JumperConfigValues + jumperConfig = JumperConfig.parseAndFillJumperConfigFrom(readOnlyRequest); + log.debug("JumperConfig decoded: {}", jumperConfig); } + return jumperConfig; } - private static String determineClientScope( - ServerWebExchange exchange, JumperConfig jc, Optional oauthCredentials) { + private Optional initializeJumperInfoRequest() { - String clientScope = ""; - String xSpacegateScope = jc.getXSpacegateScope(); + if (log.isDebugEnabled()) { + JumperInfoRequest jumperInfoRequest = new JumperInfoRequest(); + return Optional.of(jumperInfoRequest); + } - if (Objects.nonNull(xSpacegateScope)) { - log.debug("Using Scope from xSpacegateScope-Header"); - clientScope = xSpacegateScope; - HeaderUtil.removeHeader(exchange, Constants.HEADER_X_SPACEGATE_SCOPE); + return Optional.empty(); + } - } else if (oauthCredentials.isPresent() - && StringUtils.isNotBlank(oauthCredentials.get().getScopes())) { - clientScope = oauthCredentials.get().getScopes(); + private IncomingRequest createIncomingRequest( + JumperConfig jumperConfig, ServerHttpRequest request) { + IncomingRequest incReq = new IncomingRequest(); + incReq.setConsumer(jumperConfig.getConsumer()); + incReq.setBasePath(jumperConfig.getApiBasePath()); + incReq.setFinalApiUrl(jumperConfig.getFinalApiUrl()); + incReq.setMethod(request.getMethod().name()); + incReq.setRequestPath(jumperConfig.getRequestPath()); - } else { - log.debug("Using default Provider scope"); - if (StringUtils.isNotBlank(jc.getScopes())) { - clientScope = jc.getScopes(); - } - } - return clientScope; + return incReq; } - private static String determineClientSecret( - ServerWebExchange exchange, JumperConfig jc, Optional oauthCredentials) { + private URI calculateFinalApiUri( + ServerHttpRequest request, String routePathPrefix, JumperConfig jumperConfig) { - String clientSecret = jc.getClientSecret(); - String xSpacegateClientSecret = jc.getXSpacegateClientSecret(); - - if (Objects.nonNull(xSpacegateClientSecret)) { - log.debug("Using SubscriberClientSecret from xSpacegateClientSecret-Header"); - clientSecret = xSpacegateClientSecret; - HeaderUtil.removeHeader(exchange, Constants.HEADER_X_SPACEGATE_CLIENT_SECRET); + try { + URI uri = request.getURI(); - } else if (oauthCredentials.isPresent() - && StringUtils.isNotBlank(oauthCredentials.get().getClientSecret())) { - log.debug("Using SubscriberClientSecret from JumperConfig"); - clientSecret = oauthCredentials.get().getClientSecret(); + String rawPath = uri.getRawPath(); + String routingPath = + rawPath.startsWith(routePathPrefix) + ? rawPath.substring(routePathPrefix.length()) + : rawPath; - } else { - log.debug("Using default ProviderClientSecret"); - } - return clientSecret; - } + String requestPath = jumperConfig.getApiBasePath() + routingPath; - private static String determineClientId( - ServerWebExchange exchange, JumperConfig jc, Optional oauthCredentials) { + if (Objects.nonNull(uri.getRawQuery())) { + routingPath += "?" + uri.getRawQuery(); + } - String clientId = jc.getClientId(); - String xSpacegateClientId = jc.getXSpacegateClientId(); + if (Objects.nonNull(uri.getFragment())) { + routingPath += "#" + uri.getFragment(); + } - if (StringUtils.isNotBlank(xSpacegateClientId)) { - log.debug("Using SubscriberClientId {} from xSpacegateClientId-Header", xSpacegateClientId); - clientId = xSpacegateClientId; - HeaderUtil.removeHeader(exchange, Constants.HEADER_X_SPACEGATE_CLIENT_ID); + String finalApiUrl = + (jumperConfig.getRemoteApiUrl().endsWith("/") + ? jumperConfig + .getRemoteApiUrl() + .substring(0, jumperConfig.getRemoteApiUrl().length() - 1) + : jumperConfig.getRemoteApiUrl()) + + routingPath; - } else if (oauthCredentials.isPresent() - && StringUtils.isNotBlank(oauthCredentials.get().getClientId())) { + // add calculated stuff to jumperConfig + jumperConfig.setRequestPath(requestPath); + jumperConfig.setRoutingPath(routingPath); + jumperConfig.setFinalApiUrl(finalApiUrl); - log.debug( - "Using SubscriberClientId {} from JumperConfig", oauthCredentials.get().getClientId()); - clientId = oauthCredentials.get().getClientId(); + return new URI(finalApiUrl); - } else { - log.debug("Using default ProviderClientId {}", clientId); + } catch (URISyntaxException e) { + throw new RuntimeException("can not construct URL from " + request.getURI(), e); } - return clientId; } private JumperConfig evaluateTargetZone( @@ -483,47 +308,17 @@ private JumperConfig evaluateTargetZone( HttpStatus.SERVICE_UNAVAILABLE, "Non of defined failover zones available"); } - private void checkForInternetFacingZone(ServerWebExchange exchange, String zone, String token) { - if (isInternetFacingZone(zone)) { - HeaderUtil.addHeader(exchange, Constants.HEADER_X_SPACEGATE_TOKEN, token); - } - } - - private void addXtokenExchange(ServerWebExchange exchange) { - - HeaderUtil.addHeader( - exchange, - Constants.HEADER_AUTHORIZATION, - HeaderUtil.getFirstValueFromHeaderField( - exchange.getRequest(), Constants.HEADER_X_TOKEN_EXCHANGE)); - - log.debug( - "x-token-exchange: " - + HeaderUtil.getFirstValueFromHeaderField( - exchange.getRequest(), Constants.HEADER_X_TOKEN_EXCHANGE)); - } - - private boolean isInternetFacingZone(String zone) { - - return zone != null && internetFacingZones.contains(zone); - } - - private void addTracingInfo(ServerHttpRequest request) { + private void enrichTracingWithDataFrom(ServerHttpRequest request) { + Span span = this.tracer.currentSpan(); String xTardisTraceId = HeaderUtil.getLastValueFromHeaderField(request, Constants.HEADER_X_TARDIS_TRACE_ID); - String contentLength = HeaderUtil.getLastValueFromHeaderField(request, "Content-Length"); - - Span incomingRequestSpan = tracer.currentSpan(); - incomingRequestSpan.name("Incoming Request"); - - incomingRequestSpan.tag("message.size", Objects.requireNonNullElse(contentLength, "0")); - if (xTardisTraceId != null) { - incomingRequestSpan.tag(Constants.HEADER_X_TARDIS_TRACE_ID, xTardisTraceId); + span.tag(Constants.HEADER_X_TARDIS_TRACE_ID, xTardisTraceId); } - incomingRequestSpan.remoteServiceName(applicationName); + String contentLength = HeaderUtil.getLastValueFromHeaderField(request, "Content-Length"); + span.tag("message.size", Objects.requireNonNullElse(contentLength, "0")); } @AllArgsConstructor diff --git a/src/main/java/jumper/filter/RequestProcessingContext.java b/src/main/java/jumper/filter/RequestProcessingContext.java new file mode 100644 index 0000000..4ecb209 --- /dev/null +++ b/src/main/java/jumper/filter/RequestProcessingContext.java @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2023 Deutsche Telekom AG +// +// SPDX-License-Identifier: Apache-2.0 + +package jumper.filter; + +import java.net.URI; +import java.util.List; +import java.util.Optional; +import jumper.Constants; +import jumper.model.config.JumperConfig; +import jumper.model.request.JumperInfoRequest; +import lombok.Data; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; + +/** + * Context object that encapsulates all data needed for request processing in the RequestFilter. + * This helps reduce parameter passing and provides a clean interface for authentication strategies. + */ +@Data +public class RequestProcessingContext { + + private final ServerWebExchange exchange; + private final RequestFilter.Config config; + private final ServerHttpRequest readOnlyRequest; + private final ServerHttpRequest.Builder requestBuilder; + private final JumperConfig jumperConfig; + private final Optional jumperInfoRequest; + private final URI finalApiUri; + private final String currentZone; + private final List internetFacingZones; + private final String localIssuerUrl; + + private ServerWebExchange finalExchange; + + /** Checks if the request is targeting localhost issuer service. */ + public boolean isRemoteHostTarget() { + return !jumperConfig.getRemoteApiUrl().startsWith(Constants.LOCALHOST_ISSUER_SERVICE); + } + + /** Checks if the current zone is internet-facing. */ + public boolean isCurrentZoneInternetFacing() { + return currentZone != null && internetFacingZones.contains(currentZone); + } + + /** Checks if the given zone is internet-facing. */ + public boolean isInternetFacingZone(String zone) { + return zone != null && internetFacingZones.contains(zone); + } +} diff --git a/src/main/java/jumper/filter/RequestTransformationFilter.java b/src/main/java/jumper/filter/RequestTransformationFilter.java index fc9180d..d2353aa 100644 --- a/src/main/java/jumper/filter/RequestTransformationFilter.java +++ b/src/main/java/jumper/filter/RequestTransformationFilter.java @@ -24,7 +24,7 @@ public class RequestTransformationFilter implements GatewayFilter, Ordered { private final ModifyRequestBodyGatewayFilterFactory modifyRequestBodyFilter; private final RequestBodyRewrite requestBodyRewrite; - @Value("${spring.codec.max-in-memory-size}") + @Value("${spring.http.codecs.max-in-memory-size}") private int limit; public static final int REQUEST_TRANSFORM_FILTER_ORDER = diff --git a/src/main/java/jumper/filter/ResponseFilter.java b/src/main/java/jumper/filter/ResponseFilter.java index 2787cb3..dd1c1cf 100644 --- a/src/main/java/jumper/filter/ResponseFilter.java +++ b/src/main/java/jumper/filter/ResponseFilter.java @@ -4,8 +4,8 @@ package jumper.filter; -import static net.logstash.logback.argument.StructuredArguments.value; - +import io.micrometer.tracing.Span; +import io.micrometer.tracing.Tracer; import java.util.Objects; import jumper.model.response.IncomingResponse; import jumper.model.response.JumperInfoResponse; @@ -17,10 +17,6 @@ import org.springframework.cloud.gateway.filter.OrderedGatewayFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; -import org.springframework.cloud.sleuth.CurrentTraceContext; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.instrument.web.WebFluxSleuthOperators; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; @@ -29,12 +25,10 @@ @Slf4j public class ResponseFilter extends AbstractGatewayFilterFactory { - private final CurrentTraceContext currentTraceContext; private final Tracer tracer; - public ResponseFilter(CurrentTraceContext currentTraceContext, Tracer tracer) { + public ResponseFilter(Tracer tracer) { super(Config.class); - this.currentTraceContext = currentTraceContext; this.tracer = tracer; } @@ -49,46 +43,41 @@ public GatewayFilter apply(Config config) { if (exchange.getResponse().isCommitted()) { return; } - WebFluxSleuthOperators.withSpanInScope( - tracer, - currentTraceContext, - exchange, - () -> { - ServerHttpResponse response = exchange.getResponse(); - ServerHttpRequest request = exchange.getRequest(); - - if (log.isDebugEnabled()) { - JumperInfoResponse jumperInfoResponse = new JumperInfoResponse(); - IncomingResponse incomingResponse = new IncomingResponse(); - - incomingResponse.setHost( - Objects.requireNonNull( - exchange.getAttribute( - ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR)) - .toString()); - incomingResponse.setHttpStatusCode( - Objects.requireNonNull(response.getStatusCode()).value()); - incomingResponse.setMethod(request.getMethodValue()); - incomingResponse.setRequestHeaders( - request.getHeaders().toSingleValueMap()); - jumperInfoResponse.setIncomingResponse(incomingResponse); + ServerHttpResponse response = exchange.getResponse(); + ServerHttpRequest request = exchange.getRequest(); - log.debug( - "logging response: {}", value("jumperInfo", jumperInfoResponse)); - } + if (log.isDebugEnabled()) { + JumperInfoResponse jumperInfoResponse = new JumperInfoResponse(); + IncomingResponse incomingResponse = new IncomingResponse(); - long contentLength = response.getHeaders().getContentLength(); + incomingResponse.setHost( + Objects.requireNonNull( + exchange.getAttribute( + ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR)) + .toString()); + incomingResponse.setHttpStatusCode( + Objects.requireNonNull(response.getStatusCode()).value()); + incomingResponse.setMethod(request.getMethod().name()); + incomingResponse.setRequestHeaders(request.getHeaders().toSingleValueMap()); + jumperInfoResponse.setIncomingResponse(incomingResponse); - Span span = tracer.currentSpan(); + log.atDebug() + .setMessage("logging response:") + .addKeyValue("jumperInfo", jumperInfoResponse) + .log(); + } - if (Long.toString(contentLength).equals("-1")) { - span.tag("message.size_response", "0"); - } else { - span.tag("message.size_response", Long.toString(contentLength)); - } + long contentLength = response.getHeaders().getContentLength(); - span.event("jrpf"); - }); + Span span = tracer.currentSpan(); + if (span != null) { + if (contentLength == -1L) { + span.tag("message.size_response", "0"); + } else { + span.tag("message.size_response", Long.toString(contentLength)); + } + span.event("jrpf"); + } }), RequestFilter.REQUEST_FILTER_ORDER); } diff --git a/src/main/java/jumper/filter/SpectreRequestFilter.java b/src/main/java/jumper/filter/SpectreRequestFilter.java index 8e67066..059fd92 100644 --- a/src/main/java/jumper/filter/SpectreRequestFilter.java +++ b/src/main/java/jumper/filter/SpectreRequestFilter.java @@ -48,7 +48,10 @@ public GatewayFilter apply(Config config) { RouteListener listener = jc.getRouteListener().get(jc.getConsumer()); - spectreService.handleEvent(jc, exchange, exchange.getRequest(), listener, requestBody); + // Fire-and-forget: publish event asynchronously without blocking the request flow + spectreService + .handleEvent(jc, exchange, exchange.getRequest(), listener, requestBody) + .subscribe(); return chain.filter(exchange); }, AUTO_EVENT_REQUEST_FILTER_ORDER); diff --git a/src/main/java/jumper/filter/SpectreResponseFilter.java b/src/main/java/jumper/filter/SpectreResponseFilter.java index a9afb07..6fe45a8 100644 --- a/src/main/java/jumper/filter/SpectreResponseFilter.java +++ b/src/main/java/jumper/filter/SpectreResponseFilter.java @@ -59,12 +59,16 @@ public GatewayFilter apply(AbstractGatewayFilterFactory.NameConfig config) { if (jumperConfig.isListenerMatched()) { RouteListener listener = jumperConfig.getRouteListener().get(jumperConfig.getConsumer()); - spectreService.handleEvent( - jumperConfig, - exchange, - exchange.getResponse(), - listener, - responseBody); + // Fire-and-forget: publish event asynchronously without blocking the + // response flow + spectreService + .handleEvent( + jumperConfig, + exchange, + exchange.getResponse(), + listener, + responseBody) + .subscribe(); } })), AUTO_EVENT_RESPONSE_FILTER_ORDER); diff --git a/src/main/java/jumper/filter/SpectreRoutingFilter.java b/src/main/java/jumper/filter/SpectreRoutingFilter.java index a6b4338..5aebe92 100644 --- a/src/main/java/jumper/filter/SpectreRoutingFilter.java +++ b/src/main/java/jumper/filter/SpectreRoutingFilter.java @@ -25,15 +25,16 @@ public class SpectreRoutingFilter extends SetRequestHeaderGatewayFilterFactory { @Value("${jumper.issuer.url}") private String localIssuerUrl; - @Value("${horizon.publishEventUrl}") + @Value("${jumper.horizon.publishEventUrl}") private String publishEventUrl; public GatewayFilter apply() { return (exchange, chain) -> { - ServerHttpRequest req = exchange.getRequest(); + ServerHttpRequest readOnlyRequest = exchange.getRequest(); + ServerHttpRequest.Builder requestMutationBuilder = readOnlyRequest.mutate(); // no environment info sent from kong, so we dig it from token - String consumerToken = req.getHeaders().getFirst(Constants.HEADER_AUTHORIZATION); + String consumerToken = readOnlyRequest.getHeaders().getFirst(Constants.HEADER_AUTHORIZATION); String envName = Constants.DEFAULT_REALM; if (Objects.nonNull(consumerToken)) { envName = @@ -47,21 +48,19 @@ public GatewayFilter apply() { localIssuerUrl + "/" + envName, envName); // routing path is no longer fixed, so we set it here - ServerHttpRequest request = - req.mutate() - .headers(httpHeaders -> httpHeaders.set(Constants.HEADER_AUTHORIZATION, spectreToken)) - // placeholder is expected just on virtual environments like qa - .path( - URI.create( - publishEventUrl.replaceFirst(Constants.ENVIRONMENT_PLACEHOLDER, envName)) - .getPath()) - .build(); + requestMutationBuilder + .headers(httpHeaders -> httpHeaders.set(Constants.HEADER_AUTHORIZATION, spectreToken)) + // placeholder is expected just on virtual environments like qa + .path( + URI.create(publishEventUrl.replaceFirst(Constants.ENVIRONMENT_PLACEHOLDER, envName)) + .getPath()) + .build(); exchange .getAttributes() - .put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, request.getURI()); + .put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, readOnlyRequest.getURI()); - return chain.filter(exchange.mutate().request(request).build()); + return chain.filter(exchange.mutate().request(requestMutationBuilder.build()).build()); }; } } diff --git a/src/main/java/jumper/filter/UpstreamOAuthFilter.java b/src/main/java/jumper/filter/UpstreamOAuthFilter.java new file mode 100644 index 0000000..af25654 --- /dev/null +++ b/src/main/java/jumper/filter/UpstreamOAuthFilter.java @@ -0,0 +1,227 @@ +// SPDX-FileCopyrightText: 2023 Deutsche Telekom AG +// +// SPDX-License-Identifier: Apache-2.0 + +package jumper.filter; + +import java.util.Objects; +import java.util.Optional; +import jumper.Constants; +import jumper.model.TokenInfo; +import jumper.model.config.JumperConfig; +import jumper.model.config.OauthCredentials; +import jumper.service.OauthTokenUtil; +import jumper.util.HeaderUtil; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.OrderedGatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +@Component +@Slf4j +@Setter +public class UpstreamOAuthFilter extends AbstractGatewayFilterFactory { + + public static final int UPSTREAM_OAUTH_FILTER_ORDER = RequestFilter.REQUEST_FILTER_ORDER + 1; + + private final OauthTokenUtil oauthTokenUtil; + + public UpstreamOAuthFilter(OauthTokenUtil oauthTokenUtil) { + super(Config.class); + this.oauthTokenUtil = oauthTokenUtil; + } + + @Override + public GatewayFilter apply(Config config) { + return new OrderedGatewayFilter( + (exchange, chain) -> { + ServerHttpRequest readOnlyRequest = exchange.getRequest(); + + // Early exit for localhost issuer service + if (!oauthFilterNeeded(exchange)) { + log.debug("Skipping UpstreamOAuthFilter for localhost issuer service"); + return chain.filter(exchange.mutate().request(readOnlyRequest).build()); + } + + log.debug("continue with UpstreamOAuthFilter"); + JumperConfig jumperConfig = + JumperConfig.parseAndFillJumperConfigFrom(exchange.getRequest()); + log.debug("JumperConfig: {}", jumperConfig); + + // Determine which token retrieval method to use and create a reactive chain + return determineTokenRetrievalStrategy(jumperConfig, readOnlyRequest) + .map(tokenInfo -> setBearerToken(readOnlyRequest, tokenInfo)) + .flatMap( + mutatedRequest -> chain.filter(exchange.mutate().request(mutatedRequest).build())) + .onErrorResume( + throwable -> { + log.error("Error retrieving OAuth token: {}", throwable.getMessage()); + return Mono.error(throwable); + }); + }, + UPSTREAM_OAUTH_FILTER_ORDER); + } + + private boolean oauthFilterNeeded(ServerWebExchange exchange) { + return exchange.getAttributes().get(Constants.GATEWAY_ATTRIBUTE_OAUTH_FILTER_NEEDED) != null + && (Boolean) exchange.getAttributes().get(Constants.GATEWAY_ATTRIBUTE_OAUTH_FILTER_NEEDED); + } + + public static class Config {} + + /** Consolidates Bearer token setting logic to avoid duplication */ + private ServerHttpRequest setBearerToken(ServerHttpRequest request, TokenInfo tokenInfo) { + ServerHttpRequest.Builder requestBuilder = request.mutate(); + HeaderUtil.addHeader( + requestBuilder, Constants.HEADER_AUTHORIZATION, "Bearer " + tokenInfo.getAccessToken()); + return requestBuilder.build(); + } + + /** Determines the appropriate token retrieval strategy based on JumperConfig */ + private Mono determineTokenRetrievalStrategy( + JumperConfig jumperConfig, ServerHttpRequest request) { + if (Objects.nonNull(jumperConfig.getInternalTokenEndpoint())) { + // GW-2-GW MESH TOKEN GENERATION - no tracing needed for internal mesh calls + log.debug("----------------GATEWAY MESH-------------"); + return oauthTokenUtil.getInternalMeshAccessToken(jumperConfig); + + } else if (Objects.nonNull(jumperConfig.getExternalTokenEndpoint())) { + // External Authorization with OAuth - create CLIENT span for external calls + log.debug("----------------EXTERNAL AUTHORIZATION-------------"); + log.debug("Remote TokenEndpoint is set to: {}", jumperConfig.getExternalTokenEndpoint()); + + Optional oauthCredentials = jumperConfig.getOauthCredentials(); + Mono tokenMono; + + if (oauthCredentials.isPresent() + && StringUtils.isNotBlank(oauthCredentials.get().getGrantType())) { + log.debug("fetching token with OauthCredentials"); + tokenMono = + oauthTokenUtil.getAccessTokenWithOauthCredentialsObject( + jumperConfig.getExternalTokenEndpoint(), oauthCredentials.get()); + } else { + log.debug("fetching token with legacy method"); + tokenMono = getAccessTokenFromExternalIdpLegacy(request.mutate(), jumperConfig); + } + + // Add span management to the reactive chain + return tokenMono.onErrorResume(Mono::error); + + } else { + // No token endpoint configured - return empty token (this will cause an error downstream) + return Mono.error( + new ResponseStatusException( + HttpStatus.UNAUTHORIZED, + "No token endpoint configured whether internal or external, this should not happen")); + } + } + + private Mono getAccessTokenFromExternalIdpLegacy( + ServerHttpRequest.Builder builder, JumperConfig jc) { + + String consumer = jc.getConsumer(); + String tokenEndpoint = jc.getExternalTokenEndpoint(); + + Optional oauthCredentials = jc.getOauthCredentials(); + + String clientId = determineClientId(builder, jc, oauthCredentials); + String clientSecret = determineClientSecret(builder, jc, oauthCredentials); + String clientScope = determineClientScope(builder, jc, oauthCredentials); + + log.debug("Get token for consumer: {} with clientId: {}", consumer, clientId); + if (Objects.nonNull(clientId) && Objects.nonNull(clientSecret)) { + return oauthTokenUtil.getAccessTokenWithClientCredentials( + tokenEndpoint, clientId, clientSecret, clientScope); + } else { + log.warn("not specified oauth config credentials for consumer: {}", consumer); + return Mono.error( + new ResponseStatusException( + HttpStatus.UNAUTHORIZED, + "Missing oauth config credentials for consumer " + consumer)); + } + } + + private static String determineClientScope( + ServerHttpRequest.Builder builder, + JumperConfig jc, + Optional oauthCredentials) { + + String clientScope = ""; + String xSpacegateScope = jc.getXSpacegateScope(); + + if (Objects.nonNull(xSpacegateScope)) { + log.debug("Using Scope from xSpacegateScope-Header"); + clientScope = xSpacegateScope; + HeaderUtil.removeHeader(builder, Constants.HEADER_X_SPACEGATE_SCOPE); + + } else if (oauthCredentials.isPresent() + && StringUtils.isNotBlank(oauthCredentials.get().getScopes())) { + clientScope = oauthCredentials.get().getScopes(); + + } else { + log.debug("Using default Provider scope"); + if (StringUtils.isNotBlank(jc.getScopes())) { + clientScope = jc.getScopes(); + } + } + return clientScope; + } + + private static String determineClientSecret( + ServerHttpRequest.Builder builder, + JumperConfig jc, + Optional oauthCredentials) { + + String clientSecret = jc.getClientSecret(); + String xSpacegateClientSecret = jc.getXSpacegateClientSecret(); + + if (Objects.nonNull(xSpacegateClientSecret)) { + log.debug("Using SubscriberClientSecret from xSpacegateClientSecret-Header"); + clientSecret = xSpacegateClientSecret; + HeaderUtil.removeHeader(builder, Constants.HEADER_X_SPACEGATE_CLIENT_SECRET); + + } else if (oauthCredentials.isPresent() + && StringUtils.isNotBlank(oauthCredentials.get().getClientSecret())) { + log.debug("Using SubscriberClientSecret from JumperConfig"); + clientSecret = oauthCredentials.get().getClientSecret(); + + } else { + log.debug("Using default ProviderClientSecret"); + } + return clientSecret; + } + + private static String determineClientId( + ServerHttpRequest.Builder builder, + JumperConfig jc, + Optional oauthCredentials) { + + String clientId = jc.getClientId(); + String xSpacegateClientId = jc.getXSpacegateClientId(); + + if (StringUtils.isNotBlank(xSpacegateClientId)) { + log.debug("Using SubscriberClientId {} from xSpacegateClientId-Header", xSpacegateClientId); + clientId = xSpacegateClientId; + HeaderUtil.removeHeader(builder, Constants.HEADER_X_SPACEGATE_CLIENT_ID); + + } else if (oauthCredentials.isPresent() + && StringUtils.isNotBlank(oauthCredentials.get().getClientId())) { + + log.debug( + "Using SubscriberClientId {} from JumperConfig", oauthCredentials.get().getClientId()); + clientId = oauthCredentials.get().getClientId(); + + } else { + log.debug("Using default ProviderClientId {}", clientId); + } + return clientId; + } +} diff --git a/src/main/java/jumper/filter/rewrite/AbstractBodyRewrite.java b/src/main/java/jumper/filter/rewrite/AbstractBodyRewrite.java index 7990efd..581cc41 100644 --- a/src/main/java/jumper/filter/rewrite/AbstractBodyRewrite.java +++ b/src/main/java/jumper/filter/rewrite/AbstractBodyRewrite.java @@ -4,18 +4,18 @@ package jumper.filter.rewrite; +import java.util.Base64; import java.util.Objects; import jumper.config.SpectreConfiguration; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; -import org.springframework.util.Base64Utils; @Slf4j public abstract class AbstractBodyRewrite { - @Value("${spring.codec.max-in-memory-size}") + @Value("${spring.http.codecs.max-in-memory-size}") private int limit; @Autowired private SpectreConfiguration spectreConfiguration; @@ -28,7 +28,7 @@ String getBodyForContentType(MediaType mediaType, byte[] originalBody) { } else { log.debug("MediaType identified as non text, store as base64"); - bodyToStore = Base64Utils.encodeToString(originalBody); + bodyToStore = Base64.getEncoder().encodeToString(originalBody); } if (bodyToStore.length() > limit) { diff --git a/src/main/java/jumper/filter/rewrite/SpectreBodyRewrite.java b/src/main/java/jumper/filter/rewrite/SpectreBodyRewrite.java index 46a3b8a..73ddf0e 100644 --- a/src/main/java/jumper/filter/rewrite/SpectreBodyRewrite.java +++ b/src/main/java/jumper/filter/rewrite/SpectreBodyRewrite.java @@ -5,11 +5,11 @@ package jumper.filter.rewrite; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.util.Objects; import jumper.Constants; import jumper.model.config.Spectre; +import jumper.util.ObjectMapperUtil; import lombok.extern.slf4j.Slf4j; import org.reactivestreams.Publisher; import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction; @@ -35,9 +35,9 @@ public Publisher apply(ServerWebExchange exchange, String body) { String eventJson = null; try { - eventJson = new ObjectMapper().writeValueAsString(event); + eventJson = ObjectMapperUtil.getInstance().writeValueAsString(event); } catch (JsonProcessingException e) { - e.printStackTrace(); + log.error("Failed to write event type as json", e); } log.debug("Spectre: adjusted={}", eventJson); @@ -48,10 +48,10 @@ private Spectre adjustEventType(String body, String id) { Spectre event = null; try { - event = new ObjectMapper().readValue(body, Spectre.class); + event = ObjectMapperUtil.getInstance().readValue(body, Spectre.class); event.setType(event.getType() + "." + id); } catch (IOException e) { - e.printStackTrace(); + log.error("Failed to adjust event type", e); } return event; diff --git a/src/main/java/jumper/filter/strategy/AuthenticationStrategy.java b/src/main/java/jumper/filter/strategy/AuthenticationStrategy.java new file mode 100644 index 0000000..4a39fbf --- /dev/null +++ b/src/main/java/jumper/filter/strategy/AuthenticationStrategy.java @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2023 Deutsche Telekom AG +// +// SPDX-License-Identifier: Apache-2.0 + +package jumper.filter.strategy; + +import jumper.filter.RequestProcessingContext; + +/** + * Strategy interface for handling different authentication scenarios in the Gateway Jumper. Each + * implementation handles a specific authentication method (mesh tokens, basic auth, etc.). + */ +public interface AuthenticationStrategy { + + /** + * Applies the authentication strategy to the request. + * + * @param context The request processing context containing all necessary data + */ + void authenticate(RequestProcessingContext context); + + /** + * Determines if this strategy can handle the given request context. + * + * @param context The request processing context + * @return true if this strategy should be used for the request + */ + boolean canHandle(RequestProcessingContext context); + + /** + * Returns the name of this authentication strategy for logging purposes. + * + * @return strategy name + */ + String getStrategyName(); +} diff --git a/src/main/java/jumper/filter/strategy/BasicAuthStrategy.java b/src/main/java/jumper/filter/strategy/BasicAuthStrategy.java new file mode 100644 index 0000000..ac4bae3 --- /dev/null +++ b/src/main/java/jumper/filter/strategy/BasicAuthStrategy.java @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2023 Deutsche Telekom AG +// +// SPDX-License-Identifier: Apache-2.0 + +package jumper.filter.strategy; + +import java.util.Optional; +import jumper.Constants; +import jumper.filter.RequestProcessingContext; +import jumper.model.config.BasicAuthCredentials; +import jumper.util.BasicAuthUtil; +import jumper.util.HeaderUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Authentication strategy for Basic Authentication scenarios. Handles external authorization using + * username/password credentials. + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class BasicAuthStrategy implements AuthenticationStrategy { + + @Override + public void authenticate(RequestProcessingContext context) { + log.debug("----------------BASIC AUTH HEADER-------------"); + + context + .getJumperInfoRequest() + .ifPresent(i -> i.setInfoScenario(false, false, false, false, true, false)); + + Optional basicAuthCredentials = + context.getJumperConfig().getBasicAuthCredentials(); + + if (basicAuthCredentials.isPresent()) { + String encodedBasicAuth = + BasicAuthUtil.encodeBasicAuth( + basicAuthCredentials.get().getUsername(), basicAuthCredentials.get().getPassword()); + + HeaderUtil.addHeader( + context.getRequestBuilder(), + Constants.HEADER_AUTHORIZATION, + Constants.BASIC + " " + encodedBasicAuth); + } + } + + @Override + public boolean canHandle(RequestProcessingContext context) { + return context.isRemoteHostTarget() + && context.getJumperConfig().getInternalTokenEndpoint() == null + && (!context + .getReadOnlyRequest() + .getHeaders() + .containsKey(Constants.HEADER_X_TOKEN_EXCHANGE) + || !context.isCurrentZoneInternetFacing()) + && context.getJumperConfig().getBasicAuthCredentials().isPresent(); + } + + @Override + public String getStrategyName() { + return "BasicAuth"; + } +} diff --git a/src/main/java/jumper/filter/strategy/ExternalOAuthStrategy.java b/src/main/java/jumper/filter/strategy/ExternalOAuthStrategy.java new file mode 100644 index 0000000..851f971 --- /dev/null +++ b/src/main/java/jumper/filter/strategy/ExternalOAuthStrategy.java @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2023 Deutsche Telekom AG +// +// SPDX-License-Identifier: Apache-2.0 + +package jumper.filter.strategy; + +import java.util.Objects; +import jumper.Constants; +import jumper.filter.RequestProcessingContext; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Authentication strategy for external OAuth token scenarios. Handles cases where an external token + * endpoint is configured and OAuth processing is needed. + */ +@Component +@Slf4j +public class ExternalOAuthStrategy implements AuthenticationStrategy { + + @Override + public void authenticate(RequestProcessingContext context) { + log.debug("----------------EXTERNAL OAUTH-------------"); + + // Set OAuth filter needed flag - actual token retrieval happens in UpstreamOAuthFilter + setOAuthFilterNeeded(context); + } + + @Override + public boolean canHandle(RequestProcessingContext context) { + return context.isRemoteHostTarget() + && context.getJumperConfig().getInternalTokenEndpoint() == null + && (!context + .getReadOnlyRequest() + .getHeaders() + .containsKey(Constants.HEADER_X_TOKEN_EXCHANGE) + || !context.isCurrentZoneInternetFacing()) + && context.getJumperConfig().getBasicAuthCredentials().isEmpty() + && Objects.nonNull(context.getJumperConfig().getExternalTokenEndpoint()); + } + + @Override + public String getStrategyName() { + return "ExternalOAuth"; + } + + private void setOAuthFilterNeeded(RequestProcessingContext context) { + context + .getExchange() + .getAttributes() + .put(Constants.GATEWAY_ATTRIBUTE_OAUTH_FILTER_NEEDED, true); + } +} diff --git a/src/main/java/jumper/filter/strategy/LastMileSecurityStrategy.java b/src/main/java/jumper/filter/strategy/LastMileSecurityStrategy.java new file mode 100644 index 0000000..461d608 --- /dev/null +++ b/src/main/java/jumper/filter/strategy/LastMileSecurityStrategy.java @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2023 Deutsche Telekom AG +// +// SPDX-License-Identifier: Apache-2.0 + +package jumper.filter.strategy; + +import java.util.Objects; +import jumper.Constants; +import jumper.filter.RequestProcessingContext; +import jumper.service.OauthTokenUtil; +import jumper.util.HeaderUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Authentication strategy for Enhanced Last Mile Security Token scenarios. Handles cases where no + * external token endpoint is configured and generates a last mile security token for the request. + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class LastMileSecurityStrategy implements AuthenticationStrategy { + + private final OauthTokenUtil oauthTokenUtil; + + @Override + public void authenticate(RequestProcessingContext context) { + log.debug("----------------LAST MILE SECURITY (ONE TOKEN)-------------"); + + context + .getJumperInfoRequest() + .ifPresent(i -> i.setInfoScenario(true, true, false, false, false, false)); + + String enhancedLastmileSecurityToken = + oauthTokenUtil.generateEnhancedLastMileGatewayToken( + context.getJumperConfig(), + String.valueOf(context.getReadOnlyRequest().getMethod()), + context.getLocalIssuerUrl() + "/" + context.getJumperConfig().getRealmName(), + HeaderUtil.getLastValueFromHeaderField( + context.getReadOnlyRequest(), Constants.HEADER_X_PUBSUB_PUBLISHER_ID), + HeaderUtil.getLastValueFromHeaderField( + context.getReadOnlyRequest(), Constants.HEADER_X_PUBSUB_SUBSCRIBER_ID), + false); + + HeaderUtil.addHeader( + context.getRequestBuilder(), + Constants.HEADER_AUTHORIZATION, + Constants.BEARER + " " + enhancedLastmileSecurityToken); + + log.debug("lastMileSecurityToken: " + enhancedLastmileSecurityToken); + } + + @Override + public boolean canHandle(RequestProcessingContext context) { + return context.isRemoteHostTarget() + && context.getJumperConfig().getInternalTokenEndpoint() == null + && (!context + .getReadOnlyRequest() + .getHeaders() + .containsKey(Constants.HEADER_X_TOKEN_EXCHANGE) + || !context.isCurrentZoneInternetFacing()) + && context.getJumperConfig().getBasicAuthCredentials().isEmpty() + && Objects.isNull(context.getJumperConfig().getExternalTokenEndpoint()); + } + + @Override + public String getStrategyName() { + return "LastMileSecurity"; + } +} diff --git a/src/main/java/jumper/filter/strategy/MeshTokenStrategy.java b/src/main/java/jumper/filter/strategy/MeshTokenStrategy.java new file mode 100644 index 0000000..8f449a9 --- /dev/null +++ b/src/main/java/jumper/filter/strategy/MeshTokenStrategy.java @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2023 Deutsche Telekom AG +// +// SPDX-License-Identifier: Apache-2.0 + +package jumper.filter.strategy; + +import java.util.Objects; +import jumper.Constants; +import jumper.filter.RequestProcessingContext; +import jumper.util.HeaderUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Authentication strategy for Gateway-to-Gateway mesh token generation. Handles internal token + * endpoint scenarios where gateways communicate with each other. + */ +@Component +@Slf4j +public class MeshTokenStrategy implements AuthenticationStrategy { + + @Override + public void authenticate(RequestProcessingContext context) { + log.debug("----------------GATEWAY MESH-------------"); + + context + .getJumperInfoRequest() + .ifPresent(i -> i.setInfoScenario(false, false, true, false, false, false)); + + HeaderUtil.addHeader( + context.getRequestBuilder(), + Constants.HEADER_CONSUMER_TOKEN, + context.getJumperConfig().getConsumerToken()); + + checkForInternetFacingZone(context); + setOAuthFilterNeeded(context, true); + } + + @Override + public boolean canHandle(RequestProcessingContext context) { + return context.isRemoteHostTarget() + && Objects.nonNull(context.getJumperConfig().getInternalTokenEndpoint()); + } + + @Override + public String getStrategyName() { + return "MeshToken"; + } + + private void checkForInternetFacingZone(RequestProcessingContext context) { + String zone = context.getJumperConfig().getConsumerOriginZone(); + String token = context.getJumperConfig().getConsumerToken(); + + if (context.isInternetFacingZone(zone)) { + HeaderUtil.addHeader(context.getRequestBuilder(), Constants.HEADER_X_SPACEGATE_TOKEN, token); + } + } + + private void setOAuthFilterNeeded(RequestProcessingContext context, boolean isNeeded) { + context + .getExchange() + .getAttributes() + .put(Constants.GATEWAY_ATTRIBUTE_OAUTH_FILTER_NEEDED, isNeeded); + } +} diff --git a/src/main/java/jumper/filter/strategy/XTokenExchangeStrategy.java b/src/main/java/jumper/filter/strategy/XTokenExchangeStrategy.java new file mode 100644 index 0000000..a672402 --- /dev/null +++ b/src/main/java/jumper/filter/strategy/XTokenExchangeStrategy.java @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2023 Deutsche Telekom AG +// +// SPDX-License-Identifier: Apache-2.0 + +package jumper.filter.strategy; + +import jumper.Constants; +import jumper.filter.RequestProcessingContext; +import jumper.util.HeaderUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Authentication strategy for X-Token-Exchange header scenarios. Handles token exchange when the + * request contains the X-Token-Exchange header and the current zone is internet-facing. + */ +@Component +@Slf4j +public class XTokenExchangeStrategy implements AuthenticationStrategy { + + @Override + public void authenticate(RequestProcessingContext context) { + log.debug("----------------X-TOKEN-EXCHANGE HEADER-------------"); + + context + .getJumperInfoRequest() + .ifPresent(i -> i.setInfoScenario(false, false, false, false, false, true)); + + addXTokenExchange(context); + } + + @Override + public boolean canHandle(RequestProcessingContext context) { + return context.isRemoteHostTarget() + && context.getJumperConfig().getInternalTokenEndpoint() == null + && context.getReadOnlyRequest().getHeaders().containsKey(Constants.HEADER_X_TOKEN_EXCHANGE) + && context.isCurrentZoneInternetFacing(); + } + + @Override + public String getStrategyName() { + return "XTokenExchange"; + } + + private void addXTokenExchange(RequestProcessingContext context) { + HeaderUtil.addHeader( + context.getRequestBuilder(), + Constants.HEADER_AUTHORIZATION, + HeaderUtil.getFirstValueFromHeaderField( + context.getReadOnlyRequest(), Constants.HEADER_X_TOKEN_EXCHANGE)); + + log.debug( + "x-token-exchange: " + + HeaderUtil.getFirstValueFromHeaderField( + context.getReadOnlyRequest(), Constants.HEADER_X_TOKEN_EXCHANGE)); + } +} diff --git a/src/main/java/jumper/model/config/JumperConfig.java b/src/main/java/jumper/model/config/JumperConfig.java index cb5be46..74a505f 100644 --- a/src/main/java/jumper/model/config/JumperConfig.java +++ b/src/main/java/jumper/model/config/JumperConfig.java @@ -9,23 +9,24 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Header; import io.jsonwebtoken.Jwt; +import jakarta.validation.constraints.NotNull; import java.util.*; -import javax.validation.constraints.NotNull; import jumper.Constants; -import jumper.service.HeaderUtil; import jumper.service.OauthTokenUtil; +import jumper.util.ObjectMapperUtil; +import jumper.util.HeaderUtil; import lombok.Data; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.server.ServerWebExchange; @Data @JsonInclude(JsonInclude.Include.NON_NULL) +@Slf4j public class JumperConfig { private HashMap oauth; @@ -71,33 +72,31 @@ public class JumperConfig { Boolean secondaryFailover = false; @JsonIgnore - public static String toBase64(Object o) { + public static String toJsonBase64(Object o) { String jsonConfigBase64 = null; try { - String decodedJson = new ObjectMapper().writeValueAsString(o); + String decodedJson = ObjectMapperUtil.getInstance().writeValueAsString(o); jsonConfigBase64 = Base64.getEncoder().encodeToString(decodedJson.getBytes()); } catch (JsonProcessingException e) { - e.printStackTrace(); + log.error("can not base64encode object: " + o); } return jsonConfigBase64; } @JsonIgnore - private static T fromBase64(String jsonConfigBase64, TypeReference typeReference) { + private static T fromJsonBase64(String jsonConfigBase64, TypeReference typeReference) { String decodedJson = new String(Base64.getDecoder().decode(jsonConfigBase64.getBytes())); try { - return new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .readValue(decodedJson, typeReference); + return ObjectMapperUtil.getInstance().readValue(decodedJson, typeReference); } catch (JsonProcessingException e) { throw new RuntimeException("can not base64decode header: " + jsonConfigBase64); } } - private static JumperConfig fromBase64(String jsonConfigBase64) { + private static JumperConfig fromJsonBase64(String jsonConfigBase64) { if (StringUtils.isNotBlank(jsonConfigBase64)) { - return JumperConfig.fromBase64(jsonConfigBase64, new TypeReference<>() {}); + return JumperConfig.fromJsonBase64(jsonConfigBase64, new TypeReference<>() {}); } else { return new JumperConfig(); } @@ -181,7 +180,7 @@ public void fillProcessingInfo(ServerHttpRequest request) { // Spectre stuff JumperConfig jc = - JumperConfig.fromBase64( + JumperConfig.fromJsonBase64( HeaderUtil.getLastValueFromHeaderField(request, Constants.HEADER_JUMPER_CONFIG)); this.setRouteListener(jc.getRouteListener()); this.setGatewayClient(jc.getGatewayClient()); @@ -200,7 +199,7 @@ public static List parseJumperConfigListFrom(ServerHttpRequest req HeaderUtil.getLastValueFromHeaderField(request, Constants.HEADER_ROUTING_CONFIG); if (StringUtils.isNotBlank(routingConfigBase64)) { - return JumperConfig.fromBase64(routingConfigBase64, new TypeReference<>() {}); + return JumperConfig.fromJsonBase64(routingConfigBase64, new TypeReference<>() {}); } throw new RuntimeException("can not base64decode header: " + routingConfigBase64); @@ -210,7 +209,7 @@ public static List parseJumperConfigListFrom(ServerHttpRequest req public static JumperConfig parseAndFillJumperConfigFrom(ServerHttpRequest request) { JumperConfig jc = - JumperConfig.fromBase64( + JumperConfig.fromJsonBase64( HeaderUtil.getLastValueFromHeaderField(request, Constants.HEADER_JUMPER_CONFIG)); jc.fillWithLegacyHeaders( @@ -222,7 +221,7 @@ public static JumperConfig parseAndFillJumperConfigFrom(ServerHttpRequest reques @JsonIgnore public static JumperConfig parseJumperConfigFrom(ServerWebExchange exchange) { - return JumperConfig.fromBase64(exchange.getAttribute(Constants.HEADER_JUMPER_CONFIG)); + return JumperConfig.fromJsonBase64(exchange.getAttribute(Constants.HEADER_JUMPER_CONFIG)); } public boolean isListenerMatched() { diff --git a/src/main/java/jumper/model/response/JumperInfoResponse.java b/src/main/java/jumper/model/response/JumperInfoResponse.java index b09cba5..7425a14 100644 --- a/src/main/java/jumper/model/response/JumperInfoResponse.java +++ b/src/main/java/jumper/model/response/JumperInfoResponse.java @@ -5,7 +5,7 @@ package jumper.model.response; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import jumper.util.ObjectMapperUtil; import lombok.Getter; import lombok.Setter; @@ -16,9 +16,8 @@ public class JumperInfoResponse { @Override public String toString() { - ObjectMapper objectMapper = new ObjectMapper(); try { - return objectMapper.writeValueAsString(incomingResponse); + return ObjectMapperUtil.getInstance().writeValueAsString(incomingResponse); } catch (JsonProcessingException e) { throw new RuntimeException(e); } diff --git a/src/main/java/jumper/service/AuditLogService.java b/src/main/java/jumper/service/AuditLogService.java index ca8be1e..74d1ca3 100644 --- a/src/main/java/jumper/service/AuditLogService.java +++ b/src/main/java/jumper/service/AuditLogService.java @@ -11,12 +11,9 @@ @Slf4j public class AuditLogService { - public static void logInfo(String msg) { - log.info(msg); - } public static void writeFailoverAuditLog(JumperConfig jumperConfig) { - logInfo( + log.info( AuditLog.builder() .upstreamPath(jumperConfig.getFinalApiUrl()) .apiBasePath(jumperConfig.getApiBasePath()) diff --git a/src/main/java/jumper/service/HeaderUtil.java b/src/main/java/jumper/service/HeaderUtil.java deleted file mode 100644 index 16e92e1..0000000 --- a/src/main/java/jumper/service/HeaderUtil.java +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Deutsche Telekom AG -// -// SPDX-License-Identifier: Apache-2.0 - -package jumper.service; - -import java.net.MalformedURLException; -import java.net.URL; -import java.util.List; -import java.util.Objects; -import jumper.Constants; -import jumper.model.config.JumperConfig; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.web.server.ServerWebExchange; - -@Slf4j -public class HeaderUtil { - - private HeaderUtil() { - throw new IllegalStateException("Utility class"); - } - - public static String getFirstValueFromHeaderField(ServerHttpRequest request, String headerName) { - return request.getHeaders().getFirst(headerName); - } - - public static String getLastValueFromHeaderField(ServerHttpRequest request, String headerName) { - return request.getHeaders().getValuesAsList(headerName).stream() - .reduce((first, last) -> last) - .orElse(null); - } - - public static void addHeader(ServerWebExchange exchange, String headerName, String headerValue) { - exchange.getRequest().mutate().header(headerName, headerValue).build(); - } - - public static void removeHeader(ServerWebExchange exchange, String headerName) { - exchange.getRequest().mutate().headers(httpHeaders -> httpHeaders.remove(headerName)).build(); - } - - public static void removeHeaders(ServerWebExchange exchange, List headerList) { - if (Objects.isNull(headerList) || headerList.isEmpty()) return; - exchange - .getRequest() - .mutate() - .headers(httpHeaders -> headerList.forEach(httpHeaders::remove)) - .build(); - } - - public static void rewriteXForwardedHeader( - ServerWebExchange exchange, JumperConfig jumperConfig) { - - if (Objects.nonNull(jumperConfig.getConsumerOriginStargate())) { - try { - URL url = new URL(jumperConfig.getConsumerOriginStargate()); - HeaderUtil.addHeader(exchange, Constants.HEADER_X_FORWARDED_HOST, url.getHost()); - } catch (MalformedURLException e) { - log.error(e.getMessage(), e); - } - } - - addHeader(exchange, Constants.HEADER_X_FORWARDED_PORT, Constants.HEADER_X_FORWARDED_PORT_PORT); - addHeader( - exchange, Constants.HEADER_X_FORWARDED_PROTO, Constants.HEADER_X_FORWARDED_PROTO_HTTPS); - } -} diff --git a/src/main/java/jumper/service/OauthTokenUtil.java b/src/main/java/jumper/service/OauthTokenUtil.java index 6cd2a80..6188eb2 100644 --- a/src/main/java/jumper/service/OauthTokenUtil.java +++ b/src/main/java/jumper/service/OauthTokenUtil.java @@ -6,26 +6,25 @@ import static jumper.Constants.TOKEN_REQUEST_METHOD_POST; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Header; -import io.jsonwebtoken.Jwt; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.*; import io.jsonwebtoken.security.SignatureException; import io.netty.channel.ConnectTimeoutException; import io.netty.handler.ssl.SslHandshakeTimeoutException; +import java.net.UnknownHostException; +import java.time.Duration; import java.util.*; import java.util.concurrent.*; import jumper.Constants; import jumper.model.TokenInfo; import jumper.model.config.JumperConfig; import jumper.model.config.OauthCredentials; +import jumper.util.BasicAuthUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.stereotype.Service; @@ -46,10 +45,14 @@ @RequiredArgsConstructor public class OauthTokenUtil { + @Qualifier("oauthTokenUtilWebClient") private final WebClient oauthTokenUtilWebClient; + private final TokenCacheService tokenCache; private final TokenGeneratorService tokenGenerator; - private final BasicAuthUtil basicAuthUtil; + + private static final JwtParser jwtParser = + Jwts.parserBuilder().setAllowedClockSkewSeconds(3600).build(); public static String getTokenWithoutSignature(String consumerToken) { @@ -85,10 +88,7 @@ public static String getClaimFromToken(String consumerToken, String claimName) { public static Jwt getAllClaimsFromToken(String consumerToken) { try { - return Jwts.parserBuilder() - .setAllowedClockSkewSeconds(3600) - .build() - .parseClaimsJwt(consumerToken); + return jwtParser.parseClaimsJwt(consumerToken); } catch (SignatureException e) { log.error("SignatureException", e); } catch (ExpiredJwtException e) { @@ -171,7 +171,7 @@ public String generateGatewayTokenForPublisher(String issuer, String realm) { new Date(System.currentTimeMillis())); } - public TokenInfo getInternalMeshAccessToken(JumperConfig jc) { + public Mono getInternalMeshAccessToken(JumperConfig jc) { return getAccessTokenWithClientCredentials( jc.getInternalTokenEndpoint() + Constants.ISSUER_SUFFIX, jc.getClientId(), @@ -179,7 +179,7 @@ public TokenInfo getInternalMeshAccessToken(JumperConfig jc) { null); } - public TokenInfo getAccessTokenWithClientCredentials( + public Mono getAccessTokenWithClientCredentials( String tokenEndpoint, String clientID, String clientSecret, String scope) { final String tokenKey = tokenCache.generateTokenCacheKey(tokenEndpoint, clientID, scope); @@ -187,6 +187,7 @@ public TokenInfo getAccessTokenWithClientCredentials( // try to get valid token from tokenCache... return tokenCache .getToken(tokenKey) + .map(Mono::just) .orElseGet( () -> { // ...otherwise retrieve a new one MultiValueMap requestParameter = new LinkedMultiValueMap<>(); @@ -204,7 +205,7 @@ public TokenInfo getAccessTokenWithClientCredentials( }); } - public TokenInfo getAccessTokenWithOauthCredentialsObject( + public Mono getAccessTokenWithOauthCredentialsObject( String tokenEndpoint, OauthCredentials oauthCredentials) { final String tokenKey = tokenCache.generateTokenCacheKey(tokenEndpoint, oauthCredentials); @@ -212,6 +213,7 @@ public TokenInfo getAccessTokenWithOauthCredentialsObject( // try to get valid token from tokenCache... return tokenCache .getToken(tokenKey) + .map(Mono::just) .orElseGet( () -> { // ...otherwise retrieve a new one MultiValueMap requestParameter = new LinkedMultiValueMap<>(); @@ -241,7 +243,7 @@ public TokenInfo getAccessTokenWithOauthCredentialsObject( oauthCredentials.getClientSecret()); } else { basicAuth = - basicAuthUtil.encodeBasicAuth( + BasicAuthUtil.encodeBasicAuth( oauthCredentials.getClientId(), oauthCredentials.getClientSecret()); } } @@ -296,117 +298,87 @@ private String createJwtTokenForExternalIdp( oauthCredentials.getClientKey()); } - private TokenInfo getAccessTokenQuery( + private Mono getAccessTokenQuery( String tokenEndpoint, String tokenKey, MultiValueMap formData, String basicAuthHeader) { - Mono tokenInfoMono = - oauthTokenUtilWebClient - .post() - .uri(tokenEndpoint) - .headers( - httpHeaders -> { - httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - if (basicAuthHeader != null) httpHeaders.setBasicAuth(basicAuthHeader); - }) - .body(BodyInserters.fromFormData(formData)) - .retrieve() - .onStatus( - HttpStatus::is4xxClientError, - response -> { - logClientErrorResponse(response, tokenKey); - return Mono.error( - new ResponseStatusException( + return oauthTokenUtilWebClient + .post() + .uri(tokenEndpoint) + .headers( + httpHeaders -> { + httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + if (basicAuthHeader != null) httpHeaders.setBasicAuth(basicAuthHeader); + }) + .body(BodyInserters.fromFormData(formData)) + .retrieve() + .onStatus( + HttpStatusCode::is4xxClientError, + response -> { + logClientErrorResponse(response, tokenKey); + return Mono.error( + new ResponseStatusException( + HttpStatus.UNAUTHORIZED, + "Failed to retrieve token from " + + tokenEndpoint + + ", original status: " + + response.statusCode())); + }) + .onStatus( + HttpStatusCode::is5xxServerError, + response -> { + logClientErrorResponse(response, tokenKey); + return Mono.error( + new ResponseStatusException( + HttpStatus.UNAUTHORIZED, + "Failed to retrieve token from " + + tokenEndpoint + + ", original status: " + + response.statusCode())); + }) + .bodyToMono(TokenInfo.class) + .timeout(Duration.ofSeconds(15)) + .onErrorMap( + TimeoutException.class, + throwable -> + new ResponseStatusException( + HttpStatus.GATEWAY_TIMEOUT, + "Timeout occurred while fetching token from " + tokenEndpoint)) + .onErrorMap( + UnsupportedMediaTypeException.class, + throwable -> + new ResponseStatusException( + HttpStatus.NOT_ACCEPTABLE, + "Failed while fetching token from " + + tokenEndpoint + + ": " + + throwable.getMessage().replace("bodyType=jumper.model.", ""))) + .doOnError( + throwable -> + log.error( + "Error occurred class: {}, msg: {}", + throwable.getClass().getSimpleName(), + throwable.getMessage())) + .retryWhen( + Retry.max(2) + .filter( + throwable -> + throwable instanceof ConnectTimeoutException + || throwable.getCause() instanceof SslHandshakeTimeoutException + || throwable.getCause() instanceof PrematureCloseException + || throwable.getCause() instanceof UnknownHostException) + .onRetryExhaustedThrow( + (retryBackoffSpec, retrySignal) -> { + throw new ResponseStatusException( HttpStatus.UNAUTHORIZED, - "Failed to retrieve token from " + "Failed to connect to " + tokenEndpoint - + ", original status: " - + response.statusCode())); - }) - .onStatus( - HttpStatus::is5xxServerError, - response -> { - logClientErrorResponse(response, tokenKey); - return Mono.error( - new ResponseStatusException( - HttpStatus.UNAUTHORIZED, - "Failed to retrieve token from " - + tokenEndpoint - + ", original status: " - + response.statusCode())); - }) - .bodyToMono(TokenInfo.class) - .doOnError( - throwable -> - log.error( - "XXX error occurred class: {}, msg: {}", - throwable.getClass().getSimpleName(), - throwable.getMessage())) - .retryWhen( - Retry.max(2) - .filter( - throwable -> - throwable instanceof ConnectTimeoutException - || throwable.getCause() instanceof SslHandshakeTimeoutException - || throwable.getCause() instanceof PrematureCloseException) - .onRetryExhaustedThrow( - (retryBackoffSpec, retrySignal) -> { - throw new ResponseStatusException( - HttpStatus.UNAUTHORIZED, - "Failed to connect to " - + tokenEndpoint - + ", cause: " - + retrySignal.failure().getMessage()); - })); - - CompletableFuture tokenInfoCompletableFuture = - tokenInfoMono.toFuture().orTimeout(15, TimeUnit.SECONDS); - - TokenInfo accessToken; - - try { - accessToken = tokenInfoCompletableFuture.get(); - - } catch (ExecutionException e) { - String msg = e.getCause().getMessage(); - - if (e.getCause() instanceof ResponseStatusException) { - var statusCode = ((ResponseStatusException) e.getCause()).getStatus(); - throw new ResponseStatusException(statusCode, msg); - } - - if (e.getCause() instanceof TimeoutException) { - throw new ResponseStatusException( - HttpStatus.GATEWAY_TIMEOUT, - "Timeout occurred while fetching token from " + tokenEndpoint); - } - - if (e.getCause().getCause() instanceof UnsupportedMediaTypeException) { - throw new ResponseStatusException( - HttpStatus.NOT_ACCEPTABLE, - "Failed while fetching token from " - + tokenEndpoint - + ": " - + e.getCause().getCause().getMessage().replace("bodyType=jumper.model.", "")); - } - - throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, msg); - - } catch (InterruptedException e) { - throw new ResponseStatusException( - HttpStatus.INTERNAL_SERVER_ERROR, - "Error occurred while fetching token from " + tokenEndpoint); - } - - if (accessToken == null) { - throw new ResponseStatusException( - HttpStatus.NOT_ACCEPTABLE, "Empty response while fetching token from " + tokenEndpoint); - } - - tokenCache.saveToken(tokenKey, accessToken); - return accessToken; + + ", cause: " + + retrySignal.failure().getMessage()); + })) + .doOnNext(tokenInfo -> tokenCache.saveToken(tokenKey, tokenInfo)); } private void logClientErrorResponse(ClientResponse response, String tokenKey) { diff --git a/src/main/java/jumper/service/RedisZoneHealthStatusService.java b/src/main/java/jumper/service/RedisZoneHealthStatusService.java index 4db29f0..cdf0430 100644 --- a/src/main/java/jumper/service/RedisZoneHealthStatusService.java +++ b/src/main/java/jumper/service/RedisZoneHealthStatusService.java @@ -6,7 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import io.micrometer.context.ContextExecutorService; +import io.micrometer.context.ContextSnapshotFactory; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import jumper.config.RedisConfig; import jumper.model.config.HealthStatus; import jumper.model.config.ZoneHealthMessage; @@ -67,6 +72,10 @@ public void onMessage(Message message, byte[] pattern) { } void lazyInitializeRedisMessageListenerContainer() { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + ContextSnapshotFactory contextSnapshotFactory = ContextSnapshotFactory.builder().build(); + Executor wrappedExecutor = ContextExecutorService.wrap(executorService, contextSnapshotFactory); + CompletableFuture.supplyAsync( () -> { var template = @@ -101,7 +110,8 @@ void lazyInitializeRedisMessageListenerContainer() { context.getRetryCount()); return true; }); - }) + }, + wrappedExecutor) .exceptionally( throwable -> { log.error( diff --git a/src/main/java/jumper/service/SpectreService.java b/src/main/java/jumper/service/SpectreService.java index beb41a8..818769a 100644 --- a/src/main/java/jumper/service/SpectreService.java +++ b/src/main/java/jumper/service/SpectreService.java @@ -5,8 +5,13 @@ package jumper.service; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.util.*; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.Tracer; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; import jumper.Constants; import jumper.config.SpectreConfiguration; import jumper.model.config.JumperConfig; @@ -14,15 +19,14 @@ import jumper.model.config.Spectre; import jumper.model.config.SpectreData; import jumper.model.config.SpectreKind; +import jumper.util.ObjectMapperUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; -import org.springframework.cloud.sleuth.CurrentTraceContext; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.instrument.web.WebFluxSleuthOperators; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -41,7 +45,8 @@ public class SpectreService { private final OauthTokenUtil oauthTokenUtil; private final Tracer tracer; - private final CurrentTraceContext currentTraceContext; + + @Qualifier("spectreServiceWebClient") private final WebClient spectreServiceWebClient; @Value("${jumper.stargate.url}") @@ -50,22 +55,18 @@ public class SpectreService { @Value("${jumper.issuer.url}") private String localIssuerUrl; - @Value("${horizon.publishEventUrl}") + @Value("${jumper.horizon.publishEventUrl}") private String publishEventUrl; @Autowired private SpectreConfiguration spectreConfiguration; - public void handleEvent( + public Mono handleEvent( JumperConfig jc, ServerWebExchange exchange, Object http, RouteListener listener, String payload) { - WebFluxSleuthOperators.withSpanInScope( - tracer, - currentTraceContext, - exchange, - () -> publishEvent(createEvent(jc, exchange, http, listener, payload), jc)); + return publishEvent(createEvent(jc, exchange, http, listener, payload), jc); } private Spectre createEvent( @@ -118,10 +119,7 @@ private Spectre createEvent( .data(data) .build(); - String finalSpanName = spanName; - - Span newSpan = this.tracer.nextSpan().name(finalSpanName).start(); - tracer.withSpan(newSpan); + Span newSpan = this.tracer.nextSpan().name(spanName).start(); event.setSpanId(newSpan.context().spanId()); @@ -134,17 +132,21 @@ private Spectre createEvent( return event; } - private void publishEvent(Spectre event, JumperConfig jc) { + private Mono publishEvent(Spectre event, JumperConfig jc) { // determine environment for local issuer and routing path on qa String envName = determineEnvironment(jc); - publishEventMono( + return publishEventMono( publishEventUrl.replaceFirst(Constants.ENVIRONMENT_PLACEHOLDER, envName), oauthTokenUtil.generateGatewayTokenForPublisher( localIssuerUrl + "/" + envName, envName), event) - .subscribe(); + .onErrorResume( + throwable -> { + log.error("Error publishing Spectre event", throwable); + return Mono.empty(); // Don't fail the main request flow + }); } private String determineEnvironment(JumperConfig jc) { @@ -175,6 +177,7 @@ private Mono publishEventMono(String url, String token, Spectre event) { Constants.HEADER_X_B3_TRACE_ID, currentSpan.context().traceId()); httpHeaders.set(Constants.HEADER_X_B3_SPAN_ID, event.getSpanId()); } + // pass Spectre related info also as a header httpHeaders.set(Constants.HEADER_X_SPECTRE_ISSUE, event.getData().getIssue()); httpHeaders.set( @@ -186,7 +189,7 @@ private Mono publishEventMono(String url, String token, Spectre event) { .body(BodyInserters.fromValue(event)) .retrieve() .onStatus( - HttpStatus::isError, + HttpStatusCode::isError, response -> { log.error("while publishing event got error status: {}", response.statusCode()); logDebugResponse(response); @@ -225,7 +228,7 @@ private Object parsePayload(MediaType mediaType, String payload) { log.debug("json compatible content-type, will try to parse as json payload"); try { // try to return payload as json - return new ObjectMapper().readTree(payload); + return ObjectMapperUtil.getInstance().readTree(payload); } catch (JsonProcessingException e) { log.error("error while parsing json payload for spectre", e); } diff --git a/src/main/java/jumper/service/TokenCacheService.java b/src/main/java/jumper/service/TokenCacheService.java index ce74077..3956147 100644 --- a/src/main/java/jumper/service/TokenCacheService.java +++ b/src/main/java/jumper/service/TokenCacheService.java @@ -4,131 +4,86 @@ package jumper.service; -import java.util.HashMap; -import java.util.Map; import java.util.Optional; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import jumper.model.TokenInfo; import jumper.model.config.OauthCredentials; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; @Service @Slf4j public class TokenCacheService { - @Value("${jumpercache.ttlOffset}") + @Value("${jumper.tokencache.ttlOffset}") private int ttlOffset; - @Value("${jumpercache.cleanCacheInSeconds:0}") - private long cleanCacheInSeconds; - private static final String TOKEN_CACHE_KEY_DELIMITER = "."; + private static final String TOKEN_CACHE_NAME = "cache-token-info"; - Map cachingList = new HashMap<>(); - - public TokenCacheService() { - - if (cleanCacheInSeconds > 0) { - - Executors.newScheduledThreadPool(1) - .scheduleAtFixedRate( - cleanCacheJob(), cleanCacheInSeconds, cleanCacheInSeconds, TimeUnit.SECONDS); - log.debug( - "JumperCache cleanup job is enabled. the cache is cleaned every {} seconds.", - cleanCacheInSeconds); + private final Cache tokenCache; - } else { - log.debug( - "JumperCache cleanup job is not enabled. Specify a value > 0 in your properties with the key 'jumpercache.cleanCacheInSeconds'."); + public TokenCacheService(@Qualifier("caffeineCacheManager") CacheManager cacheManager) { + this.tokenCache = cacheManager.getCache(TOKEN_CACHE_NAME); + if (this.tokenCache == null) { + throw new IllegalStateException( + "Cache '" + TOKEN_CACHE_NAME + "' not found. Please check cache configuration."); } + log.debug("TokenCacheService initialized with Spring-managed cache: {}", TOKEN_CACHE_NAME); } public Optional getToken(String tokenCacheKey) { - log.debug("try to grab token from cache with key: {}", tokenCacheKey); - if (log.isDebugEnabled()) { - printCache(); - } + TokenInfo token = tokenCache.get(tokenCacheKey, TokenInfo.class); - TokenInfo token = this.cachingList.get(tokenCacheKey); + // Additional TTL validation with offset if (token != null && !isValid(token)) { - - // delete token and return empty Optional log.debug( - "TTL: {} seconds | The token has expired or will be exipred in less than {} seconds and will be deleted", + "TTL: {} seconds | Token will expire within {} seconds, removing from cache", token.getExpiresIn(), this.ttlOffset); - this.deleteTokenByKey(tokenCacheKey); + tokenCache.evict(tokenCacheKey); return Optional.empty(); - - } else { - return Optional.ofNullable(token); } + + return Optional.ofNullable(token); } public void saveToken(String tokenKey, TokenInfo gwAccessToken) { log.debug("Token saved with tokenKey: '{}'", tokenKey); - this.cachingList.put(tokenKey, gwAccessToken); + tokenCache.put(tokenKey, gwAccessToken); } public String generateTokenCacheKey(String tokenEndpoint, OauthCredentials oauthCredentials) { - return String.join( - TOKEN_CACHE_KEY_DELIMITER, - tokenEndpoint, - oauthCredentials.getId(), - oauthCredentials.getScopes()); + // Use StringBuilder to reduce object allocations + String scopes = oauthCredentials.getScopes() != null ? oauthCredentials.getScopes() : ""; + return new StringBuilder( + tokenEndpoint.length() + oauthCredentials.getId().length() + scopes.length() + 2) + .append(tokenEndpoint) + .append(TOKEN_CACHE_KEY_DELIMITER) + .append(oauthCredentials.getId()) + .append(TOKEN_CACHE_KEY_DELIMITER) + .append(scopes) + .toString(); } public String generateTokenCacheKey(String tokenEndpoint, String clientID, String scopes) { - return String.join(TOKEN_CACHE_KEY_DELIMITER, tokenEndpoint, clientID, scopes); - } - - public void printCache() { - log.debug("---Jumper-Cache-List--------------------------------------------"); - log.debug("Number of Tokens in JumperCache: {}", this.cachingList.size()); - this.cachingList.forEach( - (key, value) -> log.debug("TTL: {} seconds, CacheKey: {}", value.getExpiresIn(), key)); - log.debug("----------------------------------------------------------------"); + // Use StringBuilder to reduce object allocations - handle null scopes + String safeScopes = scopes != null ? scopes : ""; + return new StringBuilder(tokenEndpoint.length() + clientID.length() + safeScopes.length() + 2) + .append(tokenEndpoint) + .append(TOKEN_CACHE_KEY_DELIMITER) + .append(clientID) + .append(TOKEN_CACHE_KEY_DELIMITER) + .append(safeScopes) + .toString(); } private boolean isValid(TokenInfo token) { return token.getExpiresIn() > this.ttlOffset; } - - private void deleteTokenByKey(String itemKey) { - this.cachingList.remove(itemKey); - } - - private Runnable cleanCacheJob() { - return () -> { - log.debug( - "----Clean-Jumper-Cache---Size:{}--------------------------------", cachingList.size()); - cachingList - .entrySet() - .removeIf( - entry -> { - if (isValid(entry.getValue())) { - log.debug( - "Valid | TTL={} tokenKey: {}", - entry.getValue().getExpiresIn(), - entry.getKey()); - return false; - - } else { - log.debug( - "Expired -> Delete now | TTL={} tokenKey: {}", - entry.getValue().getExpiresIn(), - entry.getKey()); - return true; - } - }); - log.debug( - "----Clean-Jumper-Cache---Size:{}-after-cleaning-------------------------------", - cachingList.size()); - }; - } } diff --git a/src/main/java/jumper/service/BasicAuthUtil.java b/src/main/java/jumper/util/BasicAuthUtil.java similarity index 56% rename from src/main/java/jumper/service/BasicAuthUtil.java rename to src/main/java/jumper/util/BasicAuthUtil.java index 58c8ad4..4da3ca2 100644 --- a/src/main/java/jumper/service/BasicAuthUtil.java +++ b/src/main/java/jumper/util/BasicAuthUtil.java @@ -2,23 +2,19 @@ // // SPDX-License-Identifier: Apache-2.0 -package jumper.service; +package jumper.util; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; +import java.util.Base64; import org.springframework.util.Assert; -import org.springframework.util.Base64Utils; -@Service -@RequiredArgsConstructor public class BasicAuthUtil { - public String encodeBasicAuth(String username, String password) { + public static String encodeBasicAuth(String username, String password) { Assert.notNull(username, "Username must not be null"); Assert.doesNotContain(username, ":", "Username must not contain a colon"); Assert.notNull(password, "Password must not be null"); String basicAuthPreparation = username + ":" + password; - return Base64Utils.encodeToString(basicAuthPreparation.getBytes()); + return Base64.getEncoder().encodeToString(basicAuthPreparation.getBytes()); } } diff --git a/src/main/java/jumper/util/HeaderUtil.java b/src/main/java/jumper/util/HeaderUtil.java new file mode 100644 index 0000000..06bd740 --- /dev/null +++ b/src/main/java/jumper/util/HeaderUtil.java @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2023 Deutsche Telekom AG +// +// SPDX-License-Identifier: Apache-2.0 + +package jumper.util; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Objects; +import jumper.Constants; +import jumper.model.config.JumperConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.server.reactive.ServerHttpRequest; + +@Slf4j +public class HeaderUtil { + + private HeaderUtil() { + throw new IllegalStateException("Utility class"); + } + + public static String getFirstValueFromHeaderField(ServerHttpRequest request, String headerName) { + return request.getHeaders().getFirst(headerName); + } + + public static String getLastValueFromHeaderField(ServerHttpRequest request, String headerName) { + return request.getHeaders().getValuesAsList(headerName).stream() + .reduce((first, last) -> last) + .orElse(null); + } + + public static void addHeader( + ServerHttpRequest.Builder builder, String headerName, String headerValue) { + builder.header(headerName, headerValue); + } + + public static void removeHeader(ServerHttpRequest.Builder builder, String headerName) { + builder.headers(httpHeaders -> httpHeaders.remove(headerName)); + } + + public static void removeHeaders(ServerHttpRequest.Builder builder, List headerList) { + if (Objects.isNull(headerList) || headerList.isEmpty()) return; + builder.headers(httpHeaders -> headerList.forEach(httpHeaders::remove)); + } + + public static void rewriteXForwardedHeader( + ServerHttpRequest.Builder builder, JumperConfig jumperConfig) { + + if (Objects.nonNull(jumperConfig.getConsumerOriginStargate())) { + try { + URI url = new URI(jumperConfig.getConsumerOriginStargate()); + HeaderUtil.addHeader(builder, Constants.HEADER_X_FORWARDED_HOST, url.getHost()); + } catch (URISyntaxException e) { + log.error(e.getMessage(), e); + } + } + + addHeader(builder, Constants.HEADER_X_FORWARDED_PORT, Constants.HEADER_X_FORWARDED_PORT_PORT); + addHeader( + builder, Constants.HEADER_X_FORWARDED_PROTO, Constants.HEADER_X_FORWARDED_PROTO_HTTPS); + } +} diff --git a/src/main/java/jumper/util/ObjectMapperUtil.java b/src/main/java/jumper/util/ObjectMapperUtil.java new file mode 100644 index 0000000..9bdfdb3 --- /dev/null +++ b/src/main/java/jumper/util/ObjectMapperUtil.java @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2023 Deutsche Telekom AG +// +// SPDX-License-Identifier: Apache-2.0 + +package jumper.util; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; + +public class ObjectMapperUtil { + + private static final ObjectMapper objectMapper = + new ObjectMapper() + .registerModule(new Jdk8Module()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + public static ObjectMapper getInstance() { + return objectMapper; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 68b8f8d..6ca2c15 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,30 +4,43 @@ server: port: ${JUMPER_PORT:8080} - max-http-header-size: 16384 shutdown: GRACEFUL netty: max-initial-line-length: 8192 idle-timeout: 60000 + max-http-request-header-size: 32768 + forward-headers-strategy: none management: health: redis: enabled: false endpoint: - gateway.enabled: true # having actuators endpoint for filters e.g. host/actuator/gateway/routefilters - loggers.enabled: true # having actuators endpoint for setting log level health: - enabled: true # healthcheck endpoint - probes.enabled: true #healthcheck probes, liveness and readiness + probes: + enabled: true #healthcheck probes, liveness and readiness endpoints: web: exposure.include: gateway, loggers, prometheus, health + observations: + enable: + spring.security: false + tracing: + enabled: true + sampling: + probability: 1.0 + propagation: + type: B3_MULTI + zipkin: + tracing: + endpoint: ${TRACING_URL:http://localhost:9411}/api/v2/spans cache-manager: caffeine-caches: - cache-names: [ cache-key-info ] spec: maximumSize=1, expireAfterWrite=1m + - cache-names: [ cache-token-info ] + spec: maximumSize=${jumper.tokencache.maxSize:10000}, expireAfterWrite=${jumper.tokencache.expireAfterWriteMinutes:30}m spring: cache: @@ -35,70 +48,66 @@ spring: lifecycle: timeout-per-shutdown-phase: 1m application: - name: ${JUMPER_NAME:Jumper} + name: ${JUMPER_NAME:jumper} cloud: gateway: - metrics.enabled: true #default ## in order to "push" to prometheus a prometheus dependency is needed in the pom.xml - default-filters: - - name: Retry - args: - retries: 1 - statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE,GATEWAY_TIMEOUT - methods: GET,POST,PUT,DELETE,PATCH,HEAD - httpclient: - proxy: - host: ${FPA_PROXY_HOST:} - port: ${FPA_PROXY_PORT:} - non-proxy-hosts-pattern: ${FPA_NON_PROXY_HOSTS_REGEX:} - pool: - type: ELASTIC - #max-connections: 1000 #Only for type FIXED - #acquire-timeout: 8000 #Only for type FIXED - max-life-time: 300s - max-idle-time: 2s - #eviction-interval: 120s - metrics: true - connect-timeout: 10000 - response-timeout: 61s # ingress 60s - #ssl: - #useInsecureTrustManager: true # routing to a https backend, Gateway will trust all downstream certificates - x-forwarded: - for-enabled: false - for-append: false - host-enabled: false - host-append: false - port-enabled: false - port-append: false - proto-enabled: false - proto-append: false - prefix-enabled: false - prefix-append: false - forwarded: - enabled: false - zipkin: - baseUrl: ${TRACING_URL:https://collector-zipkin-http-drax-guardians.test.dhei.telekom.de} - sleuth: - filter-param-list: X-Amz-.*,sig - sampler.probability: 1 - propagation: - tag.enabled: false - type: b3 - reactor: - instrumentation-type: MANUAL #https://docs.spring.io/spring-cloud-sleuth/docs/current-SNAPSHOT/reference/html/integrations.html - codec: - max-in-memory-size: 12582912 + server: + webflux: + forwarded: + enabled: false + x-forwarded: + for-enabled: false + prefix-append: false + for-append: false + host-enabled: false + host-append: false + port-enabled: false + port-append: false + proto-enabled: false + proto-append: false + prefix-enabled: false + predicate: + cookie: + enabled: false + metrics: + enabled: true + httpclient: + proxy: + host: ${FPA_PROXY_HOST:} + port: ${FPA_PROXY_PORT:} + non-proxy-hosts-pattern: ${FPA_NON_PROXY_HOSTS_REGEX:} + response-timeout: 61s + connect-timeout: 10000 + pool: + metrics: true + max-idle-time: 2s + max-life-time: 300s + type: ELASTIC + default-filters: + - name: Retry + args: + retries: 1 + statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE,GATEWAY_TIMEOUT + methods: GET,POST,PUT,DELETE,PATCH,HEAD data: redis: repositories: enabled: false - redis: - connect-timeout: ${ZONE_HEALTH_DATABASE_CONNECTTIMEOUT:500} - timeout: ${ZONE_HEALTH_DATABASE_TIMEOUT:500} - host: ${ZONE_HEALTH_DATABASE_HOST:localhost} - port: ${ZONE_HEALTH_DATABASE_PORT:6379} - database: ${ZONE_HEALTH_DATABASE_INDEX:2} - password: ${ZONE_HEALTH_DATABASE_PASSWORD:foobar} + connect-timeout: ${ZONE_HEALTH_DATABASE_CONNECTTIMEOUT:500} + timeout: ${ZONE_HEALTH_DATABASE_TIMEOUT:500} + host: ${ZONE_HEALTH_DATABASE_HOST:localhost} + port: ${ZONE_HEALTH_DATABASE_PORT:6379} + database: ${ZONE_HEALTH_DATABASE_INDEX:2} + password: ${ZONE_HEALTH_DATABASE_PASSWORD:foobar} + reactor: + context-propagation: auto + http: + codecs: + max-in-memory-size: 12582912 + jumper: + tracing: + filter-param-list: X-Amz-.*,sig issuer: url: ${JUMPER_ISSUER_URL:https://stargate-integration.test.dhei.telekom.de/auth/realms} stargate: @@ -124,14 +133,16 @@ jumper: dir: /keypair pk-file: tls.key kid-file: tls.kid + tokencache: + ttlOffset: 10 + maxSize: 10000 + expireAfterWriteMinutes: 30 + horizon: + publishEventUrl: ${PUBLISH_EVENT_URL:http://producer.stage:8080/v1/events} -jumpercache.ttlOffset: 10 -jumpercache.cleanCacheInSeconds: 3600 - -############################## -# local horizon -############################## -horizon: - publishEventUrl: ${PUBLISH_EVENT_URL:http://producer.stage:8080/v1/events} -logging.level.jumper.service.AuditLogService: INFO -#logging.pattern.level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]" #https://github.com/micrometer-metrics/tracing/wiki/Spring-Cloud-Sleuth-3.1-Migration-Guide +logging: + structured: + format: + console: logstash + level: + jumper.service.AuditLogService: INFO \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml deleted file mode 100644 index 0abe334..0000000 --- a/src/main/resources/logback.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n - - - - - - - - - - diff --git a/src/test/java/jumper/BaseSteps.java b/src/test/java/jumper/BaseSteps.java index 62e963f..d9da17e 100644 --- a/src/test/java/jumper/BaseSteps.java +++ b/src/test/java/jumper/BaseSteps.java @@ -5,10 +5,12 @@ package jumper; import static jumper.config.Config.*; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.fail; import io.cucumber.java.en.And; import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; import io.cucumber.java.en.When; import java.time.Duration; import java.util.function.Consumer; @@ -22,6 +24,8 @@ import lombok.Setter; import org.json.JSONException; import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; @@ -33,6 +37,7 @@ @Setter public class BaseSteps { + private static final Logger log = LoggerFactory.getLogger(BaseSteps.class); private MockUpstreamServer mockUpstreamServer; private MockIrisServer mockIrisServer; private MockHorizonServer mockHorizonServer; @@ -93,7 +98,7 @@ public void assertErrorResponse(String msg, String error, int status) { .jsonPath("$.method") .isEqualTo("GET") .jsonPath("$.service") - .isEqualTo("Jumper") + .isEqualTo("jumper") .jsonPath("$.message") .isEqualTo(msg) .jsonPath("$.error") @@ -257,6 +262,25 @@ public void consumerCallsProxy() { requestExchange = webTestClient.get().uri("/proxy").headers(httpHeadersOfRequest).exchange(); } + @Then("metrics result does not contain {}") + public void metricsDoesntContain(String word) { + webTestClient + .get() + .uri("/actuator/prometheus") + .exchange() + .expectStatus() + .isOk() + .expectBody(String.class) + .consumeWith( + response -> { + String body = response.getResponseBody(); + // log.info(body); + if (body != null) { + assertFalse(body.contains(word)); + } + }); + } + @When("consumer calls the proxy route and runs into timeout") public void consumerCallsTheAPIAndProviderRunsIntoTimeout() { mockUpstreamServer.callbackRequestWithTimeout(); @@ -378,7 +402,7 @@ public void consumerCallsTheAPIWith(String scenario) { break; case "encodedQueryParam": setBasePathHeader("/base"); - uri += "/path?validAt=2020-11-30T23%3A00%3A00%2B01%3A00"; + uri += "/path?validAt=2020-11-30T23%3A00%3A00%2B01%3A00&sig=123"; mockUpstreamServer.testEndpoint(id, "/path"); break; default: diff --git a/src/test/java/jumper/HeaderSteps.java b/src/test/java/jumper/HeaderSteps.java index 6361aa7..8ae3537 100644 --- a/src/test/java/jumper/HeaderSteps.java +++ b/src/test/java/jumper/HeaderSteps.java @@ -36,6 +36,13 @@ public void proxyRouteHeadersSetWithXtokenExchange() { baseSteps.setHttpHeadersOfRequest(TokenUtil.getProxyRouteHeadersWithXtokenExchange(baseSteps)); } + @Given("A header {word} header is set with value {word}") + public void tardisTraceIdSet(String headerName, String headerValue) { + baseSteps.setHttpHeadersOfRequest( + baseSteps.httpHeadersOfRequest.andThen( + httpHeaders -> httpHeaders.add(headerName, headerValue))); + } + @Given("RealRoute headers are set") public void realRouteHeadersSet() { baseSteps.setHttpHeadersOfRequest( @@ -137,9 +144,7 @@ public void setSpacegateOauthScopedHeaders() { public void setAuthorizationWithAud() { baseSteps.setHttpHeadersOfRequest( baseSteps.httpHeadersOfRequest.andThen( - httpHeaders -> { - httpHeaders.setBearerAuth(getConsumerAccessTokenWithAud()); - })); + httpHeaders -> httpHeaders.setBearerAuth(getConsumerAccessTokenWithAud()))); } @And("technical headers added") @@ -161,8 +166,7 @@ public void addTechnicalHeaders() { public void setSkipZoneHeader() { baseSteps.setHttpHeadersOfRequest( baseSteps.httpHeadersOfRequest.andThen( - httpHeaders -> { - httpHeaders.set(Constants.HEADER_X_FAILOVER_SKIP_ZONE, REMOTE_ZONE_NAME); - })); + httpHeaders -> + httpHeaders.set(Constants.HEADER_X_FAILOVER_SKIP_ZONE, REMOTE_ZONE_NAME))); } } diff --git a/src/test/java/jumper/RunCucumberTest.java b/src/test/java/jumper/RunCucumberTest.java index a0941eb..2c7701a 100644 --- a/src/test/java/jumper/RunCucumberTest.java +++ b/src/test/java/jumper/RunCucumberTest.java @@ -4,11 +4,19 @@ package jumper; +import org.junit.jupiter.api.BeforeAll; import org.junit.platform.suite.api.IncludeEngines; import org.junit.platform.suite.api.SelectClasspathResource; import org.junit.platform.suite.api.Suite; +import reactor.blockhound.BlockHound; @Suite @IncludeEngines("cucumber") @SelectClasspathResource("features") -public class RunCucumberTest {} +public class RunCucumberTest { + + @BeforeAll + static void beforeAll() { + BlockHound.install(); + } +} diff --git a/src/test/java/jumper/VerificationSteps.java b/src/test/java/jumper/VerificationSteps.java index c85ef9f..4fd84ae 100644 --- a/src/test/java/jumper/VerificationSteps.java +++ b/src/test/java/jumper/VerificationSteps.java @@ -36,6 +36,14 @@ public void apiProvidersReceivesDefaultTokenHeaders() { apiProvidersReceivesDefaultHeaders(); } + @Then("API Provider receives header {word} that matches regex {word}") + public void apiProviderReceivesXTardisTraceId(String headerName, String valueRegex) { + this.baseSteps + .getRequestExchange() + .expectHeader() + .valueMatches(headerName, Pattern.compile("\\w+").pattern()); + } + @Then("API Provider receives default basic authorization headers") public void apiProvidersReceivesDefaultBasicAuthHeaders() { this.baseSteps diff --git a/src/test/java/jumper/config/CucumberContextConfiguration.java b/src/test/java/jumper/config/CucumberContextConfiguration.java index d6166e4..92121db 100644 --- a/src/test/java/jumper/config/CucumberContextConfiguration.java +++ b/src/test/java/jumper/config/CucumberContextConfiguration.java @@ -4,6 +4,7 @@ package jumper.config; +import org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability; import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; @@ -13,5 +14,6 @@ @ActiveProfiles("test") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureWebTestClient(timeout = "PT10S") // PT65S - PT = Period time, S = seconds +@AutoConfigureObservability @ContextConfiguration(classes = {CustomHttpClientConfiguration.class}) public class CucumberContextConfiguration {} diff --git a/src/test/java/jumper/config/SleuthConfigurationTest.java b/src/test/java/jumper/config/TracingConfigurationTest.java similarity index 66% rename from src/test/java/jumper/config/SleuthConfigurationTest.java rename to src/test/java/jumper/config/TracingConfigurationTest.java index 6c7b4e5..3ba53fa 100644 --- a/src/test/java/jumper/config/SleuthConfigurationTest.java +++ b/src/test/java/jumper/config/TracingConfigurationTest.java @@ -7,15 +7,18 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.List; +import java.util.regex.Pattern; import org.junit.jupiter.api.Test; -public class SleuthConfigurationTest { +public class TracingConfigurationTest { @Test void filterQueryParams() { String alreadyEncodedUri = "http://localhost:8080/actuator/health?sig=57DjUa%2F9u6KdgCgTZVrHzsm9ZOQA0U%2B3K%2BvqQ7PRrgc%3D"; - String filtered = SleuthConfiguration.filterQueryParams(alreadyEncodedUri, List.of("nothing")); + String filtered = + new TracingConfiguration() + .filterQueryParams(alreadyEncodedUri, List.of(Pattern.compile("nothing"))); assertEquals(alreadyEncodedUri, filtered); } @@ -24,7 +27,8 @@ void filterQueryParams() { void filterQueryParamsUnencodedEvenIfUrlIsInvalid() { String rawUri = "http://localhost:8080/actuator/health?sig=57DjUa/9u6KdgCgTZVrHzsm9ZOQA0U+3K+vqQ7PRrgc="; - String filtered = SleuthConfiguration.filterQueryParams(rawUri, List.of("nothing")); + String filtered = + new TracingConfiguration().filterQueryParams(rawUri, List.of(Pattern.compile("nothing"))); assertEquals(rawUri, filtered); } @@ -33,7 +37,9 @@ void filterQueryParamsUnencodedEvenIfUrlIsInvalid() { void filterBlacklistedQueryParameters() { String alreadyEncodedUri = "http://localhost:8080/actuator/health?sig-b=57DjUa%2F9u6KdgCgTZVrHzsm9ZOQA0U%2B3K%2BvqQ7PRrgc%3D"; - String filtered = SleuthConfiguration.filterQueryParams(alreadyEncodedUri, List.of("sig-.*")); + String filtered = + new TracingConfiguration() + .filterQueryParams(alreadyEncodedUri, List.of(Pattern.compile("sig-.*"))); assertEquals("http://localhost:8080/actuator/health", filtered); } diff --git a/src/test/java/jumper/mocks/MockHorizonServer.java b/src/test/java/jumper/mocks/MockHorizonServer.java index f077bf5..e0f8bb6 100644 --- a/src/test/java/jumper/mocks/MockHorizonServer.java +++ b/src/test/java/jumper/mocks/MockHorizonServer.java @@ -27,6 +27,7 @@ import jumper.config.Config; import jumper.model.config.Spectre; import jumper.model.config.SpectreKind; +import jumper.util.ObjectMapperUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.mockserver.integration.ClientAndServer; @@ -75,7 +76,8 @@ public void createVerifyStructure(String method, String stargateUrl) { String seResponseString = recordedRequests[1].getBodyAsString(); ObjectMapper om = - new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + ObjectMapperUtil.getInstance() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); try { assertSpectreEvent(om.readValue(seRequestString, Spectre.class), method, true, stargateUrl); @@ -93,13 +95,21 @@ public void createVerifyPayload() { String seRequestString = recordedRequests[0].getBodyAsString(); String seResponseString = recordedRequests[1].getBodyAsString(); - ObjectMapper om = - new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - try { - Object expected = om.readValue(getTestJson().toString(), LinkedHashMap.class); - assertEquals(expected, om.readValue(seRequestString, Spectre.class).getData().getPayload()); - assertEquals(expected, om.readValue(seResponseString, Spectre.class).getData().getPayload()); + Object expected = + ObjectMapperUtil.getInstance().readValue(getTestJson().toString(), LinkedHashMap.class); + assertEquals( + expected, + ObjectMapperUtil.getInstance() + .readValue(seRequestString, Spectre.class) + .getData() + .getPayload()); + assertEquals( + expected, + ObjectMapperUtil.getInstance() + .readValue(seResponseString, Spectre.class) + .getData() + .getPayload()); } catch (JsonProcessingException e) { throw new RuntimeException(e); } @@ -144,12 +154,10 @@ public void createVerifyEventType() { String seEventString = recordedRequests[0].getBodyAsString(); - ObjectMapper om = - new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - try { assertEquals( - "de.telekom.ei.listener.spectre", om.readValue(seEventString, Spectre.class).getType()); + "de.telekom.ei.listener.spectre", + ObjectMapperUtil.getInstance().readValue(seEventString, Spectre.class).getType()); } catch (JsonProcessingException e) { throw new RuntimeException(e); } diff --git a/src/test/java/jumper/mocks/MockIrisServer.java b/src/test/java/jumper/mocks/MockIrisServer.java index c6bded8..f9ddba0 100644 --- a/src/test/java/jumper/mocks/MockIrisServer.java +++ b/src/test/java/jumper/mocks/MockIrisServer.java @@ -12,11 +12,12 @@ import static org.mockserver.model.HttpResponse.response; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import java.util.ArrayList; +import java.util.Base64; import java.util.List; import java.util.concurrent.TimeUnit; import jumper.model.TokenInfo; +import jumper.util.ObjectMapperUtil; import jumper.util.AccessToken; import lombok.extern.slf4j.Slf4j; import org.mockserver.client.MockServerClient; @@ -25,7 +26,6 @@ import org.mockserver.model.HttpError; import org.mockserver.model.RegexBody; import org.springframework.http.HttpHeaders; -import org.springframework.util.Base64Utils; @Slf4j public class MockIrisServer { @@ -207,8 +207,10 @@ public void createExpectationExternalBasicAuthCredentials(String id) { .withHeader( "Authorization", "Basic " - + Base64Utils.encodeToString( - (addIdSuffix("external_configured", id) + ":" + "secret").getBytes())), + + Base64.getEncoder() + .encodeToString( + (addIdSuffix("external_configured", id) + ":" + "secret") + .getBytes())), exactly(1)) .respond( response() @@ -232,8 +234,10 @@ public void createExpectationExternalTokenFromUsernamePassword(String id) { .withHeader( "Authorization", "Basic " - + Base64Utils.encodeToString( - (addIdSuffix("external_configured", id) + ":" + "secret").getBytes())) + + Base64.getEncoder() + .encodeToString( + (addIdSuffix("external_configured", id) + ":" + "secret") + .getBytes())) .withBody("username=username&password=geheim&grant_type=password"), exactly(1)) .respond( @@ -401,8 +405,7 @@ private List
getHeaderList() { headersList.add(new Header(HttpHeaders.HOST, irisLocalHost + ":" + irisLocalPort)); headersList.add(new Header(HttpHeaders.ACCEPT, "*/*")); headersList.add(new Header(HttpHeaders.CONTENT_LENGTH, "86")); - headersList.add( - new Header(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded;charset=UTF-8")); + headersList.add(new Header(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded")); return headersList; } @@ -417,10 +420,9 @@ private String getTokenInfoJson(String client) { tokenInfo.setSessionState("69fc4e8-77e9-45f9-93e4-646a34f802cc"); tokenInfo.setScope("profile email"); - ObjectMapper mapper = new ObjectMapper(); String tokenInfoJson = null; try { - tokenInfoJson = mapper.writeValueAsString(tokenInfo); + tokenInfoJson = ObjectMapperUtil.getInstance().writeValueAsString(tokenInfo); } catch (JsonProcessingException e) { log.error(e.getMessage()); } diff --git a/src/test/java/jumper/regression/CloudXForwardedConfiguration.java b/src/test/java/jumper/regression/CloudXForwardedConfiguration.java new file mode 100644 index 0000000..2c5cf6f --- /dev/null +++ b/src/test/java/jumper/regression/CloudXForwardedConfiguration.java @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2023 Deutsche Telekom AG +// +// SPDX-License-Identifier: Apache-2.0 + +package jumper.regression; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class CloudXForwardedConfiguration { + + @Bean + public RouteLocator customRouteLocator( + RouteLocatorBuilder builder, @Value("${mockserver.port}") String port) { + return builder + .routes() + .route("sample", r -> r.path("/get").uri("http://localhost:" + port)) + .build(); + } +} diff --git a/src/test/java/jumper/regression/GatewayForwardRepoApplicationTest.java b/src/test/java/jumper/regression/GatewayForwardRepoApplicationTest.java new file mode 100644 index 0000000..4ea36d1 --- /dev/null +++ b/src/test/java/jumper/regression/GatewayForwardRepoApplicationTest.java @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2023 Deutsche Telekom AG +// +// SPDX-License-Identifier: Apache-2.0 + +package jumper.regression; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockserver.model.HttpRequest.request; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockserver.integration.ClientAndServer; +import org.mockserver.model.HttpResponse; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.web.client.RestClient; + +/* + * regression for https://github.com/spring-cloud/spring-cloud-gateway/issues/3677 + */ +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "spring.main.cloud-platform=kubernetes") +@Import(CloudXForwardedConfiguration.class) +public class GatewayForwardRepoApplicationTest { + + private static ClientAndServer mockserver; + + @LocalServerPort private int port; + + private String uri; + + @BeforeEach + void setUp() { + uri = "http://localhost:" + port + "/get?value=a+nice+response"; + } + + @BeforeAll + static void startMockServer() { + mockserver = ClientAndServer.startClientAndServer(0); + mockserver + .when(request().withMethod("GET")) + .respond(HttpResponse.response().withStatusCode(200).withBody("a nice response")); + } + + @AfterAll + static void stopMockServer() { + mockserver.stop(); + } + + @DynamicPropertySource + static void dynamicProperty(DynamicPropertyRegistry registry) { + registry.add("mockserver.port", () -> mockserver.getPort()); + } + + @Test + void niceRequest() { + var result = RestClient.create().get().uri(uri).retrieve().toEntity(String.class); + + assertEquals(HttpStatus.OK, result.getStatusCode(), "Body\n%s".formatted(result.getBody())); + assertNotNull(result.getBody()); + assertTrue( + result.getBody().contains("a nice response"), + "Contained instead\n%s".formatted(result.getBody())); + } + + @Test + void evilRequest() { + var result = + RestClient.create() + .get() + .uri(uri) + .header("X-Forwarded-For", "192.123.23.2") + .header("X-Forwarded-Host", "example.org") + .header("X-Forwarded-Port", "443") + .header("X-Forwarded-Proto", "https") + .header("X-Forwarded-Prefix", "/evil") + .retrieve() + .toEntity(String.class); + + assertEquals(HttpStatus.OK, result.getStatusCode(), "Body\n%s".formatted(result.getBody())); + assertNotNull(result.getBody()); + assertTrue( + result.getBody().contains("a nice response"), "Body\n%s".formatted(result.getBody())); + } +} diff --git a/src/test/java/jumper/service/RedisZoneHealthStatusServiceTest.java b/src/test/java/jumper/service/RedisZoneHealthStatusServiceTest.java index 254fe5b..7018f93 100644 --- a/src/test/java/jumper/service/RedisZoneHealthStatusServiceTest.java +++ b/src/test/java/jumper/service/RedisZoneHealthStatusServiceTest.java @@ -20,10 +20,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.data.redis.connection.DefaultMessage; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles("test") @@ -38,7 +38,7 @@ class RedisZoneHealthStatusServiceTest extends AbstractIntegrationTest { @Autowired private RedisZoneHealthStatusService redisZoneHealthStatusService; - @SpyBean private ZoneHealthCheckService zoneHealthCheckService; + @MockitoSpyBean private ZoneHealthCheckService zoneHealthCheckService; @BeforeEach void setUp() { diff --git a/src/test/java/jumper/util/AbstractIntegrationTest.java b/src/test/java/jumper/util/AbstractIntegrationTest.java index d29df6d..8f168fc 100644 --- a/src/test/java/jumper/util/AbstractIntegrationTest.java +++ b/src/test/java/jumper/util/AbstractIntegrationTest.java @@ -27,8 +27,8 @@ public class AbstractIntegrationTest { @DynamicPropertySource static void dynamicProperties(DynamicPropertyRegistry registry) { - registry.add("spring.redis.host", REDIS_CONTAINER::getHost); - registry.add("spring.redis.port", () -> REDIS_CONTAINER.getMappedPort(REDIS_PORT)); + registry.add("spring.data.redis.host", REDIS_CONTAINER::getHost); + registry.add("spring.data.redis.port", () -> REDIS_CONTAINER.getMappedPort(REDIS_PORT)); registry.add("jumper.zone.health.enabled", () -> true); } } diff --git a/src/test/java/jumper/util/JumperConfigUtil.java b/src/test/java/jumper/util/JumperConfigUtil.java index 6d9ee2b..194f9d1 100644 --- a/src/test/java/jumper/util/JumperConfigUtil.java +++ b/src/test/java/jumper/util/JumperConfigUtil.java @@ -6,7 +6,7 @@ import static jumper.config.Config.*; import static jumper.config.Config.CONSUMER; -import static jumper.model.config.JumperConfig.toBase64; +import static jumper.model.config.JumperConfig.toJsonBase64; import java.util.HashMap; import java.util.List; @@ -23,7 +23,7 @@ public static String getJcSecurity() { oauth.put(CONSUMER, oc); JumperConfig jc = new JumperConfig(); jc.setOauth(oauth); - return toBase64(jc); + return toJsonBase64(jc); } public static String getJcBasicAuthConsumer(String id) { @@ -34,7 +34,7 @@ public static String getJcBasicAuthConsumer(String id) { basicAuthCredentialsHashMap.put(CONSUMER, ba); JumperConfig jc = new JumperConfig(); jc.setBasicAuth(basicAuthCredentialsHashMap); - return toBase64(jc); + return toJsonBase64(jc); } public static String getJcBasicAuthProvider(String id) { @@ -45,7 +45,7 @@ public static String getJcBasicAuthProvider(String id) { basicAuthCredentialsHashMap.put(Constants.BASIC_AUTH_PROVIDER_KEY, ba); JumperConfig jc = new JumperConfig(); jc.setBasicAuth(basicAuthCredentialsHashMap); - return toBase64(jc); + return toJsonBase64(jc); } public static String getJcBasicAuthConsumerAndProvider(String id) { @@ -60,7 +60,7 @@ public static String getJcBasicAuthConsumerAndProvider(String id) { basicAuthCredentialsHashMap.put(Constants.BASIC_AUTH_PROVIDER_KEY, baProvider); JumperConfig jc = new JumperConfig(); jc.setBasicAuth(basicAuthCredentialsHashMap); - return toBase64(jc); + return toJsonBase64(jc); } public static String getJcBasicAuthOtherConsumer(String id) { @@ -71,7 +71,7 @@ public static String getJcBasicAuthOtherConsumer(String id) { basicAuthCredentialsHashMap.put(CONSUMER_GATEWAY, ba); JumperConfig jc = new JumperConfig(); jc.setBasicAuth(basicAuthCredentialsHashMap); - return toBase64(jc); + return toJsonBase64(jc); } public static String getJcLoadBalancing() { @@ -82,7 +82,7 @@ public static String getJcLoadBalancing() { JumperConfig jc = new JumperConfig(); jc.setLoadBalancing(loadBalancing); - return toBase64(jc); + return toJsonBase64(jc); } public static String getEmptyJcLoadBalancing() { @@ -91,13 +91,13 @@ public static String getEmptyJcLoadBalancing() { JumperConfig jc = new JumperConfig(); jc.setLoadBalancing(loadBalancing); - return toBase64(jc); + return toJsonBase64(jc); } public static String getJcRemoveHeaders(List values) { JumperConfig jc = new JumperConfig(); jc.setRemoveHeaders(values); - return toBase64(jc); + return toJsonBase64(jc); } public enum JcOauthConfig { @@ -122,7 +122,7 @@ public String getJcOauthGrantType(String id) { determineKeys().forEach(key -> oauth.put(key, oc)); JumperConfig jc = new JumperConfig(); jc.setOauth(oauth); - return toBase64(jc); + return toJsonBase64(jc); } public String getJcOauthGrantTypePost(String id) { @@ -135,7 +135,7 @@ public String getJcOauthGrantTypePost(String id) { determineKeys().forEach(key -> oauth.put(key, oc)); JumperConfig jc = new JumperConfig(); jc.setOauth(oauth); - return toBase64(jc); + return toJsonBase64(jc); } public String getJcOauthGrantTypePassword(String id) { @@ -149,7 +149,7 @@ public String getJcOauthGrantTypePassword(String id) { determineKeys().forEach(key -> oauth.put(key, oc)); JumperConfig jc = new JumperConfig(); jc.setOauth(oauth); - return toBase64(jc); + return toJsonBase64(jc); } public String getJcOauthGrantTypePasswordOnly(String id) { @@ -161,7 +161,7 @@ public String getJcOauthGrantTypePasswordOnly(String id) { determineKeys().forEach(key -> oauth.put(key, oc)); JumperConfig jc = new JumperConfig(); jc.setOauth(oauth); - return toBase64(jc); + return toJsonBase64(jc); } public String getJcOauth(String id) { @@ -172,7 +172,7 @@ public String getJcOauth(String id) { determineKeys().forEach(key -> oauth.put(key, oc)); JumperConfig jc = new JumperConfig(); jc.setOauth(oauth); - return toBase64(jc); + return toJsonBase64(jc); } public String getJcOauthWithScope(String id) { @@ -184,7 +184,7 @@ public String getJcOauthWithScope(String id) { determineKeys().forEach(key -> oauth.put(key, oc)); JumperConfig jc = new JumperConfig(); jc.setOauth(oauth); - return toBase64(jc); + return toJsonBase64(jc); } public String getJcOauthGrantTypeWithKey(String id) { @@ -196,7 +196,7 @@ public String getJcOauthGrantTypeWithKey(String id) { determineKeys().forEach(key -> oauth.put(key, oc)); JumperConfig jc = new JumperConfig(); jc.setOauth(oauth); - return toBase64(jc); + return toJsonBase64(jc); } public void setJcOauthKeyType(String clientKey) { @@ -231,7 +231,7 @@ public static String getJcRouteListener(String consumer) { GatewayClient gc = new GatewayClient(); gc.setIssuer("realms/default"); jc.setGatewayClient(gc); - return toBase64(jc); + return toJsonBase64(jc); } public static String addIdSuffix(String from, String id) { diff --git a/src/test/java/jumper/util/RoutingConfigUtil.java b/src/test/java/jumper/util/RoutingConfigUtil.java index d66bb45..8627dea 100644 --- a/src/test/java/jumper/util/RoutingConfigUtil.java +++ b/src/test/java/jumper/util/RoutingConfigUtil.java @@ -5,7 +5,7 @@ package jumper.util; import static jumper.config.Config.*; -import static jumper.model.config.JumperConfig.toBase64; +import static jumper.model.config.JumperConfig.toJsonBase64; import java.util.List; import java.util.function.Consumer; @@ -41,17 +41,17 @@ public static Consumer getProxyRouteHeaders(BaseSteps baseSteps) { public static String getRcSecondary(String id) { // proxy + real - return toBase64(List.of(getProxyRouteJc(REMOTE_ZONE_NAME, id), getRealRouteJc())); + return toJsonBase64(List.of(getProxyRouteJc(REMOTE_ZONE_NAME, id), getRealRouteJc())); } public static String getRcSecondaryLoadbalancing(String id) { // proxy + real (with loadbalancing) - return toBase64(List.of(getProxyRouteJc(REMOTE_ZONE_NAME, id), getRealRouteJcLb())); + return toJsonBase64(List.of(getProxyRouteJc(REMOTE_ZONE_NAME, id), getRealRouteJcLb())); } public static String getRcProxy(String id) { // proxy + proxy - return toBase64( + return toJsonBase64( List.of( getProxyRouteJc(REMOTE_ZONE_NAME, id), getProxyRouteJc(REMOTE_FAILOVER_ZONE_NAME, id))); } diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 3495550..c610607 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -7,14 +7,15 @@ spring: allow-bean-definition-overriding: true cloud: gateway: - httpclient: - connect-timeout: 2000 - response-timeout: 5s # ingress 60s - max-initial-line-length-tardis: ${MAX_INITIAL_LINE_LENGTH:8192} #available scg parameter does not have effect, so we add custom one + server: + webflux: + httpclient: + connect-timeout: 2000 + response-timeout: 5s # ingress 60s jumper: issuer: - url: ${JUMPER_ISSUER_URL:https://stargate-test.de/auth/realms} + url: https://stargate-test.de/auth/realms security: dir: src/test/resources/keypair zone: @@ -23,14 +24,10 @@ jumper: redis: channel: stargate-zone-status checkConnectionInterval: 15000 + horizon: + publishEventUrl: http://localhost:1082/v1/events test: upstream: ssl: handshake-timeout: 1 - -############################## -# local horizon -############################## -horizon: - publishEventUrl: ${PUBLISH_EVENT_URL:http://localhost:1082/v1/events} diff --git a/src/test/resources/features/authorizationHeader.feature b/src/test/resources/features/authorizationHeader.feature index 65ca5da..2f428f5 100644 --- a/src/test/resources/features/authorizationHeader.feature +++ b/src/test/resources/features/authorizationHeader.feature @@ -9,19 +9,24 @@ Feature: proper authorization token reaches provider endpoint Scenario: Consumer calls proxy route with real route headers, OneToken sent Given RealRoute headers are set And API provider set to respond with a 200 status code + And A header x-tardis-traceid header is set with value dummy When consumer calls the proxy route Then API Provider receives default bearer authorization headers Then API Provider receives authorization OneToken And API consumer receives a 200 status code + And API Provider receives header x-tardis-traceid that matches regex dummy + And metrics result does not contain dummy Scenario: Consumer calls proxy route with real route headers, jc with BasicAuth for other consumer present, OneToken sent Given RealRoute headers are set And jumperConfig basic auth "other consumer present" set And API provider set to respond with a 200 status code + And A header x-tardis-traceid header is set with value dummy When consumer calls the proxy route Then API Provider receives default bearer authorization headers Then API Provider receives authorization OneToken And API consumer receives a 200 status code + And API Provider receives header x-tardis-traceid that matches regex dummy Scenario: Horizon calls proxy route with pub/sub info, OneToken contains pub/sub info Given RealRoute headers are set @@ -41,30 +46,36 @@ Feature: proper authorization token reaches provider endpoint Scenario: Consumer calls proxy route with iris token containing aud, OneToken contains audience claim Given RealRoute headers are set + And A header x-tardis-traceid header is set with value dummy And authorization token with aud set And API provider set to respond with a 200 status code When consumer calls the proxy route Then API Provider receives authorization OneTokenWithAud And API consumer receives a 200 status code + And API Provider receives header x-tardis-traceid that matches regex dummy Scenario: Consumer calls proxy route and realm header contains several values, correct issuer set in OneToken Given RealRoute headers are set And several realm fields are contained in the header And API provider set to respond with a 200 status code + And A header x-tardis-traceid header is set with value dummy When consumer calls the proxy route Then API Provider receives default bearer authorization headers Then API Provider receives authorization OneToken And API consumer receives a 200 status code + And API Provider receives header x-tardis-traceid that matches regex dummy ################ mesh ################ Scenario: Consumer calls proxy route with proxy route headers, mesh token sent Given ProxyRoute headers are set And IDP set to provide internal token And API provider set to respond with a 200 status code + And A header x-tardis-traceid header is set with value dummy When consumer calls the proxy route Then API Provider receives default bearer authorization headers Then API Provider receives authorization MeshToken And API consumer receives a 200 status code + And API Provider receives header x-tardis-traceid that matches regex dummy ################ external legacy ################ Scenario: Consumer calls proxy route with jc with oauth, external authorization token sent @@ -192,37 +203,45 @@ Feature: proper authorization token reaches provider endpoint And jumperConfig oauth "provider grant_type key" set And IDP set to provide externalKey token And API provider set to respond with a 200 status code + And A header x-tardis-traceid header is set with value dummy When consumer calls the proxy route Then API Provider receives authorization ExternalConfigured And API consumer receives a 200 status code + And API Provider receives header x-tardis-traceid that matches regex dummy ################ basic auth ################ Scenario: Consumer calls proxy route with real route headers, jc with consumer specific basic auth provided, consumer specific basic auth authorization sent Given RealRoute headers are set And API provider set to respond with a 200 status code And jumperConfig basic auth "consumer key only" set + And A header x-tardis-traceid header is set with value dummy When consumer calls the proxy route Then API Provider receives default basic authorization headers Then API Provider receives authorization BasicAuthConsumer And API consumer receives a 200 status code + And API Provider receives header x-tardis-traceid that matches regex dummy Scenario: Consumer calls proxy route with real route headers, jc with provider specific basic auth provided, provider specific basic auth authorization sent Given RealRoute headers are set And API provider set to respond with a 200 status code And jumperConfig basic auth "provider key only" set + And A header x-tardis-traceid header is set with value dummy When consumer calls the proxy route Then API Provider receives default basic authorization headers Then API Provider receives authorization BasicAuthProvider And API consumer receives a 200 status code + And API Provider receives header x-tardis-traceid that matches regex dummy Scenario: Consumer calls proxy route with real route headers, jc with consumer and provider specific basic auth provided, consumer specific basic auth authorization sent Given RealRoute headers are set And API provider set to respond with a 200 status code And jumperConfig basic auth "consumer and provider" set + And A header x-tardis-traceid header is set with value dummy When consumer calls the proxy route Then API Provider receives default basic authorization headers Then API Provider receives authorization BasicAuthConsumer And API consumer receives a 200 status code + And API Provider receives header x-tardis-traceid that matches regex dummy ################ auth header not present ################ Scenario: Service configured with authorization on removeHeaders list, no authorization sent to provider diff --git a/src/test/resources/features/errorResponse.feature b/src/test/resources/features/errorResponse.feature index 902753c..b646e62 100644 --- a/src/test/resources/features/errorResponse.feature +++ b/src/test/resources/features/errorResponse.feature @@ -23,7 +23,7 @@ Feature: proper error message returned based on conditions And API provider set to respond with a 200 status code When consumer calls the proxy route And API consumer receives a 401 status code - And error response contains msg "401 UNAUTHORIZED \"Failed to connect to http://localhost:1081/auth/realms/default/protocol/openid-connect/token, cause: Connection prematurely closed BEFORE response; nested exception is reactor.netty.http.client.PrematureCloseException: Connection prematurely closed BEFORE response\"" error "Unauthorized" status 401 + And error response contains msg "401 UNAUTHORIZED \"Failed to connect to http://localhost:1081/auth/realms/default/protocol/openid-connect/token, cause: Connection prematurely closed BEFORE response\"" error "Unauthorized" status 401 Scenario: Consumer calls proxy route with jc with oauth, oauth wrong credential headers set Given RealRoute headers are set @@ -152,7 +152,7 @@ Feature: proper error message returned based on conditions And API provider set to respond with a 200 status code When consumer calls the proxy route And API consumer receives a 401 status code - And error response contains msg "401 UNAUTHORIZED \"Failed to connect to http://localhost:1081/external, cause: Connection prematurely closed BEFORE response; nested exception is reactor.netty.http.client.PrematureCloseException: Connection prematurely closed BEFORE response\"" error "Unauthorized" status 401 + And error response contains msg "401 UNAUTHORIZED \"Failed to connect to http://localhost:1081/external, cause: Connection prematurely closed BEFORE response\"" error "Unauthorized" status 401 ################ external IDP - jwt authorization ################ Scenario: external IDP weak key configured diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml deleted file mode 100644 index 9197b3e..0000000 --- a/src/test/resources/logback-test.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n - - - - - - - - - -