diff --git a/doc/release-notes/11161-do-not-assume-default-license.md b/doc/release-notes/11161-do-not-assume-default-license.md new file mode 100644 index 00000000000..5ac23b11584 --- /dev/null +++ b/doc/release-notes/11161-do-not-assume-default-license.md @@ -0,0 +1,8 @@ +### New Feature Flag: do-not-assume-default-license + +A new feature flag `do-not-assume-default-license` has been added. This flag controls the behavior when creating a dataset via API without providing a license or terms of use. + +- By default (flag disabled), the default license will be automatically assigned if no license and no terms are provided in the input JSON, regardless of whether custom terms are allowed. +- When enabled, no license (and no terms) will be assigned if none are provided. + +**Note:** Previously, if custom terms were allowed, the system would not assign a default license in this case. To retain that behavior, you must now enable this feature flag. diff --git a/doc/sphinx-guides/source/api/changelog.rst b/doc/sphinx-guides/source/api/changelog.rst index 889c929de14..ca36d972326 100644 --- a/doc/sphinx-guides/source/api/changelog.rst +++ b/doc/sphinx-guides/source/api/changelog.rst @@ -25,6 +25,8 @@ v6.12 - **/api/admin/index/perms/{id}** +- Dataset creation API calls may now behave differently when neither a license nor terms are provided, depending on the new :ref:`dataverse.feature.do-not-assume-default-license` feature flag. + v6.11 ----- diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index e9ad3bdfe24..d676791f480 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -4055,6 +4055,15 @@ In a future Dataverse version, the (currently) experimental response message sty See also :ref:`dataverse.legacy.api-response-message-style`. +.. _dataverse.feature.do-not-assume-default-license: + +dataverse.feature.do-not-assume-default-license ++++++++++++++++++++++++++++++++++++++++++++++++ + +When creating a dataset via API, if neither a license nor any terms of use are provided, the system normally assigns the default license. If this feature flag is enabled, no license is assigned (and no terms) in this case. + +Defaults to ``false``. + .. _dataverse.feature.avoid-expensive-solr-join: dataverse.feature.avoid-expensive-solr-join diff --git a/src/main/java/edu/harvard/iq/dataverse/TermsOfUseAndAccess.java b/src/main/java/edu/harvard/iq/dataverse/TermsOfUseAndAccess.java index 9e48c6c0165..c91533e8677 100644 --- a/src/main/java/edu/harvard/iq/dataverse/TermsOfUseAndAccess.java +++ b/src/main/java/edu/harvard/iq/dataverse/TermsOfUseAndAccess.java @@ -161,6 +161,9 @@ public String getTermsOfUse() { public void setTermsOfUse(String termsOfUse) { this.termsOfUse = termsOfUse; + if (termsOfUse != null) { + this.license = null; + } } public String getTermsOfAccess() { @@ -177,6 +180,9 @@ public String getConfidentialityDeclaration() { public void setConfidentialityDeclaration(String confidentialityDeclaration) { this.confidentialityDeclaration = confidentialityDeclaration; + if (confidentialityDeclaration != null) { + this.license = null; + } } public String getSpecialPermissions() { @@ -185,6 +191,9 @@ public String getSpecialPermissions() { public void setSpecialPermissions(String specialPermissions) { this.specialPermissions = specialPermissions; + if (specialPermissions != null) { + this.license = null; + } } public String getRestrictions() { @@ -193,6 +202,9 @@ public String getRestrictions() { public void setRestrictions(String restrictions) { this.restrictions = restrictions; + if (restrictions != null) { + this.license = null; + } } public String getCitationRequirements() { @@ -201,6 +213,9 @@ public String getCitationRequirements() { public void setCitationRequirements(String citationRequirements) { this.citationRequirements = citationRequirements; + if (citationRequirements != null) { + this.license = null; + } } public String getDepositorRequirements() { @@ -209,6 +224,9 @@ public String getDepositorRequirements() { public void setDepositorRequirements(String depositorRequirements) { this.depositorRequirements = depositorRequirements; + if (depositorRequirements != null) { + this.license = null; + } } public String getConditions() { @@ -217,6 +235,9 @@ public String getConditions() { public void setConditions(String conditions) { this.conditions = conditions; + if (conditions != null) { + this.license = null; + } } public String getDisclaimer() { @@ -225,6 +246,9 @@ public String getDisclaimer() { public void setDisclaimer(String disclaimer) { this.disclaimer = disclaimer; + if (disclaimer != null) { + this.license = null; + } } public String getDataAccessPlace() { diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java index fe607eaf55e..330b6b1c3f3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java @@ -98,6 +98,17 @@ public enum FeatureFlags { */ API_BEARER_AUTH_USE_OAUTH_USER_ON_ID_MATCH("api-bearer-auth-use-oauth-user-on-id-match"), + /** + * When creating a dataset via API, if neither a license nor any terms of use are provided, + * the system normally assigns the default license. + * + *
If this feature flag is enabled, no license is assigned (and no terms) in this case.
+ * + * @apiNote Raise flag by setting "dataverse.feature.do-not-assume-default-license" + * @since Dataverse 6.5 + */ + DO_NOT_ASSUME_DEFAULT_LICENSE("do-not-assume-default-license"), + /** * For published (public) objects, don't use a join when searching Solr. * Experimental! Requires a reindex with the following feature flag enabled, diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java index 2caefd856e2..8ed18b4c63f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java @@ -96,7 +96,6 @@ public enum JvmSettings { // API: MDC Citation updates SCOPE_API_MDC(SCOPE_API, "mdc"), API_MDC_UPDATE_MIN_DELAY_MS(SCOPE_API_MDC, "min-delay-ms"), - // SIGNPOSTING SETTINGS SCOPE_SIGNPOSTING(PREFIX, "signposting"), @@ -321,7 +320,7 @@ public enum JvmSettings { SCOPE_COARNOTIFY(SCOPE_LINKEDDATANOTIFICATION, "coar-notify"), SCOPE_COARNOTIFY_RELATIONSHIP_ANNOUNCEMENT(SCOPE_COARNOTIFY, "relationship-announcement"), COARNOTIFY_RELATIONSHIP_ANNOUNCEMENT_NOTIFY_SUPERUSERS_ONLY(SCOPE_COARNOTIFY_RELATIONSHIP_ANNOUNCEMENT, "notify-superusers-only"), - ; + ; private static final String SCOPE_SEPARATOR = "."; public static final String PLACEHOLDER_KEY = "%s"; diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java index 7f3db716456..e6e0c71c396 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java @@ -470,27 +470,57 @@ public DatasetVersion parseDatasetVersion(JsonObject obj, DatasetVersion dsv) th License license = null; - try { - // This method will attempt to parse the license in the format - // in which it appears in our json exports, as a compound - // field, for ex.: - // "license": { - // "name": "CC0 1.0", - // "uri": "http://creativecommons.org/publicdomain/zero/1.0" - // } - license = parseLicense(obj.getJsonObject("license")); - } catch (ClassCastException cce) { - logger.fine("class cast exception parsing the license section (will try parsing as a string)"); - // attempt to parse as string: - // i.e. this is for backward compatibility, after the bug in #9155 - // was fixed, with the old style of encoding the license info - // in input json, for ex.: - // "license" : "CC0 1.0" - license = parseLicense(obj.getString("license", null)); + if (obj.containsKey("license")) { + try { + // This method will attempt to parse the license in the format + // in which it appears in our json exports, as a compound + // field, for ex.: + // "license": { + // "name": "CC0 1.0", + // "uri": "http://creativecommons.org/publicdomain/zero/1.0" + // } + license = parseLicense(obj.getJsonObject("license")); + } catch (ClassCastException cce) { + logger.fine("class cast exception parsing the license section (will try parsing as a string)"); + // attempt to parse as string: + // i.e. this is for backward compatibility, after the bug in #9155 + // was fixed, with the old style of encoding the license info + // in input json, for ex.: + // "license" : "CC0 1.0" + license = parseLicense(obj.getString("license", null)); + } } - - //test to see if license exists in dataset type - //if not set it to null - + + terms.setTermsOfUse(obj.getString("termsOfUse", null)); + terms.setConfidentialityDeclaration(obj.getString("confidentialityDeclaration", null)); + terms.setSpecialPermissions(obj.getString("specialPermissions", null)); + terms.setRestrictions(obj.getString("restrictions", null)); + terms.setCitationRequirements(obj.getString("citationRequirements", null)); + terms.setDepositorRequirements(obj.getString("depositorRequirements", null)); + terms.setConditions(obj.getString("conditions", null)); + terms.setDisclaimer(obj.getString("disclaimer", null)); + + if (license == null) { + // If no license was provided or the provided license was invalid, + // we check if terms were provided. + boolean termsProvided = terms.getTermsOfUse() != null + || terms.getConfidentialityDeclaration() != null + || terms.getSpecialPermissions() != null + || terms.getRestrictions() != null + || terms.getCitationRequirements() != null + || terms.getDepositorRequirements() != null + || terms.getConditions() != null + || terms.getDisclaimer() != null; + + if (!FeatureFlags.DO_NOT_ASSUME_DEFAULT_LICENSE.enabled()) { + if (!termsProvided) { + license = licenseService.getDefault(); + } + } + } + + //test to see if license exists in dataset type + //if not set it to null - //only test if Dataset has a type and if it has custom available licenses if (dsv.getDataset() != null) { DatasetType dst = dsv.getDataset().getDatasetType(); @@ -505,21 +535,9 @@ public DatasetVersion parseDatasetVersion(JsonObject obj, DatasetVersion dsv) th license = null; } } - } - - if (license == null) { - terms.setLicense(license); - terms.setTermsOfUse(obj.getString("termsOfUse", null)); - terms.setConfidentialityDeclaration(obj.getString("confidentialityDeclaration", null)); - terms.setSpecialPermissions(obj.getString("specialPermissions", null)); - terms.setRestrictions(obj.getString("restrictions", null)); - terms.setCitationRequirements(obj.getString("citationRequirements", null)); - terms.setDepositorRequirements(obj.getString("depositorRequirements", null)); - terms.setConditions(obj.getString("conditions", null)); - terms.setDisclaimer(obj.getString("disclaimer", null)); - } else { - terms.setLicense(license); } + + terms.setLicense(license); terms.setTermsOfAccess(obj.getString("termsOfAccess", null)); terms.setDataAccessPlace(obj.getString("dataAccessPlace", null)); terms.setOriginalArchive(obj.getString("originalArchive", null)); @@ -530,6 +548,7 @@ public DatasetVersion parseDatasetVersion(JsonObject obj, DatasetVersion dsv) th terms.setFileAccessRequest(obj.getBoolean("fileAccessRequest", false)); dsv.setTermsOfUseAndAccess(terms); terms.setDatasetVersion(dsv); + JsonObject metadataBlocks = obj.getJsonObject("metadataBlocks"); if (metadataBlocks == null){ throw new JsonParseException(BundleUtil.getStringFromBundle("jsonparser.error.metadatablocks.not.found")); @@ -700,12 +719,7 @@ public GuestbookResponse parseGuestbookResponse(JsonObject obj, GuestbookRespons private edu.harvard.iq.dataverse.license.License parseLicense(String licenseNameOrUri) throws JsonParseException { if (licenseNameOrUri == null){ - boolean safeDefaultIfKeyNotFound = true; - if (settingsService.isTrueForKey(SettingsServiceBean.Key.AllowCustomTermsOfUse, safeDefaultIfKeyNotFound)){ - return null; - } else { - return licenseService.getDefault(); - } + return null; } License license = licenseService.getByNameOrUri(licenseNameOrUri); if (license == null) throw new JsonParseException("Invalid license: " + licenseNameOrUri); @@ -714,12 +728,7 @@ private edu.harvard.iq.dataverse.license.License parseLicense(String licenseName private edu.harvard.iq.dataverse.license.License parseLicense(JsonObject licenseObj) throws JsonParseException { if (licenseObj == null){ - boolean safeDefaultIfKeyNotFound = true; - if (settingsService.isTrueForKey(SettingsServiceBean.Key.AllowCustomTermsOfUse, safeDefaultIfKeyNotFound)){ - return null; - } else { - return licenseService.getDefault(); - } + return null; } String licenseName = licenseObj.getString("name", null); diff --git a/src/test/java/edu/harvard/iq/dataverse/TermsOfUseAndAccessTest.java b/src/test/java/edu/harvard/iq/dataverse/TermsOfUseAndAccessTest.java new file mode 100644 index 00000000000..cb542db4b3c --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/TermsOfUseAndAccessTest.java @@ -0,0 +1,54 @@ +package edu.harvard.iq.dataverse; + +import org.junit.jupiter.api.Test; +import edu.harvard.iq.dataverse.license.License; +import static org.junit.jupiter.api.Assertions.*; + +public class TermsOfUseAndAccessTest { + + @Test + public void testLicenseAndTermsMutualExclusivity() { + License license = new License(); + license.setName("CC0"); + + TermsOfUseAndAccess terms = new TermsOfUseAndAccess(); + + // 1. Setting license should clear terms + terms.setTermsOfUse("Some terms"); + assertEquals("Some terms", terms.getTermsOfUse()); + assertNull(terms.getLicense()); + + terms.setLicense(license); + assertNull(terms.getTermsOfUse()); + assertEquals(license, terms.getLicense()); + + // 2. Setting terms should clear license + terms.setTermsOfUse("New terms"); + assertNull(terms.getLicense()); + assertEquals("New terms", terms.getTermsOfUse()); + + // 3. Test other fields clear license too + terms.setLicense(license); + terms.setConfidentialityDeclaration("Confidential"); + assertNull(terms.getLicense()); + assertEquals("Confidential", terms.getConfidentialityDeclaration()); + } + + @Test + public void testCopyTermsOfUseAndAccess() { + License license = new License(); + license.setName("CC0"); + + TermsOfUseAndAccess terms = new TermsOfUseAndAccess(); + terms.setLicense(license); + + TermsOfUseAndAccess copy = terms.copyTermsOfUseAndAccess(); + assertEquals(license, copy.getLicense()); + assertNull(copy.getTermsOfUse()); + + terms.setTermsOfUse("Some terms"); + TermsOfUseAndAccess copy2 = terms.copyTermsOfUseAndAccess(); + assertNull(copy2.getLicense()); + assertEquals("Some terms", copy2.getTermsOfUse()); + } +} diff --git a/src/test/java/edu/harvard/iq/dataverse/util/json/JsonParserTest.java b/src/test/java/edu/harvard/iq/dataverse/util/json/JsonParserTest.java index 262aa38d5d0..f5d19400462 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/json/JsonParserTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/json/JsonParserTest.java @@ -19,6 +19,7 @@ import edu.harvard.iq.dataverse.dataset.DatasetType; import edu.harvard.iq.dataverse.dataset.DatasetTypeServiceBean; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.license.License; import edu.harvard.iq.dataverse.license.LicenseServiceBean; import edu.harvard.iq.dataverse.mocks.MockDatasetFieldSvc; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; @@ -758,7 +759,42 @@ public boolean assertFieldsEqual( DatasetField ex, DatasetField act ) { throw new IllegalArgumentException("Unknown dataset field type '" + ex.getDatasetFieldType() + "'"); } + @Test + public void testParseDatasetVersion_LicenseAndTerms() throws JsonParseException { + // Prepare mocks + License defaultLicense = new License(); + defaultLicense.setName("CC0 1.0"); + Mockito.when(licenseService.getDefault()).thenReturn(defaultLicense); + + String baseJson = "{\"metadataBlocks\":{\"citation\":{\"fields\":[]}}}"; + + // Case 1: Flag false (default), terms NOT provided -> should pick default + System.setProperty("dataverse.feature.do-not-assume-default-license", "false"); + DatasetVersion dsv1 = sut.parseDatasetVersion(JsonUtil.getJsonObject(baseJson)); + assertEquals(defaultLicense, dsv1.getTermsOfUseAndAccess().getLicense()); + + // Case 2: Flag false (default), terms PROVIDED -> should NOT pick default + String jsonWithTerms = "{\"metadataBlocks\":{\"citation\":{\"fields\":[]}}, \"termsOfUse\":\"Some terms\"}"; + DatasetVersion dsv2 = sut.parseDatasetVersion(JsonUtil.getJsonObject(jsonWithTerms)); + assertNull(dsv2.getTermsOfUseAndAccess().getLicense()); + assertEquals("Some terms", dsv2.getTermsOfUseAndAccess().getTermsOfUse()); + + // Case 3: Flag true, terms NOT provided -> should NOT pick default + System.setProperty("dataverse.feature.do-not-assume-default-license", "true"); + DatasetVersion dsv3 = sut.parseDatasetVersion(JsonUtil.getJsonObject(baseJson)); + assertNull(dsv3.getTermsOfUseAndAccess().getLicense()); + + // Cleanup + System.clearProperty("dataverse.feature.do-not-assume-default-license"); + } + private static class MockSettingsSvc extends SettingsServiceBean { + private boolean allowCustomTermsOfUse = false; + + public void setAllowCustomTermsOfUse(boolean allow) { + this.allowCustomTermsOfUse = allow; + } + @Override public String getValueForKey( Key key /*, String defaultValue */) { switch (key) { @@ -775,7 +811,7 @@ public String getValueForKey( Key key /*, String defaultValue */) { @Override public boolean isTrueForKey(Key key, boolean safeDefaultIfKeyNotFound) { if (key == Key.AllowCustomTermsOfUse) { - return false; + return this.allowCustomTermsOfUse; } return safeDefaultIfKeyNotFound; }