Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions doc/release-notes/6.12-release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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).

Expand All @@ -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).

Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions doc/sphinx-guides/source/installation/prerequisites.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
=================
Expand All @@ -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/
Expand Down
2 changes: 1 addition & 1 deletion modules/dataverse-parent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@
<argLine>-Duser.timezone=${project.timezone} -Dfile.encoding=${project.build.sourceEncoding} -Duser.language=${project.language} -Duser.region=${project.region}</argLine>

<!-- Major system components and dependencies -->
<payara.version>7.2026.8</payara.version>
<payara.version>7.2026.9</payara.version>
<postgresql.version>42.7.12</postgresql.version>
<solr.version>9.8.0</solr.version>
<postgresql.server.version>16</postgresql.server.version>
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Check warning on line 41 in src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this field injection and use constructor injection instead.

See more on https://sonarcloud.io/project/issues?id=IQSS_dataverse&issues=AaCrfgY3Yq4L2Ok9N-Gq&open=AaCrfgY3Yq4L2Ok9N-Gq&pullRequest=12716
private SettingsServiceBean settingsService;

@Inject

Check warning on line 44 in src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this field injection and use constructor injection instead.

See more on https://sonarcloud.io/project/issues?id=IQSS_dataverse&issues=AaCrfgY3Yq4L2Ok9N-Gr&open=AaCrfgY3Yq4L2Ok9N-Gr&pullRequest=12716
private PasswordValidatorServiceBean passwordValidatorService;

@Inject

Check warning on line 47 in src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this field injection and use constructor injection instead.

See more on https://sonarcloud.io/project/issues?id=IQSS_dataverse&issues=AaCrfgY3Yq4L2Ok9N-Gs&open=AaCrfgY3Yq4L2Ok9N-Gs&pullRequest=12716
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<String, String> 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<Pattern> blockedApiEndpointPatterns = new ArrayList<>();

Check warning on line 64 in src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a thread-safe type; adding "volatile" is not enough to make this field thread-safe.

See more on https://sonarcloud.io/project/issues?id=IQSS_dataverse&issues=AaCrfgY3Yq4L2Ok9N-Gt&open=AaCrfgY3Yq4L2Ok9N-Gt&pullRequest=12716
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));

Check warning on line 74 in src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this call to a deprecated field, it has been marked for removal.

See more on https://sonarcloud.io/project/issues?id=IQSS_dataverse&issues=AaCrfgY3Yq4L2Ok9N-Gu&open=AaCrfgY3Yq4L2Ok9N-Gu&pullRequest=12716

if(!(DROP.equals(policy) || LOCALHOST_ONLY.equals(policy) || UNBLOCK_KEY.equals(policy))) {
logger.severe("Invalid BlockedApiPolicy setting: " + policy + ". Using policy 'drop'");

Check warning on line 77 in src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the built-in formatting to construct this argument.

See more on https://sonarcloud.io/project/issues?id=IQSS_dataverse&issues=AaCrfgY3Yq4L2Ok9N-Go&open=AaCrfgY3Yq4L2Ok9N-Go&pullRequest=12716
policy = DROP;
}
Optional<String> jvmEndpointList = JvmSettings.API_BLOCKED_ENDPOINTS.lookupOptional();
if (!jvmEndpointList.isPresent()) {
checkSettings = true;
}
endpointList = jvmEndpointList
.orElse(settingsService.getValueForKey(SettingsServiceBean.Key.BlockedApiEndpoints, ""));

Check warning on line 85 in src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this call to a deprecated field, it has been marked for removal.

See more on https://sonarcloud.io/project/issues?id=IQSS_dataverse&issues=AaCrfgY3Yq4L2Ok9N-Gv&open=AaCrfgY3Yq4L2Ok9N-Gv&pullRequest=12716
logger.info("Using policy: " + policy + " to block API endpoints: " + endpointList);

Check warning on line 86 in src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the built-in formatting to construct this argument.

See more on https://sonarcloud.io/project/issues?id=IQSS_dataverse&issues=AaCrfgY3Yq4L2Ok9N-Gp&open=AaCrfgY3Yq4L2Ok9N-Gp&pullRequest=12716
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<String> jvmKey = JvmSettings.API_BLOCKED_KEY.lookupOptional();
if (!jvmKey.isPresent()) {
checkSettings = true;
}
key = jvmKey.orElse(settingsService.getValueForKey(SettingsServiceBean.Key.BlockedApiKey));

Check warning on line 96 in src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this call to a deprecated field, it has been marked for removal.

See more on https://sonarcloud.io/project/issues?id=IQSS_dataverse&issues=AaCrfgY3Yq4L2Ok9N-Gw&open=AaCrfgY3Yq4L2Ok9N-Gw&pullRequest=12716
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) {

Check warning on line 100 in src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use isEmpty() to check whether the collection is empty or not.

See more on https://sonarcloud.io/project/issues?id=IQSS_dataverse&issues=AaCrfgY3Yq4L2Ok9N-Gx&open=AaCrfgY3Yq4L2Ok9N-Gx&pullRequest=12716
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("//", "/");

Check failure on line 127 in src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this call to "replaceAll()" by a call to the "replace()" method.

See more on https://sonarcloud.io/project/issues?id=IQSS_dataverse&issues=AaCrfgY3Yq4L2Ok9N-Gz&open=AaCrfgY3Yq4L2Ok9N-Gz&pullRequest=12716

Check warning on line 127 in src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this hard-coded path-delimiter.

See more on https://sonarcloud.io/project/issues?id=IQSS_dataverse&issues=AaCrfgY3Yq4L2Ok9N-Gy&open=AaCrfgY3Yq4L2Ok9N-Gy&pullRequest=12716
context.register(new ApiBlockingFilter(this, fullPath));
}
}

public boolean shouldBlock(String fullPath, ContainerRequestContext requestContext) {
if (checkSettings) {
updateSettingsIfChanged();
}

boolean isBlockableEndpoint = false;
List<Pattern> 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,

Check warning on line 157 in src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this call to a deprecated field, it has been marked for removal.

See more on https://sonarcloud.io/project/issues?id=IQSS_dataverse&issues=AaCrfgY3Yq4L2Ok9N-G0&open=AaCrfgY3Yq4L2Ok9N-G0&pullRequest=12716
JvmSettings.API_BLOCKED_POLICY.lookupOptional().orElse(DROP));
String newEndpointList = settingsService.getValueForKey(SettingsServiceBean.Key.BlockedApiEndpoints,

Check warning on line 159 in src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this call to a deprecated field, it has been marked for removal.

See more on https://sonarcloud.io/project/issues?id=IQSS_dataverse&issues=AaCrfgY3Yq4L2Ok9N-G1&open=AaCrfgY3Yq4L2Ok9N-G1&pullRequest=12716
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,

Check warning on line 177 in src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this call to a deprecated field, it has been marked for removal.

See more on https://sonarcloud.io/project/issues?id=IQSS_dataverse&issues=AaCrfgY3Yq4L2Ok9N-G2&open=AaCrfgY3Yq4L2Ok9N-G2&pullRequest=12716
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) {

Check failure on line 187 in src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a default case to this switch.

See more on https://sonarcloud.io/project/issues?id=IQSS_dataverse&issues=AaCrfgY3Yq4L2Ok9N-G3&open=AaCrfgY3Yq4L2Ok9N-G3&pullRequest=12716
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()) {

Check failure on line 196 in src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Fix this access that will throw a NullPointerException when executed.

See more on https://sonarcloud.io/project/issues?id=IQSS_dataverse&issues=AaCrfgY3Yq4L2Ok9N-G5&open=AaCrfgY3Yq4L2Ok9N-G5&pullRequest=12716
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)) {

Check warning on line 206 in src/main/java/edu/harvard/iq/dataverse/api/filter/ApiBlockingFeature.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this if-then-else statement by a single return statement.

See more on https://sonarcloud.io/project/issues?id=IQSS_dataverse&issues=AaCrfgY3Yq4L2Ok9N-G4&open=AaCrfgY3Yq4L2Ok9N-G4&pullRequest=12716
return false;
}
return true;
}
return false;
}

private void updateBlockedPoints(String endpointList) {
List<Pattern> 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;
}
}
Loading
Loading