From 68a90ddb4c11d81ad863e67e2d6a78d06b0b37f5 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 16 Sep 2026 11:40:04 -0400 Subject: [PATCH 1/4] upgrade to Payara 7.2026.9 #12715 --- doc/release-notes/6.12-release-notes.md | 12 ++++++------ .../source/installation/prerequisites.rst | 6 +++--- modules/dataverse-parent/pom.xml | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/doc/release-notes/6.12-release-notes.md b/doc/release-notes/6.12-release-notes.md index b50a5be5ebf..a3b6243233e 100644 --- a/doc/release-notes/6.12-release-notes.md +++ b/doc/release-notes/6.12-release-notes.md @@ -41,7 +41,7 @@ These are features that weren't already mentioned under "highlights" above. ## Other Changes -- Payara 7.2026.8 is now the supported and recommended version. See upgrade instructions below and #12498. +- Payara 7.2026.9 is now the supported and recommended version. See upgrade instructions below, #12498 and #12715. - Database indexes were added to the guestbookresponse table to speed up queries. See #12616. - The QA Guide was added as an experiment in #10103 but has been removed because it has proved less useful than imagined. It can still be [viewed](https://guides.dataverse.org/en/6.11/qa/index.html) as it existed as of Dataverse 6.11. See #12582. @@ -164,7 +164,7 @@ If you are running Payara as a non-root user (and you should be!), **remember no Also, we assume that Payara is installed in `/usr/local/payara7`. If not, adjust as needed. -The instructions below describe the upgrade procedure based on moving your existing Payara 7.2026.2 domain directory into the new Payara 7.2026.8 distribution. We recommend this method because it is the easiest way to recreate your current configuration and preserve your data. +The instructions below describe the upgrade procedure based on moving your existing Payara 7.2026.2 domain directory into the new Payara 7.2026.9 distribution. We recommend this method because it is the easiest way to recreate your current configuration and preserve your data. 1. Undeploy Dataverse, if deployed, using the unprivileged service account ("dataverse", by default). @@ -188,12 +188,12 @@ The instructions below describe the upgrade procedure based on moving your exist sudo mv /usr/local/payara7 /usr/local/payara7-2026.2 ``` -1. Download the new Payara version 7.2026.8, and unzip it. +1. Download the new Payara version 7.2026.9, and unzip it. ```shell - curl -L -O https://nexus.payara.fish/repository/payara-community/fish/payara/distributions/payara/7.2026.8/payara-7.2026.8.zip + curl -L -O https://nexus.payara.fish/repository/payara-community/fish/payara/distributions/payara/7.2026.9/payara-7.2026.9.zip - sudo unzip payara-7.2026.8.zip -d /usr/local/ + sudo unzip payara-7.2026.9.zip -d /usr/local/ ``` 1. Set permission for the service account ("dataverse" by default). @@ -223,7 +223,7 @@ The instructions below describe the upgrade procedure based on moving your exist 1. Update configuration files in the moved domain. - The file `glassfish-acc.xml` from Payara 7.2026.2 has been renamed to `payara-acc.xml` in Payara 7.2026.8. Also `default-logging.properties` has been updated in the Payara 7.2026.8. We recommend copying these new files from the Payara 7.2026.8 distribution domain into the domain you copied over from your Payara 7.2026.2 installation. + The file `glassfish-acc.xml` from Payara 7.2026.2 has been renamed to `payara-acc.xml` in Payara 7.2026.9. Also `default-logging.properties` has been updated in the Payara 7.2026.9. We recommend copying these new files from the Payara 7.2026.9 distribution domain into the domain you copied over from your Payara 7.2026.2 installation. ```shell sudo rm /usr/local/payara7/glassfish/domains/domain1/config/glassfish-acc.xml diff --git a/doc/sphinx-guides/source/installation/prerequisites.rst b/doc/sphinx-guides/source/installation/prerequisites.rst index 1d7cfa99081..00203b77d9b 100644 --- a/doc/sphinx-guides/source/installation/prerequisites.rst +++ b/doc/sphinx-guides/source/installation/prerequisites.rst @@ -44,7 +44,7 @@ On RHEL/derivative you can make Java 21 the default with the ``alternatives`` co Payara ------ -Payara 7.2026.8 is recommended. Newer versions might work fine. Regular updates are recommended. +Payara 7.2026.9 is recommended. Newer versions might work fine. Regular updates are recommended. Installing Payara ================= @@ -55,8 +55,8 @@ Installing Payara - Download and install Payara (installed in ``/usr/local/payara7`` in the example commands below):: - # wget https://nexus.payara.fish/repository/payara-community/fish/payara/distributions/payara/7.2026.8/payara-7.2026.8.zip - # unzip payara-7.2026.8.zip + # wget https://nexus.payara.fish/repository/payara-community/fish/payara/distributions/payara/7.2026.9/payara-7.2026.9.zip + # unzip payara-7.2026.9.zip # mv payara7 /usr/local If nexus.payara.fish is ever down for maintenance, Payara distributions are also available from https://repo1.maven.org/maven2/fish/payara/distributions/payara/ diff --git a/modules/dataverse-parent/pom.xml b/modules/dataverse-parent/pom.xml index 2cd63f57da0..49feb52b7bb 100644 --- a/modules/dataverse-parent/pom.xml +++ b/modules/dataverse-parent/pom.xml @@ -149,7 +149,7 @@ -Duser.timezone=${project.timezone} -Dfile.encoding=${project.build.sourceEncoding} -Duser.language=${project.language} -Duser.region=${project.region} - 7.2026.8 + 7.2026.9 42.7.12 9.8.0 16 From ac6f078115b881a7b95c4cf0aa05c2ae5f7f8739 Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Wed, 16 Sep 2026 12:48:46 -0400 Subject: [PATCH 2/4] @Context to Inject --- .../harvard/iq/dataverse/api/filter/ApiBlockingFilter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFilter.java b/src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFilter.java index 84497b479d4..8ad36ade32d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFilter.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFilter.java @@ -56,10 +56,10 @@ public class ApiBlockingFilter implements ContainerRequestFilter { @Inject private PasswordValidatorServiceBean passwordValidatorService; - @Context + @Inject private ResourceInfo resourceInfo; - @Context + @Inject private HttpServletRequest httpServletRequest; private String policy = null; From a33f3560df81308877e801299af60b8ca6cf2568 Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Wed, 16 Sep 2026 13:09:14 -0400 Subject: [PATCH 3/4] ResourceProvider to address weld error --- .../harvard/iq/dataverse/api/filter/ApiBlockingFilter.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFilter.java b/src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFilter.java index 8ad36ade32d..9fe8fe1111a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFilter.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFilter.java @@ -8,14 +8,13 @@ import edu.harvard.iq.dataverse.validation.PasswordValidatorServiceBean; import jakarta.annotation.PostConstruct; import jakarta.inject.Inject; -import jakarta.json.Json; + import jakarta.json.JsonObject; import jakarta.servlet.http.HttpServletRequest; import jakarta.ws.rs.Path; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; import jakarta.ws.rs.container.ResourceInfo; -import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; import java.io.IOException; @@ -57,7 +56,7 @@ public class ApiBlockingFilter implements ContainerRequestFilter { private PasswordValidatorServiceBean passwordValidatorService; @Inject - private ResourceInfo resourceInfo; + private jakarta.inject.Provider resourceProvider; @Inject private HttpServletRequest httpServletRequest; @@ -118,7 +117,7 @@ public void init() { @Override public void filter(ContainerRequestContext requestContext) throws IOException { - + ResourceInfo resourceInfo = resourceProvider.get(); Method method = resourceInfo.getResourceMethod(); Class clazz = resourceInfo.getResourceClass(); From 06e6226c38ab613d559c83ab4626122f0b9c0811 Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Wed, 16 Sep 2026 13:56:14 -0400 Subject: [PATCH 4/4] Dynamic Feature --- .../api/filter/ApiBlockingFeature.java | 250 ++++++++++++++++++ .../api/filter/ApiBlockingFilter.java | 234 +--------------- 2 files changed, 263 insertions(+), 221 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java diff --git a/src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java b/src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java new file mode 100644 index 00000000000..d5651f01e55 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java @@ -0,0 +1,250 @@ +package edu.harvard.iq.dataverse.api.filter; + +import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.json.JsonUtil; +import edu.harvard.iq.dataverse.validation.PasswordValidatorServiceBean; +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.json.JsonObject; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.DynamicFeature; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.FeatureContext; +import jakarta.ws.rs.ext.Provider; +import org.eclipse.jetty.util.StringUtil; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +/** + * A DynamicFeature that registers ApiBlockingFilter for Dataverse API endpoints. + * This approach avoids the need to inject ResourceInfo at request time, which + * is no longer supported via @Context in Jakarta EE 11 / Payara 7. + */ +@Provider +public class ApiBlockingFeature implements DynamicFeature { + + private static final Logger logger = Logger.getLogger(ApiBlockingFeature.class.getName()); + + @Inject + private SettingsServiceBean settingsService; + + @Inject + private PasswordValidatorServiceBean passwordValidatorService; + + @Inject + private HttpServletRequest httpServletRequest; + + // Policies + private static final String DROP = "drop"; + private static final String LOCALHOST_ONLY = "localhost-only"; + private static final String UNBLOCK_KEY = "unblock-key"; + + private static final Map POLICY_ERROR_MESSAGES = new HashMap<>(); + static { + POLICY_ERROR_MESSAGES.put(DROP, "Endpoint blocked. Access denied."); + POLICY_ERROR_MESSAGES.put(LOCALHOST_ONLY, "Endpoint restricted to localhost access only."); + POLICY_ERROR_MESSAGES.put(UNBLOCK_KEY, "Endpoint requires an unblock key for access."); + } + + private String policy = null; + private JsonObject errorJson = null; + private volatile List blockedApiEndpointPatterns = new ArrayList<>(); + private String key; + + // If any of the JvmSettings are not set, revert to checking the db settings on every call + private boolean checkSettings = false; + private String endpointList = null; + + @PostConstruct + public void init() { + // Check JvmSettings first for BlockedApiPolicy + policy = JvmSettings.API_BLOCKED_POLICY.lookupOptional().orElse(settingsService.getValueForKey(SettingsServiceBean.Key.BlockedApiPolicy, DROP)); + + if(!(DROP.equals(policy) || LOCALHOST_ONLY.equals(policy) || UNBLOCK_KEY.equals(policy))) { + logger.severe("Invalid BlockedApiPolicy setting: " + policy + ". Using policy 'drop'"); + policy = DROP; + } + Optional jvmEndpointList = JvmSettings.API_BLOCKED_ENDPOINTS.lookupOptional(); + if (!jvmEndpointList.isPresent()) { + checkSettings = true; + } + endpointList = jvmEndpointList + .orElse(settingsService.getValueForKey(SettingsServiceBean.Key.BlockedApiEndpoints, "")); + logger.info("Using policy: " + policy + " to block API endpoints: " + endpointList); + if (!(endpointList.contains("admin") && endpointList.contains("builtin-users"))) { + logger.warning( + "Not blocking admin and builtin-user endpoints is a security issue unless you are blocking them in an external proxy."); + } + if (UNBLOCK_KEY.equals(policy)) { + Optional jvmKey = JvmSettings.API_BLOCKED_KEY.lookupOptional(); + if (!jvmKey.isPresent()) { + checkSettings = true; + } + key = jvmKey.orElse(settingsService.getValueForKey(SettingsServiceBean.Key.BlockedApiKey)); + if (StringUtil.isBlank(key)) { + logger.severe( + "Using unblock-key policy and no unblock key found in JvmSettings.API_BLOCKED_KEY or SettingsService.BlockedApiKey"); + } else if (passwordValidatorService.validate(key).size() == 0) { + logger.warning("Weak unblock key detected. Please use a stronger key for better security."); + } + } + updateBlockedPoints(endpointList); + if(checkSettings) { + logger.warning("Not all required dataverse.api.blocked.* settings not found. Dataverse use deprecated db settings and check for updates on every API call."); + } + } + + @Override + public void configure(ResourceInfo resourceInfo, FeatureContext context) { + Class clazz = resourceInfo.getResourceClass(); + Method method = resourceInfo.getResourceMethod(); + + if (clazz.getName().startsWith("edu.harvard.iq.dataverse")) { + String classPath = ""; + String methodPath = ""; + + if (clazz.isAnnotationPresent(Path.class)) { + classPath = clazz.getAnnotation(Path.class).value(); + } + + if (method.isAnnotationPresent(Path.class)) { + methodPath = method.getAnnotation(Path.class).value(); + } + + String fullPath = (classPath + "/" + methodPath).replaceAll("//", "/"); + context.register(new ApiBlockingFilter(this, fullPath)); + } + } + + public boolean shouldBlock(String fullPath, ContainerRequestContext requestContext) { + if (checkSettings) { + updateSettingsIfChanged(); + } + + boolean isBlockableEndpoint = false; + List currentPatterns = blockedApiEndpointPatterns; + for (Pattern blockedEndpointPattern : currentPatterns) { + if (blockedEndpointPattern.matcher(fullPath).matches()) { + isBlockableEndpoint = true; + break; + } + } + + if (!isBlockableEndpoint) { + return false; + } + + // Blockable endpoint - now check policy + return isBlocked(policy, requestContext); + } + + private synchronized void updateSettingsIfChanged() { + // Backward compatibility, e.g. for setup scripts, dev environments where + // dynamic update from the db settings is expected + String newPolicy = settingsService.getValueForKey(SettingsServiceBean.Key.BlockedApiPolicy, + JvmSettings.API_BLOCKED_POLICY.lookupOptional().orElse(DROP)); + String newEndpointList = settingsService.getValueForKey(SettingsServiceBean.Key.BlockedApiEndpoints, + JvmSettings.API_BLOCKED_ENDPOINTS.lookupOptional().orElse("")); + + boolean changed = false; + if (!newPolicy.equals(policy)) { + policy = newPolicy; + changed = true; + } + if (!endpointList.equals(newEndpointList)) { + endpointList = newEndpointList; + changed = true; + } + + if (changed) { + updateBlockedPoints(endpointList); + } + + if (policy.equals(UNBLOCK_KEY)) { + key = settingsService.getValueForKey(SettingsServiceBean.Key.BlockedApiKey, + JvmSettings.API_BLOCKED_KEY.lookupOptional().orElse("")); + if (StringUtil.isBlank(key)) { + logger.severe( + "Using unblock-key policy and no unblock key found in JvmSettings.API_BLOCKED_KEY or SettingsService.BlockedApiKey"); + } + } + } + + private boolean isBlocked(String policy, ContainerRequestContext requestContext) { + switch (policy) { + case DROP: + return true; + case LOCALHOST_ONLY: + if (httpServletRequest == null) { + logger.warning("Unable to obtain HttpServletRequest"); + return true; + } + IpAddress origin = new DataverseRequest(null, httpServletRequest).getSourceAddress(); + if (!origin.isLocalhost()) { + return true; + } + break; + case UNBLOCK_KEY: + String providedKey = requestContext.getHeaderString(ApiBlockingFilter.UNBLOCK_KEY_HEADER); + if (StringUtil.isBlank(providedKey)) { + providedKey = requestContext.getUriInfo().getQueryParameters().getFirst(ApiBlockingFilter.UNBLOCK_KEY_QUERYPARAM); + } + // Must have a non-blank key defined and the query param must match it + if (StringUtil.isNotBlank(key) && key.equals(providedKey)) { + return false; + } + return true; + } + return false; + } + + private void updateBlockedPoints(String endpointList) { + List newPatterns = new ArrayList<>(); + + String currentErrorMessage = POLICY_ERROR_MESSAGES.getOrDefault(policy, + "Endpoint blocked. Please contact the dataverse administrator."); + + errorJson = JsonUtil.createObjectBuilder().add("status", "error").add("message", currentErrorMessage).build(); + + for (String endpoint : endpointList.split(",")) { + String endpointPrefix = canonicalize(endpoint); + if (!endpointPrefix.isEmpty()) { + logger.log(Level.INFO, "Blocking API endpoint: {0}", endpointPrefix); + newPatterns.add(Pattern.compile(convertPathToRegex(endpointPrefix))); + } + } + blockedApiEndpointPatterns = newPatterns; + } + + private String convertPathToRegex(String path) { + return "^" + path.replaceAll("\\{[^}]+\\}", "[^/]+").replace("/", "\\/") + "(\\/.*)?$"; + } + + private String canonicalize(String in) { + in = in.trim(); + if (in.startsWith("/")) { + in = in.substring(1); + } + if (in.endsWith("/")) { + in = in.substring(0, in.length() - 1); + } + return in; + } + + public JsonObject getErrorJson() { + return errorJson; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFilter.java b/src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFilter.java index 9fe8fe1111a..0285fae3410 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFilter.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFilter.java @@ -1,245 +1,37 @@ package edu.harvard.iq.dataverse.api.filter; -import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress; -import edu.harvard.iq.dataverse.engine.command.DataverseRequest; -import edu.harvard.iq.dataverse.settings.JvmSettings; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; -import edu.harvard.iq.dataverse.util.json.JsonUtil; -import edu.harvard.iq.dataverse.validation.PasswordValidatorServiceBean; -import jakarta.annotation.PostConstruct; -import jakarta.inject.Inject; - -import jakarta.json.JsonObject; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.ws.rs.Path; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; -import jakarta.ws.rs.container.ResourceInfo; import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.ext.Provider; import java.io.IOException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.logging.Level; import java.util.logging.Logger; -import java.util.regex.Pattern; - -import org.eclipse.jetty.util.StringUtil; -@Provider +/** + * Filter registered by ApiBlockingFeature to block specific Dataverse API endpoints. + */ public class ApiBlockingFilter implements ContainerRequestFilter { private static final Logger logger = Logger.getLogger(ApiBlockingFilter.class.getName()); public static final String UNBLOCK_KEY_QUERYPARAM = "unblock-key"; public static final String UNBLOCK_KEY_HEADER = "X-Dataverse-unblock-key"; - // Policies - private static final String DROP = "drop"; - private static final String LOCALHOST_ONLY = "localhost-only"; - private static final String UNBLOCK_KEY = "unblock-key"; - - private static final Map POLICY_ERROR_MESSAGES = new HashMap<>(); - static { - POLICY_ERROR_MESSAGES.put(DROP, "Endpoint blocked. Access denied."); - POLICY_ERROR_MESSAGES.put(LOCALHOST_ONLY, "Endpoint restricted to localhost access only."); - POLICY_ERROR_MESSAGES.put(UNBLOCK_KEY, "Endpoint requires an unblock key for access."); - } - - @Inject - private SettingsServiceBean settingsService; - - @Inject - private PasswordValidatorServiceBean passwordValidatorService; - - @Inject - private jakarta.inject.Provider resourceProvider; - - @Inject - private HttpServletRequest httpServletRequest; - - private String policy = null; - - private JsonObject errorJson = null; - - private List blockedApiEndpointPatterns = new ArrayList<>(); - - private String key; - // If any of the JvmSettings are not set, revert to checking the db settings on - // every call - private boolean checkSettings = false; - - private String endpointList = null; - - @PostConstruct - public void init() { - // Check JvmSettings first for BlockedApiPolicy - policy = JvmSettings.API_BLOCKED_POLICY.lookupOptional().orElse(settingsService.getValueForKey(SettingsServiceBean.Key.BlockedApiPolicy, DROP)); - - if(!(DROP.equals(policy) || LOCALHOST_ONLY.equals(policy) || UNBLOCK_KEY.equals(policy))) { - logger.severe("Invalid BlockedApiPolicy setting: " + policy + ". Using policy 'drop'"); - policy = DROP; - } - Optional jvmEndpointList = JvmSettings.API_BLOCKED_ENDPOINTS.lookupOptional(); - if (!jvmEndpointList.isPresent()) { - checkSettings = true; - } - endpointList = jvmEndpointList - .orElse(settingsService.getValueForKey(SettingsServiceBean.Key.BlockedApiEndpoints, "")); - logger.info("Using policy: " + policy + " to block API endpoints: " + endpointList); - if (!(endpointList.contains("admin") && endpointList.contains("builtin-users"))) { - logger.warning( - "Not blocking admin and builtin-user endpoints is a security issue unless you are blocking them in an external proxy."); - } - if (UNBLOCK_KEY.equals(policy)) { - Optional jvmKey = JvmSettings.API_BLOCKED_KEY.lookupOptional(); - if (!jvmKey.isPresent()) { - checkSettings = true; - } - key = jvmKey.orElse(settingsService.getValueForKey(SettingsServiceBean.Key.BlockedApiKey)); - if (StringUtil.isBlank(key)) { - logger.severe( - "Using unblock-key policy and no unblock key found in JvmSettings.API_BLOCKED_KEY or SettingsService.BlockedApiKey"); - } else if (passwordValidatorService.validate(key).size() == 0) { - logger.warning("Weak unblock key detected. Please use a stronger key for better security."); - } - } - updateBlockedPoints(endpointList); - if(checkSettings) { - logger.warning("Not all required dataverse.api.blocked.* settings not found. Dataverse use deprecated db settings and check for updates on every API call."); - } + private final ApiBlockingFeature feature; + private final String fullPath; + public ApiBlockingFilter(ApiBlockingFeature feature, String fullPath) { + this.feature = feature; + this.fullPath = fullPath; } @Override public void filter(ContainerRequestContext requestContext) throws IOException { - ResourceInfo resourceInfo = resourceProvider.get(); - Method method = resourceInfo.getResourceMethod(); - Class clazz = resourceInfo.getResourceClass(); - - String classPath = ""; - String methodPath = ""; - - if (clazz.isAnnotationPresent(Path.class)) { - classPath = clazz.getAnnotation(Path.class).value(); - } - - if (method.isAnnotationPresent(Path.class)) { - methodPath = method.getAnnotation(Path.class).value(); - } - - if (checkSettings) { - // Backward compatibility, e.g. for setup scripts, dev environments where - // dynamic update from the db settings is expected - policy = settingsService.getValueForKey(SettingsServiceBean.Key.BlockedApiPolicy, - JvmSettings.API_BLOCKED_POLICY.lookupOptional().orElse(DROP)); - String newEndpointList = settingsService.getValueForKey(SettingsServiceBean.Key.BlockedApiEndpoints, - JvmSettings.API_BLOCKED_ENDPOINTS.lookupOptional().orElse("")); - if (!endpointList.equals(newEndpointList)) { - endpointList = newEndpointList; - updateBlockedPoints(endpointList); - } - if (policy.equals(UNBLOCK_KEY)) { - key = settingsService.getValueForKey(SettingsServiceBean.Key.BlockedApiKey, - JvmSettings.API_BLOCKED_KEY.lookupOptional().orElse("")); - if (StringUtil.isBlank(key)) { - logger.severe( - "Using unblock-key policy and no unblock key found in JvmSettings.API_BLOCKED_KEY or SettingsService.BlockedApiKey"); - } - } - } - String fullPath = (classPath + "/" + methodPath).replaceAll("//", "/"); - logger.fine("Full path is " + fullPath); - - boolean isBlockableEndpoint = false; - for (Pattern blockedEndpointPattern : blockedApiEndpointPatterns) { - if (blockedEndpointPattern.matcher(fullPath).matches()) { - isBlockableEndpoint = true; - break; - } - } - if (!isBlockableEndpoint) { - return; - } - // Blocakble endpoint - now check policy - if (isBlocked(policy, requestContext)) { + if (feature.shouldBlock(fullPath, requestContext)) { logger.fine("Blocked " + fullPath); - requestContext.abortWith(Response.status(Response.Status.SERVICE_UNAVAILABLE).entity(errorJson) - .type(jakarta.ws.rs.core.MediaType.APPLICATION_JSON).build()); - return; - } - } - - private boolean isBlocked(String policy, ContainerRequestContext requestContext) { - switch (policy) { - case DROP: - return true; - case LOCALHOST_ONLY: - if (httpServletRequest == null) { - logger.warning("Unable to obtain HttpServletRequest from ContainerRequestContext"); - // Handle the case where HttpServletRequest is not available - return true; - } - IpAddress origin = new DataverseRequest(null, httpServletRequest).getSourceAddress(); - if (!origin.isLocalhost()) { - return true; - } - break; - case UNBLOCK_KEY: - String providedKey = requestContext.getHeaderString(UNBLOCK_KEY_HEADER); - if (StringUtil.isBlank(providedKey)) { - providedKey = requestContext.getUriInfo().getQueryParameters().getFirst(UNBLOCK_KEY_QUERYPARAM); - } - // Must have a non-blank key defined and the query param must match it - if (StringUtil.isNotBlank(key) && key.equals(providedKey)) { - return false; - } - // Otherwise we have a blocked endpoint and the key doesn't work (not set or - // doesn't match what's sent) - return true; - } - return false; - } - - private void updateBlockedPoints(String endpointList) { - blockedApiEndpointPatterns.clear(); - - String currentErrorMessage = POLICY_ERROR_MESSAGES.getOrDefault(policy, - "Endpoint blocked. Please contact the dataverse administrator."); - - errorJson = JsonUtil.createObjectBuilder().add("status", "error").add("message", currentErrorMessage).build(); - - for (String endpoint : endpointList.split(",")) { - String endpointPrefix = canonicalize(endpoint); - if (!endpointPrefix.isEmpty()) { - logger.log(Level.INFO, "Blocking API endpoint: {0}", endpointPrefix); - blockedApiEndpointPatterns.add(Pattern.compile(convertPathToRegex(endpointPrefix))); - } - } - } - - private String convertPathToRegex(String path) { - return "^" + path.replaceAll("\\{[^}]+\\}", "[^/]+").replace("/", "\\/") + "(\\/.*)?$"; - } - - /** - * Creates a canonical representation of {@code in}: trimmed spaces and slashes - * - * @param in the raw string - * @return {@code in} with no trailing and leading spaces and slashes. - */ - private String canonicalize(String in) { - in = in.trim(); - if (in.startsWith("/")) { - in = in.substring(1); - } - if (in.endsWith("/")) { - in = in.substring(0, in.length() - 1); + requestContext.abortWith(Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(feature.getErrorJson()) + .type(jakarta.ws.rs.core.MediaType.APPLICATION_JSON) + .build()); } - return in; } } \ No newline at end of file