Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d3a1d58
allow api call to ignore setting
stevenwinship Sep 15, 2025
58652bf
add release note
stevenwinship Sep 15, 2025
d3cf158
adding more tests
stevenwinship Sep 15, 2025
27eca77
Merge branch 'develop' into 11714-prevent-exclude-email-from-export-f…
stevenwinship Sep 26, 2025
e8657fb
Merge branch 'develop' into 11714-prevent-exclude-email-from-export-f…
stevenwinship Sep 30, 2025
3f7a68e
Merge branch 'develop' into 11714-prevent-exclude-email-from-export-f…
stevenwinship Sep 30, 2025
c6bbe44
Merge branch 'develop' into 11714-prevent-exclude-email-from-export-f…
stevenwinship Sep 30, 2025
337e520
Merge branch 'develop' into 11714-prevent-exclude-email-from-export-f…
stevenwinship Oct 1, 2025
098e073
Merge branch 'develop' into 11714-prevent-exclude-email-from-export-f…
stevenwinship Oct 8, 2025
882390f
Merge branch 'develop' into 11714-prevent-exclude-email-from-export-f…
stevenwinship Oct 10, 2025
85c45a2
add to native api doc
stevenwinship Oct 10, 2025
779b1aa
Merge branch 'develop' into 11714-prevent-exclude-email-from-export-f…
stevenwinship Oct 24, 2025
10184e1
addressing comments
stevenwinship Oct 24, 2025
a52cd0f
fix NPE in test
stevenwinship Oct 24, 2025
18478b1
Merge branch 'develop' into 11714-prevent-exclude-email-from-export-f…
stevenwinship Oct 24, 2025
e1d6ace
Merge branch 'develop' into 11714-prevent-exclude-email-from-export-f…
stevenwinship Nov 5, 2025
a9e6535
Merge branch 'develop' into 11714-prevent-exclude-email-from-export-f…
stevenwinship Nov 5, 2025
28e89a6
Merge branch 'develop' into 11714-prevent-exclude-email-from-export-f…
stevenwinship Nov 21, 2025
3cfa793
Merge branch 'develop' into 11714-prevent-exclude-email-from-export-f…
stevenwinship Dec 18, 2025
0e8bfc4
Merge branch 'develop' into 11714-prevent-exclude-email-from-export-f…
sekmiller Dec 23, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
New query parameter (ignoreSettingExcludeEmailFromExport) for API /api/datasets/:persistentId/versions/{versionId}

SPA requires the ability to have the contact emails included in the response for this API call
This query parameter prevents the contact email from being excluded when the setting (ExcludeEmailFromExport) is set to true and the user has EditDataset permissions.

See:
- [#11714](https://github.com/IQSS/dataverse/issues/11714)
6 changes: 4 additions & 2 deletions doc/sphinx-guides/source/api/native-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1692,20 +1692,22 @@ Get JSON Representation of a Dataset

.. note:: Datasets can be accessed using persistent identifiers. This is done by passing the constant ``:persistentId`` where the numeric id of the dataset is expected, and then passing the actual persistent id as a query parameter with the name ``persistentId``.

If a user with EditDataset permissions wants to ignore the setting ``ExcludeEmailFromExport`` in order to see the contact email, they must include the ``ignoreSettingExcludeEmailFromExport`` query parameter (Required by SPA).

Example: Getting the dataset whose DOI is *10.5072/FK2/J8SJZB*:

.. code-block:: bash

export SERVER_URL=https://demo.dataverse.org
export PERSISTENT_IDENTIFIER=doi:10.5072/FK2/J8SJZB

curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/datasets/:persistentId/?persistentId=$PERSISTENT_IDENTIFIER"
curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/datasets/:persistentId/?persistentId=$PERSISTENT_IDENTIFIER&ignoreSettingExcludeEmailFromExport"

The fully expanded example above (without environment variables) looks like this:

.. code-block:: bash

curl -H "X-Dataverse-key:$API_TOKEN" "https://demo.dataverse.org/api/datasets/:persistentId/?persistentId=doi:10.5072/FK2/J8SJZB"
curl -H "X-Dataverse-key:$API_TOKEN" "https://demo.dataverse.org/api/datasets/:persistentId/?persistentId=doi:10.5072/FK2/J8SJZB&ignoreSettingExcludeEmailFromExport"

Getting its draft version:

Expand Down
23 changes: 15 additions & 8 deletions src/main/java/edu/harvard/iq/dataverse/api/Datasets.java
Original file line number Diff line number Diff line change
Expand Up @@ -475,12 +475,16 @@ public Response getVersion(@Context ContainerRequestContext crc,
@QueryParam("excludeMetadataBlocks") Boolean excludeMetadataBlocks,
@QueryParam("includeDeaccessioned") boolean includeDeaccessioned,
@QueryParam("returnOwners") boolean returnOwners,
@QueryParam("ignoreSettingExcludeEmailFromExport") Boolean ignoreSettingToExcludeEmailFromExport,
@Context UriInfo uriInfo,
@Context HttpHeaders headers) {
return response( req -> {
boolean includeMetadataBlocks = excludeMetadataBlocks == null ? true : !excludeMetadataBlocks;
boolean includeFiles = excludeFiles == null ? true : !excludeFiles;
boolean ignoreSettingExcludeEmailFromExport = ignoreSettingToExcludeEmailFromExport != null ? ignoreSettingToExcludeEmailFromExport : false;

//If excludeFiles is null the default is to provide the files and because of this we need to check permissions.
boolean checkPerms = excludeFiles == null ? true : !excludeFiles;
boolean checkPerms = includeFiles;

Dataset dataset = findDatasetOrDie(datasetId);
DatasetVersion requestedDatasetVersion = getDatasetVersionOrDie(req,
Expand All @@ -494,16 +498,19 @@ public Response getVersion(@Context ContainerRequestContext crc,
if (requestedDatasetVersion == null || requestedDatasetVersion.getId() == null) {
return notFound("Dataset version not found");
}

if (excludeFiles == null ? true : !excludeFiles) {
if (includeFiles) {
requestedDatasetVersion = datasetversionService.findDeep(requestedDatasetVersion.getId());
}
Boolean includeMetadataBlocks = excludeMetadataBlocks == null ? true : !excludeMetadataBlocks;

JsonObjectBuilder jsonBuilder = json(requestedDatasetVersion,
null,
excludeFiles == null ? true : !excludeFiles,
returnOwners, includeMetadataBlocks);
// Check to see if the caller wants to ignore the ExcludeEmailFromExport setting in the metadata block and that they have permission to do so
// Let the JsonPrinter know to ignore the ExcludeEmailFromExport setting so the emails will show for this API call by permitted user
if (ignoreSettingExcludeEmailFromExport && (!includeMetadataBlocks || !permissionService.userOn(getRequestUser(crc), dataset).has(Permission.EditDataset))) {
// either not showing metadata block or user isn't allowed to override the setting
ignoreSettingExcludeEmailFromExport = false;
}

JsonObjectBuilder jsonBuilder = json(requestedDatasetVersion, null, includeFiles,
returnOwners, includeMetadataBlocks, ignoreSettingExcludeEmailFromExport);
return ok(jsonBuilder);

}, getRequestUser(crc));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@
import edu.harvard.iq.dataverse.DatasetFieldCompoundValue;
import edu.harvard.iq.dataverse.DatasetFieldType;
import edu.harvard.iq.dataverse.DatasetFieldValue;
import edu.harvard.iq.dataverse.settings.SettingsServiceBean;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import jakarta.json.Json;
import jakarta.json.JsonObject;

/**
Expand Down Expand Up @@ -46,21 +44,20 @@ public interface Listener {
*/
public static void walk( DatasetField dsf, Listener l, Map<Long, JsonObject> cvocMap ) {
DatasetFieldWalker joe = new DatasetFieldWalker(l, cvocMap);
SettingsServiceBean nullServiceBean = null;
joe.walk(dsf, nullServiceBean);
joe.walk(dsf, Collections.emptyList());
}

/**
* Convenience method to walk over a list of fields. Traversal
* is done in display order.
* @param fields the fields to go over. Does not have to be sorted.
* @param exclude the fields to skip
* @param excludedFieldTypeList the fields to skip
* @param l the listener to execute on each field values and structure.
*/
public static void walk(List<DatasetField> fields, SettingsServiceBean settingsService, Map<Long, JsonObject> cvocMap, Listener l) {
public static void walk(List<DatasetField> fields, List<DatasetFieldType.FieldType> excludedFieldTypeList, Map<Long, JsonObject> cvocMap, Listener l) {
DatasetFieldWalker joe = new DatasetFieldWalker(l, cvocMap);
for ( DatasetField dsf : sort( fields, DatasetField.DisplayOrder) ) {
joe.walk(dsf, settingsService);
joe.walk(dsf, excludedFieldTypeList);
}
}

Expand All @@ -77,7 +74,7 @@ public DatasetFieldWalker(){
this( null, null);
}

public void walk(DatasetField fld, SettingsServiceBean settingsService) {
public void walk(DatasetField fld, List<DatasetFieldType.FieldType> excludedFieldTypeList) {
l.startField(fld);
DatasetFieldType datasetFieldType = fld.getDatasetFieldType();

Expand All @@ -89,7 +86,7 @@ public void walk(DatasetField fld, SettingsServiceBean settingsService) {

} else if ( datasetFieldType.isPrimitive() ) {
for ( DatasetFieldValue pv : sort(fld.getDatasetFieldValues(), DatasetFieldValue.DisplayOrder) ) {
if (settingsService != null && settingsService.isTrueForKey(SettingsServiceBean.Key.ExcludeEmailFromExport, false) && DatasetFieldType.FieldType.EMAIL.equals(pv.getDatasetField().getDatasetFieldType().getFieldType())) {
if (excludedFieldTypeList.contains(pv.getDatasetField().getDatasetFieldType().getFieldType())) {
continue;
}
l.primitiveValue(pv);
Expand All @@ -99,15 +96,15 @@ public void walk(DatasetField fld, SettingsServiceBean settingsService) {
for ( DatasetFieldCompoundValue dsfcv : sort( fld.getDatasetFieldCompoundValues(), DatasetFieldCompoundValue.DisplayOrder) ) {
l.startCompoundValue(dsfcv);
for ( DatasetField dsf : sort(dsfcv.getChildDatasetFields(), DatasetField.DisplayOrder ) ) {
walk(dsf, settingsService);
walk(dsf, excludedFieldTypeList);
}
l.endCompoundValue(dsfcv);
}
}
l.addExpandedValuesArray(fld);
if(datasetFieldType.isPrimitive() && cvocMap.containsKey(datasetFieldType.getId())) {
for ( DatasetFieldValue evv : sort(fld.getDatasetFieldValues(), DatasetFieldValue.DisplayOrder) ) {
if (settingsService != null && settingsService.isTrueForKey(SettingsServiceBean.Key.ExcludeEmailFromExport, false) && DatasetFieldType.FieldType.EMAIL.equals(evv.getDatasetField().getDatasetFieldType().getFieldType())) {
if (excludedFieldTypeList.contains(evv.getDatasetField().getDatasetFieldType().getFieldType())) {
continue;
}
l.externalVocabularyValue(evv, cvocMap.get(datasetFieldType.getId()));
Expand Down
31 changes: 19 additions & 12 deletions src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java
Original file line number Diff line number Diff line change
Expand Up @@ -513,17 +513,17 @@ public static JsonObjectBuilder json(FileDetailsHolder ds) {
}

public static JsonObjectBuilder json(DatasetVersion dsv, boolean includeFiles) {
return json(dsv, null, includeFiles, false, true);
return json(dsv, null, includeFiles, false, true, false);
}
public static JsonObjectBuilder json(DatasetVersion dsv, boolean includeFiles, boolean includeMetadataBlocks) {
return json(dsv, null, includeFiles, false, includeMetadataBlocks);
return json(dsv, null, includeFiles, false, includeMetadataBlocks, false);
}
public static JsonObjectBuilder json(DatasetVersion dsv, List<String> anonymizedFieldTypeNamesList,
boolean includeFiles, boolean returnOwners) {
return json( dsv, anonymizedFieldTypeNamesList, includeFiles, returnOwners,true);
return json(dsv, anonymizedFieldTypeNamesList, includeFiles, returnOwners, true, false);
}
public static JsonObjectBuilder json(DatasetVersion dsv, List<String> anonymizedFieldTypeNamesList,
boolean includeFiles, boolean returnOwners, boolean includeMetadataBlocks) {
boolean includeFiles, boolean returnOwners, boolean includeMetadataBlocks, boolean ignoreSettingExcludeEmailFromExport) {
Dataset dataset = dsv.getDataset();
JsonObjectBuilder bld = jsonObjectBuilder()
.add("id", dsv.getId()).add("datasetId", dataset.getId())
Expand Down Expand Up @@ -572,10 +572,8 @@ public static JsonObjectBuilder json(DatasetVersion dsv, List<String> anonymized
.add("studyCompletion", dsv.getTermsOfUseAndAccess().getStudyCompletion())
.add("fileAccessRequest", dsv.getTermsOfUseAndAccess().isFileAccessRequest());
if(includeMetadataBlocks) {
bld.add("metadataBlocks", (anonymizedFieldTypeNamesList != null) ?
jsonByBlocks(dsv.getDatasetFields(), anonymizedFieldTypeNamesList)
: jsonByBlocks(dsv.getDatasetFields())
);
bld.add("metadataBlocks",
jsonByBlocks(dsv.getDatasetFields(), anonymizedFieldTypeNamesList, ignoreSettingExcludeEmailFromExport));
}
if(returnOwners){
bld.add("isPartOf", getOwnersFromDvObject(dataset));
Expand Down Expand Up @@ -659,15 +657,15 @@ public static JsonObjectBuilder json(DatasetDistributor dist) {
}

public static JsonObjectBuilder jsonByBlocks(List<DatasetField> fields) {
return jsonByBlocks(fields, null);
return jsonByBlocks(fields, null, false);
}

public static JsonObjectBuilder jsonByBlocks(List<DatasetField> fields, List<String> anonymizedFieldTypeNamesList) {
public static JsonObjectBuilder jsonByBlocks(List<DatasetField> fields, List<String> anonymizedFieldTypeNamesList, boolean ignoreSettingExcludeEmailFromExport) {
JsonObjectBuilder blocksBld = jsonObjectBuilder();

for (Map.Entry<MetadataBlock, List<DatasetField>> blockAndFields : DatasetField.groupByBlock(fields).entrySet()) {
MetadataBlock block = blockAndFields.getKey();
blocksBld.add(block.getName(), JsonPrinter.json(block, blockAndFields.getValue(), anonymizedFieldTypeNamesList));
blocksBld.add(block.getName(), JsonPrinter.json(block, blockAndFields.getValue(), anonymizedFieldTypeNamesList, ignoreSettingExcludeEmailFromExport));
}
return blocksBld;
}
Expand All @@ -685,14 +683,23 @@ public static JsonObjectBuilder json(MetadataBlock block, List<DatasetField> fie
}

public static JsonObjectBuilder json(MetadataBlock block, List<DatasetField> fields, List<String> anonymizedFieldTypeNamesList) {
return json(block, fields, anonymizedFieldTypeNamesList, false);
}

public static JsonObjectBuilder json(MetadataBlock block, List<DatasetField> fields, List<String> anonymizedFieldTypeNamesList, boolean ignoreSettingExcludeEmailFromExport) {
JsonObjectBuilder blockBld = jsonObjectBuilder();

blockBld.add("displayName", block.getDisplayName());
blockBld.add("name", block.getName());

final JsonArrayBuilder fieldsArray = Json.createArrayBuilder();
Map<Long, JsonObject> cvocMap = (datasetFieldService==null) ? new HashMap<Long, JsonObject>() :datasetFieldService.getCVocConf(true);
DatasetFieldWalker.walk(fields, settingsService, cvocMap, new DatasetFieldsToJson(fieldsArray, anonymizedFieldTypeNamesList));
List<DatasetFieldType.FieldType> excludedFieldTypeList = new ArrayList<>();
// Exclude the Email field or override the exclusion of the Email field type based on the settings ExcludeEmailFromExport and ignoreSettingExcludeEmailFromExport
if ((settingsService != null) && settingsService.isTrueForKey(SettingsServiceBean.Key.ExcludeEmailFromExport, false) && !ignoreSettingExcludeEmailFromExport) {
excludedFieldTypeList.add(DatasetFieldType.FieldType.EMAIL);
}
DatasetFieldWalker.walk(fields, excludedFieldTypeList, cvocMap, new DatasetFieldsToJson(fieldsArray, anonymizedFieldTypeNamesList));

blockBld.add("fields", fieldsArray);
return blockBld;
Expand Down
72 changes: 55 additions & 17 deletions src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,7 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.*;
import org.skyscreamer.jsonassert.JSONAssert;

import javax.xml.stream.XMLInputFactory;
Expand Down Expand Up @@ -79,10 +76,6 @@ public static void setUpClass() {
removeIdentifierGenerationStyle.then().assertThat()
.statusCode(200);

Response removeExcludeEmail = UtilIT.deleteSetting(SettingsServiceBean.Key.ExcludeEmailFromExport);
removeExcludeEmail.then().assertThat()
.statusCode(200);

Response removeAnonymizedFieldTypeNames = UtilIT.deleteSetting(SettingsServiceBean.Key.AnonymizedFieldTypeNames);
removeAnonymizedFieldTypeNames.then().assertThat()
.statusCode(200);
Expand All @@ -101,6 +94,10 @@ public static void setUpClass() {
*/
}

@AfterEach
public void afterEach() {
UtilIT.deleteSetting(SettingsServiceBean.Key.ExcludeEmailFromExport);
}

@AfterAll
public static void afterClass() {
Expand All @@ -109,10 +106,6 @@ public static void afterClass() {
removeIdentifierGenerationStyle.then().assertThat()
.statusCode(200);

Response removeExcludeEmail = UtilIT.deleteSetting(SettingsServiceBean.Key.ExcludeEmailFromExport);
removeExcludeEmail.then().assertThat()
.statusCode(200);

Response removeAnonymizedFieldTypeNames = UtilIT.deleteSetting(SettingsServiceBean.Key.AnonymizedFieldTypeNames);
removeAnonymizedFieldTypeNames.then().assertThat()
.statusCode(200);
Expand Down Expand Up @@ -1757,11 +1750,6 @@ public void testExcludeEmail() {
Response deleteUserResponse = UtilIT.deleteUser(username);
deleteUserResponse.prettyPrint();
assertEquals(200, deleteUserResponse.getStatusCode());

Response removeExcludeEmail = UtilIT.deleteSetting(SettingsServiceBean.Key.ExcludeEmailFromExport);
removeExcludeEmail.then().assertThat()
.statusCode(200);

}

@Disabled
Expand Down Expand Up @@ -7375,6 +7363,56 @@ public void testUpdateLicense() {
.statusCode(UNAUTHORIZED.getStatusCode());
}

@Test
public void testExcludeEmailOverride() {
// Create super user
String apiToken = getSuperuserToken();
// Create user with no permission
String apiTokenNoPerms = UtilIT.createRandomUserGetToken();
// Create Collection
String collectionAlias = UtilIT.createRandomCollectionGetAlias(apiToken);
// Publish Collection
UtilIT.publishDataverseViaNativeApi(collectionAlias, apiToken).prettyPrint();
// Create Dataset
Response createDataset = UtilIT.createRandomDatasetViaNativeApi(collectionAlias, apiToken);
createDataset.then().assertThat()
.statusCode(CREATED.getStatusCode());
Integer datasetId = UtilIT.getDatasetIdFromResponse(createDataset);
String datasetPid = JsonPath.from(createDataset.asString()).getString("data.persistentId");
// Publish Dataset
UtilIT.publishDatasetViaNativeApi(datasetId, "major", apiToken).prettyPrint();

// Setting is not set - datasetContactEmail will NOT be excluded
Response response = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiToken);
response.then().assertThat().statusCode(OK.getStatusCode());
String json = response.prettyPrint();
assertTrue(json.contains("datasetContactName"));
assertTrue(json.contains("datasetContactEmail"));

UtilIT.setSetting(SettingsServiceBean.Key.ExcludeEmailFromExport, "true");

// User does not ignore the setting - datasetContactEmail will be excluded
response = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiToken);
response.then().assertThat().statusCode(OK.getStatusCode());
json = response.prettyPrint();
assertTrue(json.contains("datasetContactName"));
assertTrue(!json.contains("datasetContactEmail"));

// User has permission to ignore the setting allowing the datasetContactEmail to be included in the response
response = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiToken, true, false, false, true);
response.then().assertThat().statusCode(OK.getStatusCode());
json = response.prettyPrint();
assertTrue(json.contains("datasetContactName"));
assertTrue(json.contains("datasetContactEmail"));

// User has no permission to override the setting - datasetContactEmail will be excluded
response = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiTokenNoPerms, true, false, false, true);
response.then().assertThat().statusCode(OK.getStatusCode());
json = response.prettyPrint();
assertTrue(json.contains("datasetContactName"));
assertTrue(!json.contains("datasetContactEmail"));
}

private String getSuperuserToken() {
Response createResponse = UtilIT.createRandomUser();
String adminApiToken = UtilIT.getApiTokenFromResponse(createResponse);
Expand Down
Loading