From 74eedeaf4b6f85d658b9ce30785bd1b58f2d9708 Mon Sep 17 00:00:00 2001 From: Gabriel Roldan Date: Wed, 27 Mar 2024 20:49:27 -0300 Subject: [PATCH] Fix swagger-ui behind a reverse proxy Despite spring-doc documentation on how to set up the swagger-ui behind a reverse proxy, the X-Forwarded-Prefix request header is not honored. Fix by tweaking `SpringDocAutoConfiguration` to apply the prefix as appropriate. --- Makefile | 1 + compose.yml | 41 ----- compose/.env | 3 + compose/.gitignore | 2 + compose/compose.yml | 77 ++++++++++ compose/gateway-service.yml | 35 +++++ compose/nginx.conf | 19 +++ compose/run.sh | 8 + docs/api/index.html | 64 ++++---- src/artifacts/api/pom.xml | 14 ++ .../api/RulesApiAutoConfiguration.java | 16 +- .../springdoc/SpringDocAutoConfiguration.java | 140 +++++++++++++++++- .../SpringDocHomeRedirectController.java | 26 ++-- .../api/src/main/resources/application.yml | 67 +++------ .../api/src/main/resources/security.yml | 32 ++++ .../api/src/main/resources/springdoc.yml | 21 +++ src/openapi/acl-api.yaml | 3 +- 17 files changed, 433 insertions(+), 136 deletions(-) delete mode 100644 compose.yml create mode 100644 compose/.env create mode 100644 compose/.gitignore create mode 100644 compose/compose.yml create mode 100644 compose/gateway-service.yml create mode 100644 compose/nginx.conf create mode 100755 compose/run.sh create mode 100644 src/artifacts/api/src/main/resources/security.yml create mode 100644 src/artifacts/api/src/main/resources/springdoc.yml diff --git a/Makefile b/Makefile index 4b38e0f..cc9dfc0 100644 --- a/Makefile +++ b/Makefile @@ -45,6 +45,7 @@ test-examples: # https://stackoverflow.com/questions/51115856/docker-failed-to-export-image-failed-to-create-image-failed-to-get-layer build-image: @VERSION=`./mvnw help:evaluate -q -DforceStdout -Dexpression=project.version` && \ + ./mvnw clean package -f src/artifacts/api -DskipTests -T4 && \ DOCKER_BUILDKIT=1 docker build -t $(DOCKER_REPO):$${VERSION} src/artifacts/api/ push-image: diff --git a/compose.yml b/compose.yml deleted file mode 100644 index cddbe0f..0000000 --- a/compose.yml +++ /dev/null @@ -1,41 +0,0 @@ -version: "3.1" - -volumes: - acl_data: - -services: - acldb: - image: postgis/postgis:15-3.3 - environment: - - POSTGRES_DB=acl - - POSTGRES_USER=acl - - POSTGRES_PASSWORD=acls3cr3t - volumes: - - acl_data:/var/lib/postgresql/data - restart: always - ports: - - 6432:5432 - deploy: - resources: - limits: - cpus: '4.0' - memory: 2G - - acl: - image: geoservercloud/geoserver-acl:2.1-SNAPSHOT - environment: - - PG_HOST=acldb - - PG_PORT=5432 - - PG_DB=acl - - PG_SCHEMA=acl - - PG_USER=acl - - PG_PASSWORD=acls3cr3t - ports: - - 8080:8080 - - 8081:8081 - deploy: - resources: - limits: - cpus: '4.0' - memory: 2G - diff --git a/compose/.env b/compose/.env new file mode 100644 index 0000000..b44e4f9 --- /dev/null +++ b/compose/.env @@ -0,0 +1,3 @@ +COMPOSE_PROJECT_NAME=acldev +TAG=2.1-SNAPSHOT +GATEWAY_TAG=1.7.0 diff --git a/compose/.gitignore b/compose/.gitignore new file mode 100644 index 0000000..f933710 --- /dev/null +++ b/compose/.gitignore @@ -0,0 +1,2 @@ +cert.pem +key.pem diff --git a/compose/compose.yml b/compose/compose.yml new file mode 100644 index 0000000..32a30da --- /dev/null +++ b/compose/compose.yml @@ -0,0 +1,77 @@ +version: "3.8" + +volumes: + acl_data: + +services: + acldb: + image: postgis/postgis:15-3.3 + environment: + - POSTGRES_DB=acl + - POSTGRES_USER=acl + - POSTGRES_PASSWORD=acls3cr3t + volumes: + - acl_data:/var/lib/postgresql/data + restart: always + ports: + - 6432:5432 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U acl"] + interval: 5s + timeout: 5s + retries: 5 + deploy: + resources: + limits: + cpus: '4.0' + memory: 2G + + acl: + image: geoservercloud/geoserver-acl:${TAG} + environment: + - PG_HOST=acldb + - PG_PORT=5432 + - PG_DB=acl + - PG_SCHEMA=acl + - PG_USER=acl + - PG_PASSWORD=acls3cr3t + - SPRING_PROFILES_ACTIVE=logging_debug_requests + depends_on: + acldb: + condition: service_healthy + required: true + ports: + - 8080:8080 + - 8081:8081 + deploy: + resources: + limits: + cpus: '4.0' + memory: 2G + + gateway: + image: geoservercloud/geoserver-cloud-gateway:${GATEWAY_TAG} + user: 1000:1000 + environment: + SPRING_PROFILES_ACTIVE: standalone + GEOSERVER_BASE_PATH: /geoserver/cloud + volumes: + - ./gateway-service.yml:/etc/geoserver/gateway-service.yml + ports: + - 9090:8080 + deploy: + resources: + limits: + cpus: '4.0' + memory: 1G + + nginx: + image: nginx + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf + - ./key.pem:/root/ssl/key.pem + - ./cert.pem:/root/ssl/cert.pem + ports: + - "443:443" + depends_on: + - gateway diff --git a/compose/gateway-service.yml b/compose/gateway-service.yml new file mode 100644 index 0000000..c2ddc49 --- /dev/null +++ b/compose/gateway-service.yml @@ -0,0 +1,35 @@ +geoserver.base-path: ${geoserver_base_path:} + +targets.acl: http://10.0.0.71:8080 + +server: + forward-headers-strategy: framework + +spring: + cloud: + gateway: + x-forwarded: + for-enabled: true + host-enabled: true + port-enabled: true + proto-enabled: true + prefix-enabled: true + globalcors: + cors-configurations: + "[/**]": + allowedOrigins: "*" + allowedHeaders: "*" + allowedMethods: GET, PUT, POST, DELETE, OPTIONS, HEAD + default-filters: + - StripBasePath=${geoserver.base-path} #remove the base path on downstream requests + routes: + - id: acl + uri: ${targets.acl} + predicates: + - Path=${geoserver.base-path}/acl,${geoserver.base-path}/acl/** + filters: + - RewritePath=/acl,/acl/ +--- +spring.config.activate.on-profile: debug +logging.level.root: debug + diff --git a/compose/nginx.conf b/compose/nginx.conf new file mode 100644 index 0000000..6574ad2 --- /dev/null +++ b/compose/nginx.conf @@ -0,0 +1,19 @@ +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name localhost; + ssl_certificate /root/ssl/cert.pem; + ssl_certificate_key /root/ssl/key.pem; + + location / { + proxy_pass "http://gateway:8080/"; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto https; + } + + error_page 500 502 503 504 /50x.html; + +} \ No newline at end of file diff --git a/compose/run.sh b/compose/run.sh new file mode 100755 index 0000000..dafd599 --- /dev/null +++ b/compose/run.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +# generate a certificate to mount on the nginx container +openssl req -x509 -nodes -newkey rsa:2048 -keyout key.pem -out cert.pem -sha256 -days 365 \ + -subj "/C=GB/ST=London/L=London/O=Alros/OU=IT Department/CN=localhost" + + +docker compose up \ No newline at end of file diff --git a/docs/api/index.html b/docs/api/index.html index c375264..5543495 100644 --- a/docs/api/index.html +++ b/docs/api/index.html @@ -1502,7 +1502,7 @@

Usage and SDK Samples

-H "Authorization: Basic [[basicHash]]" \ -H "Accept: application/json,application/x-jackson-smile" \ -H "Content-Type: application/json,application/x-jackson-smile" \ - "http://localhost:8080/api/authorization/resources" \ + "/api/authorization/resources" \ -d '{ "request" : "*", "workspace" : "*", @@ -1914,7 +1914,7 @@

Usage and SDK Samples

-H "Authorization: Basic [[basicHash]]" \ -H "Accept: application/json,application/x-jackson-smile" \ -H "Content-Type: application/json,application/x-jackson-smile" \ - "http://localhost:8080/api/authorization/admin" \ + "/api/authorization/admin" \ -d '{ "workspace" : "*", "sourceAddress" : "*", @@ -2322,7 +2322,7 @@

Usage and SDK Samples

-H "Authorization: Basic [[basicHash]]" \ -H "Accept: application/json,application/x-jackson-smile" \ -H "Content-Type: application/json,application/x-jackson-smile" \ - "http://localhost:8080/api/authorization/resources/matchingrules" \ + "/api/authorization/resources/matchingrules" \ -d '{ "request" : "*", "workspace" : "*", @@ -2755,7 +2755,7 @@

Usage and SDK Samples

curl -X GET \
  -H "Authorization: Basic [[basicHash]]" \
  -H "Accept: application/json" \
- "http://localhost:8080/api/rules/query/count"
+ "/api/rules/query/count"
 
@@ -3094,7 +3094,7 @@

Usage and SDK Samples

-H "Authorization: Basic [[basicHash]]" \ -H "Accept: application/json" \ -H "Content-Type: application/json,application/x-jackson-smile" \ - "http://localhost:8080/api/rules/query/count" \ + "/api/rules/query/count" \ -d '{ "request" : { "includeDefault" : true, @@ -3530,7 +3530,7 @@

Usage and SDK Samples

-H "Authorization: Basic [[basicHash]]" \ -H "Accept: application/json,application/x-jackson-smile" \ -H "Content-Type: application/json,application/x-jackson-smile" \ - "http://localhost:8080/api/rules?position=" \ + "/api/rules?position=" \ -d '{ "request" : "request", "workspace" : "workspace", @@ -4049,7 +4049,7 @@

Usage and SDK Samples

curl -X DELETE \
  -H "Authorization: Basic [[basicHash]]" \
- "http://localhost:8080/api/rules/id/{id}"
+ "/api/rules/id/{id}"
 
@@ -4401,7 +4401,7 @@

Usage and SDK Samples

curl -X GET \
  -H "Authorization: Basic [[basicHash]]" \
  -H "Accept: application/json,application/x-jackson-smile" \
- "http://localhost:8080/api/rules/query/one/priority/{priority}"
+ "/api/rules/query/one/priority/{priority}"
 
@@ -4827,7 +4827,7 @@

Usage and SDK Samples

curl -X GET \
  -H "Authorization: Basic [[basicHash]]" \
  -H "Accept: application/json,application/x-jackson-smile" \
- "http://localhost:8080/api/rules/id/{id}/layer-details"
+ "/api/rules/id/{id}/layer-details"
 
@@ -5272,7 +5272,7 @@

Usage and SDK Samples

curl -X GET \
  -H "Authorization: Basic [[basicHash]]" \
  -H "Accept: application/json,application/x-jackson-smile" \
- "http://localhost:8080/api/rules/id/{id}"
+ "/api/rules/id/{id}"
 
@@ -5673,7 +5673,7 @@

Usage and SDK Samples

curl -X GET \
  -H "Authorization: Basic [[basicHash]]" \
  -H "Accept: application/json,application/x-jackson-smile" \
- "http://localhost:8080/api/rules?limit=56&nextCursor=nextCursor_example"
+ "/api/rules?limit=56&nextCursor=nextCursor_example"
 
@@ -6102,7 +6102,7 @@

Usage and SDK Samples

-H "Authorization: Basic [[basicHash]]" \ -H "Accept: application/json,application/x-jackson-smile" \ -H "Content-Type: application/json,application/x-jackson-smile" \ - "http://localhost:8080/api/rules/query?limit=56&nextCursor=nextCursor_example" \ + "/api/rules/query?limit=56&nextCursor=nextCursor_example" \ -d '{ "request" : { "includeDefault" : true, @@ -6627,7 +6627,7 @@

Usage and SDK Samples

curl -X GET \
  -H "Authorization: Basic [[basicHash]]" \
  -H "Accept: application/json,application/x-jackson-smile" \
- "http://localhost:8080/api/rules/id/{id}/exists"
+ "/api/rules/id/{id}/exists"
 
@@ -7006,7 +7006,7 @@

Usage and SDK Samples

curl -X PUT \
  -H "Authorization: Basic [[basicHash]]" \
  -H "Content-Type: application/json,application/x-jackson-smile" \
- "http://localhost:8080/api/rules/id/{id}/styles" \
+ "/api/rules/id/{id}/styles" \
  -d 'Custom MIME type example not yet supported: application/x-jackson-smile'
 
@@ -7433,7 +7433,7 @@

Usage and SDK Samples

curl -X PUT \
  -H "Authorization: Basic [[basicHash]]" \
  -H "Content-Type: application/json,application/x-jackson-smile" \
- "http://localhost:8080/api/rules/id/{id}/layer-details" \
+ "/api/rules/id/{id}/layer-details" \
  -d '{
   "cqlFilterWrite" : "cqlFilterWrite",
   "cqlFilterRead" : "cqlFilterRead",
@@ -7866,7 +7866,7 @@ 

Usage and SDK Samples

curl -X PUT \
  -H "Authorization: Basic [[basicHash]]" \
  -H "Content-Type: application/json,application/x-jackson-smile" \
- "http://localhost:8080/api/rules/id/{id}/limits" \
+ "/api/rules/id/{id}/limits" \
  -d '{ }' \
  -d 'Custom MIME type example not yet supported: application/x-jackson-smile'
 
@@ -8306,7 +8306,7 @@

Usage and SDK Samples

curl -X POST \
  -H "Authorization: Basic [[basicHash]]" \
  -H "Accept: application/json,application/x-jackson-smile" \
- "http://localhost:8080/api/rules/shift?priorityStart=789&offset=789"
+ "/api/rules/shift?priorityStart=789&offset=789"
 
@@ -8746,7 +8746,7 @@

Usage and SDK Samples

curl -X POST \
  -H "Authorization: Basic [[basicHash]]" \
- "http://localhost:8080/api/rules/id/{id}/swapwith/{id2}"
+ "/api/rules/id/{id}/swapwith/{id2}"
 
@@ -9133,7 +9133,7 @@

Usage and SDK Samples

-H "Authorization: Basic [[basicHash]]" \ -H "Accept: application/json,application/x-jackson-smile" \ -H "Content-Type: application/json,application/x-jackson-smile" \ - "http://localhost:8080/api/rules/id/{id}" \ + "/api/rules/id/{id}" \ -d '{ "request" : "request", "workspace" : "workspace", @@ -9660,7 +9660,7 @@

Usage and SDK Samples

curl -X GET \
  -H "Authorization: Basic [[basicHash]]" \
  -H "Accept: application/json,application/x-jackson-smile" \
- "http://localhost:8080/api/adminrules/id/{id}/exists"
+ "/api/adminrules/id/{id}/exists"
 
@@ -10040,7 +10040,7 @@

Usage and SDK Samples

-H "Authorization: Basic [[basicHash]]" \ -H "Accept: application/json" \ -H "Content-Type: application/json,application/x-jackson-smile" \ - "http://localhost:8080/api/adminrules/query/count" \ + "/api/adminrules/query/count" \ -d '{ "workspace" : { "includeDefault" : true, @@ -10459,7 +10459,7 @@

Usage and SDK Samples

curl -X GET \
  -H "Authorization: Basic [[basicHash]]" \
  -H "Accept: application/json" \
- "http://localhost:8080/api/adminrules/query/count"
+ "/api/adminrules/query/count"
 
@@ -10798,7 +10798,7 @@

Usage and SDK Samples

-H "Authorization: Basic [[basicHash]]" \ -H "Accept: application/json,application/x-jackson-smile" \ -H "Content-Type: application/json,application/x-jackson-smile" \ - "http://localhost:8080/api/adminrules?position=" \ + "/api/adminrules?position=" \ -d '{ "workspace" : "workspace", "role" : "role", @@ -11249,7 +11249,7 @@

Usage and SDK Samples

curl -X DELETE \
  -H "Authorization: Basic [[basicHash]]" \
- "http://localhost:8080/api/adminrules/id/{id}"
+ "/api/adminrules/id/{id}"
 
@@ -11602,7 +11602,7 @@

Usage and SDK Samples

-H "Authorization: Basic [[basicHash]]" \ -H "Accept: application/json,application/x-jackson-smile" \ -H "Content-Type: application/json,application/x-jackson-smile" \ - "http://localhost:8080/api/adminrules/query?limit=56&nextCursor=nextCursor_example" \ + "/api/adminrules/query?limit=56&nextCursor=nextCursor_example" \ -d '{ "workspace" : { "includeDefault" : true, @@ -12111,7 +12111,7 @@

Usage and SDK Samples

curl -X GET \
  -H "Authorization: Basic [[basicHash]]" \
  -H "Accept: application/json,application/x-jackson-smile" \
- "http://localhost:8080/api/adminrules?limit=56&nextCursor=nextCursor_example"
+ "/api/adminrules?limit=56&nextCursor=nextCursor_example"
 
@@ -12540,7 +12540,7 @@

Usage and SDK Samples

-H "Authorization: Basic [[basicHash]]" \ -H "Accept: application/json,application/x-jackson-smile" \ -H "Content-Type: application/json,application/x-jackson-smile" \ - "http://localhost:8080/api/adminrules/query/first" \ + "/api/adminrules/query/first" \ -d '{ "workspace" : { "includeDefault" : true, @@ -12981,7 +12981,7 @@

Usage and SDK Samples

curl -X GET \
  -H "Authorization: Basic [[basicHash]]" \
  -H "Accept: application/json,application/x-jackson-smile" \
- "http://localhost:8080/api/adminrules/query/one/priority/{priority}"
+ "/api/adminrules/query/one/priority/{priority}"
 
@@ -13407,7 +13407,7 @@

Usage and SDK Samples

curl -X GET \
  -H "Authorization: Basic [[basicHash]]" \
  -H "Accept: application/json,application/x-jackson-smile" \
- "http://localhost:8080/api/adminrules/id/{id}"
+ "/api/adminrules/id/{id}"
 
@@ -13808,7 +13808,7 @@

Usage and SDK Samples

curl -X POST \
  -H "Authorization: Basic [[basicHash]]" \
  -H "Accept: application/json" \
- "http://localhost:8080/api/adminrules/shift?priorityStart=789&offset=789"
+ "/api/adminrules/shift?priorityStart=789&offset=789"
 
@@ -14248,7 +14248,7 @@

Usage and SDK Samples

curl -X POST \
  -H "Authorization: Basic [[basicHash]]" \
- "http://localhost:8080/api/adminrules/id/{id}/swapwith/{id2}"
+ "/api/adminrules/id/{id}/swapwith/{id2}"
 
@@ -14635,7 +14635,7 @@

Usage and SDK Samples

-H "Authorization: Basic [[basicHash]]" \ -H "Accept: application/json,application/x-jackson-smile" \ -H "Content-Type: application/json,application/x-jackson-smile" \ - "http://localhost:8080/api/adminrules/id/{id}" \ + "/api/adminrules/id/{id}" \ -d '{ "workspace" : "workspace", "role" : "role", diff --git a/src/artifacts/api/pom.xml b/src/artifacts/api/pom.xml index 8d86742..6a2cb37 100644 --- a/src/artifacts/api/pom.xml +++ b/src/artifacts/api/pom.xml @@ -56,7 +56,21 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot spring-boot-starter-validation diff --git a/src/artifacts/api/src/main/java/org/geoserver/acl/autoconfigure/api/RulesApiAutoConfiguration.java b/src/artifacts/api/src/main/java/org/geoserver/acl/autoconfigure/api/RulesApiAutoConfiguration.java index bc8a3b9..7ee9221 100644 --- a/src/artifacts/api/src/main/java/org/geoserver/acl/autoconfigure/api/RulesApiAutoConfiguration.java +++ b/src/artifacts/api/src/main/java/org/geoserver/acl/autoconfigure/api/RulesApiAutoConfiguration.java @@ -7,8 +7,22 @@ import org.geoserver.acl.api.server.config.AuthorizationApiConfiguration; import org.geoserver.acl.api.server.config.RulesApiConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; +import org.springframework.web.filter.CommonsRequestLoggingFilter; @AutoConfiguration @Import({RulesApiConfiguration.class, AuthorizationApiConfiguration.class}) -public class RulesApiAutoConfiguration {} +public class RulesApiAutoConfiguration { + + @Bean + CommonsRequestLoggingFilter commonsRequestLoggingFilter() { + var filter = new CommonsRequestLoggingFilter(); + filter.setHeaderPredicate(h -> h.toLowerCase().startsWith("x-forward")); + filter.setIncludeHeaders(true); + filter.setIncludeQueryString(true); + filter.setIncludePayload(true); + filter.setIncludeClientInfo(true); + return filter; + } +} diff --git a/src/artifacts/api/src/main/java/org/geoserver/acl/autoconfigure/springdoc/SpringDocAutoConfiguration.java b/src/artifacts/api/src/main/java/org/geoserver/acl/autoconfigure/springdoc/SpringDocAutoConfiguration.java index 457378f..abc9f39 100644 --- a/src/artifacts/api/src/main/java/org/geoserver/acl/autoconfigure/springdoc/SpringDocAutoConfiguration.java +++ b/src/artifacts/api/src/main/java/org/geoserver/acl/autoconfigure/springdoc/SpringDocAutoConfiguration.java @@ -4,15 +4,151 @@ */ package org.geoserver.acl.autoconfigure.springdoc; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springdoc.core.SpringDocConfigProperties; +import org.springdoc.core.SwaggerUiConfigParameters; +import org.springdoc.core.SwaggerUiConfigProperties; +import org.springdoc.core.customizers.ServerBaseUrlCustomizer; +import org.springdoc.core.providers.SpringWebProvider; +import org.springdoc.webmvc.ui.SwaggerConfig; +import org.springdoc.webmvc.ui.SwaggerWelcomeWebMvc; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; /** {@link AutoConfiguration} redirect the home page to the swagger-ui */ @AutoConfiguration +@Slf4j(topic = "org.geoserver.acl.autoconfigure.springdoc") public class SpringDocAutoConfiguration { @Bean - SpringDocHomeRedirectController homeController() { - return new SpringDocHomeRedirectController("/swagger-ui/index.html"); + SpringDocHomeRedirectController homeRedirectController(NativeWebRequest req) { + return new SpringDocHomeRedirectController(req); + } + + @Bean + ServerBaseUrlCustomizer xForwardedPrefixAwareServerBaseUrlCustomizer(NativeWebRequest req) { + return new XForwardedPrefixBaseUrlCustomizer(req); + } + + /** + * Override the one defined in {@link SwaggerConfig} to apply the{@literal X-Forwarded-Prefix} + * request header prefix to the swagger ui config urls + */ + @Bean + SwaggerWelcomeWebMvc xForwardedPrefixAwareSwaggerWelcome( + SwaggerUiConfigProperties swaggerUiConfig, + SpringDocConfigProperties springDocConfigProperties, + SwaggerUiConfigParameters swaggerUiConfigParameters, + SpringWebProvider springWebProvider, + NativeWebRequest nativeWebRequest) { + return new XForwardedPrefixAwareSwaggerWelcomeWebMvc( + swaggerUiConfig, + springDocConfigProperties, + swaggerUiConfigParameters, + springWebProvider, + nativeWebRequest); + } + + /** + * Springdoc {@link ServerBaseUrlCustomizer} to apply the {@literal X-Forwarded-Prefix} request + * header prefix to the base server url presented in the swagger- + */ + @RequiredArgsConstructor + static class XForwardedPrefixBaseUrlCustomizer implements ServerBaseUrlCustomizer { + private final @NonNull NativeWebRequest req; + + @Override + public String customize(String serverBaseUrl) { + return customizeUrl(serverBaseUrl, req); + } + } + + static class XForwardedPrefixAwareSwaggerWelcomeWebMvc extends SwaggerWelcomeWebMvc { + + private final NativeWebRequest nativeWebRequest; + + public XForwardedPrefixAwareSwaggerWelcomeWebMvc( + SwaggerUiConfigProperties swaggerUiConfig, + SpringDocConfigProperties springDocConfigProperties, + SwaggerUiConfigParameters swaggerUiConfigParameters, + SpringWebProvider springWebProvider, + NativeWebRequest nativeWebRequest) { + super( + swaggerUiConfig, + springDocConfigProperties, + swaggerUiConfigParameters, + springWebProvider); + this.nativeWebRequest = nativeWebRequest; + } + + @Override + protected String buildApiDocUrl() { + var url = super.buildApiDocUrl(); + url = applyForwardedPrefix(url, nativeWebRequest); + log.debug("buildApiDocUrl: {}", url); + return url; + } + + @Override + protected String buildSwaggerConfigUrl() { + var url = super.buildSwaggerConfigUrl(); + url = applyForwardedPrefix(url, nativeWebRequest); + log.debug("buildSwaggerConfigUrl: {}", url); + return url; + } + + @Override + protected String buildUrl(String contextPath, final String docsUrl) { + var url = super.buildUrl(contextPath, docsUrl); + url = applyForwardedPrefix(url, nativeWebRequest); + log.debug("buildUrl({}, {}): {}", contextPath, docsUrl, url); + return url; + } + + @Override + protected String buildUrlWithContextPath(String swaggerUiUrl) { + var url = super.buildUrlWithContextPath(swaggerUiUrl); + url = applyForwardedPrefix(url, nativeWebRequest); + log.debug("buildUrlWithContextPath({}): {}", swaggerUiUrl, url); + return url; + } + } + + private static String applyForwardedPrefix(String path, NativeWebRequest req) { + String prefix = getFirstHeader(req, "X-Forwarded-Prefix"); + if (null != prefix && !path.startsWith(prefix)) { + return prefix + path; + } + return path; + } + + private static String getFirstHeader(NativeWebRequest req, String headerName) { + String[] headerValues = req.getHeaderValues(headerName); + final String value; + if (null != headerValues && headerValues.length > 0) { + value = headerValues[0]; + } else { + value = null; + } + return value; + } + + /** + * Applies the {@literal X-Forwarded-Prefix} header prefix to a full URL, if provided in the + * request + */ + static String customizeUrl(String url, NativeWebRequest req) { + String path = URI.create(url).getPath(); + UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url); + String prefixedPath = applyForwardedPrefix(path, req); + builder.replacePath(prefixedPath); + return builder.build().toString(); } } diff --git a/src/artifacts/api/src/main/java/org/geoserver/acl/autoconfigure/springdoc/SpringDocHomeRedirectController.java b/src/artifacts/api/src/main/java/org/geoserver/acl/autoconfigure/springdoc/SpringDocHomeRedirectController.java index d5348c0..9be63b8 100644 --- a/src/artifacts/api/src/main/java/org/geoserver/acl/autoconfigure/springdoc/SpringDocHomeRedirectController.java +++ b/src/artifacts/api/src/main/java/org/geoserver/acl/autoconfigure/springdoc/SpringDocHomeRedirectController.java @@ -4,23 +4,29 @@ */ package org.geoserver.acl.autoconfigure.springdoc; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.util.UriComponentsBuilder; -@Controller -public class SpringDocHomeRedirectController { +import javax.servlet.http.HttpServletRequest; - private String basePath; +@Controller +@RequiredArgsConstructor +class SpringDocHomeRedirectController { - /** - * @param basePath e.g. {@literal /swagger-ui/index.html"} - */ - public SpringDocHomeRedirectController(String basePath) { - this.basePath = basePath; - } + private final @NonNull NativeWebRequest req; @GetMapping(value = "/") public String redirectToSwaggerUI() { - return "redirect:" + basePath; + String url = ((HttpServletRequest) req.getNativeRequest()).getRequestURL().toString(); + UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url); + builder.path("openapi/swagger-ui/index.html"); + String fullUrl = builder.build().toString(); + String xForwardedPrefixUrl = SpringDocAutoConfiguration.customizeUrl(fullUrl, req); + return "redirect:" + xForwardedPrefixUrl; } } diff --git a/src/artifacts/api/src/main/resources/application.yml b/src/artifacts/api/src/main/resources/application.yml index 5d6d80f..a6434bb 100644 --- a/src/artifacts/api/src/main/resources/application.yml +++ b/src/artifacts/api/src/main/resources/application.yml @@ -11,10 +11,9 @@ info: # or through environment variables or system properties. server: port: 8080 - servlet: - context-path: /acl + servlet.context-path: /acl # Let spring-boot's ForwardedHeaderFilter take care of reflecting the client-originated protocol and address in the HttpServletRequest - forward-headers-strategy: framework + forward-headers-strategy: native error: # one of never, always, on_trace_param (deprecated), on_param include-stacktrace: on-param @@ -43,6 +42,8 @@ server: spring: config: import: + - classpath:/springdoc.yml + - classpath:/security.yml - classpath:/values.yml - optional:file:/etc/geoserver/acl-service.yml main: @@ -89,9 +90,17 @@ spring: management: server: - base-path: /acl port: 8081 + info: + defaults.enabled: true + env.enabled: true + build.enabled: true + git.enabled: true + java.enabled: true + os.enabled: true endpoint: + httptrace: + enabled: true health: probes: enabled: true @@ -101,20 +110,6 @@ management: endpoints: web.exposure.include: ['*'] -springdoc: - # see https://springdoc.org/#how-can-i-disable-springdoc-openapi-cache - cache.disabled: true - api-docs: - enabled: true - path: /api-docs - swagger-ui: - enabled: true - #results in /swagger-ui/index.html - path: /index.html - disable-swagger-default-url: true - try-it-out-enabled: true - tags-sorter: alpha - jndi: datasources: acl: @@ -150,36 +145,6 @@ geoserver: default_schema: ${pg.schema} hbm2ddl.auto: ${acl.db.hbm2ddl.auto:validate} dialect: ${acl.db.dialect:org.hibernate.spatial.dialect.postgis.PostgisPG10Dialect} - security: - headers: - enabled: ${acl.security.headers.enabled} - user-header: ${acl.security.headers.user-header} - roles-header: ${acl.security.headers.roles-header} - admin-roles: ${acl.security.headers.admin-roles} - internal: - enabled: ${acl.security.basic.enabled} - users: - admin: - admin: true - enabled: ${acl.users.admin.enabled} - password: "${acl.users.admin.password}" - # the following sample password is the bcrypt encoded value, for example, for pwd s3cr3t: - # password: "{bcrypt}$2a$10$eMyaZRLZBAZdor8nOX.qwuwOyWazXjR2hddGLCT6f6c382WiwdQGG" - geoserver: - # special user for GeoServer to ACL communication - # Using a `{noop}` default credentials for performance, since bcrypt adds a significant per-request overhead - # in the orther of 100ms. In production it should be replaced by a docker/k8s secret. To simplify defining and - # reusing secrets for both the server and client config, a noop encrypted password is allowed not to have the - # {noop} prefix. - admin: true - enabled: ${acl.users.geoserver.enabled} - password: "${acl.users.geoserver.password}" -# Sample non-admin user: -# user: -# admin: false -# enabled: true -# # password is the bcrypt encoded value for s3cr3t -# password: "{noop}changeme" --- spring.config.activate.on-profile: local @@ -245,3 +210,9 @@ logging: level: root: error org.geoserver.acl: info + +--- +spring.config.activate.on-profile: logging_debug_requests +logging: + level: + org.springframework.web.filter.CommonsRequestLoggingFilter: trace diff --git a/src/artifacts/api/src/main/resources/security.yml b/src/artifacts/api/src/main/resources/security.yml new file mode 100644 index 0000000..9eeceaf --- /dev/null +++ b/src/artifacts/api/src/main/resources/security.yml @@ -0,0 +1,32 @@ +geoserver: + acl: + security: + headers: + enabled: ${acl.security.headers.enabled} + user-header: ${acl.security.headers.user-header} + roles-header: ${acl.security.headers.roles-header} + admin-roles: ${acl.security.headers.admin-roles} + internal: + enabled: ${acl.security.basic.enabled} + users: + admin: + admin: true + enabled: ${acl.users.admin.enabled} + password: "${acl.users.admin.password}" + # the following sample password is the bcrypt encoded value, for example, for pwd s3cr3t: + # password: "{bcrypt}$2a$10$eMyaZRLZBAZdor8nOX.qwuwOyWazXjR2hddGLCT6f6c382WiwdQGG" + geoserver: + # special user for GeoServer to ACL communication + # Using a `{noop}` default credentials for performance, since bcrypt adds a significant per-request overhead + # in the orther of 100ms. In production it should be replaced by a docker/k8s secret. To simplify defining and + # reusing secrets for both the server and client config, a noop encrypted password is allowed not to have the + # {noop} prefix. + admin: true + enabled: ${acl.users.geoserver.enabled} + password: "${acl.users.geoserver.password}" +# Sample non-admin user: +# user: +# admin: false +# enabled: true +# # password is the bcrypt encoded value for s3cr3t +# password: "{noop}changeme" diff --git a/src/artifacts/api/src/main/resources/springdoc.yml b/src/artifacts/api/src/main/resources/springdoc.yml new file mode 100644 index 0000000..82e2bb9 --- /dev/null +++ b/src/artifacts/api/src/main/resources/springdoc.yml @@ -0,0 +1,21 @@ +#springdoc.swagger-ui.disable-swagger-default-url: true +#springdoc.swagger-ui.configUrl: /openapi/swagger-config +#springdoc.swagger-ui.url: /openapi +#springdoc.api-docs.path: /openapi + +geoserver.base-path: /geoserver/cloud +springdoc: + # see https://springdoc.org/#how-can-i-disable-springdoc-openapi-cache + cache.disabled: true + api-docs: + enabled: true + path: /openapi + swagger-ui: + enabled: true + disable-swagger-default-url: true + url: /openapi + path: /openapi/ + try-it-out-enabled: true + tags-sorter: alpha + syntax-highlight: + activated: true diff --git a/src/openapi/acl-api.yaml b/src/openapi/acl-api.yaml index 2d81729..93b9407 100644 --- a/src/openapi/acl-api.yaml +++ b/src/openapi/acl-api.yaml @@ -4,8 +4,7 @@ info: description: GeoServer Access Control List API version: 1.0.0 servers: -- url: http://localhost:8080/api -- url: http://localhost:8080/geoserver/rest/acl +- url: /api tags: - name: DataRules description: CRUD operations on GeoServer ACL Rules.