diff --git a/doc/release-notes/11714-prevent-exclude-email-from-export-for-permitted-user-get-dataset-version.md b/doc/release-notes/11714-prevent-exclude-email-from-export-for-permitted-user-get-dataset-version.md new file mode 100644 index 00000000000..6a87c065573 --- /dev/null +++ b/doc/release-notes/11714-prevent-exclude-email-from-export-for-permitted-user-get-dataset-version.md @@ -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) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 6c1720e2b5b..84c1db6d338 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1692,6 +1692,8 @@ 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 @@ -1699,13 +1701,13 @@ Example: Getting the dataset whose DOI is *10.5072/FK2/J8SJZB*: 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: diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 2378388c540..c15efb4c651 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -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, @@ -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)); diff --git a/src/main/java/edu/harvard/iq/dataverse/util/DatasetFieldWalker.java b/src/main/java/edu/harvard/iq/dataverse/util/DatasetFieldWalker.java index 25032860d11..207a12438f9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/DatasetFieldWalker.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/DatasetFieldWalker.java @@ -5,7 +5,6 @@ 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; @@ -13,7 +12,6 @@ import java.util.Map; import java.util.logging.Logger; -import jakarta.json.Json; import jakarta.json.JsonObject; /** @@ -46,21 +44,20 @@ public interface Listener { */ public static void walk( DatasetField dsf, Listener l, Map 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 fields, SettingsServiceBean settingsService, Map cvocMap, Listener l) { + public static void walk(List fields, List excludedFieldTypeList, Map cvocMap, Listener l) { DatasetFieldWalker joe = new DatasetFieldWalker(l, cvocMap); for ( DatasetField dsf : sort( fields, DatasetField.DisplayOrder) ) { - joe.walk(dsf, settingsService); + joe.walk(dsf, excludedFieldTypeList); } } @@ -77,7 +74,7 @@ public DatasetFieldWalker(){ this( null, null); } - public void walk(DatasetField fld, SettingsServiceBean settingsService) { + public void walk(DatasetField fld, List excludedFieldTypeList) { l.startField(fld); DatasetFieldType datasetFieldType = fld.getDatasetFieldType(); @@ -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); @@ -99,7 +96,7 @@ 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); } @@ -107,7 +104,7 @@ public void walk(DatasetField fld, SettingsServiceBean settingsService) { 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())); diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index aa96d5dd20f..27b7a122c93 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -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 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 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()) @@ -572,10 +572,8 @@ public static JsonObjectBuilder json(DatasetVersion dsv, List 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)); @@ -659,15 +657,15 @@ public static JsonObjectBuilder json(DatasetDistributor dist) { } public static JsonObjectBuilder jsonByBlocks(List fields) { - return jsonByBlocks(fields, null); + return jsonByBlocks(fields, null, false); } - public static JsonObjectBuilder jsonByBlocks(List fields, List anonymizedFieldTypeNamesList) { + public static JsonObjectBuilder jsonByBlocks(List fields, List anonymizedFieldTypeNamesList, boolean ignoreSettingExcludeEmailFromExport) { JsonObjectBuilder blocksBld = jsonObjectBuilder(); for (Map.Entry> 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; } @@ -685,6 +683,10 @@ public static JsonObjectBuilder json(MetadataBlock block, List fie } public static JsonObjectBuilder json(MetadataBlock block, List fields, List anonymizedFieldTypeNamesList) { + return json(block, fields, anonymizedFieldTypeNamesList, false); + } + + public static JsonObjectBuilder json(MetadataBlock block, List fields, List anonymizedFieldTypeNamesList, boolean ignoreSettingExcludeEmailFromExport) { JsonObjectBuilder blockBld = jsonObjectBuilder(); blockBld.add("displayName", block.getDisplayName()); @@ -692,7 +694,12 @@ public static JsonObjectBuilder json(MetadataBlock block, List fie final JsonArrayBuilder fieldsArray = Json.createArrayBuilder(); Map cvocMap = (datasetFieldService==null) ? new HashMap() :datasetFieldService.getCVocConf(true); - DatasetFieldWalker.walk(fields, settingsService, cvocMap, new DatasetFieldsToJson(fieldsArray, anonymizedFieldTypeNamesList)); + List 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; diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index d4531ec21cf..6ff21a84a3d 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -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; @@ -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); @@ -101,6 +94,10 @@ public static void setUpClass() { */ } + @AfterEach + public void afterEach() { + UtilIT.deleteSetting(SettingsServiceBean.Key.ExcludeEmailFromExport); + } @AfterAll public static void afterClass() { @@ -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); @@ -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 @@ -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); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index f09b33a0b5b..6675d6f0fd5 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -1780,7 +1780,15 @@ static Response getDatasetVersion(String persistentId, String versionNumber, Str static Response getDatasetVersion(String persistentId, String versionNumber, String apiToken, boolean excludeFiles, boolean includeDeaccessioned) { return getDatasetVersion(persistentId,versionNumber,apiToken,excludeFiles,false,includeDeaccessioned); } - static Response getDatasetVersion(String persistentId, String versionNumber, String apiToken, boolean excludeFiles,boolean excludeMetadataBlocks, boolean includeDeaccessioned) { + static Response getDatasetVersion(String persistentId, String versionNumber, String apiToken, boolean excludeFiles, boolean excludeMetadataBlocks, boolean includeDeaccessioned) { + return getDatasetVersion(persistentId, versionNumber, apiToken, excludeFiles, excludeMetadataBlocks, includeDeaccessioned, false); + } + // includeMetadataBlocksEmail is an override of the Setting ExcludeEmailFromExport. excludeMetadataBlocks must be false and user needs EditDataset permission + static Response getDatasetVersion(String persistentId, String versionNumber, String apiToken, + boolean excludeFiles, + boolean excludeMetadataBlocks, + boolean includeDeaccessioned, + boolean ignoreSettingExcludeEmailFromExport) { return given() .header(API_TOKEN_HTTP_HEADER, apiToken) .queryParam("includeDeaccessioned", includeDeaccessioned) @@ -1789,7 +1797,8 @@ static Response getDatasetVersion(String persistentId, String versionNumber, Str + "?persistentId=" + persistentId + (excludeFiles ? "&excludeFiles=true" : "") - + (excludeMetadataBlocks ? "&excludeMetadataBlocks=true" : "")); + + (excludeMetadataBlocks ? "&excludeMetadataBlocks=true" : "") + + (ignoreSettingExcludeEmailFromExport ? "&ignoreSettingExcludeEmailFromExport=true" : "")); } static Response compareDatasetVersions(String persistentId, String versionNumber1, String versionNumber2, String apiToken) { return given()