diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6ad3da36..75ea0025 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -58,5 +58,13 @@ "onAutoForward": "ignore" } }, - "otherPortsAttributes": {"onAutoForward": "ignore"} + "otherPortsAttributes": {"onAutoForward": "ignore"}, + "shutdownAction": "none", + "mounts": [ + "source=/data/home/akumar87/.m2,target=/home/vscode/.m2,type=bind,consistency=consistent", + "source=/data/home/akumar87/codebase/bitbucket/api-gateway-custom-plugin,target=/workspaces/api-gateway-custom-plugin,type=bind,consistency=cached", + "source=/data/home/akumar87/codebase/scripts/,target=/workspaces/scripts,type=bind,consistency=cached", + "source=/data/home/akumar87/codebase/ecsp/token-validator,target=/workspaces/token-validator,type=bind,consistency=cached", + "source=/data/home/akumar87/codebase/harman-auto/sp-platform-productenablers-api-gateway-test,target=/workspaces/sp-platform-productenablers-api-gateway-test,type=bind,consistency=cached" + ] } \ No newline at end of file diff --git a/.github/workflows/maven-build.yml b/.github/workflows/maven-build.yml index 683fe608..2d95419a 100644 --- a/.github/workflows/maven-build.yml +++ b/.github/workflows/maven-build.yml @@ -58,5 +58,13 @@ jobs: with: context_path: ./api-gateway maven_args: 'clean package --file pom.xml' + java_version: '25' + java_distribution: 'temurin' + + deploy-snapshot: + uses: eclipse-ecsp/.github/.github/workflows/workflow-publish-artifacts.yml@52c11f63f46f741e4f56c5dace97b44982a4372b + secrets: inherit + needs: sonar_analysis + with: java_version: '25' java_distribution: 'temurin' \ No newline at end of file diff --git a/.github/workflows/maven-deploy.yml b/.github/workflows/maven-deploy.yml deleted file mode 100644 index dbaa4632..00000000 --- a/.github/workflows/maven-deploy.yml +++ /dev/null @@ -1,69 +0,0 @@ -# This workflow will deploy JAR to Maven Central repository - -name: Maven Deploy - -on: - workflow_dispatch: - -env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -jobs: - secret-presence: - runs-on: ubuntu-latest - outputs: - HAS_CENTRAL_SONATYPE_SECRETS: ${{ steps.secret-presence.outputs.HAS_CENTRAL_SONATYPE_SECRETS }} - steps: - - name: Check whether secrets exist - id: secret-presence - run: | - [ ! -z "${{ secrets.GPG_PASSPHRASE }}" ] && - [ ! -z "${{ secrets.GPG_PRIVATE_KEY }}" ] && - [ ! -z "${{ secrets.CENTRAL_SONATYPE_TOKEN_USERNAME }}" ] && - [ ! -z "${{ secrets.CENTRAL_SONATYPE_TOKEN_PASSWORD }}" ] && - echo "HAS_CENTRAL_SONATYPE_SECRETS=true" >> $GITHUB_OUTPUT - exit 0 - - publish-to-sonatype: - name: "Publish artifacts to MavenCentral" - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - needs: [ secret-presence ] - - if: | - needs.secret-presence.outputs.HAS_CENTRAL_SONATYPE_SECRETS - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up JDK 25 - uses: actions/setup-java@v4 - with: - java-version: '25' - distribution: 'temurin' - settings-path: ${{ github.workspace }} - - - uses: eclipse-ecsp/.github/.github/actions/import-gpg-key@main - name: "Import GPG Key" - with: - gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} - - - name: Configure Maven settings - run: | - mkdir -p $HOME/.m2 - echo " - - - central - ${{ secrets.CENTRAL_SONATYPE_TOKEN_USERNAME }} - ${{ secrets.CENTRAL_SONATYPE_TOKEN_PASSWORD }} - - - " > $HOME/.m2/settings.xml - - - name: Publish version - run: |- - VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) - mvn clean deploy -DskipMavenPublishing=false --file pom.xml -s $HOME/.m2/settings.xml -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}" -Prelease -Drevision=$VERSION \ No newline at end of file diff --git a/DEPENDENCIES b/DEPENDENCIES index 59d60a98..e7f93b50 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -26,6 +26,7 @@ maven/mavencentral/com.google.j2objc/j2objc-annotations/3.0.0, Apache-2.0, appro maven/mavencentral/com.jayway.jsonpath/json-path/2.10.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.networknt/json-schema-validator/1.5.9, Apache-2.0 AND Unicode-TOU, approved, #15630 maven/mavencentral/com.nimbusds/nimbus-jose-jwt/10.5, Apache-2.0, approved, #23321 +maven/mavencentral/com.nimbusds/nimbus-jose-jwt/10.9, Apache-2.0, approved, #28183 maven/mavencentral/com.redis/testcontainers-redis-common/2.2.4, Apache-2.0, approved, clearlydefined maven/mavencentral/com.redis/testcontainers-redis/2.2.4, Apache-2.0, approved, clearlydefined maven/mavencentral/com.sun.istack/istack-commons-runtime/4.1.2, BSD-3-Clause, approved, #15290 @@ -38,8 +39,8 @@ maven/mavencentral/commons-io/commons-io/2.20.0, Apache-2.0, approved, #22562 maven/mavencentral/commons-logging/commons-logging/1.3.6, Apache-2.0, approved, #11783 maven/mavencentral/de.siegmar/logback-gelf/6.1.2, LGPL-2.1-or-later AND (LGPL-2.1-or-later AND MIT), approved, #20666 maven/mavencentral/dev.morphia.morphia/morphia-core/2.2.3, Apache-2.0, approved, clearlydefined -maven/mavencentral/io.dropwizard.metrics/metrics-core/4.2.19, Apache-2.0, approved, clearlydefined -maven/mavencentral/io.dropwizard.metrics/metrics-healthchecks/4.2.19, Apache-2.0, approved, clearlydefined +maven/mavencentral/io.dropwizard.metrics/metrics-core/4.2.19, Apache-2.0, approved, #27707 +maven/mavencentral/io.dropwizard.metrics/metrics-healthchecks/4.2.19, Apache-2.0, approved, #27696 maven/mavencentral/io.github.classgraph/classgraph/4.8.78, MIT, approved, CQ22530 maven/mavencentral/io.github.resilience4j/resilience4j-annotations/2.3.0, Apache-2.0, approved, clearlydefined maven/mavencentral/io.github.resilience4j/resilience4j-bulkhead/2.3.0, Apache-2.0, approved, clearlydefined @@ -61,12 +62,12 @@ maven/mavencentral/io.jsonwebtoken/jjwt-api/0.13.0, Apache-2.0, approved, clearl maven/mavencentral/io.jsonwebtoken/jjwt-impl/0.13.0, Apache-2.0, approved, clearlydefined maven/mavencentral/io.jsonwebtoken/jjwt-jackson/0.13.0, Apache-2.0, approved, clearlydefined maven/mavencentral/io.lettuce/lettuce-core/6.8.2.RELEASE, Apache-2.0 AND MIT, approved, #26070 -maven/mavencentral/io.micrometer/micrometer-commons/1.16.4, Apache-2.0 AND (Apache-2.0 AND MIT), approved, #24726 -maven/mavencentral/io.micrometer/micrometer-core/1.16.4, Apache-2.0 AND (Apache-2.0 AND MIT), approved, #24722 -maven/mavencentral/io.micrometer/micrometer-jakarta9/1.16.4, Apache-2.0, approved, #25608 -maven/mavencentral/io.micrometer/micrometer-observation/1.16.4, Apache-2.0, approved, #24713 -maven/mavencentral/io.micrometer/micrometer-registry-datadog/1.16.4, Apache-2.0, approved, #26093 -maven/mavencentral/io.micrometer/micrometer-registry-prometheus/1.16.4, Apache-2.0, approved, #24729 +maven/mavencentral/io.micrometer/micrometer-commons/1.16.5, Apache-2.0 AND (Apache-2.0 AND MIT), approved, #24726 +maven/mavencentral/io.micrometer/micrometer-core/1.16.5, Apache-2.0 AND (Apache-2.0 AND MIT), approved, #24722 +maven/mavencentral/io.micrometer/micrometer-jakarta9/1.16.5, Apache-2.0, approved, #25608 +maven/mavencentral/io.micrometer/micrometer-observation/1.16.5, Apache-2.0, approved, #24713 +maven/mavencentral/io.micrometer/micrometer-registry-datadog/1.16.5, Apache-2.0, approved, #26093 +maven/mavencentral/io.micrometer/micrometer-registry-prometheus/1.16.5, Apache-2.0, approved, #24729 maven/mavencentral/io.netty/netty-buffer/4.2.11.Final, Apache-2.0, approved, #16116 maven/mavencentral/io.netty/netty-codec-base/4.2.12.Final, Apache-2.0, approved, #21322 maven/mavencentral/io.netty/netty-codec-classes-quic/4.2.12.Final, Apache-2.0, approved, #21311 @@ -91,9 +92,9 @@ maven/mavencentral/io.netty/netty-transport-native-epoll/4.2.11.Final, Apache-2. maven/mavencentral/io.netty/netty-transport-native-unix-common/4.2.11.Final, Apache-2.0, approved, #21329 maven/mavencentral/io.netty/netty-transport/4.2.11.Final, Apache-2.0, approved, #16113 maven/mavencentral/io.projectreactor.addons/reactor-extra/3.6.0, Apache-2.0, approved, clearlydefined -maven/mavencentral/io.projectreactor.netty/reactor-netty-core/1.3.4, Apache-2.0, approved, #25603 -maven/mavencentral/io.projectreactor.netty/reactor-netty-http/1.3.4, Apache-2.0, approved, #25597 -maven/mavencentral/io.projectreactor/reactor-core/3.8.4, Apache-2.0, approved, #25635 +maven/mavencentral/io.projectreactor.netty/reactor-netty-core/1.3.5, Apache-2.0, approved, #25603 +maven/mavencentral/io.projectreactor.netty/reactor-netty-http/1.3.5, Apache-2.0, approved, #25597 +maven/mavencentral/io.projectreactor/reactor-core/3.8.5, Apache-2.0, approved, #25635 maven/mavencentral/io.projectreactor/reactor-test/3.7.11, Apache-2.0, approved, #22536 maven/mavencentral/io.prometheus/prometheus-metrics-config/1.4.3, Apache-2.0, approved, #23433 maven/mavencentral/io.prometheus/prometheus-metrics-core/1.4.3, Apache-2.0, approved, #23432 @@ -136,11 +137,11 @@ maven/mavencentral/org.apache.commons/commons-lang3/3.18.0, Apache-2.0, approved maven/mavencentral/org.apache.httpcomponents.client5/httpclient5/5.5.2, Apache-2.0 AND MPL-2.0, approved, #24483 maven/mavencentral/org.apache.httpcomponents.core5/httpcore5-h2/5.3.6, Apache-2.0, approved, #16867 maven/mavencentral/org.apache.httpcomponents.core5/httpcore5/5.3.6, Apache-2.0, approved, #16866 -maven/mavencentral/org.apache.logging.log4j/log4j-api/2.25.3, Apache-2.0, approved, #21940 -maven/mavencentral/org.apache.logging.log4j/log4j-to-slf4j/2.25.3, Apache-2.0, approved, #21937 -maven/mavencentral/org.apache.tomcat.embed/tomcat-embed-core/11.0.20, Apache-2.0 AND (EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0) AND (CDDL-1.0 OR GPL-2.0-or-later), approved, #19217 -maven/mavencentral/org.apache.tomcat.embed/tomcat-embed-el/11.0.20, Apache-2.0, approved, #22421 -maven/mavencentral/org.apache.tomcat.embed/tomcat-embed-websocket/11.0.20, Apache-2.0, approved, #21065 +maven/mavencentral/org.apache.logging.log4j/log4j-api/2.25.4, Apache-2.0, approved, #21940 +maven/mavencentral/org.apache.logging.log4j/log4j-to-slf4j/2.25.4, Apache-2.0, approved, #21937 +maven/mavencentral/org.apache.tomcat.embed/tomcat-embed-core/11.0.21, Apache-2.0 AND (EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0) AND (CDDL-1.0 OR GPL-2.0-or-later), approved, #19217 +maven/mavencentral/org.apache.tomcat.embed/tomcat-embed-el/11.0.21, Apache-2.0, approved, #22421 +maven/mavencentral/org.apache.tomcat.embed/tomcat-embed-websocket/11.0.21, Apache-2.0, approved, #21065 maven/mavencentral/org.apiguardian/apiguardian-api/1.1.2, Apache-2.0, approved, #17641 maven/mavencentral/org.aspectj/aspectjrt/1.9.25.1, EPL-1.0, approved, tools.aspectj maven/mavencentral/org.aspectj/aspectjweaver/1.9.25.1, EPL-1.0, approved, tools.aspectj @@ -150,33 +151,34 @@ maven/mavencentral/org.bouncycastle/bcprov-jdk18on/1.82, MIT AND CC0-1.0, approv maven/mavencentral/org.checkerframework/checker-qual/3.42.0, MIT, approved, #20003 maven/mavencentral/org.checkerframework/checker-qual/3.52.0, MIT, approved, #26057 maven/mavencentral/org.eclipse.angus/angus-activation/2.0.3, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.angus -maven/mavencentral/org.eclipse.ecsp/api-registry-common/1.5.3-SNAPSHOT, Apache-2.0, approved, automotive.ecsp +maven/mavencentral/org.eclipse.ecsp/api-registry-common/1.6.0-SNAPSHOT, Apache-2.0, approved, automotive.ecsp maven/mavencentral/org.eclipse.ecsp/entities/1.2.0, Apache-2.0, approved, automotive.ecsp maven/mavencentral/org.eclipse.ecsp/nosql-dao/1.2.2, Apache-2.0, approved, automotive.ecsp maven/mavencentral/org.eclipse.ecsp/sql-dao/1.2.3, Apache-2.0, approved, automotive.ecsp +maven/mavencentral/org.eclipse.ecsp/token-validator/0.0.1-20260518.091343-8, Apache-2.0, approved, automotive.ecsp maven/mavencentral/org.eclipse.ecsp/utils/1.2.0, Apache-2.0, approved, automotive.ecsp -maven/mavencentral/org.eclipse.jetty.compression/jetty-compression-common/12.1.7, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty.compression/jetty-compression-gzip/12.1.7, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty.compression/jetty-compression-common/12.1.8, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty.compression/jetty-compression-gzip/12.1.8, EPL-2.0 OR Apache-2.0, approved, rt.jetty maven/mavencentral/org.eclipse.jetty.ee10/jetty-ee10-servlet/12.0.30, EPL-2.0 OR Apache-2.0, approved, rt.jetty maven/mavencentral/org.eclipse.jetty.ee10/jetty-ee10-servlets/12.0.30, EPL-2.0 OR Apache-2.0, approved, rt.jetty maven/mavencentral/org.eclipse.jetty.ee10/jetty-ee10-webapp/12.0.30, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty.http2/jetty-http2-common/12.1.7, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty.http2/jetty-http2-hpack/12.1.7, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty.http2/jetty-http2-server/12.1.7, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-alpn-client/12.1.7, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-alpn-java-client/12.1.7, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-alpn-java-server/12.1.7, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-alpn-server/12.1.7, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-client/12.1.7, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty.http2/jetty-http2-common/12.1.8, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty.http2/jetty-http2-hpack/12.1.8, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty.http2/jetty-http2-server/12.1.8, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-alpn-client/12.1.8, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-alpn-java-client/12.1.8, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-alpn-java-server/12.1.8, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-alpn-server/12.1.8, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-client/12.1.8, EPL-2.0 OR Apache-2.0, approved, rt.jetty maven/mavencentral/org.eclipse.jetty/jetty-ee/12.0.30, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-http/12.1.7, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-io/12.1.7, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-proxy/12.1.7, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-security/12.1.7, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-server/12.1.7, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-session/12.1.7, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-util/12.1.7, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.eclipse.jetty/jetty-xml/12.1.7, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-http/12.1.8, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-io/12.1.8, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-proxy/12.1.8, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-security/12.1.8, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-server/12.1.8, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-session/12.1.8, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-util/12.1.8, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-xml/12.1.8, EPL-2.0 OR Apache-2.0, approved, rt.jetty maven/mavencentral/org.glassfish.expressly/expressly/5.0.0, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.expressly maven/mavencentral/org.glassfish.jaxb/jaxb-core/4.0.6, BSD-3-Clause, approved, ee4j.jaxb-impl maven/mavencentral/org.glassfish.jaxb/jaxb-runtime/4.0.6, BSD-3-Clause, approved, ee4j.jaxb-impl @@ -228,70 +230,70 @@ maven/mavencentral/org.slf4j/jul-to-slf4j/2.0.17, MIT, approved, #7698 maven/mavencentral/org.slf4j/slf4j-api/2.0.17, MIT, approved, #5915 maven/mavencentral/org.springdoc/springdoc-openapi-starter-common/3.0.1, Apache-2.0, approved, #25648 maven/mavencentral/org.springdoc/springdoc-openapi-starter-webmvc-api/3.0.1, Apache-2.0, approved, #25723 -maven/mavencentral/org.springframework.boot/spring-boot-actuator-autoconfigure/4.0.5, Apache-2.0, approved, #26090 -maven/mavencentral/org.springframework.boot/spring-boot-actuator/4.0.5, Apache-2.0, approved, #26087 -maven/mavencentral/org.springframework.boot/spring-boot-autoconfigure/4.0.5, Apache-2.0, approved, #24987 -maven/mavencentral/org.springframework.boot/spring-boot-cache/4.0.5, Apache-2.0, approved, #26128 -maven/mavencentral/org.springframework.boot/spring-boot-configuration-processor/4.0.5, Apache-2.0, approved, #26123 -maven/mavencentral/org.springframework.boot/spring-boot-data-commons/4.0.5, Apache-2.0, approved, #25784 -maven/mavencentral/org.springframework.boot/spring-boot-data-jpa/4.0.5, Apache-2.0, approved, #25785 -maven/mavencentral/org.springframework.boot/spring-boot-data-mongodb/4.0.5, Apache-2.0, approved, #26133 -maven/mavencentral/org.springframework.boot/spring-boot-data-redis/4.0.5, Apache-2.0, approved, #26080 -maven/mavencentral/org.springframework.boot/spring-boot-health/4.0.5, Apache-2.0, approved, #26072 -maven/mavencentral/org.springframework.boot/spring-boot-hibernate/4.0.5, Apache-2.0, approved, #25787 -maven/mavencentral/org.springframework.boot/spring-boot-http-client/4.0.5, Apache-2.0, approved, #26125 -maven/mavencentral/org.springframework.boot/spring-boot-http-codec/4.0.5, Apache-2.0, approved, #26127 -maven/mavencentral/org.springframework.boot/spring-boot-http-converter/4.0.5, Apache-2.0, approved, #26122 -maven/mavencentral/org.springframework.boot/spring-boot-jackson/4.0.5, Apache-2.0, approved, #26138 -maven/mavencentral/org.springframework.boot/spring-boot-jackson2/4.0.5, Apache-2.0, approved, #26137 -maven/mavencentral/org.springframework.boot/spring-boot-jdbc/4.0.5, Apache-2.0, approved, #25788 -maven/mavencentral/org.springframework.boot/spring-boot-jpa/4.0.5, Apache-2.0, approved, #25783 -maven/mavencentral/org.springframework.boot/spring-boot-micrometer-metrics/4.0.5, Apache-2.0, approved, #26096 -maven/mavencentral/org.springframework.boot/spring-boot-micrometer-observation/4.0.5, Apache-2.0, approved, #26082 -maven/mavencentral/org.springframework.boot/spring-boot-mongodb/4.0.5, Apache-2.0, approved, #26131 -maven/mavencentral/org.springframework.boot/spring-boot-netty/4.0.5, Apache-2.0, approved, #26139 -maven/mavencentral/org.springframework.boot/spring-boot-persistence/4.0.5, Apache-2.0, approved, #25782 -maven/mavencentral/org.springframework.boot/spring-boot-reactor-netty/4.0.5, Apache-2.0, approved, #26121 -maven/mavencentral/org.springframework.boot/spring-boot-reactor/4.0.5, Apache-2.0, approved, #25618 -maven/mavencentral/org.springframework.boot/spring-boot-restclient/4.0.5, Apache-2.0, approved, #26124 -maven/mavencentral/org.springframework.boot/spring-boot-servlet/4.0.5, Apache-2.0, approved, #26101 -maven/mavencentral/org.springframework.boot/spring-boot-sql/4.0.5, Apache-2.0, approved, #25605 -maven/mavencentral/org.springframework.boot/spring-boot-starter-actuator/4.0.5, Apache-2.0, approved, #25622 -maven/mavencentral/org.springframework.boot/spring-boot-starter-aspectj/4.0.5, Apache-2.0, approved, #26060 -maven/mavencentral/org.springframework.boot/spring-boot-starter-cache/4.0.5, Apache-2.0, approved, #26077 -maven/mavencentral/org.springframework.boot/spring-boot-starter-data-jpa/4.0.5, Apache-2.0, approved, #25626 -maven/mavencentral/org.springframework.boot/spring-boot-starter-data-mongodb/4.0.5, Apache-2.0, approved, #26091 -maven/mavencentral/org.springframework.boot/spring-boot-starter-data-redis-reactive/4.0.5, Apache-2.0, approved, #26102 -maven/mavencentral/org.springframework.boot/spring-boot-starter-data-redis/4.0.5, Apache-2.0, approved, #26136 -maven/mavencentral/org.springframework.boot/spring-boot-starter-jackson-test/4.0.5, Apache-2.0, approved, #26076 -maven/mavencentral/org.springframework.boot/spring-boot-starter-jackson/4.0.5, Apache-2.0, approved, #26126 -maven/mavencentral/org.springframework.boot/spring-boot-starter-jdbc/4.0.5, Apache-2.0, approved, #25616 -maven/mavencentral/org.springframework.boot/spring-boot-starter-logging/4.0.5, Apache-2.0, approved, #25602 -maven/mavencentral/org.springframework.boot/spring-boot-starter-micrometer-metrics/4.0.5, Apache-2.0, approved, #26120 -maven/mavencentral/org.springframework.boot/spring-boot-starter-mongodb/4.0.5, Apache-2.0, approved, #26119 -maven/mavencentral/org.springframework.boot/spring-boot-starter-reactor-netty/4.0.5, Apache-2.0, approved, #26130 -maven/mavencentral/org.springframework.boot/spring-boot-starter-test/4.0.5, Apache-2.0, approved, #25598 -maven/mavencentral/org.springframework.boot/spring-boot-starter-tomcat-runtime/4.0.5, Apache-2.0, approved, #25612 -maven/mavencentral/org.springframework.boot/spring-boot-starter-tomcat/4.0.5, Apache-2.0, approved, #26097 -maven/mavencentral/org.springframework.boot/spring-boot-starter-validation/4.0.5, Apache-2.0, approved, #25629 -maven/mavencentral/org.springframework.boot/spring-boot-starter-web/4.0.5, Apache-2.0, approved, #26075 -maven/mavencentral/org.springframework.boot/spring-boot-starter-webclient-test/4.0.5, Apache-2.0, approved, #26092 -maven/mavencentral/org.springframework.boot/spring-boot-starter-webclient/4.0.5, Apache-2.0, approved, #26134 -maven/mavencentral/org.springframework.boot/spring-boot-starter-webflux/4.0.5, Apache-2.0, approved, #26129 -maven/mavencentral/org.springframework.boot/spring-boot-starter/4.0.5, Apache-2.0, approved, #25636 -maven/mavencentral/org.springframework.boot/spring-boot-test-autoconfigure/4.0.5, Apache-2.0, approved, #24996 -maven/mavencentral/org.springframework.boot/spring-boot-test/4.0.5, Apache-2.0, approved, #24997 -maven/mavencentral/org.springframework.boot/spring-boot-tomcat/4.0.5, Apache-2.0, approved, #26083 -maven/mavencentral/org.springframework.boot/spring-boot-transaction/4.0.5, Apache-2.0, approved, #25786 -maven/mavencentral/org.springframework.boot/spring-boot-validation/4.0.5, Apache-2.0, approved, #26084 -maven/mavencentral/org.springframework.boot/spring-boot-web-server/4.0.5, Apache-2.0, approved, #24984 -maven/mavencentral/org.springframework.boot/spring-boot-webclient-test/4.0.5, Apache-2.0, approved, #26071 -maven/mavencentral/org.springframework.boot/spring-boot-webclient/4.0.5, Apache-2.0, approved, #26095 -maven/mavencentral/org.springframework.boot/spring-boot-webflux/4.0.5, Apache-2.0, approved, #26135 -maven/mavencentral/org.springframework.boot/spring-boot-webmvc-test/4.0.5, Apache-2.0, approved, #26132 -maven/mavencentral/org.springframework.boot/spring-boot-webmvc/4.0.5, Apache-2.0, approved, #26105 -maven/mavencentral/org.springframework.boot/spring-boot-webtestclient/4.0.5, Apache-2.0, approved, #26104 -maven/mavencentral/org.springframework.boot/spring-boot/4.0.5, Apache-2.0, approved, #24993 +maven/mavencentral/org.springframework.boot/spring-boot-actuator-autoconfigure/4.0.6, Apache-2.0, approved, #26090 +maven/mavencentral/org.springframework.boot/spring-boot-actuator/4.0.6, Apache-2.0, approved, #26087 +maven/mavencentral/org.springframework.boot/spring-boot-autoconfigure/4.0.6, Apache-2.0, approved, #24987 +maven/mavencentral/org.springframework.boot/spring-boot-cache/4.0.6, Apache-2.0, approved, #26128 +maven/mavencentral/org.springframework.boot/spring-boot-configuration-processor/4.0.6, Apache-2.0, approved, #26123 +maven/mavencentral/org.springframework.boot/spring-boot-data-commons/4.0.6, Apache-2.0, approved, #25784 +maven/mavencentral/org.springframework.boot/spring-boot-data-jpa/4.0.6, Apache-2.0, approved, #25785 +maven/mavencentral/org.springframework.boot/spring-boot-data-mongodb/4.0.6, Apache-2.0, approved, #26133 +maven/mavencentral/org.springframework.boot/spring-boot-data-redis/4.0.6, Apache-2.0, approved, #26080 +maven/mavencentral/org.springframework.boot/spring-boot-health/4.0.6, Apache-2.0, approved, #26072 +maven/mavencentral/org.springframework.boot/spring-boot-hibernate/4.0.6, Apache-2.0, approved, #25787 +maven/mavencentral/org.springframework.boot/spring-boot-http-client/4.0.6, Apache-2.0, approved, #26125 +maven/mavencentral/org.springframework.boot/spring-boot-http-codec/4.0.6, Apache-2.0, approved, #26127 +maven/mavencentral/org.springframework.boot/spring-boot-http-converter/4.0.6, Apache-2.0, approved, #26122 +maven/mavencentral/org.springframework.boot/spring-boot-jackson/4.0.6, Apache-2.0, approved, #26138 +maven/mavencentral/org.springframework.boot/spring-boot-jackson2/4.0.6, Apache-2.0, approved, #26137 +maven/mavencentral/org.springframework.boot/spring-boot-jdbc/4.0.6, Apache-2.0, approved, #25788 +maven/mavencentral/org.springframework.boot/spring-boot-jpa/4.0.6, Apache-2.0, approved, #25783 +maven/mavencentral/org.springframework.boot/spring-boot-micrometer-metrics/4.0.6, Apache-2.0, approved, #26096 +maven/mavencentral/org.springframework.boot/spring-boot-micrometer-observation/4.0.6, Apache-2.0, approved, #26082 +maven/mavencentral/org.springframework.boot/spring-boot-mongodb/4.0.6, Apache-2.0, approved, #26131 +maven/mavencentral/org.springframework.boot/spring-boot-netty/4.0.6, Apache-2.0, approved, #26139 +maven/mavencentral/org.springframework.boot/spring-boot-persistence/4.0.6, Apache-2.0, approved, #25782 +maven/mavencentral/org.springframework.boot/spring-boot-reactor-netty/4.0.6, Apache-2.0, approved, #26121 +maven/mavencentral/org.springframework.boot/spring-boot-reactor/4.0.6, Apache-2.0, approved, #25618 +maven/mavencentral/org.springframework.boot/spring-boot-restclient/4.0.6, Apache-2.0, approved, #26124 +maven/mavencentral/org.springframework.boot/spring-boot-servlet/4.0.6, Apache-2.0, approved, #26101 +maven/mavencentral/org.springframework.boot/spring-boot-sql/4.0.6, Apache-2.0, approved, #25605 +maven/mavencentral/org.springframework.boot/spring-boot-starter-actuator/4.0.6, Apache-2.0, approved, #25622 +maven/mavencentral/org.springframework.boot/spring-boot-starter-aspectj/4.0.6, Apache-2.0, approved, #26060 +maven/mavencentral/org.springframework.boot/spring-boot-starter-cache/4.0.6, Apache-2.0, approved, #26077 +maven/mavencentral/org.springframework.boot/spring-boot-starter-data-jpa/4.0.6, Apache-2.0, approved, #25626 +maven/mavencentral/org.springframework.boot/spring-boot-starter-data-mongodb/4.0.6, Apache-2.0, approved, #26091 +maven/mavencentral/org.springframework.boot/spring-boot-starter-data-redis-reactive/4.0.6, Apache-2.0, approved, #26102 +maven/mavencentral/org.springframework.boot/spring-boot-starter-data-redis/4.0.6, Apache-2.0, approved, #26136 +maven/mavencentral/org.springframework.boot/spring-boot-starter-jackson-test/4.0.6, Apache-2.0, approved, #26076 +maven/mavencentral/org.springframework.boot/spring-boot-starter-jackson/4.0.6, Apache-2.0, approved, #26126 +maven/mavencentral/org.springframework.boot/spring-boot-starter-jdbc/4.0.6, Apache-2.0, approved, #25616 +maven/mavencentral/org.springframework.boot/spring-boot-starter-logging/4.0.6, Apache-2.0, approved, #25602 +maven/mavencentral/org.springframework.boot/spring-boot-starter-micrometer-metrics/4.0.6, Apache-2.0, approved, #26120 +maven/mavencentral/org.springframework.boot/spring-boot-starter-mongodb/4.0.6, Apache-2.0, approved, #26119 +maven/mavencentral/org.springframework.boot/spring-boot-starter-reactor-netty/4.0.6, Apache-2.0, approved, #26130 +maven/mavencentral/org.springframework.boot/spring-boot-starter-test/4.0.6, Apache-2.0, approved, #25598 +maven/mavencentral/org.springframework.boot/spring-boot-starter-tomcat-runtime/4.0.6, Apache-2.0, approved, #25612 +maven/mavencentral/org.springframework.boot/spring-boot-starter-tomcat/4.0.6, Apache-2.0, approved, #26097 +maven/mavencentral/org.springframework.boot/spring-boot-starter-validation/4.0.6, Apache-2.0, approved, #25629 +maven/mavencentral/org.springframework.boot/spring-boot-starter-web/4.0.6, Apache-2.0, approved, #26075 +maven/mavencentral/org.springframework.boot/spring-boot-starter-webclient-test/4.0.6, Apache-2.0, approved, #26092 +maven/mavencentral/org.springframework.boot/spring-boot-starter-webclient/4.0.6, Apache-2.0, approved, #26134 +maven/mavencentral/org.springframework.boot/spring-boot-starter-webflux/4.0.6, Apache-2.0, approved, #26129 +maven/mavencentral/org.springframework.boot/spring-boot-starter/4.0.6, Apache-2.0, approved, #25636 +maven/mavencentral/org.springframework.boot/spring-boot-test-autoconfigure/4.0.6, Apache-2.0, approved, #24996 +maven/mavencentral/org.springframework.boot/spring-boot-test/4.0.6, Apache-2.0, approved, #24997 +maven/mavencentral/org.springframework.boot/spring-boot-tomcat/4.0.6, Apache-2.0, approved, #26083 +maven/mavencentral/org.springframework.boot/spring-boot-transaction/4.0.6, Apache-2.0, approved, #25786 +maven/mavencentral/org.springframework.boot/spring-boot-validation/4.0.6, Apache-2.0, approved, #26084 +maven/mavencentral/org.springframework.boot/spring-boot-web-server/4.0.6, Apache-2.0, approved, #24984 +maven/mavencentral/org.springframework.boot/spring-boot-webclient-test/4.0.6, Apache-2.0, approved, #26071 +maven/mavencentral/org.springframework.boot/spring-boot-webclient/4.0.6, Apache-2.0, approved, #26095 +maven/mavencentral/org.springframework.boot/spring-boot-webflux/4.0.6, Apache-2.0, approved, #26135 +maven/mavencentral/org.springframework.boot/spring-boot-webmvc-test/4.0.6, Apache-2.0, approved, #26132 +maven/mavencentral/org.springframework.boot/spring-boot-webmvc/4.0.6, Apache-2.0, approved, #26105 +maven/mavencentral/org.springframework.boot/spring-boot-webtestclient/4.0.6, Apache-2.0, approved, #26104 +maven/mavencentral/org.springframework.boot/spring-boot/4.0.6, Apache-2.0, approved, #24993 maven/mavencentral/org.springframework.cloud/spring-cloud-circuitbreaker-resilience4j/5.0.1, Apache-2.0, approved, #26088 maven/mavencentral/org.springframework.cloud/spring-cloud-commons/5.0.1, Apache-2.0, approved, #26106 maven/mavencentral/org.springframework.cloud/spring-cloud-context/5.0.1, Apache-2.0, approved, #26094 @@ -301,34 +303,35 @@ maven/mavencentral/org.springframework.cloud/spring-cloud-kubernetes-discovery/5 maven/mavencentral/org.springframework.cloud/spring-cloud-starter-circuitbreaker-reactor-resilience4j/5.0.1, Apache-2.0, approved, #26073 maven/mavencentral/org.springframework.cloud/spring-cloud-starter-gateway-server-webflux/5.0.1, Apache-2.0, approved, #26089 maven/mavencentral/org.springframework.cloud/spring-cloud-starter/5.0.1, Apache-2.0, approved, #26100 -maven/mavencentral/org.springframework.data/spring-data-commons/4.0.4, Apache-2.0, approved, #25623 -maven/mavencentral/org.springframework.data/spring-data-jpa/4.0.4, Apache-2.0, approved, #25600 -maven/mavencentral/org.springframework.data/spring-data-keyvalue/4.0.4, Apache-2.0 AND BSD-3-Clause AND Apache-2.0, approved, #26085 -maven/mavencentral/org.springframework.data/spring-data-mongodb/5.0.4, Apache-2.0, approved, #26099 -maven/mavencentral/org.springframework.data/spring-data-redis/4.0.4, Apache-2.0, approved, #26086 +maven/mavencentral/org.springframework.data/spring-data-commons/4.0.5, Apache-2.0, approved, #25623 +maven/mavencentral/org.springframework.data/spring-data-jpa/4.0.5, Apache-2.0, approved, #25600 +maven/mavencentral/org.springframework.data/spring-data-keyvalue/4.0.5, Apache-2.0 AND BSD-3-Clause AND Apache-2.0, approved, #26085 +maven/mavencentral/org.springframework.data/spring-data-mongodb/5.0.5, Apache-2.0, approved, #26099 +maven/mavencentral/org.springframework.data/spring-data-redis/4.0.5, Apache-2.0, approved, #26086 maven/mavencentral/org.springframework.retry/spring-retry/2.0.12, Apache-2.0, approved, #16889 maven/mavencentral/org.springframework.security/spring-security-crypto/7.0.4, Apache-2.0 AND ISC, approved, #25599 -maven/mavencentral/org.springframework/spring-aop/7.0.6, Apache-2.0, approved, #24985 -maven/mavencentral/org.springframework/spring-aspects/7.0.6, Apache-2.0, approved, #25619 -maven/mavencentral/org.springframework/spring-beans/7.0.6, Apache-2.0, approved, #24991 -maven/mavencentral/org.springframework/spring-context-support/7.0.6, Apache-2.0, approved, #26103 -maven/mavencentral/org.springframework/spring-context/7.0.6, Apache-2.0, approved, #24986 -maven/mavencentral/org.springframework/spring-core/7.0.6, Apache-2.0 AND BSD-3-Clause, approved, #24992 -maven/mavencentral/org.springframework/spring-expression/7.0.6, Apache-2.0, approved, #24988 -maven/mavencentral/org.springframework/spring-jdbc/7.0.6, Apache-2.0, approved, #25610 -maven/mavencentral/org.springframework/spring-orm/7.0.6, Apache-2.0, approved, #25628 -maven/mavencentral/org.springframework/spring-oxm/7.0.6, Apache-2.0, approved, #26069 -maven/mavencentral/org.springframework/spring-test/7.0.6, Apache-2.0, approved, #24998 -maven/mavencentral/org.springframework/spring-tx/7.0.6, Apache-2.0, approved, #25615 -maven/mavencentral/org.springframework/spring-web/7.0.6, Apache-2.0, approved, #24989 -maven/mavencentral/org.springframework/spring-webflux/7.0.6, Apache-2.0, approved, #25625 -maven/mavencentral/org.springframework/spring-webmvc/7.0.6, Apache-2.0, approved, #25606 -maven/mavencentral/org.testcontainers/testcontainers-database-commons/2.0.4, Apache-2.0, approved, #25271 -maven/mavencentral/org.testcontainers/testcontainers-jdbc/2.0.4, Apache-2.0, approved, #25272 +maven/mavencentral/org.springframework/spring-aop/7.0.7, Apache-2.0, approved, #24985 +maven/mavencentral/org.springframework/spring-aspects/7.0.7, Apache-2.0, approved, #25619 +maven/mavencentral/org.springframework/spring-beans/7.0.7, Apache-2.0, approved, #24991 +maven/mavencentral/org.springframework/spring-context-support/7.0.7, Apache-2.0, approved, #26103 +maven/mavencentral/org.springframework/spring-context/7.0.7, Apache-2.0, approved, #24986 +maven/mavencentral/org.springframework/spring-core/7.0.7, Apache-2.0 AND BSD-3-Clause, approved, #24992 +maven/mavencentral/org.springframework/spring-expression/7.0.7, Apache-2.0, approved, #24988 +maven/mavencentral/org.springframework/spring-jdbc/7.0.7, Apache-2.0, approved, #25610 +maven/mavencentral/org.springframework/spring-orm/7.0.7, Apache-2.0, approved, #25628 +maven/mavencentral/org.springframework/spring-oxm/7.0.7, Apache-2.0, approved, #26069 +maven/mavencentral/org.springframework/spring-test/7.0.7, Apache-2.0, approved, #24998 +maven/mavencentral/org.springframework/spring-tx/7.0.7, Apache-2.0, approved, #25615 +maven/mavencentral/org.springframework/spring-web/7.0.7, Apache-2.0, approved, #24989 +maven/mavencentral/org.springframework/spring-webflux/7.0.7, Apache-2.0, approved, #25625 +maven/mavencentral/org.springframework/spring-webmvc/7.0.7, Apache-2.0, approved, #25606 +maven/mavencentral/org.testcontainers/testcontainers-database-commons/2.0.5, Apache-2.0, approved, #25271 +maven/mavencentral/org.testcontainers/testcontainers-jdbc/2.0.5, Apache-2.0, approved, #25272 maven/mavencentral/org.testcontainers/testcontainers-junit-jupiter/2.0.4, MIT, approved, #26032 +maven/mavencentral/org.testcontainers/testcontainers-junit-jupiter/2.0.5, MIT, approved, #26032 maven/mavencentral/org.testcontainers/testcontainers-mongodb/2.0.4, MIT, approved, #26949 maven/mavencentral/org.testcontainers/testcontainers-postgresql/2.0.4, MIT, approved, #25273 -maven/mavencentral/org.testcontainers/testcontainers/2.0.4, MIT, approved, #25274 +maven/mavencentral/org.testcontainers/testcontainers/2.0.5, MIT, approved, #25274 maven/mavencentral/org.wiremock/wiremock-jetty12/3.13.2, Apache-2.0, approved, #21027 maven/mavencentral/org.wiremock/wiremock-standalone/3.13.2, MIT AND Apache-2.0, approved, #21026 maven/mavencentral/org.wiremock/wiremock/3.13.2, MIT AND Apache-2.0, approved, #21025 @@ -337,5 +340,5 @@ maven/mavencentral/org.xmlunit/xmlunit-legacy/2.10.4, Apache-2.0 AND BSD-3-Claus maven/mavencentral/org.xmlunit/xmlunit-placeholders/2.10.4, Apache-2.0, approved, #22214 maven/mavencentral/org.yaml/snakeyaml/2.5, Apache-2.0, approved, #23100 maven/mavencentral/redis.clients.authentication/redis-authx-core/0.1.1-beta2, MIT, approved, clearlydefined -maven/mavencentral/tools.jackson.core/jackson-core/3.1.0, Apache-2.0 AND MIT, approved, #26415 -maven/mavencentral/tools.jackson.core/jackson-databind/3.1.0, Apache-2.0, approved, #26439 +maven/mavencentral/tools.jackson.core/jackson-core/3.1.2, Apache-2.0 AND MIT, approved, #26415 +maven/mavencentral/tools.jackson.core/jackson-databind/3.1.2, Apache-2.0, approved, #26439 diff --git a/api-gateway/pom.xml b/api-gateway/pom.xml index bf2673ae..6fa69cc7 100644 --- a/api-gateway/pom.xml +++ b/api-gateway/pom.xml @@ -6,7 +6,7 @@ org.eclipse.ecsp api-gateway-parent - 1.5.3-SNAPSHOT + 1.6.0-SNAPSHOT api-gateway diff --git a/api-gateway/src/main/java/org/eclipse/ecsp/gateway/config/GatewayConfig.java b/api-gateway/src/main/java/org/eclipse/ecsp/gateway/config/GatewayConfig.java index 22bdeb64..52284b96 100644 --- a/api-gateway/src/main/java/org/eclipse/ecsp/gateway/config/GatewayConfig.java +++ b/api-gateway/src/main/java/org/eclipse/ecsp/gateway/config/GatewayConfig.java @@ -27,10 +27,12 @@ import org.eclipse.ecsp.gateway.utils.ObjectMapperUtil; import org.eclipse.ecsp.utils.logger.IgniteLogger; import org.eclipse.ecsp.utils.logger.IgniteLoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.endpoint.EndpointFilter; import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.web.WebProperties; +import org.springframework.boot.http.codec.CodecCustomizer; import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.web.server.autoconfigure.ServerProperties; import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory; @@ -46,6 +48,7 @@ import org.springframework.retry.backoff.ExponentialBackOffPolicy; import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.support.RetryTemplate; +import org.springframework.util.unit.DataSize; import org.springframework.web.client.RestClientException; import org.springframework.web.reactive.function.server.RequestPredicates; import org.springframework.web.reactive.function.server.RouterFunction; @@ -297,4 +300,10 @@ public RetryTemplate routesRefreshRetryTemplate(RouteRefreshProperties propertie return retryTemplate; } + //@Bean + public CodecCustomizer codecCustomizer(@Value("${spring.codec.max-in-memory-size:1MB}") String maxInMemorySize) { + int maxInMemorySizeBytes = (int) DataSize.parse(maxInMemorySize).toBytes(); + return configurer -> configurer.defaultCodecs().maxInMemorySize(maxInMemorySizeBytes); + } + } diff --git a/api-registry-common/pom.xml b/api-registry-common/pom.xml index ecd1fa9d..5d23c33c 100644 --- a/api-registry-common/pom.xml +++ b/api-registry-common/pom.xml @@ -6,7 +6,7 @@ org.eclipse.ecsp api-gateway-parent - 1.5.3-SNAPSHOT + 1.6.0-SNAPSHOT api-registry-common @@ -77,5 +77,15 @@ junit-platform-suite test + + org.eclipse.ecsp + token-validator + ${ecsp.token-validator.version} + + + org.springframework.boot + spring-boot-starter-webflux + true + \ No newline at end of file diff --git a/api-registry-common/src/main/java/org/eclipse/ecsp/config/InterceptorConfig.java b/api-registry-common/src/main/java/org/eclipse/ecsp/config/InterceptorConfig.java index a0374d67..8b8538fa 100644 --- a/api-registry-common/src/main/java/org/eclipse/ecsp/config/InterceptorConfig.java +++ b/api-registry-common/src/main/java/org/eclipse/ecsp/config/InterceptorConfig.java @@ -19,29 +19,44 @@ package org.eclipse.ecsp.config; import org.eclipse.ecsp.interceptors.HeaderInterceptor; +import org.eclipse.ecsp.interceptors.TokenValidationInterceptor; +import org.eclipse.ecsp.utils.logger.IgniteLogger; +import org.eclipse.ecsp.utils.logger.IgniteLoggerFactory; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.Optional; /** * InterceptorConfig. */ @Configuration +@Order(10) public class InterceptorConfig implements WebMvcConfigurer { - + private static final IgniteLogger LOGGER = IgniteLoggerFactory.getLogger(InterceptorConfig.class); private final HeaderInterceptor headerInterceptor; + private final Optional tokenValidationInterceptor; /** * Constructor to initialize the InterceptorConfig. * - * @param headerInterceptor the HeaderInterceptor + * @param headerInterceptor the HeaderInterceptor + * @param tokenValidationInterceptor the optional TokenValidationInterceptor */ - public InterceptorConfig(HeaderInterceptor headerInterceptor) { + public InterceptorConfig(HeaderInterceptor headerInterceptor, + Optional tokenValidationInterceptor) { this.headerInterceptor = headerInterceptor; + this.tokenValidationInterceptor = tokenValidationInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(headerInterceptor); + LOGGER.debug("Added HeaderInterceptor to the interceptor registry"); + tokenValidationInterceptor.ifPresent(interceptor -> { + registry.addInterceptor(interceptor); + LOGGER.debug("Added TokenValidationInterceptor to the interceptor registry"); + }); } } \ No newline at end of file diff --git a/api-registry-common/src/main/java/org/eclipse/ecsp/restclient/RestTemplateConfig.java b/api-registry-common/src/main/java/org/eclipse/ecsp/config/RestTemplateConfig.java similarity index 68% rename from api-registry-common/src/main/java/org/eclipse/ecsp/restclient/RestTemplateConfig.java rename to api-registry-common/src/main/java/org/eclipse/ecsp/config/RestTemplateConfig.java index 87bc0ac7..a885802d 100644 --- a/api-registry-common/src/main/java/org/eclipse/ecsp/restclient/RestTemplateConfig.java +++ b/api-registry-common/src/main/java/org/eclipse/ecsp/config/RestTemplateConfig.java @@ -16,10 +16,14 @@ *

SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -package org.eclipse.ecsp.restclient; +package org.eclipse.ecsp.config; +import org.eclipse.ecsp.restclient.RestTemplateErrorHandler; +import org.eclipse.ecsp.restclient.RestTemplateLogInterceptor; +import org.eclipse.ecsp.restclient.RestTemplateTokenInterceptor; import org.eclipse.ecsp.utils.logger.IgniteLogger; import org.eclipse.ecsp.utils.logger.IgniteLoggerFactory; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -44,16 +48,24 @@ public RestTemplateConfig() { /** * Create and returns the object of RestTemplate. * + *

If a {@link RestTemplateTokenInterceptor} bean is available it is added to the + * interceptor chain so that Bearer tokens are forwarded to downstream services. + * + * @param tokenInterceptorProvider provider for the optional token propagation interceptor * @return RestTemplate Object */ @Bean @ConditionalOnMissingBean - public RestTemplate restTemplate() { + public RestTemplate restTemplate(ObjectProvider tokenInterceptorProvider) { RestTemplate restTemplate = new RestTemplate(); restTemplate.setErrorHandler(new RestTemplateErrorHandler()); if (LOGGER.isDebugEnabled()) { restTemplate.getInterceptors().add(new RestTemplateLogInterceptor()); } + RestTemplateTokenInterceptor tokenInterceptor = tokenInterceptorProvider.getIfAvailable(); + if (tokenInterceptor != null) { + restTemplate.getInterceptors().add(tokenInterceptor); + } return restTemplate; } diff --git a/api-registry-common/src/main/java/org/eclipse/ecsp/config/TokenValidationConfiguration.java b/api-registry-common/src/main/java/org/eclipse/ecsp/config/TokenValidationConfiguration.java new file mode 100644 index 00000000..e0f506bb --- /dev/null +++ b/api-registry-common/src/main/java/org/eclipse/ecsp/config/TokenValidationConfiguration.java @@ -0,0 +1,150 @@ +/******************************************************************************** + * Copyright (c) 2023-24 Harman International + * + *

Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *

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. + * + *

SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.ecsp.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.ecsp.interceptors.SecurityRequirementCache; +import org.eclipse.ecsp.interceptors.TokenValidationInterceptor; +import org.eclipse.ecsp.restclient.RestClientTokenInterceptor; +import org.eclipse.ecsp.restclient.RestTemplateTokenInterceptor; +import org.eclipse.ecsp.security.ScopeOverrideProperties; +import org.eclipse.ecsp.security.ValidationConfigProperties; +import org.eclipse.ecsp.tokenvalidator.TokenValidator; +import org.eclipse.ecsp.tokenvalidator.config.TokenValidatorAutoConfiguration; +import org.eclipse.ecsp.utils.RegistryCommonConstants; +import org.eclipse.ecsp.utils.logger.IgniteLogger; +import org.eclipse.ecsp.utils.logger.IgniteLoggerFactory; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Auto-configuration that wires together the JWT token validation and token propagation + * beans for the {@code api-registry-common} library. + * + *

Activated by the presence of the library on the classpath — no additional user + * configuration is required unless behaviour needs to be overridden. + */ +@Configuration +@ConditionalOnProperty( + prefix = RegistryCommonConstants.API_REGISTRY_SECURITY_PREFIX, + name = "enabled", + havingValue = "true" +) +@ImportAutoConfiguration(TokenValidatorAutoConfiguration.class) +@EnableConfigurationProperties(ValidationConfigProperties.class) +public class TokenValidationConfiguration { + private static final IgniteLogger LOGGER = IgniteLoggerFactory.getLogger(TokenValidationConfiguration.class); + + + /** + * Default constructor. + */ + public TokenValidationConfiguration() { + LOGGER.debug("security is enabled, configuring the token validation configuration"); + } + + /** + * Creates the annotation-lookup cache bean when security is enabled. + * + * @return the cache + */ + @Bean + public SecurityRequirementCache securityRequirementCache() { + LOGGER.debug("Creating SecurityRequirementCache bean"); + return new SecurityRequirementCache(); + } + + /** + * Creates the token-validation interceptor when security is enabled. + * + * @param tokenValidator the JWT validator + * @param config the validation configuration properties + * @param securityRequirementCache the annotation-lookup cache + * @param objectMapper the object mapper for JSON serialization + * @param scopeOverrideProperties the scope-override configuration properties + * @return the interceptor + */ + @Bean + public TokenValidationInterceptor tokenValidationInterceptor( + TokenValidator tokenValidator, + ValidationConfigProperties config, + SecurityRequirementCache securityRequirementCache, + ObjectMapper objectMapper, + ScopeOverrideProperties scopeOverrideProperties) { + LOGGER.debug("Creating TokenValidationInterceptor bean"); + return new TokenValidationInterceptor(tokenValidator, config, securityRequirementCache, objectMapper, + scopeOverrideProperties); + } + + /** + * Creates the RestTemplate token propagation interceptor when enabled. + * + * @param config the validation / propagation configuration properties + * @return the interceptor + */ + @Bean + @ConditionalOnProperty( + prefix = RegistryCommonConstants.API_REGISTRY_REST_TEMPLATE_PROPAGATION_PREFIX, + name = "enabled", + havingValue = "true", + matchIfMissing = true + ) + public RestTemplateTokenInterceptor restTemplateTokenInterceptor(ValidationConfigProperties config) { + LOGGER.debug("Creating RestTemplateTokenInterceptor bean"); + return new RestTemplateTokenInterceptor(config); + } + + /** + * Creates a {@link RestClientTokenInterceptor} bean for token propagation. + * + * @param config the validation / propagation configuration properties + * @return the interceptor + */ + @Bean + @ConditionalOnProperty( + prefix = RegistryCommonConstants.API_REGISTRY_REST_CLIENT_PROPAGATION_PREFIX, + name = "enabled", + havingValue = "true", + matchIfMissing = true + ) + @ConditionalOnClass(org.springframework.web.client.RestClient.class) + public RestClientTokenInterceptor restClientTokenInterceptor(ValidationConfigProperties config) { + LOGGER.debug("Creating RestClientTokenInterceptor bean"); + return new RestClientTokenInterceptor(config); + } + + /** + * Creates a default ObjectMapper bean if one is not already defined. + * + * @return the object mapper + */ + @Bean + @ConditionalOnMissingBean(ObjectMapper.class) + public ObjectMapper objectMapper() { + LOGGER.debug("Creating ObjectMapper bean"); + ObjectMapper om = new ObjectMapper(); + om.findAndRegisterModules(); + return om; + } +} diff --git a/api-registry-common/src/main/java/org/eclipse/ecsp/interceptors/HeaderInterceptor.java b/api-registry-common/src/main/java/org/eclipse/ecsp/interceptors/HeaderInterceptor.java index 2dd1c8ef..4eb89731 100644 --- a/api-registry-common/src/main/java/org/eclipse/ecsp/interceptors/HeaderInterceptor.java +++ b/api-registry-common/src/main/java/org/eclipse/ecsp/interceptors/HeaderInterceptor.java @@ -69,4 +69,20 @@ public boolean preHandle(@NotNull HttpServletRequest request, } return true; } + + /** + * Clears the {@link HeaderContext} ThreadLocal after every request, preventing + * memory leaks caused by servlet-container thread reuse. + * + * @param request the current HTTP request + * @param response the current HTTP response + * @param handler the chosen handler + * @param ex any exception thrown during handler execution, or {@code null} + */ + @Override + public void afterCompletion(@NotNull HttpServletRequest request, + @NotNull HttpServletResponse response, + @NotNull Object handler, Exception ex) { + HeaderContext.clear(); + } } \ No newline at end of file diff --git a/api-registry-common/src/main/java/org/eclipse/ecsp/interceptors/SecurityRequirementCache.java b/api-registry-common/src/main/java/org/eclipse/ecsp/interceptors/SecurityRequirementCache.java new file mode 100644 index 00000000..f2169c24 --- /dev/null +++ b/api-registry-common/src/main/java/org/eclipse/ecsp/interceptors/SecurityRequirementCache.java @@ -0,0 +1,84 @@ +/******************************************************************************** + * Copyright (c) 2023-24 Harman International + * + *

Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *

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. + * + *

SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.ecsp.interceptors; + +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import org.springframework.web.method.HandlerMethod; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Caches the result of inspecting whether a controller method is annotated with + * {@link SecurityRequirement}, eliminating repeated reflection calls on hot paths. + * + *

Routes are fixed at startup, so the cache never needs to be evicted. + * {@link ConcurrentHashMap#computeIfAbsent} ensures the reflection call is performed + * at most once per {@link HandlerMethod}, even under concurrent first-requests. + * + *

This class is not annotated with {@code @Component} — it is registered exclusively + * by {@link org.eclipse.ecsp.config.TokenValidationConfiguration} to prevent + * duplicate-bean conflicts if a consuming application scans the library packages. + */ +public class SecurityRequirementCache { + + private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + + /** + * Default constructor. + */ + public SecurityRequirementCache() { + // Default constructor + } + + /** + * Returns {@code true} if the handler method is annotated with {@link SecurityRequirement}. + * + *

The result is cached after the first call for each method, making subsequent + * calls an O(1) map lookup. + * + * @param handlerMethod the resolved handler method + * @return {@code true} if the endpoint requires JWT authentication; {@code false} if public + */ + public boolean isSecured(HandlerMethod handlerMethod) { + return cache.computeIfAbsent(handlerMethod, + m -> m.getMethodAnnotation(SecurityRequirement.class) != null); + } + + /** + * Returns the scopes required by the handler method's {@link SecurityRequirement} annotation. + * + * @param handlerMethod the resolved handler method + * @return a list of required scopes, or an empty list if none are specified + */ + public List getScopes(HandlerMethod handlerMethod) { + SecurityRequirement securityRequirement = handlerMethod.getMethodAnnotation(SecurityRequirement.class); + if (securityRequirement != null) { + // Assuming only one security requirement is used, get the first one + // split scopes by comma and trim whitespace, then filter out empty scopes + return Arrays.stream(securityRequirement.scopes()) + .flatMap(s -> Arrays.stream(s.split(","))) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .toList(); + } + return List.of(); + } +} diff --git a/api-registry-common/src/main/java/org/eclipse/ecsp/interceptors/TokenValidationInterceptor.java b/api-registry-common/src/main/java/org/eclipse/ecsp/interceptors/TokenValidationInterceptor.java new file mode 100644 index 00000000..1b20b989 --- /dev/null +++ b/api-registry-common/src/main/java/org/eclipse/ecsp/interceptors/TokenValidationInterceptor.java @@ -0,0 +1,224 @@ +/******************************************************************************** + * Copyright (c) 2023-24 Harman International + * + *

Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *

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. + * + *

SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.ecsp.interceptors; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.ecsp.security.ScopeOverrideProperties; +import org.eclipse.ecsp.security.SecurityContext; +import org.eclipse.ecsp.security.ValidationConfigProperties; +import org.eclipse.ecsp.tokenvalidator.ScopeMatchMode; +import org.eclipse.ecsp.tokenvalidator.ScopeValidator; +import org.eclipse.ecsp.tokenvalidator.TokenValidator; +import org.eclipse.ecsp.tokenvalidator.exception.InvalidClaimException; +import org.eclipse.ecsp.tokenvalidator.exception.TokenValidatorException; +import org.eclipse.ecsp.tokenvalidator.impl.DefaultScopeValidator; +import org.eclipse.ecsp.tokenvalidator.model.TokenClaim; +import org.eclipse.ecsp.utils.logger.IgniteLogger; +import org.eclipse.ecsp.utils.logger.IgniteLoggerFactory; +import org.jetbrains.annotations.NotNull; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Spring MVC interceptor that validates JWT Bearer tokens on secured endpoints. + * + *

An endpoint is considered secured when its handler method carries the + * {@code @SecurityRequirement} annotation. Public endpoints (no annotation) are + * passed through without any token inspection. + * + *

All {@link TokenValidatorException} sub-types are mapped to HTTP 401 to avoid + * catch-order bugs arising from the {@code TokenExpiredException} / + * {@code InvalidIssuerException} inheritance chain. + * + *

On successful validation the verified claims are stored in {@link SecurityContext}. + * {@link SecurityContext#clear()} is always called in {@code afterCompletion} to + * prevent ThreadLocal memory leaks. + */ +public class TokenValidationInterceptor implements HandlerInterceptor { + + private static final IgniteLogger LOGGER = IgniteLoggerFactory.getLogger(TokenValidationInterceptor.class); + + private static final String BEARER_PREFIX = "Bearer "; + private static final int BEARER_PREFIX_LENGTH = BEARER_PREFIX.length(); + private static final String ERROR_FIELD = "error"; + + private final TokenValidator tokenValidator; + private final ValidationConfigProperties config; + private final SecurityRequirementCache securityRequirementCache; + private final ObjectMapper objectMapper; + private final ScopeValidator scopeValidator = new DefaultScopeValidator(Set.of(), ScopeMatchMode.ANY); + private final ScopeOverrideProperties scopeOverrideProperties; + + /** + * Constructs a {@code TokenValidationInterceptor}. + * + * @param tokenValidator the JWT validator delegate + * @param config the validation configuration properties + * @param securityRequirementCache the annotation-lookup cache + * @param objectMapper the object mapper for JSON serialization + * @param scopeOverrideProperties the scope-override configuration properties + */ + public TokenValidationInterceptor(TokenValidator tokenValidator, + ValidationConfigProperties config, + SecurityRequirementCache securityRequirementCache, + ObjectMapper objectMapper, + ScopeOverrideProperties scopeOverrideProperties) { + LOGGER.debug("Initializing TokenValidationInterceptor"); + this.tokenValidator = tokenValidator; + this.config = config; + this.securityRequirementCache = securityRequirementCache; + this.objectMapper = objectMapper; + this.scopeOverrideProperties = scopeOverrideProperties; + LOGGER.debug("TokenValidationInterceptor initialized"); + } + + @Override + public boolean preHandle(@NotNull HttpServletRequest request, + @NotNull HttpServletResponse response, + @NotNull Object handler) throws IOException { + if (!config.getSecurity().isEnabled()) { + LOGGER.debug("security not enabled, skipping token validation"); + return true; + } + if (!(handler instanceof HandlerMethod)) { + return true; + } + HandlerMethod handlerMethod = (HandlerMethod) handler; + if (!securityRequirementCache.isSecured(handlerMethod)) { + LOGGER.debug("Endpoint is not secured, skipping token validation"); + return true; + } + + String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) { + LOGGER.warn("Missing or malformed authentication token for endpoint {}", request.getRequestURI()); + return writeUnauthorized(response, "Invalid token"); + } + + String token = authHeader.substring(BEARER_PREFIX_LENGTH); + try { + List claims = tokenValidator.validate(token); + if (!validateScopes(handlerMethod, claims, request, response)) { + return false; + } + SecurityContext.set(token, claims); + return true; + } catch (TokenValidatorException ex) { + LOGGER.warn("Token validation failed: {}, for the endpoint {}", ex.getMessage(), request.getRequestURI()); + return writeUnauthorized(response, "Token validation failed"); + } + } + + private boolean validateScopes(HandlerMethod handlerMethod, + List claims, + HttpServletRequest request, + HttpServletResponse response) throws IOException { + // token is valid , perform authorization checks. + List requiredScopes = securityRequirementCache.getScopes(handlerMethod); + List effectiveScopes = resolveEffectiveScopes(handlerMethod, requiredScopes); + try { + scopeValidator.validateClaims(claims, effectiveScopes); + return true; + } catch (InvalidClaimException exInvalidClaimException) { + LOGGER.warn("Token does not have required scopes for endpoint {}", + request.getRequestURI(), exInvalidClaimException); + return writeUnauthorized(response, "Token validation failed"); + } + } + + /** + * Clears the {@link SecurityContext} after every request, including error cases. + * + * @param request the current HTTP request + * @param response the current HTTP response + * @param handler the chosen handler + * @param ex any exception thrown during handler execution, or {@code null} + */ + @Override + public void afterCompletion(@NotNull HttpServletRequest request, + @NotNull HttpServletResponse response, + @NotNull Object handler, Exception ex) { + SecurityContext.clear(); + } + + /** + * Merges annotation-declared scopes with the configured override scopes for the route. + * + *

When {@code scopes.override.enabled} is {@code true} and the route ID is present + * in the {@code scopesMap}, the effective allowed scope set is the + * configured override scopes. + * This makes scope validation self-contained: no dependency on the {@code override-scope} header + * forwarded by the API Gateway. + * + * @param handlerMethod the resolved handler method for the current request + * @param annotationScopes the scopes declared on the {@code @SecurityRequirement} annotation + * @return the effective list of allowed scopes + */ + private List resolveEffectiveScopes(HandlerMethod handlerMethod, List annotationScopes) { + Map> scopesMap = scopeOverrideProperties.getScopesMap(); + if (!scopeOverrideProperties.getOverride().isEnabled() || scopesMap == null) { + return annotationScopes; + } + String routeId = resolveRouteId(handlerMethod); + List overrideScopes = scopesMap.getOrDefault(routeId, scopesMap.get(routeId.toLowerCase())); + if (overrideScopes == null) { + return annotationScopes; + } + LOGGER.debug("Override scopes found for routeId {}: {}", routeId, overrideScopes); + return overrideScopes.stream().distinct().toList(); + } + + /** + * Derives the route ID from a {@link HandlerMethod} using the same convention as + * {@link org.eclipse.ecsp.security.ScopeTagger}: {@code -}. + * + *

The tag is the controller class simple name converted from CamelCase to + * kebab-case and lowercased — matching the OpenAPI tag that SpringDoc generates, + * which retains the full class name including the {@code Controller} suffix. + * The operation ID is the method name with underscores replaced by hyphens. + * + * @param handlerMethod the resolved handler method + * @return the derived route ID + */ + private String resolveRouteId(HandlerMethod handlerMethod) { + String className = handlerMethod.getBeanType().getSimpleName(); + String tag = className.replaceAll("([a-z])([A-Z])", "$1-$2").toLowerCase(); + String operationId = handlerMethod.getMethod().getName().replace("_", "-"); + return tag + "-" + operationId; + } + + private boolean writeUnauthorized(HttpServletResponse response, String message) throws IOException { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + Map body = new HashMap<>(); + body.put(ERROR_FIELD, message); + body.put("code", "api.unauthorized"); + response.getWriter().write(objectMapper.writeValueAsString(body)); + return false; + } +} diff --git a/api-registry-common/src/main/java/org/eclipse/ecsp/register/ApiRoutesLoader.java b/api-registry-common/src/main/java/org/eclipse/ecsp/register/ApiRoutesLoader.java index 1fa295ca..2d09f619 100644 --- a/api-registry-common/src/main/java/org/eclipse/ecsp/register/ApiRoutesLoader.java +++ b/api-registry-common/src/main/java/org/eclipse/ecsp/register/ApiRoutesLoader.java @@ -30,13 +30,12 @@ import io.swagger.v3.oas.models.parameters.RequestBody; import io.swagger.v3.oas.models.security.SecurityRequirement; import jakarta.servlet.http.HttpServletRequest; -import lombok.Getter; -import lombok.Setter; import org.eclipse.ecsp.customizers.CustomGatewayFilterCustomizer; import org.eclipse.ecsp.register.model.FilterDefinition; import org.eclipse.ecsp.register.model.PredicateDefinition; import org.eclipse.ecsp.register.model.RouteDefinition; import org.eclipse.ecsp.security.CachingTagger; +import org.eclipse.ecsp.security.ScopeOverrideProperties; import org.eclipse.ecsp.security.Security; import org.eclipse.ecsp.utils.ObjectMapperUtil; import org.eclipse.ecsp.utils.RegistryCommonConstants; @@ -54,7 +53,6 @@ import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.http.HttpMethod; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; @@ -86,7 +84,6 @@ * @author SBala2 */ @Service -@ConfigurationProperties(prefix = "scopes") @ConditionalOnProperty(value = "api.registry.enabled", havingValue = "true", matchIfMissing = false) public class ApiRoutesLoader extends OpenApiResource { /** @@ -105,9 +102,7 @@ public class ApiRoutesLoader extends OpenApiResource { * API Routes configuration. */ protected ApiRoutesConfig apiRoutesConfig; - @Getter - @Setter - private Map> scopesMap; + private final ScopeOverrideProperties scopeOverrideProperties; @Value("${spring.application.name}") private String appName; @Value("${spring.application.servicename}") @@ -116,8 +111,6 @@ public class ApiRoutesLoader extends OpenApiResource { private String contextPath; @Value("${server.port}") private String port; - @Value("${scopes.override.enabled:false}") - private boolean isOverrideScopeEnabled; @Value("${api_gateway_caching_ttl:10m}") private String timeToLive; @Value("${api_gateway_cachesize:50MB}") @@ -137,6 +130,7 @@ public class ApiRoutesLoader extends OpenApiResource { * @param springDocProviders SpringDocProviders for providing SpringDoc services. * @param springDocCustomizers SpringDocCustomizers for customizing SpringDoc. * @param apiRoutesConfig apiRoutesConfig + * @param scopeOverrideProperties the scope-override configuration properties */ public ApiRoutesLoader(final List groupedOpenApis, ObjectFactory openApiBuilderObjectFactory, @@ -144,12 +138,14 @@ public ApiRoutesLoader(final List groupedOpenApis, GenericResponseService responseBuilder, OperationService operationParser, SpringDocConfigProperties springDocConfigProperties, SpringDocProviders springDocProviders, SpringDocCustomizers springDocCustomizers, - ApiRoutesConfig apiRoutesConfig) { + ApiRoutesConfig apiRoutesConfig, + ScopeOverrideProperties scopeOverrideProperties) { super(openApiBuilderObjectFactory, requestBuilder, responseBuilder, operationParser, springDocConfigProperties, springDocProviders, springDocCustomizers); this.apiRoutes = new LinkedList<>(); this.groupedOpenApis = groupedOpenApis; this.apiRoutesConfig = apiRoutesConfig; + this.scopeOverrideProperties = scopeOverrideProperties; } /** @@ -179,7 +175,7 @@ public List getApiRoutes() throws URISyntaxException { */ private void prepareApiRoutes() throws URISyntaxException { // Load from configuration - LOGGER.debug("Scopes Map config: " + scopesMap); + LOGGER.debug("Scopes Map config: " + scopeOverrideProperties.getScopesMap()); LOGGER.debug("Routes List: " + apiRoutesConfig.getRoutes()); prepareStaticRoutes(); // Load from Swagger Annotations @@ -469,7 +465,8 @@ private void setSecurityFilters(Operation operation, RouteDefinition route) { */ private void enabledOverrideScope(RouteDefinition route, List scopes) { String routeId = route.getId(); - if (isOverrideScopeEnabled && scopesMap != null + Map> scopesMap = scopeOverrideProperties.getScopesMap(); + if (scopeOverrideProperties.getOverride().isEnabled() && scopesMap != null && (scopesMap.get(routeId) != null || scopesMap.get(routeId.toLowerCase()) != null)) { List scopesList = scopesMap.get(routeId) != null ? scopesMap.get(routeId) : scopesMap.get(routeId.toLowerCase()); diff --git a/api-registry-common/src/main/java/org/eclipse/ecsp/restclient/AbstractTokenPropagationInterceptor.java b/api-registry-common/src/main/java/org/eclipse/ecsp/restclient/AbstractTokenPropagationInterceptor.java new file mode 100644 index 00000000..64016521 --- /dev/null +++ b/api-registry-common/src/main/java/org/eclipse/ecsp/restclient/AbstractTokenPropagationInterceptor.java @@ -0,0 +1,117 @@ +/******************************************************************************** + * Copyright (c) 2023-24 Harman International + * + *

Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *

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. + * + *

SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.ecsp.restclient; + +import org.eclipse.ecsp.security.SecurityContext; +import org.eclipse.ecsp.security.ValidationConfigProperties; +import org.eclipse.ecsp.utils.logger.IgniteLogger; +import org.eclipse.ecsp.utils.logger.IgniteLoggerFactory; +import java.net.URI; +import java.util.List; +import java.util.Optional; + +/** + * Base class for token-propagation interceptors and filters. + * + *

Provides shared host-classification logic and token-retrieval from + * {@link SecurityContext}. Subclasses implement the HTTP-client-specific + * {@code intercept} / {@code filter} method. + * + *

Thread-safety note: {@link #resolveToken(URI)} reads from the + * {@link SecurityContext} ThreadLocal. For {@code RestTemplate} and {@code RestClient} + * interceptors this is always called on the same servlet thread as the original request, + * so no additional synchronisation is needed. + */ +public abstract class AbstractTokenPropagationInterceptor { + + private static final IgniteLogger LOGGER = + IgniteLoggerFactory.getLogger(AbstractTokenPropagationInterceptor.class); + + private final ValidationConfigProperties config; + + /** + * Constructs an interceptor with the given configuration. + * + * @param config the validation / propagation configuration properties + */ + protected AbstractTokenPropagationInterceptor(ValidationConfigProperties config) { + this.config = config; + } + + /** + * Returns {@code true} when the Bearer token must NOT be forwarded to the given target. + * + *

Token propagation is skipped when: + *

+ * + * @param targetUri the URI of the downstream service + * @return {@code true} if propagation should be skipped + */ + protected boolean shouldSkipPropagation(URI targetUri) { + String host = targetUri.getHost(); + if (host == null) { + return false; + } + List excludeHosts = config.getTokenPropagation().getExcludeHosts(); + for (String excluded : excludeHosts) { + if (host.equalsIgnoreCase(excluded)) { + return true; + } + } + if (config.getTokenPropagation().isAllowExternalHosts()) { + return false; + } + // If allow-external-hosts is false, only forward when host is in include-hosts + List includeHosts = config.getTokenPropagation().getIncludeHosts(); + if (!includeHosts.isEmpty()) { + for (String included : includeHosts) { + if (host.equalsIgnoreCase(included)) { + return false; + } + } + return true; + } + // No include-hosts restriction — allow all non-excluded hosts + return false; + } + + /** + * Retrieves the current Bearer token from {@link SecurityContext}. + * + *

Returns {@link Optional#empty()} and logs a warning if the token is expired. + * + * @param targetUri the URI of the downstream service (used only for logging) + * @return the token, or empty if absent or expired + */ + protected Optional resolveToken(URI targetUri) { + Optional token = SecurityContext.getToken(); + if (token.isEmpty()) { + return Optional.empty(); + } + if (SecurityContext.isTokenExpired()) { + LOGGER.warn("Token is expired; skipping propagation to {}", targetUri); + return Optional.empty(); + } + return token; + } +} diff --git a/api-registry-common/src/main/java/org/eclipse/ecsp/restclient/RestClientTokenInterceptor.java b/api-registry-common/src/main/java/org/eclipse/ecsp/restclient/RestClientTokenInterceptor.java new file mode 100644 index 00000000..d09b82a2 --- /dev/null +++ b/api-registry-common/src/main/java/org/eclipse/ecsp/restclient/RestClientTokenInterceptor.java @@ -0,0 +1,62 @@ +/******************************************************************************** + * Copyright (c) 2023-24 Harman International + * + *

Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *

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. + * + *

SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.ecsp.restclient; + +import org.eclipse.ecsp.security.ValidationConfigProperties; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import java.io.IOException; +import java.util.Optional; + +/** + * {@link ClientHttpRequestInterceptor} for {@code RestClient} that propagates the + * current thread's Bearer token to downstream service calls. + * + *

Separate from {@link RestTemplateTokenInterceptor} to allow independent + * bean-lifecycle management for the {@code RestClient} integration. + */ +public class RestClientTokenInterceptor extends AbstractTokenPropagationInterceptor + implements ClientHttpRequestInterceptor { + + /** + * Constructs an interceptor with the given configuration. + * + * @param config the validation / propagation configuration properties + */ + public RestClientTokenInterceptor(ValidationConfigProperties config) { + super(config); + } + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution) throws IOException { + if (request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION) != null) { + return execution.execute(request, body); + } + Optional token = resolveToken(request.getURI()); + if (token.isEmpty() || shouldSkipPropagation(request.getURI())) { + return execution.execute(request, body); + } + request.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + token.get()); + return execution.execute(request, body); + } +} diff --git a/api-registry-common/src/main/java/org/eclipse/ecsp/restclient/RestTemplateTokenInterceptor.java b/api-registry-common/src/main/java/org/eclipse/ecsp/restclient/RestTemplateTokenInterceptor.java new file mode 100644 index 00000000..9675173e --- /dev/null +++ b/api-registry-common/src/main/java/org/eclipse/ecsp/restclient/RestTemplateTokenInterceptor.java @@ -0,0 +1,62 @@ +/******************************************************************************** + * Copyright (c) 2023-24 Harman International + * + *

Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *

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. + * + *

SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.ecsp.restclient; + +import org.eclipse.ecsp.security.ValidationConfigProperties; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import java.io.IOException; +import java.util.Optional; + +/** + * {@link ClientHttpRequestInterceptor} for {@code RestTemplate} that propagates the + * current thread's Bearer token to downstream service calls. + * + *

The token is read from {@link org.eclipse.ecsp.security.SecurityContext} on the same + * servlet thread as the original request — no reactive or cross-thread concerns apply. + */ +public class RestTemplateTokenInterceptor extends AbstractTokenPropagationInterceptor + implements ClientHttpRequestInterceptor { + + /** + * Constructs an interceptor with the given configuration. + * + * @param config the validation / propagation configuration properties + */ + public RestTemplateTokenInterceptor(ValidationConfigProperties config) { + super(config); + } + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution) throws IOException { + if (request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION) != null) { + return execution.execute(request, body); + } + Optional token = resolveToken(request.getURI()); + if (token.isEmpty() || shouldSkipPropagation(request.getURI())) { + return execution.execute(request, body); + } + request.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + token.get()); + return execution.execute(request, body); + } +} diff --git a/api-registry-common/src/main/java/org/eclipse/ecsp/security/ScopeOverrideProperties.java b/api-registry-common/src/main/java/org/eclipse/ecsp/security/ScopeOverrideProperties.java new file mode 100644 index 00000000..3f079baa --- /dev/null +++ b/api-registry-common/src/main/java/org/eclipse/ecsp/security/ScopeOverrideProperties.java @@ -0,0 +1,131 @@ +/******************************************************************************** + * Copyright (c) 2023-24 Harman International + * + *

Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *

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. + * + *

SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.ecsp.security; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import java.util.List; +import java.util.Map; + +/** + * Configuration properties for scope-override behaviour. + * + *

Binds to the {@code scopes} prefix and centralises the two scope-related + * settings that were previously duplicated across {@code ScopeTagger}, + * {@code ApiRoutesLoader}, {@code ScopeValidator}, and + * {@code TokenValidationInterceptor}: + * + *

    + *
  • {@code scopes.override.enabled} — whether the override-scope feature is active. + *
  • {@code scopes.scopes-map} — per-route override scope lists, keyed by route ID. + *
+ * + *

Registered as a {@code @Component} so it is available in any application context + * regardless of which optional features are enabled. + */ +@Component +@ConfigurationProperties(prefix = "scopes") +public class ScopeOverrideProperties { + + /** + * Default constructor. + */ + public ScopeOverrideProperties() { + // Default constructor + } + + private Override override = new Override(); + + /** + * Per-route override scope lists. + * + *

Keys are route IDs in the form {@code -}. + * Values are the replacement scope lists to apply when override is enabled. + */ + private Map> scopesMap; + + /** + * Returns the override sub-properties. + * + * @return the override configuration + */ + public Override getOverride() { + return override; + } + + /** + * Sets the override sub-properties. + * + * @param override the override configuration + */ + public void setOverride(Override override) { + this.override = override; + } + + /** + * Returns the per-route override scope map. + * + * @return map of route ID to override scope list, or {@code null} if not configured + */ + public Map> getScopesMap() { + return scopesMap; + } + + /** + * Sets the per-route override scope map. + * + * @param scopesMap map of route ID to override scope list + */ + public void setScopesMap(Map> scopesMap) { + this.scopesMap = scopesMap; + } + + /** + * Nested properties for the {@code scopes.override} sub-key. + */ + public static class Override { + + /** + * Default constructor. + */ + public Override() { + // Default constructor + } + + private boolean enabled = false; + + /** + * Returns whether the override-scope feature is enabled. + * + * @return {@code true} if override is active + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Sets whether the override-scope feature is enabled. + * + * @param enabled {@code true} to activate override + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + } +} diff --git a/api-registry-common/src/main/java/org/eclipse/ecsp/security/ScopeTagger.java b/api-registry-common/src/main/java/org/eclipse/ecsp/security/ScopeTagger.java index d0aac73a..c55453f6 100644 --- a/api-registry-common/src/main/java/org/eclipse/ecsp/security/ScopeTagger.java +++ b/api-registry-common/src/main/java/org/eclipse/ecsp/security/ScopeTagger.java @@ -20,13 +20,9 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.models.Operation; -import lombok.Getter; -import lombok.Setter; import org.eclipse.ecsp.utils.logger.IgniteLogger; import org.eclipse.ecsp.utils.logger.IgniteLoggerFactory; import org.springdoc.core.customizers.OperationCustomizer; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import java.util.Arrays; @@ -37,30 +33,20 @@ * ScopeTagger to load the scopes dynamically. */ @Component -@ConfigurationProperties(prefix = "scopes") -@Getter -@Setter public class ScopeTagger implements OperationCustomizer { - /** - * Default constructor. - */ - public ScopeTagger() { - // Default constructor - } - private static final IgniteLogger LOGGER = IgniteLoggerFactory.getLogger(ScopeTagger.class); - /** - * Property holds scopesMap. - * {@link Map} of routeId and list of scopes. - */ - private Map> scopesMap; + + private final ScopeOverrideProperties scopeOverrideProperties; /** - * Property holds overrideScopeEnabled. + * Constructor for ScopeTagger. + * + * @param scopeOverrideProperties the scope-override configuration properties */ - @Value("${scopes.override.enabled:false}") - private boolean isOverrideScopeEnabled; + public ScopeTagger(ScopeOverrideProperties scopeOverrideProperties) { + this.scopeOverrideProperties = scopeOverrideProperties; + } @Override public Operation customize(final Operation operation, HandlerMethod handlerMethod) { @@ -93,9 +79,10 @@ public Operation customize(final Operation operation, HandlerMethod handlerMetho + Arrays.toString(annotation.scopes()) + "

"); // override scope config - if (isOverrideScopeEnabled && scopesMap != null + Map> scopesMap = scopeOverrideProperties.getScopesMap(); + if (scopeOverrideProperties.getOverride().isEnabled() && scopesMap != null && (scopesMap.get(routeId) != null || scopesMap.get(routeId.toLowerCase()) != null)) { - updateNewScopes(operation, routeId); + updateNewScopes(operation, routeId, scopesMap); } } else { operation.description(operation.getDescription() + "

SCOPE: EMPTY

"); @@ -103,7 +90,7 @@ public Operation customize(final Operation operation, HandlerMethod handlerMetho return operation; } - private void updateNewScopes(final Operation operation, String routeId) { + private void updateNewScopes(final Operation operation, String routeId, Map> scopesMap) { List scopesList = scopesMap.get(routeId) != null ? scopesMap.get(routeId) : scopesMap.get(routeId.toLowerCase()); LOGGER.debug("Override Scopes Map Config: " + scopesMap); diff --git a/api-registry-common/src/main/java/org/eclipse/ecsp/security/ScopeValidator.java b/api-registry-common/src/main/java/org/eclipse/ecsp/security/ScopeValidator.java index 756bdd52..372a4089 100644 --- a/api-registry-common/src/main/java/org/eclipse/ecsp/security/ScopeValidator.java +++ b/api-registry-common/src/main/java/org/eclipse/ecsp/security/ScopeValidator.java @@ -25,7 +25,6 @@ import org.aspectj.lang.reflect.MethodSignature; import org.eclipse.ecsp.utils.logger.IgniteLogger; import org.eclipse.ecsp.utils.logger.IgniteLoggerFactory; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import java.lang.reflect.Method; @@ -40,18 +39,19 @@ @Component @ConditionalOnProperty(value = "api.security.enabled", havingValue = "true", matchIfMissing = false) public class ScopeValidator { + + private static final IgniteLogger LOGGER = IgniteLoggerFactory.getLogger(ScopeValidator.class); + private final ScopeOverrideProperties scopeOverrideProperties; + /** - * Default constructor. + * Constructor for ScopeValidator. + * + * @param scopeOverrideProperties the scope-override configuration properties */ - public ScopeValidator() { - // Default constructor + public ScopeValidator(ScopeOverrideProperties scopeOverrideProperties) { + this.scopeOverrideProperties = scopeOverrideProperties; } - - private static final IgniteLogger LOGGER = IgniteLoggerFactory.getLogger(ScopeValidator.class); - @Value("${scopes.override.enabled:false}") - private boolean isOverrideScopeEnabled; - /** * validate the scopes. * @@ -75,7 +75,7 @@ public void validate(JoinPoint joinPoint) { LOGGER.debug("Method Scope Set: {}", methodScopes); LOGGER.info("Override Scope Set: {}", HeaderContext.getUserDetails().getOverrideScopes()); - if (isOverrideScopeEnabled) { + if (scopeOverrideProperties.getOverride().isEnabled()) { validateScope(HeaderContext.getUserDetails().getOverrideScopes(), methodScopes); } else { validateScope(HeaderContext.getUserDetails().getScope(), methodScopes); diff --git a/api-registry-common/src/main/java/org/eclipse/ecsp/security/SecurityContext.java b/api-registry-common/src/main/java/org/eclipse/ecsp/security/SecurityContext.java new file mode 100644 index 00000000..a2309ee5 --- /dev/null +++ b/api-registry-common/src/main/java/org/eclipse/ecsp/security/SecurityContext.java @@ -0,0 +1,260 @@ +/******************************************************************************** + * Copyright (c) 2023-24 Harman International + * + *

Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *

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. + * + *

SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.ecsp.security; + +import org.eclipse.ecsp.tokenvalidator.model.TokenClaim; +import org.eclipse.ecsp.utils.logger.IgniteLogger; +import org.eclipse.ecsp.utils.logger.IgniteLoggerFactory; +import java.time.Instant; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * Thread-local holder for JWT-validated security data. + * + *

Populated exclusively by {@code TokenValidationInterceptor} after successful JWT + * validation. Completely separate from {@link HeaderContext}, which holds raw HTTP + * header values that may or may not be JWT-validated. + * + *

All storage is per-thread. {@link #clear()} must be called in + * {@code afterCompletion} to prevent ThreadLocal memory leaks in servlet-container + * thread pools. + */ +public abstract class SecurityContext { + + private static final IgniteLogger LOGGER = IgniteLoggerFactory.getLogger(SecurityContext.class); + + private static final ThreadLocal SECURITY_CONTEXT = new ThreadLocal<>(); + + private static final String EXP_CLAIM = "exp"; + private static final String SUB_CLAIM = "sub"; + private static final String SCOPE_CLAIM = "scope"; + private static final long MILLIS_PER_SECOND = 1000L; + + /** + * Private constructor — utility class must not be instantiated. + */ + private SecurityContext() { + // Utility class + } + + /** + * Populates the current thread's security context from a validated JWT. + * + *

Parses the {@code exp}, {@code sub}, and {@code scope} claims from + * {@code claims} and stores the result alongside the raw token string. + * + * @param rawToken the Bearer token string (without the "Bearer " prefix) + * @param claims the verified claims returned by the token validator + */ + public static void set(String rawToken, List claims) { + Instant expiry = parseExpiry(claims); + String userId = extractClaim(claims, SUB_CLAIM); + Set scopes = parseScopes(claims); + SECURITY_CONTEXT.set(new SecurityDetails(rawToken, claims, expiry, userId, scopes)); + } + + /** + * Returns the raw Bearer token for the current thread, if present. + * + * @return an {@link Optional} containing the raw token, or empty if no context is set + */ + public static Optional getToken() { + SecurityDetails details = SECURITY_CONTEXT.get(); + return details == null ? Optional.empty() : Optional.ofNullable(details.rawToken()); + } + + /** + * Returns all verified JWT claims for the current thread. + * + * @return the claim list, or an empty list if no context is set + */ + public static List getClaims() { + SecurityDetails details = SECURITY_CONTEXT.get(); + return details == null ? Collections.emptyList() : details.claims(); + } + + /** + * Returns the user-id (subject) from the validated JWT for the current thread. + * + * @return an {@link Optional} containing the user-id, or empty if not present + */ + public static Optional getUserId() { + SecurityDetails details = SECURITY_CONTEXT.get(); + return details == null ? Optional.empty() : Optional.ofNullable(details.userId()); + } + + /** + * Returns the raw scope strings from the validated JWT for the current thread. + * + * @return the scope set, or an empty set if no context is set + */ + public static Set getScopes() { + SecurityDetails details = SECURITY_CONTEXT.get(); + return details == null ? Collections.emptySet() : details.scopes(); + } + + /** + * Returns whether the JWT stored in the current thread's context is expired. + * + *

Returns {@code true} if no context is set (treat absent token as expired). + * + * @return {@code true} if the token is expired or absent + */ + public static boolean isTokenExpired() { + SecurityDetails details = SECURITY_CONTEXT.get(); + if (details == null || details.expiry() == null) { + return true; + } + return Instant.now().isAfter(details.expiry()); + } + + /** + * Clears the security context for the current thread. + * + *

Must be called in {@code afterCompletion} of the interceptor to prevent + * ThreadLocal memory leaks in servlet-container thread pools. + */ + public static void clear() { + SECURITY_CONTEXT.remove(); + } + + private static Instant parseExpiry(List claims) { + for (TokenClaim claim : claims) { + if (EXP_CLAIM.equals(claim.getName())) { + Object value = claim.getValue(); + if (value instanceof Number num) { + return Instant.ofEpochMilli(num.longValue() * MILLIS_PER_SECOND); + } + LOGGER.warn("Unexpected type for 'exp' claim: {}", value == null ? "null" : value.getClass()); + return null; + } + } + return null; + } + + private static String extractClaim(List claims, String claimName) { + for (TokenClaim claim : claims) { + if (claimName.equals(claim.getName()) && claim.getValue() != null) { + return claim.getValue().toString(); + } + } + return null; + } + + @SuppressWarnings("unchecked") + private static Set parseScopes(List claims) { + for (TokenClaim claim : claims) { + if (SCOPE_CLAIM.equals(claim.getName()) && claim.getValue() != null) { + Object value = claim.getValue(); + if (value instanceof List) { + return new HashSet<>((List) value); + } + // space-separated string + String[] parts = value.toString().split(" "); + Set result = new HashSet<>(); + for (String part : parts) { + if (!part.isEmpty()) { + result.add(part); + } + } + return result; + } + } + return Collections.emptySet(); + } + + /** + * Immutable holder for JWT-validated data associated with a single request thread. + */ + public static class SecurityDetails { + + private final String rawToken; + private final List claims; + private final Instant expiry; + private final String userId; + private final Set scopes; + + /** + * Constructs a {@code SecurityDetails} instance. + * + * @param rawToken the original Bearer token string + * @param claims the verified JWT claims + * @param expiry the token expiry instant parsed from the {@code exp} claim + * @param userId the subject ({@code sub}) claim value + * @param scopes the raw scope strings from the JWT + */ + public SecurityDetails(String rawToken, List claims, + Instant expiry, String userId, Set scopes) { + this.rawToken = rawToken; + this.claims = claims; + this.expiry = expiry; + this.userId = userId; + this.scopes = scopes; + } + + /** + * Returns the raw Bearer token string. + * + * @return the raw token + */ + public String rawToken() { + return rawToken; + } + + /** + * Returns the verified JWT claims. + * + * @return list of claims + */ + public List claims() { + return claims; + } + + /** + * Returns the token expiry instant. + * + * @return the expiry instant, or {@code null} if the {@code exp} claim was absent + */ + public Instant expiry() { + return expiry; + } + + /** + * Returns the user-id (subject) from the JWT. + * + * @return the userId, or {@code null} if not present + */ + public String userId() { + return userId; + } + + /** + * Returns the raw scope strings from the JWT. + * + * @return the scope set + */ + public Set scopes() { + return scopes; + } + } +} diff --git a/api-registry-common/src/main/java/org/eclipse/ecsp/security/ValidationConfigProperties.java b/api-registry-common/src/main/java/org/eclipse/ecsp/security/ValidationConfigProperties.java new file mode 100644 index 00000000..35be20c0 --- /dev/null +++ b/api-registry-common/src/main/java/org/eclipse/ecsp/security/ValidationConfigProperties.java @@ -0,0 +1,341 @@ +/******************************************************************************** + * Copyright (c) 2023-24 Harman International + * + *

Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *

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. + * + *

SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.ecsp.security; + +import org.eclipse.ecsp.utils.RegistryCommonConstants; +import org.springframework.boot.context.properties.ConfigurationProperties; +import java.util.ArrayList; +import java.util.List; + +/** + * Configuration properties for token validation and token propagation. + * + *

Bound to the {@code api.registry} prefix. Controls whether JWT validation + * is active and how tokens are forwarded to downstream services. + */ +@ConfigurationProperties(prefix = RegistryCommonConstants.API_REGISTRY_PREFIX) +public class ValidationConfigProperties { + + private Security security = new Security(); + private TokenPropagation tokenPropagation = new TokenPropagation(); + + /** + * Default constructor. + */ + public ValidationConfigProperties() { + // Default constructor + } + + /** + * Returns the security sub-properties. + * + * @return the security configuration + */ + public Security getSecurity() { + return security; + } + + /** + * Sets the security sub-properties. + * + * @param security the security configuration + */ + public void setSecurity(Security security) { + this.security = security; + } + + /** + * Returns the token-propagation sub-properties. + * + * @return the token-propagation configuration + */ + public TokenPropagation getTokenPropagation() { + return tokenPropagation; + } + + /** + * Sets the token-propagation sub-properties. + * + * @param tokenPropagation the token-propagation configuration + */ + public void setTokenPropagation(TokenPropagation tokenPropagation) { + this.tokenPropagation = tokenPropagation; + } + + /** + * Security sub-properties controlling whether JWT validation is enabled. + */ + public static class Security { + + private boolean enabled = false; + + /** + * Default constructor. + */ + public Security() { + // Default constructor + } + + /** + * Returns whether JWT token validation is enabled. + * + * @return {@code true} if validation is active + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Sets whether JWT token validation is enabled. + * + * @param enabled {@code true} to activate validation + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + } + + /** + * Token-propagation sub-properties controlling how Bearer tokens are forwarded + * to downstream services via RestTemplate, WebClient, and RestClient. + */ + public static class TokenPropagation { + + private RestTemplate restTemplate = new RestTemplate(); + private WebClient webClient = new WebClient(); + private RestClient restClient = new RestClient(); + private List includeHosts = new ArrayList<>(); + private List excludeHosts = new ArrayList<>(); + private boolean allowExternalHosts = false; + + /** + * Default constructor. + */ + public TokenPropagation() { + // Default constructor + } + + /** + * Returns the RestTemplate propagation settings. + * + * @return the RestTemplate propagation settings + */ + public RestTemplate getRestTemplate() { + return restTemplate; + } + + /** + * Sets the RestTemplate propagation settings. + * + * @param restTemplate the settings to apply + */ + public void setRestTemplate(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + /** + * Returns the WebClient propagation settings. + * + * @return the WebClient propagation settings + */ + public WebClient getWebClient() { + return webClient; + } + + /** + * Sets the WebClient propagation settings. + * + * @param webClient the settings to apply + */ + public void setWebClient(WebClient webClient) { + this.webClient = webClient; + } + + /** + * Returns the RestClient propagation settings. + * + * @return the RestClient propagation settings + */ + public RestClient getRestClient() { + return restClient; + } + + /** + * Sets the RestClient propagation settings. + * + * @param restClient the settings to apply + */ + public void setRestClient(RestClient restClient) { + this.restClient = restClient; + } + + /** + * Returns the hosts that should always receive the propagated token. + * + * @return list of included host patterns + */ + public List getIncludeHosts() { + return includeHosts; + } + + /** + * Sets the hosts that should always receive the propagated token. + * + * @param includeHosts list of included host patterns + */ + public void setIncludeHosts(List includeHosts) { + this.includeHosts = includeHosts; + } + + /** + * Returns the hosts that must never receive the propagated token. + * + * @return list of excluded host patterns + */ + public List getExcludeHosts() { + return excludeHosts; + } + + /** + * Sets the hosts that must never receive the propagated token. + * + * @param excludeHosts list of excluded host patterns + */ + public void setExcludeHosts(List excludeHosts) { + this.excludeHosts = excludeHosts; + } + + /** + * Returns whether tokens may be forwarded to external (non-internal) hosts. + * + * @return {@code true} if external hosts are allowed + */ + public boolean isAllowExternalHosts() { + return allowExternalHosts; + } + + /** + * Sets whether tokens may be forwarded to external hosts. + * + * @param allowExternalHosts {@code true} to permit external forwarding + */ + public void setAllowExternalHosts(boolean allowExternalHosts) { + this.allowExternalHosts = allowExternalHosts; + } + + /** + * RestTemplate-specific propagation toggle. + */ + public static class RestTemplate { + + private boolean enabled = true; + + /** + * Default constructor. + */ + public RestTemplate() { + // Default constructor + } + + /** + * Returns whether RestTemplate token propagation is enabled. + * + * @return {@code true} if enabled + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Sets whether RestTemplate token propagation is enabled. + * + * @param enabled {@code true} to enable + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + } + + /** + * WebClient-specific propagation toggle. + */ + public static class WebClient { + + private boolean enabled = true; + + /** + * Default constructor. + */ + public WebClient() { + // Default constructor + } + + /** + * Returns whether WebClient token propagation is enabled. + * + * @return {@code true} if enabled + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Sets whether WebClient token propagation is enabled. + * + * @param enabled {@code true} to enable + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + } + + /** + * RestClient-specific propagation toggle. + */ + public static class RestClient { + + private boolean enabled = true; + + /** + * Default constructor. + */ + public RestClient() { + // Default constructor + } + + /** + * Returns whether RestClient token propagation is enabled. + * + * @return {@code true} if enabled + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Sets whether RestClient token propagation is enabled. + * + * @param enabled {@code true} to enable + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + } + } +} diff --git a/api-registry-common/src/main/java/org/eclipse/ecsp/utils/RegistryCommonConstants.java b/api-registry-common/src/main/java/org/eclipse/ecsp/utils/RegistryCommonConstants.java index 1718c77b..2b35131d 100644 --- a/api-registry-common/src/main/java/org/eclipse/ecsp/utils/RegistryCommonConstants.java +++ b/api-registry-common/src/main/java/org/eclipse/ecsp/utils/RegistryCommonConstants.java @@ -148,4 +148,44 @@ private RegistryCommonConstants() { * Replacement regular expression. */ public static final String REPLACEMENT_REGEX = "/${segment}"; + + /** + * Base configuration prefix for all {@code api.registry} properties. + */ + public static final String API_REGISTRY_PREFIX = "api.registry"; + + /** + * Configuration prefix for {@code api.registry.security} properties. + * Used as the {@code prefix} in {@code @ConditionalOnProperty} and + * {@code @ConfigurationProperties} annotations that control JWT security. + */ + public static final String API_REGISTRY_SECURITY_PREFIX = API_REGISTRY_PREFIX + ".security"; + + /** + * Full property key for enabling/disabling JWT security + * ({@code api.registry.security.enabled}). + * Used in programmatic property lookups such as {@link org.springframework.boot.env.EnvironmentPostProcessor}. + */ + public static final String API_REGISTRY_SECURITY_ENABLED = API_REGISTRY_SECURITY_PREFIX + ".enabled"; + + /** + * Configuration prefix for RestTemplate token-propagation properties + * ({@code api.registry.token-propagation.rest-template}). + */ + public static final String API_REGISTRY_REST_TEMPLATE_PROPAGATION_PREFIX = + API_REGISTRY_PREFIX + ".token-propagation.rest-template"; + + /** + * Configuration prefix for RestClient token-propagation properties + * ({@code api.registry.token-propagation.rest-client}). + */ + public static final String API_REGISTRY_REST_CLIENT_PROPAGATION_PREFIX = + API_REGISTRY_PREFIX + ".token-propagation.rest-client"; + + /** + * Configuration prefix for WebClient token-propagation properties + * ({@code api.registry.token-propagation.web-client}). + */ + public static final String API_REGISTRY_WEB_CLIENT_PROPAGATION_PREFIX = + API_REGISTRY_PREFIX + ".token-propagation.web-client"; } diff --git a/api-registry-common/src/main/java/org/eclipse/ecsp/webclient/WebClientConfig.java b/api-registry-common/src/main/java/org/eclipse/ecsp/webclient/WebClientConfig.java new file mode 100644 index 00000000..8b80228b --- /dev/null +++ b/api-registry-common/src/main/java/org/eclipse/ecsp/webclient/WebClientConfig.java @@ -0,0 +1,65 @@ +/******************************************************************************** + * Copyright (c) 2023-24 Harman International + * + *

Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *

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. + * + *

SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.ecsp.webclient; + +import org.eclipse.ecsp.security.ValidationConfigProperties; +import org.eclipse.ecsp.utils.RegistryCommonConstants; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * Auto-configuration for WebClient bean with token propagation support. + * + *

Creates a {@link WebClient.Builder} bean pre-configured with + * {@link WebClientTokenFilter} when {@code spring-webflux} is on the classpath and + * {@code api.registry.token-propagation.web-client.enabled=true}. + */ +@Configuration +@ConditionalOnClass(WebClient.class) +@EnableConfigurationProperties(ValidationConfigProperties.class) +public class WebClientConfig { + + /** + * Default constructor. + */ + public WebClientConfig() { + // Default constructor + } + + /** + * Creates a {@link WebClientTokenFilter} bean for token propagation. + * + * @param config the validation / propagation configuration properties + * @return the filter + */ + @Bean + @ConditionalOnProperty( + prefix = RegistryCommonConstants.API_REGISTRY_WEB_CLIENT_PROPAGATION_PREFIX, + name = "enabled", + havingValue = "true", + matchIfMissing = true + ) + public WebClientTokenFilter webClientTokenFilter(ValidationConfigProperties config) { + return new WebClientTokenFilter(config); + } +} diff --git a/api-registry-common/src/main/java/org/eclipse/ecsp/webclient/WebClientTokenFilter.java b/api-registry-common/src/main/java/org/eclipse/ecsp/webclient/WebClientTokenFilter.java new file mode 100644 index 00000000..96b81617 --- /dev/null +++ b/api-registry-common/src/main/java/org/eclipse/ecsp/webclient/WebClientTokenFilter.java @@ -0,0 +1,115 @@ +/******************************************************************************** + * Copyright (c) 2023-24 Harman International + * + *

Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *

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. + * + *

SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.ecsp.webclient; + +import org.eclipse.ecsp.security.SecurityContext; +import org.eclipse.ecsp.security.ValidationConfigProperties; +import org.eclipse.ecsp.utils.logger.IgniteLogger; +import org.eclipse.ecsp.utils.logger.IgniteLoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeFunction; +import reactor.core.publisher.Mono; +import java.net.URI; +import java.util.List; +import java.util.Optional; + +/** + * Reactive {@link ExchangeFilterFunction} that propagates the current thread's Bearer token + * to outbound WebClient calls. + * + *

Thread-safety: The token is captured synchronously from the calling + * thread before any reactive operator is applied. This ensures the ThreadLocal value is + * read on the correct (servlet) thread rather than a scheduler thread. + */ +public class WebClientTokenFilter implements ExchangeFilterFunction { + + private static final IgniteLogger LOGGER = IgniteLoggerFactory.getLogger(WebClientTokenFilter.class); + + private final ValidationConfigProperties config; + + /** + * Constructs the filter with the given configuration. + * + * @param config the validation / propagation configuration properties + */ + public WebClientTokenFilter(ValidationConfigProperties config) { + this.config = config; + } + + @Override + public Mono filter(ClientRequest request, ExchangeFunction next) { + // SYNC — token is captured on the calling (servlet) thread BEFORE any reactive operator. + Optional token = resolveToken(request.url()); + + if (token.isEmpty()) { + return next.exchange(request); + } + if (request.headers().getFirst(HttpHeaders.AUTHORIZATION) != null) { + return next.exchange(request); + } + ClientRequest outbound = ClientRequest.from(request) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token.get()) + .build(); + return next.exchange(outbound); + } + + private Optional resolveToken(URI targetUri) { + if (shouldSkipPropagation(targetUri)) { + return Optional.empty(); + } + Optional token = SecurityContext.getToken(); + if (token.isEmpty()) { + return Optional.empty(); + } + if (SecurityContext.isTokenExpired()) { + LOGGER.warn("Token is expired; skipping propagation to {}", targetUri); + return Optional.empty(); + } + return token; + } + + private boolean shouldSkipPropagation(URI targetUri) { + String host = targetUri.getHost(); + if (host == null) { + return false; + } + List excludeHosts = config.getTokenPropagation().getExcludeHosts(); + for (String excluded : excludeHosts) { + if (host.equalsIgnoreCase(excluded)) { + return true; + } + } + if (config.getTokenPropagation().isAllowExternalHosts()) { + return false; + } + List includeHosts = config.getTokenPropagation().getIncludeHosts(); + if (!includeHosts.isEmpty()) { + for (String included : includeHosts) { + if (host.equalsIgnoreCase(included)) { + return false; + } + } + return true; + } + return false; + } +} diff --git a/api-registry-common/src/main/resources/application.properties b/api-registry-common/src/main/resources/application.properties new file mode 100644 index 00000000..cdef377d --- /dev/null +++ b/api-registry-common/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.autoconfigure.exclude=org.eclipse.ecsp.tokenvalidator.config.TokenValidatorAutoConfiguration \ No newline at end of file diff --git a/api-registry-common/src/test/java/org/eclipse/ecsp/customizers/CustomGatewayFilterCustomizerIntegrationTest.java b/api-registry-common/src/test/java/org/eclipse/ecsp/customizers/CustomGatewayFilterCustomizerIntegrationTest.java index e25f8483..fe1858ef 100644 --- a/api-registry-common/src/test/java/org/eclipse/ecsp/customizers/CustomGatewayFilterCustomizerIntegrationTest.java +++ b/api-registry-common/src/test/java/org/eclipse/ecsp/customizers/CustomGatewayFilterCustomizerIntegrationTest.java @@ -41,6 +41,7 @@ @ActiveProfiles("test") @TestPropertySource(properties = { "api.registry.enabled=true", + "api.registry.security.enabled=false", "api.registry.service_name=http://registry", "api.registry.custom-gateway-filter.enabled=true", "openapi.path.include=/v1/test/**", diff --git a/api-registry-common/src/test/java/org/eclipse/ecsp/register/ApiRoutesLoaderTest.java b/api-registry-common/src/test/java/org/eclipse/ecsp/register/ApiRoutesLoaderTest.java index 8c120c7a..a0ac0ee6 100644 --- a/api-registry-common/src/test/java/org/eclipse/ecsp/register/ApiRoutesLoaderTest.java +++ b/api-registry-common/src/test/java/org/eclipse/ecsp/register/ApiRoutesLoaderTest.java @@ -32,6 +32,7 @@ import io.swagger.v3.oas.models.parameters.RequestBody; import io.swagger.v3.oas.models.security.SecurityRequirement; import org.eclipse.ecsp.register.model.RouteDefinition; +import org.eclipse.ecsp.security.ScopeOverrideProperties; import org.eclipse.ecsp.utils.RegistryCommonConstants; import org.eclipse.ecsp.utils.RegistryCommonTestUtil; import org.junit.jupiter.api.Assertions; @@ -79,6 +80,7 @@ class ApiRoutesLoaderTest { SpringDocCustomizers springDocCustomizers = Mockito.mock(SpringDocCustomizers.class); ObjectFactory openApiServiceObjectFactory = () -> openApiService; + ScopeOverrideProperties scopeOverrideProperties = new ScopeOverrideProperties(); @InjectMocks private ApiRoutesLoader apiRoutesLoader; @@ -108,9 +110,10 @@ void before() { Mockito.when(springDocProviders.jsonMapper()).thenReturn(new ObjectMapper()); ApiRoutesConfig apiRouteConfig = new ApiRoutesConfig(); apiRouteConfig.setRoutes(List.of()); + scopeOverrideProperties = new ScopeOverrideProperties(); apiRoutesLoader = new ApiRoutesLoader(List.of(groupedOpenApi), openApiServiceObjectFactory, abstractRequestService, genericResponseService, operationService, springDocConfigProperties, - springDocProviders, springDocCustomizers, apiRouteConfig); + springDocProviders, springDocCustomizers, apiRouteConfig, scopeOverrideProperties); } @Test @@ -122,8 +125,8 @@ void testGetRoutes() throws Exception { Map> scopeMap = new HashMap<>(); apiRoutesLoader.getApiRoutes(); scopeMap.put("test-controller-create", List.of("SelfManage", "IgniteSystem")); - apiRoutesLoader.setScopesMap(scopeMap); - Assertions.assertEquals(apiRoutesLoader.getScopesMap(), scopeMap); + scopeOverrideProperties.setScopesMap(scopeMap); + Assertions.assertEquals(scopeOverrideProperties.getScopesMap(), scopeMap); apiRoutesLoader.getApiRoutes(); } @@ -233,8 +236,8 @@ void testExtension() { SecurityRequirement securityRequirement = new SecurityRequirement(); securityRequirement.put("filterName", List.of("ABCScope", "ABDScope")); operation.setSecurity(List.of(securityRequirement)); - apiRoutesLoader.setScopesMap(Map.of("GET-route123", List.of("ABCScope"))); - ReflectionTestUtils.setField(apiRoutesLoader, "isOverrideScopeEnabled", true); + scopeOverrideProperties.setScopesMap(Map.of("GET-route123", List.of("ABCScope"))); + scopeOverrideProperties.getOverride().setEnabled(true); ReflectionTestUtils.invokeMethod(apiRoutesLoader, "setOperation", HttpMethod.GET, "/v2/users", operation); @SuppressWarnings("unchecked") List apiRotes = (List) @@ -258,8 +261,8 @@ void testHeaderMetadata() { SecurityRequirement securityRequirement = new SecurityRequirement(); securityRequirement.put("filterName", List.of("ABCScope", "ABDScope")); operation.setSecurity(List.of(securityRequirement)); - apiRoutesLoader.setScopesMap(Map.of("GET-testHeaderMetadata", List.of("ABCScope"))); - ReflectionTestUtils.setField(apiRoutesLoader, "isOverrideScopeEnabled", true); + scopeOverrideProperties.setScopesMap(Map.of("GET-testHeaderMetadata", List.of("ABCScope"))); + scopeOverrideProperties.getOverride().setEnabled(true); ReflectionTestUtils.invokeMethod(apiRoutesLoader, "setOperation", HttpMethod.GET, "/v2/users", operation); @SuppressWarnings("unchecked") List apiRotes = (List) @@ -289,8 +292,8 @@ void testHeaderOptionalMetadata() { SecurityRequirement securityRequirement = new SecurityRequirement(); securityRequirement.put("filterName", List.of("ABCScope", "ABDScope")); operation.setSecurity(List.of(securityRequirement)); - apiRoutesLoader.setScopesMap(Map.of("GET-testHeaderOptionalMetadata", List.of("ABCScope"))); - ReflectionTestUtils.setField(apiRoutesLoader, "isOverrideScopeEnabled", true); + scopeOverrideProperties.setScopesMap(Map.of("GET-testHeaderOptionalMetadata", List.of("ABCScope"))); + scopeOverrideProperties.getOverride().setEnabled(true); ReflectionTestUtils.invokeMethod(apiRoutesLoader, "setOperation", HttpMethod.GET, "/v2/users", operation); @SuppressWarnings("unchecked") List apiRotes = (List) @@ -314,8 +317,8 @@ void testEmptyHeaders() { SecurityRequirement securityRequirement = new SecurityRequirement(); securityRequirement.put("filterName", List.of("ABCScope", "ABDScope")); operation.setSecurity(List.of(securityRequirement)); - apiRoutesLoader.setScopesMap(Map.of("GET-testEmptyHeaders", List.of("ABCScope"))); - ReflectionTestUtils.setField(apiRoutesLoader, "isOverrideScopeEnabled", true); + scopeOverrideProperties.setScopesMap(Map.of("GET-testEmptyHeaders", List.of("ABCScope"))); + scopeOverrideProperties.getOverride().setEnabled(true); ReflectionTestUtils.invokeMethod(apiRoutesLoader, "setOperation", HttpMethod.GET, "/v2/users", operation); @SuppressWarnings("unchecked") List apiRotes = (List) diff --git a/api-registry-common/src/test/java/org/eclipse/ecsp/restclient/RestClientTokenInterceptorTest.java b/api-registry-common/src/test/java/org/eclipse/ecsp/restclient/RestClientTokenInterceptorTest.java new file mode 100644 index 00000000..ae27f86e --- /dev/null +++ b/api-registry-common/src/test/java/org/eclipse/ecsp/restclient/RestClientTokenInterceptorTest.java @@ -0,0 +1,160 @@ +/******************************************************************************** + * Copyright (c) 2023-24 Harman International + * + *

Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *

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. + * + *

SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.ecsp.restclient; + +import org.eclipse.ecsp.security.SecurityContext; +import org.eclipse.ecsp.security.ValidationConfigProperties; +import org.eclipse.ecsp.tokenvalidator.model.TokenClaim; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpResponse; +import java.net.URI; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Tests for {@link RestClientTokenInterceptor}. + */ +@ExtendWith(MockitoExtension.class) +class RestClientTokenInterceptorTest { + + @Mock + private HttpRequest httpRequest; + + @Mock + private ClientHttpRequestExecution execution; + + @Mock + private ClientHttpResponse httpResponse; + + private HttpHeaders headers; + private ValidationConfigProperties config; + private RestClientTokenInterceptor underTest; + + @BeforeEach + void beforeEach() { + config = new ValidationConfigProperties(); + underTest = new RestClientTokenInterceptor(config); + headers = new HttpHeaders(); + Mockito.when(httpRequest.getHeaders()).thenReturn(headers); + } + + @AfterEach + void tearDown() { + SecurityContext.clear(); + } + + @Test + void shouldAddAuthHeaderWhenTokenPresent() throws Exception { + setValidToken("valid-token"); + Mockito.when(httpRequest.getURI()).thenReturn(URI.create("http://internal-svc/api")); + Mockito.when(execution.execute(Mockito.any(), Mockito.any())).thenReturn(httpResponse); + + ClientHttpResponse result = underTest.intercept(httpRequest, new byte[0], execution); + + Assertions.assertNotNull(result); + Assertions.assertEquals("Bearer valid-token", headers.getFirst(HttpHeaders.AUTHORIZATION)); + } + + @Test + void shouldSkipWhenAuthHeaderAlreadyPresent() throws Exception { + setValidToken("valid-token"); + headers.add(HttpHeaders.AUTHORIZATION, "Bearer existing-token"); + Mockito.when(execution.execute(Mockito.any(), Mockito.any())).thenReturn(httpResponse); + + underTest.intercept(httpRequest, new byte[0], execution); + + Assertions.assertEquals("Bearer existing-token", headers.getFirst(HttpHeaders.AUTHORIZATION)); + } + + @Test + void shouldSkipWhenTokenExpired() throws Exception { + setExpiredToken("expired-token"); + Mockito.when(httpRequest.getURI()).thenReturn(URI.create("http://internal-svc/api")); + Mockito.when(execution.execute(Mockito.any(), Mockito.any())).thenReturn(httpResponse); + + underTest.intercept(httpRequest, new byte[0], execution); + + Assertions.assertNull(headers.getFirst(HttpHeaders.AUTHORIZATION)); + } + + @Test + void shouldSkipWhenHostIsExcluded() throws Exception { + setValidToken("valid-token"); + config.getTokenPropagation().setExcludeHosts(Collections.singletonList("external-api.com")); + Mockito.when(httpRequest.getURI()).thenReturn(URI.create("http://external-api.com/resource")); + Mockito.when(execution.execute(Mockito.any(), Mockito.any())).thenReturn(httpResponse); + + underTest.intercept(httpRequest, new byte[0], execution); + + Assertions.assertNull(headers.getFirst(HttpHeaders.AUTHORIZATION)); + } + + @Test + void shouldSkipWhenHostIsExternalAndExternalDisabled() throws Exception { + setValidToken("valid-token"); + config.getTokenPropagation().setAllowExternalHosts(false); + config.getTokenPropagation().setIncludeHosts(Collections.singletonList("internal.svc")); + Mockito.when(httpRequest.getURI()).thenReturn(URI.create("http://unknown-external.com/resource")); + Mockito.when(execution.execute(Mockito.any(), Mockito.any())).thenReturn(httpResponse); + + underTest.intercept(httpRequest, new byte[0], execution); + + Assertions.assertNull(headers.getFirst(HttpHeaders.AUTHORIZATION)); + } + + @Test + void shouldForwardWhenHostIsInIncludeList() throws Exception { + setValidToken("valid-token"); + config.getTokenPropagation().setAllowExternalHosts(false); + config.getTokenPropagation().setIncludeHosts(Collections.singletonList("internal.svc")); + Mockito.when(httpRequest.getURI()).thenReturn(URI.create("http://internal.svc/resource")); + Mockito.when(execution.execute(Mockito.any(), Mockito.any())).thenReturn(httpResponse); + + underTest.intercept(httpRequest, new byte[0], execution); + + Assertions.assertEquals("Bearer valid-token", headers.getFirst(HttpHeaders.AUTHORIZATION)); + } + + private void setValidToken(String token) { + long futureEpoch = Instant.now().plusSeconds(3600).getEpochSecond(); + List claims = Arrays.asList( + new TokenClaim("sub", "user-1"), + new TokenClaim("exp", futureEpoch) + ); + SecurityContext.set(token, claims); + } + + private void setExpiredToken(String token) { + long pastEpoch = Instant.now().minusSeconds(3600).getEpochSecond(); + List claims = Collections.singletonList(new TokenClaim("exp", pastEpoch)); + SecurityContext.set(token, claims); + } +} diff --git a/api-registry-common/src/test/java/org/eclipse/ecsp/restclient/RestTemplateConfigTest.java b/api-registry-common/src/test/java/org/eclipse/ecsp/restclient/RestTemplateConfigTest.java index 50046e82..84905c6b 100644 --- a/api-registry-common/src/test/java/org/eclipse/ecsp/restclient/RestTemplateConfigTest.java +++ b/api-registry-common/src/test/java/org/eclipse/ecsp/restclient/RestTemplateConfigTest.java @@ -18,11 +18,15 @@ package org.eclipse.ecsp.restclient; +import org.eclipse.ecsp.config.RestTemplateConfig; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.web.client.RestTemplate; /** @@ -31,6 +35,9 @@ @ExtendWith(MockitoExtension.class) class RestTemplateConfigTest { + @Mock + private ObjectProvider tokenInterceptorProvider; + private RestTemplateConfig restTemplateConfig; @BeforeEach @@ -40,7 +47,8 @@ void setUp() { @Test void restTemplateTest() { - RestTemplate restTemplate = restTemplateConfig.restTemplate(); + Mockito.when(tokenInterceptorProvider.getIfAvailable()).thenReturn(null); + RestTemplate restTemplate = restTemplateConfig.restTemplate(tokenInterceptorProvider); Assertions.assertNotNull(restTemplate); } diff --git a/api-registry-common/src/test/java/org/eclipse/ecsp/restclient/RestTemplateTokenInterceptorTest.java b/api-registry-common/src/test/java/org/eclipse/ecsp/restclient/RestTemplateTokenInterceptorTest.java new file mode 100644 index 00000000..29b477f8 --- /dev/null +++ b/api-registry-common/src/test/java/org/eclipse/ecsp/restclient/RestTemplateTokenInterceptorTest.java @@ -0,0 +1,161 @@ +/******************************************************************************** + * Copyright (c) 2023-24 Harman International + * + *

Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *

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. + * + *

SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.ecsp.restclient; + +import org.eclipse.ecsp.security.SecurityContext; +import org.eclipse.ecsp.security.ValidationConfigProperties; +import org.eclipse.ecsp.tokenvalidator.model.TokenClaim; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpResponse; +import java.net.URI; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Tests for {@link RestTemplateTokenInterceptor}. + */ +@ExtendWith(MockitoExtension.class) +class RestTemplateTokenInterceptorTest { + + @Mock + private HttpRequest httpRequest; + + @Mock + private ClientHttpRequestExecution execution; + + @Mock + private ClientHttpResponse httpResponse; + + private HttpHeaders headers; + private ValidationConfigProperties config; + private RestTemplateTokenInterceptor underTest; + + @BeforeEach + void beforeEach() { + config = new ValidationConfigProperties(); + underTest = new RestTemplateTokenInterceptor(config); + headers = new HttpHeaders(); + Mockito.when(httpRequest.getHeaders()).thenReturn(headers); + } + + @AfterEach + void tearDown() { + SecurityContext.clear(); + } + + @Test + void shouldAddAuthHeaderWhenTokenPresent() throws Exception { + setValidToken("valid-token"); + Mockito.when(httpRequest.getURI()).thenReturn(URI.create("http://internal-svc/api")); + Mockito.when(execution.execute(Mockito.any(), Mockito.any())).thenReturn(httpResponse); + + ClientHttpResponse result = underTest.intercept(httpRequest, new byte[0], execution); + + Assertions.assertNotNull(result); + Assertions.assertEquals("Bearer valid-token", headers.getFirst(HttpHeaders.AUTHORIZATION)); + } + + @Test + void shouldSkipWhenAuthHeaderAlreadyPresent() throws Exception { + setValidToken("valid-token"); + headers.add(HttpHeaders.AUTHORIZATION, "Bearer existing-token"); + Mockito.when(execution.execute(Mockito.any(), Mockito.any())).thenReturn(httpResponse); + + underTest.intercept(httpRequest, new byte[0], execution); + + // Should not have overwritten the header + Assertions.assertEquals("Bearer existing-token", headers.getFirst(HttpHeaders.AUTHORIZATION)); + } + + @Test + void shouldSkipWhenTokenExpired() throws Exception { + setExpiredToken("expired-token"); + Mockito.when(httpRequest.getURI()).thenReturn(URI.create("http://internal-svc/api")); + Mockito.when(execution.execute(Mockito.any(), Mockito.any())).thenReturn(httpResponse); + + underTest.intercept(httpRequest, new byte[0], execution); + + Assertions.assertNull(headers.getFirst(HttpHeaders.AUTHORIZATION)); + } + + @Test + void shouldSkipWhenHostIsExcluded() throws Exception { + setValidToken("valid-token"); + config.getTokenPropagation().setExcludeHosts(Collections.singletonList("external-api.com")); + Mockito.when(httpRequest.getURI()).thenReturn(URI.create("http://external-api.com/resource")); + Mockito.when(execution.execute(Mockito.any(), Mockito.any())).thenReturn(httpResponse); + + underTest.intercept(httpRequest, new byte[0], execution); + + Assertions.assertNull(headers.getFirst(HttpHeaders.AUTHORIZATION)); + } + + @Test + void shouldSkipWhenHostIsExternalAndExternalDisabled() throws Exception { + setValidToken("valid-token"); + config.getTokenPropagation().setAllowExternalHosts(false); + config.getTokenPropagation().setIncludeHosts(Collections.singletonList("internal.svc")); + Mockito.when(httpRequest.getURI()).thenReturn(URI.create("http://unknown-external.com/resource")); + Mockito.when(execution.execute(Mockito.any(), Mockito.any())).thenReturn(httpResponse); + + underTest.intercept(httpRequest, new byte[0], execution); + + Assertions.assertNull(headers.getFirst(HttpHeaders.AUTHORIZATION)); + } + + @Test + void shouldForwardWhenHostIsInIncludeList() throws Exception { + setValidToken("valid-token"); + config.getTokenPropagation().setAllowExternalHosts(false); + config.getTokenPropagation().setIncludeHosts(Collections.singletonList("internal.svc")); + Mockito.when(httpRequest.getURI()).thenReturn(URI.create("http://internal.svc/resource")); + Mockito.when(execution.execute(Mockito.any(), Mockito.any())).thenReturn(httpResponse); + + underTest.intercept(httpRequest, new byte[0], execution); + + Assertions.assertEquals("Bearer valid-token", headers.getFirst(HttpHeaders.AUTHORIZATION)); + } + + private void setValidToken(String token) { + long futureEpoch = Instant.now().plusSeconds(3600).getEpochSecond(); + List claims = Arrays.asList( + new TokenClaim("sub", "user-1"), + new TokenClaim("exp", futureEpoch) + ); + SecurityContext.set(token, claims); + } + + private void setExpiredToken(String token) { + long pastEpoch = Instant.now().minusSeconds(3600).getEpochSecond(); + List claims = Collections.singletonList(new TokenClaim("exp", pastEpoch)); + SecurityContext.set(token, claims); + } +} diff --git a/api-registry-common/src/test/java/org/eclipse/ecsp/security/HeaderInterceptorTest.java b/api-registry-common/src/test/java/org/eclipse/ecsp/security/HeaderInterceptorTest.java index 8d1913ef..1c3277ea 100644 --- a/api-registry-common/src/test/java/org/eclipse/ecsp/security/HeaderInterceptorTest.java +++ b/api-registry-common/src/test/java/org/eclipse/ecsp/security/HeaderInterceptorTest.java @@ -65,6 +65,14 @@ void setUp() { headerInterceptor = new HeaderInterceptor(); } + @Test + void shouldClearHeaderContextOnAfterCompletion() { + HeaderContext.setUser("user-1", java.util.Collections.emptySet(), java.util.Collections.emptySet()); + Assertions.assertNotNull(HeaderContext.getUserDetails()); + headerInterceptor.afterCompletion(new Request(), new Response(), new Object(), null); + Assertions.assertNull(HeaderContext.getUserDetails()); + } + @Test void testPreHandle() { Assertions.assertTrue(headerInterceptor.preHandle(new Request(), new Response(), new Handler() { diff --git a/api-registry-common/src/test/java/org/eclipse/ecsp/security/InterceptorConfigTest.java b/api-registry-common/src/test/java/org/eclipse/ecsp/security/InterceptorConfigTest.java index a2a685cf..fcf3e673 100644 --- a/api-registry-common/src/test/java/org/eclipse/ecsp/security/InterceptorConfigTest.java +++ b/api-registry-common/src/test/java/org/eclipse/ecsp/security/InterceptorConfigTest.java @@ -27,6 +27,7 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import java.util.Optional; /** * InterceptorConfigTest. @@ -41,7 +42,7 @@ class InterceptorConfigTest { @BeforeEach void setUp() { - interceptorConfig = new InterceptorConfig(headerInterceptor); + interceptorConfig = new InterceptorConfig(headerInterceptor, Optional.empty()); } @Test diff --git a/api-registry-common/src/test/java/org/eclipse/ecsp/security/ScopeTaggerTest.java b/api-registry-common/src/test/java/org/eclipse/ecsp/security/ScopeTaggerTest.java index ada0f51a..ce98838c 100644 --- a/api-registry-common/src/test/java/org/eclipse/ecsp/security/ScopeTaggerTest.java +++ b/api-registry-common/src/test/java/org/eclipse/ecsp/security/ScopeTaggerTest.java @@ -20,13 +20,12 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.models.Operation; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.method.HandlerMethod; import java.lang.annotation.Annotation; import java.util.ArrayList; @@ -51,12 +50,18 @@ class ScopeTaggerTest { private static final int EXPECTED_SECURITY_REQUIREMENT_COUNT = 2; - @InjectMocks + private ScopeOverrideProperties scopeOverrideProperties; private ScopeTagger scopeTagger; @Mock private HandlerMethod handlerMethod; + @BeforeEach + void setUp() { + scopeOverrideProperties = new ScopeOverrideProperties(); + scopeTagger = new ScopeTagger(scopeOverrideProperties); + } + // ----------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------- @@ -146,7 +151,7 @@ void testCustomize_WithScopes_OverrideDisabled_DoesNotChangeSecurity() { Operation operation = buildOperation("myOp"); Mockito.when(handlerMethod.getMethodAnnotation(SecurityRequirement.class)) .thenReturn(createAnnotation("OriginalScope")); - ReflectionTestUtils.setField(scopeTagger, "isOverrideScopeEnabled", false); + scopeOverrideProperties.getOverride().setEnabled(false); Operation result = scopeTagger.customize(operation, handlerMethod); @@ -167,8 +172,8 @@ void testCustomize_OverrideEnabled_NoMatchingRouteId_DoesNotChangeScopes() { final Operation operation = buildOperation("myOp"); Mockito.when(handlerMethod.getMethodAnnotation(SecurityRequirement.class)) .thenReturn(createAnnotation("OriginalScope")); - ReflectionTestUtils.setField(scopeTagger, "isOverrideScopeEnabled", true); - scopeTagger.setScopesMap(Map.of("some-other-controller-otherOp", List.of("NewScope"))); + scopeOverrideProperties.getOverride().setEnabled(true); + scopeOverrideProperties.setScopesMap(Map.of("some-other-controller-otherOp", List.of("NewScope"))); Operation result = scopeTagger.customize(operation, handlerMethod); @@ -190,8 +195,8 @@ void testCustomize_OverrideEnabled_ExactRouteIdMatch_ReplacesScopes() { final Operation operation = buildOperation("myOp"); Mockito.when(handlerMethod.getMethodAnnotation(SecurityRequirement.class)) .thenReturn(createAnnotation("OriginalScope")); - ReflectionTestUtils.setField(scopeTagger, "isOverrideScopeEnabled", true); - scopeTagger.setScopesMap( + scopeOverrideProperties.getOverride().setEnabled(true); + scopeOverrideProperties.setScopesMap( Map.of("test-controller-myOp", List.of("SelfManage", "ManageNotifications"))); Operation result = scopeTagger.customize(operation, handlerMethod); @@ -214,8 +219,8 @@ void testCustomize_OverrideEnabled_LowercaseRouteIdFallback_ReplacesScopes() { final Operation operation = buildOperation("MYOP"); Mockito.when(handlerMethod.getMethodAnnotation(SecurityRequirement.class)) .thenReturn(createAnnotation("OriginalScope")); - ReflectionTestUtils.setField(scopeTagger, "isOverrideScopeEnabled", true); - scopeTagger.setScopesMap(Map.of("test-controller-myop", List.of("SelfManage"))); + scopeOverrideProperties.getOverride().setEnabled(true); + scopeOverrideProperties.setScopesMap(Map.of("test-controller-myop", List.of("SelfManage"))); Operation result = scopeTagger.customize(operation, handlerMethod); @@ -237,8 +242,8 @@ void testCustomize_OverrideEnabled_NullSecurity_NoNpeAndLabelAdded() { operation.setSecurity(null); Mockito.when(handlerMethod.getMethodAnnotation(SecurityRequirement.class)) .thenReturn(createAnnotation("OriginalScope")); - ReflectionTestUtils.setField(scopeTagger, "isOverrideScopeEnabled", true); - scopeTagger.setScopesMap(Map.of("test-controller-myOp", List.of("SelfManage"))); + scopeOverrideProperties.getOverride().setEnabled(true); + scopeOverrideProperties.setScopesMap(Map.of("test-controller-myOp", List.of("SelfManage"))); Operation result = scopeTagger.customize(operation, handlerMethod); @@ -262,8 +267,8 @@ void testCustomize_OverrideEnabled_MultipleSecurityRequirements_AllScopesReplace Mockito.when(handlerMethod.getMethodAnnotation(SecurityRequirement.class)) .thenReturn(createAnnotation("OriginalScope")); - ReflectionTestUtils.setField(scopeTagger, "isOverrideScopeEnabled", true); - scopeTagger.setScopesMap( + scopeOverrideProperties.getOverride().setEnabled(true); + scopeOverrideProperties.setScopesMap( Map.of("test-controller-myOp", List.of("SelfManage", "ManageNotifications"))); Operation result = scopeTagger.customize(operation, handlerMethod); @@ -316,8 +321,8 @@ void testCustomize_OverrideEnabled_NullScopesMap_NoOverrideApplied() { final Operation operation = buildOperation("myOp"); Mockito.when(handlerMethod.getMethodAnnotation(SecurityRequirement.class)) .thenReturn(createAnnotation("OriginalScope")); - ReflectionTestUtils.setField(scopeTagger, "isOverrideScopeEnabled", true); - scopeTagger.setScopesMap(null); + scopeOverrideProperties.getOverride().setEnabled(true); + scopeOverrideProperties.setScopesMap(null); Operation result = scopeTagger.customize(operation, handlerMethod); diff --git a/api-registry-common/src/test/java/org/eclipse/ecsp/security/ScopeValidatorTest.java b/api-registry-common/src/test/java/org/eclipse/ecsp/security/ScopeValidatorTest.java index 5875be74..105333f4 100644 --- a/api-registry-common/src/test/java/org/eclipse/ecsp/security/ScopeValidatorTest.java +++ b/api-registry-common/src/test/java/org/eclipse/ecsp/security/ScopeValidatorTest.java @@ -49,7 +49,7 @@ class ScopeValidatorTest { @BeforeEach void setUp() { - scopeValidator = new ScopeValidator(); + scopeValidator = new ScopeValidator(new ScopeOverrideProperties()); } private void quickSetup(String methodName, Set userScopes, Set overrideScopes) diff --git a/api-registry-common/src/test/java/org/eclipse/ecsp/security/SecurityContextTest.java b/api-registry-common/src/test/java/org/eclipse/ecsp/security/SecurityContextTest.java new file mode 100644 index 00000000..d250db1d --- /dev/null +++ b/api-registry-common/src/test/java/org/eclipse/ecsp/security/SecurityContextTest.java @@ -0,0 +1,160 @@ +/******************************************************************************** + * Copyright (c) 2023-24 Harman International + * + *

Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *

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. + * + *

SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.ecsp.security; + +import org.eclipse.ecsp.tokenvalidator.model.TokenClaim; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Tests for {@link SecurityContext}. + */ +@ExtendWith(MockitoExtension.class) +class SecurityContextTest { + + @AfterEach + void tearDown() { + SecurityContext.clear(); + } + + @Test + void shouldStoreAndRetrieveToken() { + List claims = Collections.emptyList(); + SecurityContext.set("my-token", claims); + + Optional token = SecurityContext.getToken(); + Assertions.assertTrue(token.isPresent()); + Assertions.assertEquals("my-token", token.get()); + } + + @Test + void shouldReturnEmptyWhenNoToken() { + Optional token = SecurityContext.getToken(); + Assertions.assertFalse(token.isPresent()); + } + + @Test + void shouldParseExpiryFromExpClaim() { + long futureEpoch = Instant.now().plusSeconds(3600).getEpochSecond(); + List claims = Collections.singletonList(new TokenClaim("exp", futureEpoch)); + SecurityContext.set("token", claims); + + Assertions.assertFalse(SecurityContext.isTokenExpired()); + } + + @Test + void shouldParseSpaceSeparatedScopes() { + List claims = Collections.singletonList(new TokenClaim("scope", "read write admin")); + SecurityContext.set("token", claims); + + Set scopes = SecurityContext.getScopes(); + Assertions.assertTrue(scopes.contains("read")); + Assertions.assertTrue(scopes.contains("write")); + Assertions.assertTrue(scopes.contains("admin")); + Assertions.assertEquals(3, scopes.size()); + } + + @Test + void shouldParseArrayScopes() { + List scopeList = Arrays.asList("read", "write"); + List claims = Collections.singletonList(new TokenClaim("scope", scopeList)); + SecurityContext.set("token", claims); + + Set scopes = SecurityContext.getScopes(); + Assertions.assertTrue(scopes.contains("read")); + Assertions.assertTrue(scopes.contains("write")); + Assertions.assertEquals(2, scopes.size()); + } + + @Test + void shouldDetectExpiredToken() { + long pastEpoch = Instant.now().minusSeconds(3600).getEpochSecond(); + List claims = Collections.singletonList(new TokenClaim("exp", pastEpoch)); + SecurityContext.set("token", claims); + + Assertions.assertTrue(SecurityContext.isTokenExpired()); + } + + @Test + void shouldClearContext() { + List claims = Collections.emptyList(); + SecurityContext.set("token", claims); + SecurityContext.clear(); + + Assertions.assertFalse(SecurityContext.getToken().isPresent()); + Assertions.assertTrue(SecurityContext.getClaims().isEmpty()); + Assertions.assertFalse(SecurityContext.getUserId().isPresent()); + Assertions.assertTrue(SecurityContext.getScopes().isEmpty()); + Assertions.assertTrue(SecurityContext.isTokenExpired()); + } + + @Test + void shouldIsolateThreads() throws InterruptedException { + List mainClaims = Collections.singletonList(new TokenClaim("sub", "main-user")); + SecurityContext.set("main-token", mainClaims); + + AtomicReference> threadToken = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + + Thread thread = new Thread(() -> { + threadToken.set(SecurityContext.getToken()); + latch.countDown(); + }); + thread.start(); + latch.await(); + + Assertions.assertFalse(threadToken.get().isPresent()); + Assertions.assertTrue(SecurityContext.getToken().isPresent()); + Assertions.assertEquals("main-token", SecurityContext.getToken().get()); + } + + @Test + void shouldExtractUserId() { + List claims = Collections.singletonList(new TokenClaim("sub", "user-123")); + SecurityContext.set("token", claims); + + Optional userId = SecurityContext.getUserId(); + Assertions.assertTrue(userId.isPresent()); + Assertions.assertEquals("user-123", userId.get()); + } + + @Test + void shouldReturnEmptyUserIdWhenNoSubClaim() { + List claims = Collections.emptyList(); + SecurityContext.set("token", claims); + + Assertions.assertFalse(SecurityContext.getUserId().isPresent()); + } + + @Test + void shouldReturnTrueForIsTokenExpiredWhenNoContextSet() { + Assertions.assertTrue(SecurityContext.isTokenExpired()); + } +} diff --git a/api-registry-common/src/test/java/org/eclipse/ecsp/security/ValidationConfigPropertiesTest.java b/api-registry-common/src/test/java/org/eclipse/ecsp/security/ValidationConfigPropertiesTest.java new file mode 100644 index 00000000..a444bcc5 --- /dev/null +++ b/api-registry-common/src/test/java/org/eclipse/ecsp/security/ValidationConfigPropertiesTest.java @@ -0,0 +1,77 @@ +/******************************************************************************** + * Copyright (c) 2023-24 Harman International + * + *

Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *

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. + * + *

SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.ecsp.security; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Configuration; + +/** + * Tests for {@link ValidationConfigProperties}. + */ +@ExtendWith(MockitoExtension.class) +class ValidationConfigPropertiesTest { + + @Configuration + @EnableConfigurationProperties(ValidationConfigProperties.class) + static class TestConfig { + } + + @Test + void shouldBindDefaultValues() { + ValidationConfigProperties props = new ValidationConfigProperties(); + Assertions.assertFalse(props.getSecurity().isEnabled()); + Assertions.assertTrue(props.getTokenPropagation().getRestTemplate().isEnabled()); + Assertions.assertTrue(props.getTokenPropagation().getWebClient().isEnabled()); + Assertions.assertTrue(props.getTokenPropagation().getRestClient().isEnabled()); + Assertions.assertFalse(props.getTokenPropagation().isAllowExternalHosts()); + Assertions.assertTrue(props.getTokenPropagation().getIncludeHosts().isEmpty()); + Assertions.assertTrue(props.getTokenPropagation().getExcludeHosts().isEmpty()); + } + + @Test + void shouldBindCustomValues() { + ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues( + "api.registry.security.enabled=true", + "api.registry.token-propagation.rest-template.enabled=false", + "api.registry.token-propagation.web-client.enabled=false", + "api.registry.token-propagation.rest-client.enabled=false", + "api.registry.token-propagation.allow-external-hosts=true", + "api.registry.token-propagation.include-hosts=internal.svc", + "api.registry.token-propagation.exclude-hosts=external.com" + ) + .withUserConfiguration(TestConfig.class); + + contextRunner.run(ctx -> { + ValidationConfigProperties props = ctx.getBean(ValidationConfigProperties.class); + Assertions.assertTrue(props.getSecurity().isEnabled()); + Assertions.assertFalse(props.getTokenPropagation().getRestTemplate().isEnabled()); + Assertions.assertFalse(props.getTokenPropagation().getWebClient().isEnabled()); + Assertions.assertFalse(props.getTokenPropagation().getRestClient().isEnabled()); + Assertions.assertTrue(props.getTokenPropagation().isAllowExternalHosts()); + Assertions.assertFalse(props.getTokenPropagation().getIncludeHosts().isEmpty()); + Assertions.assertFalse(props.getTokenPropagation().getExcludeHosts().isEmpty()); + }); + } +} diff --git a/api-registry-common/src/test/java/org/eclipse/ecsp/security/validator/SecurityRequirementCacheTest.java b/api-registry-common/src/test/java/org/eclipse/ecsp/security/validator/SecurityRequirementCacheTest.java new file mode 100644 index 00000000..45dd842f --- /dev/null +++ b/api-registry-common/src/test/java/org/eclipse/ecsp/security/validator/SecurityRequirementCacheTest.java @@ -0,0 +1,124 @@ +/******************************************************************************** + * Copyright (c) 2023-24 Harman International + * + *

Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *

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. + * + *

SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.ecsp.security.validator; + +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import org.eclipse.ecsp.interceptors.SecurityRequirementCache; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.method.HandlerMethod; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +/** + * Tests for {@link SecurityRequirementCache}. + */ +@ExtendWith(MockitoExtension.class) +class SecurityRequirementCacheTest { + + private SecurityRequirementCache underTest; + + @BeforeEach + void beforeEach() { + underTest = new SecurityRequirementCache(); + } + + @Test + void shouldReturnFalseWhenAnnotationAbsent() throws Exception { + Method method = SampleController.class.getMethod("publicEndpoint"); + HandlerMethod handlerMethod = Mockito.mock(HandlerMethod.class); + Mockito.when(handlerMethod.getMethodAnnotation(SecurityRequirement.class)).thenReturn(null); + + boolean result = underTest.isSecured(handlerMethod); + + Assertions.assertFalse(result); + } + + @Test + void shouldReturnTrueWhenAnnotationPresent() throws Exception { + Method method = SampleController.class.getMethod("securedEndpoint"); + SecurityRequirement annotation = method.getAnnotation(SecurityRequirement.class); + + HandlerMethod handlerMethod = Mockito.mock(HandlerMethod.class); + Mockito.when(handlerMethod.getMethodAnnotation(SecurityRequirement.class)).thenReturn(annotation); + + boolean result = underTest.isSecured(handlerMethod); + + Assertions.assertTrue(result); + } + + @Test + void shouldReturnCachedResultOnSecondCall() { + HandlerMethod handlerMethod = Mockito.mock(HandlerMethod.class); + Mockito.when(handlerMethod.getMethodAnnotation(SecurityRequirement.class)).thenReturn(null); + + underTest.isSecured(handlerMethod); + underTest.isSecured(handlerMethod); + + // getMethodAnnotation should be called exactly once due to caching + Mockito.verify(handlerMethod, Mockito.times(1)).getMethodAnnotation(SecurityRequirement.class); + } + + @Test + void shouldHandleConcurrentFirstCalls() throws Exception { + HandlerMethod handlerMethod = Mockito.mock(HandlerMethod.class); + Mockito.when(handlerMethod.getMethodAnnotation(SecurityRequirement.class)).thenReturn(null); + + int threadCount = 10; + CountDownLatch startLatch = new CountDownLatch(1); + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + List> futures = new ArrayList<>(); + + for (int i = 0; i < threadCount; i++) { + futures.add(executor.submit(() -> { + startLatch.await(); + return underTest.isSecured(handlerMethod); + })); + } + startLatch.countDown(); + + for (Future future : futures) { + Assertions.assertFalse(future.get()); + } + executor.shutdown(); + } + + static class SampleController { + + /** Public endpoint with no security annotation. */ + public void publicEndpoint() { + // sample + } + + /** Secured endpoint with @SecurityRequirement annotation. */ + @SecurityRequirement(name = "bearerAuth") + public void securedEndpoint() { + // sample + } + } +} diff --git a/api-registry-common/src/test/java/org/eclipse/ecsp/security/validator/TokenValidationInterceptorTest.java b/api-registry-common/src/test/java/org/eclipse/ecsp/security/validator/TokenValidationInterceptorTest.java new file mode 100644 index 00000000..7e15b806 --- /dev/null +++ b/api-registry-common/src/test/java/org/eclipse/ecsp/security/validator/TokenValidationInterceptorTest.java @@ -0,0 +1,202 @@ +/******************************************************************************** + * Copyright (c) 2023-24 Harman International + * + *

Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *

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. + * + *

SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.ecsp.security.validator; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.ecsp.interceptors.SecurityRequirementCache; +import org.eclipse.ecsp.interceptors.TokenValidationInterceptor; +import org.eclipse.ecsp.security.ScopeOverrideProperties; +import org.eclipse.ecsp.security.SecurityContext; +import org.eclipse.ecsp.security.ValidationConfigProperties; +import org.eclipse.ecsp.tokenvalidator.TokenValidator; +import org.eclipse.ecsp.tokenvalidator.exception.InvalidSignatureException; +import org.eclipse.ecsp.tokenvalidator.exception.TokenExpiredException; +import org.eclipse.ecsp.tokenvalidator.model.TokenClaim; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpHeaders; +import org.springframework.web.method.HandlerMethod; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Collections; +import java.util.List; + +/** + * Tests for {@link TokenValidationInterceptor}. + */ +@ExtendWith(MockitoExtension.class) +class TokenValidationInterceptorTest { + + @Mock + private TokenValidator tokenValidator; + + @Mock + private SecurityRequirementCache securityRequirementCache; + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private HandlerMethod handlerMethod; + + private ScopeOverrideProperties scopeOverrideProperties; + + private ValidationConfigProperties config; + private TokenValidationInterceptor underTest; + private StringWriter responseWriter; + + @BeforeEach + void beforeEach() throws Exception { + config = new ValidationConfigProperties(); + config.getSecurity().setEnabled(true); + scopeOverrideProperties = new ScopeOverrideProperties(); + underTest = new TokenValidationInterceptor(tokenValidator, config, securityRequirementCache, new ObjectMapper(), + scopeOverrideProperties); + responseWriter = new StringWriter(); + Mockito.lenient().when(response.getWriter()).thenReturn(new PrintWriter(responseWriter)); + } + + @AfterEach + void tearDown() { + SecurityContext.clear(); + } + + @Test + void shouldReturnTrueWhenSecurityDisabled() throws Exception { + config.getSecurity().setEnabled(false); + + boolean result = underTest.preHandle(request, response, handlerMethod); + + Assertions.assertTrue(result); + Mockito.verifyNoInteractions(tokenValidator, securityRequirementCache); + } + + @Test + void shouldReturnTrueWhenHandlerIsNotHandlerMethod() throws Exception { + boolean result = underTest.preHandle(request, response, new Object()); + + Assertions.assertTrue(result); + Mockito.verifyNoInteractions(tokenValidator, securityRequirementCache); + } + + @Test + void shouldReturnTrueWhenCacheReportsNotSecured() throws Exception { + Mockito.when(securityRequirementCache.isSecured(handlerMethod)).thenReturn(false); + + boolean result = underTest.preHandle(request, response, handlerMethod); + + Assertions.assertTrue(result); + Mockito.verifyNoInteractions(tokenValidator); + } + + @Test + void shouldReturn401WhenAuthorizationHeaderMissing() throws Exception { + Mockito.when(securityRequirementCache.isSecured(handlerMethod)).thenReturn(true); + Mockito.when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(null); + + boolean result = underTest.preHandle(request, response, handlerMethod); + + Assertions.assertFalse(result); + Mockito.verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + + @Test + void shouldReturn401WhenAuthorizationHeaderMalformed() throws Exception { + Mockito.when(securityRequirementCache.isSecured(handlerMethod)).thenReturn(true); + Mockito.when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Basic dXNlcjpwYXNz"); + + boolean result = underTest.preHandle(request, response, handlerMethod); + + Assertions.assertFalse(result); + Mockito.verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + + @Test + void shouldReturn401WhenTokenValidationFails() throws Exception { + Mockito.when(securityRequirementCache.isSecured(handlerMethod)).thenReturn(true); + Mockito.when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer bad-token"); + Mockito.when(tokenValidator.validate("bad-token")) + .thenThrow(new InvalidSignatureException("invalid signature")); + + boolean result = underTest.preHandle(request, response, handlerMethod); + + Assertions.assertFalse(result); + Mockito.verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + + @Test + void shouldReturn401WhenTokenExpired() throws Exception { + Mockito.when(securityRequirementCache.isSecured(handlerMethod)).thenReturn(true); + Mockito.when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer expired-token"); + Mockito.when(tokenValidator.validate("expired-token")) + .thenThrow(new TokenExpiredException("token is expired")); + + boolean result = underTest.preHandle(request, response, handlerMethod); + + Assertions.assertFalse(result); + Mockito.verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + + @Test + void shouldStoreClaimsInSecurityContextWhenTokenIsValid() throws Exception { + List claims = Collections.singletonList(new TokenClaim("sub", "user-42")); + Mockito.when(securityRequirementCache.isSecured(handlerMethod)).thenReturn(true); + Mockito.when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer valid-token"); + Mockito.when(tokenValidator.validate("valid-token")).thenReturn(claims); + + boolean result = underTest.preHandle(request, response, handlerMethod); + + Assertions.assertTrue(result); + Assertions.assertTrue(SecurityContext.getToken().isPresent()); + Assertions.assertEquals("valid-token", SecurityContext.getToken().get()); + Assertions.assertEquals("user-42", SecurityContext.getUserId().orElse(null)); + } + + @Test + void shouldClearSecurityContextInAfterCompletion() { + List claims = Collections.emptyList(); + SecurityContext.set("some-token", claims); + + underTest.afterCompletion(request, response, handlerMethod, null); + + Assertions.assertFalse(SecurityContext.getToken().isPresent()); + } + + @Test + void shouldClearSecurityContextWhenExceptionOccurs() { + List claims = Collections.emptyList(); + SecurityContext.set("some-token", claims); + + underTest.afterCompletion(request, response, handlerMethod, new RuntimeException("oops")); + + Assertions.assertFalse(SecurityContext.getToken().isPresent()); + } +} diff --git a/api-registry-common/src/test/java/org/eclipse/ecsp/webclient/WebClientTokenFilterTest.java b/api-registry-common/src/test/java/org/eclipse/ecsp/webclient/WebClientTokenFilterTest.java new file mode 100644 index 00000000..2790c4a6 --- /dev/null +++ b/api-registry-common/src/test/java/org/eclipse/ecsp/webclient/WebClientTokenFilterTest.java @@ -0,0 +1,178 @@ +/******************************************************************************** + * Copyright (c) 2023-24 Harman International + * + *

Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *

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. + * + *

SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.ecsp.webclient; + +import org.eclipse.ecsp.security.SecurityContext; +import org.eclipse.ecsp.security.ValidationConfigProperties; +import org.eclipse.ecsp.tokenvalidator.model.TokenClaim; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpHeaders; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFunction; +import reactor.core.publisher.Mono; +import java.net.URI; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Tests for {@link WebClientTokenFilter}. + */ +@ExtendWith(MockitoExtension.class) +class WebClientTokenFilterTest { + + @Mock + private ExchangeFunction exchangeFunction; + + @Mock + private ClientResponse clientResponse; + + private ValidationConfigProperties config; + private WebClientTokenFilter underTest; + + @BeforeEach + void beforeEach() { + config = new ValidationConfigProperties(); + underTest = new WebClientTokenFilter(config); + Mockito.when(exchangeFunction.exchange(Mockito.any())).thenReturn(Mono.just(clientResponse)); + } + + @AfterEach + void tearDown() { + SecurityContext.clear(); + } + + @Test + void shouldAddAuthHeaderWhenTokenPresent() { + setValidToken("valid-token"); + ClientRequest request = ClientRequest.create( + org.springframework.http.HttpMethod.GET, URI.create("http://internal-svc/api")) + .build(); + + underTest.filter(request, exchangeFunction).block(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ClientRequest.class); + Mockito.verify(exchangeFunction).exchange(captor.capture()); + Assertions.assertEquals("Bearer valid-token", + captor.getValue().headers().getFirst(HttpHeaders.AUTHORIZATION)); + } + + @Test + void shouldSkipWhenAuthHeaderAlreadyPresent() { + setValidToken("valid-token"); + ClientRequest request = ClientRequest.create( + org.springframework.http.HttpMethod.GET, URI.create("http://internal-svc/api")) + .header(HttpHeaders.AUTHORIZATION, "Bearer existing-token") + .build(); + + underTest.filter(request, exchangeFunction).block(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ClientRequest.class); + Mockito.verify(exchangeFunction).exchange(captor.capture()); + Assertions.assertEquals("Bearer existing-token", + captor.getValue().headers().getFirst(HttpHeaders.AUTHORIZATION)); + } + + @Test + void shouldSkipWhenTokenExpired() { + setExpiredToken("expired-token"); + ClientRequest request = ClientRequest.create( + org.springframework.http.HttpMethod.GET, URI.create("http://internal-svc/api")) + .build(); + + underTest.filter(request, exchangeFunction).block(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ClientRequest.class); + Mockito.verify(exchangeFunction).exchange(captor.capture()); + Assertions.assertNull(captor.getValue().headers().getFirst(HttpHeaders.AUTHORIZATION)); + } + + @Test + void shouldSkipWhenHostIsExcluded() { + setValidToken("valid-token"); + config.getTokenPropagation().setExcludeHosts(Collections.singletonList("external-api.com")); + ClientRequest request = ClientRequest.create( + org.springframework.http.HttpMethod.GET, URI.create("http://external-api.com/resource")) + .build(); + + underTest.filter(request, exchangeFunction).block(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ClientRequest.class); + Mockito.verify(exchangeFunction).exchange(captor.capture()); + Assertions.assertNull(captor.getValue().headers().getFirst(HttpHeaders.AUTHORIZATION)); + } + + @Test + void shouldSkipWhenHostIsExternalAndExternalDisabled() { + setValidToken("valid-token"); + config.getTokenPropagation().setAllowExternalHosts(false); + config.getTokenPropagation().setIncludeHosts(Collections.singletonList("internal.svc")); + ClientRequest request = ClientRequest.create( + org.springframework.http.HttpMethod.GET, URI.create("http://unknown-external.com/resource")) + .build(); + + underTest.filter(request, exchangeFunction).block(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ClientRequest.class); + Mockito.verify(exchangeFunction).exchange(captor.capture()); + Assertions.assertNull(captor.getValue().headers().getFirst(HttpHeaders.AUTHORIZATION)); + } + + @Test + void shouldForwardWhenHostIsInIncludeList() { + setValidToken("valid-token"); + config.getTokenPropagation().setAllowExternalHosts(false); + config.getTokenPropagation().setIncludeHosts(Collections.singletonList("internal.svc")); + ClientRequest request = ClientRequest.create( + org.springframework.http.HttpMethod.GET, URI.create("http://internal.svc/resource")) + .build(); + + underTest.filter(request, exchangeFunction).block(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ClientRequest.class); + Mockito.verify(exchangeFunction).exchange(captor.capture()); + Assertions.assertEquals("Bearer valid-token", + captor.getValue().headers().getFirst(HttpHeaders.AUTHORIZATION)); + } + + private void setValidToken(String token) { + long futureEpoch = Instant.now().plusSeconds(3600).getEpochSecond(); + List claims = Arrays.asList( + new TokenClaim("sub", "user-1"), + new TokenClaim("exp", futureEpoch) + ); + SecurityContext.set(token, claims); + } + + private void setExpiredToken(String token) { + long pastEpoch = Instant.now().minusSeconds(3600).getEpochSecond(); + List claims = Collections.singletonList(new TokenClaim("exp", pastEpoch)); + SecurityContext.set(token, claims); + } +} diff --git a/api-registry/pom.xml b/api-registry/pom.xml index aa61b724..e8aa2b9c 100644 --- a/api-registry/pom.xml +++ b/api-registry/pom.xml @@ -6,7 +6,7 @@ org.eclipse.ecsp api-gateway-parent - 1.5.3-SNAPSHOT + 1.6.0-SNAPSHOT api-registry diff --git a/api-registry/src/main/resources/application.yml b/api-registry/src/main/resources/application.yml index 649e6238..447d3648 100644 --- a/api-registry/src/main/resources/application.yml +++ b/api-registry/src/main/resources/application.yml @@ -46,6 +46,9 @@ api: enabled: ${api_security_enabled:false} health: monitor: ${api_health_monitor:0 */1 * ? * *} + registry: + security: + enabled: ${api_security_enabled:false} api-registry: database: diff --git a/pom.xml b/pom.xml index a3653c5a..2f3b08a6 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.eclipse.ecsp api-gateway-parent - 1.5.3-SNAPSHOT + 1.6.0-SNAPSHOT pom @@ -58,12 +58,13 @@ ${java.version} 1.2.0 + 0.0.1-SNAPSHOT 2.21.1 2.21.2 0.13.0 1.18.42 - 4.0.5 + 4.0.6 2025.1.1 3.3.1 5.0.1