From 67345fd43631a7dcea876037264340001a191d96 Mon Sep 17 00:00:00 2001 From: tylerjmchugh Date: Wed, 15 Jul 2026 12:53:28 -0400 Subject: [PATCH 1/7] Exclude templates from isValid and isApproved publication checks --- .../system-configuration.md | 7 ++ .../api/records/MetadataSharingApi.java | 24 +++-- .../api/records/MetadataSharingApiTest.java | 91 +++++++++++++++++++ .../resources/catalog/locales/en-admin.json | 4 +- 4 files changed, 117 insertions(+), 9 deletions(-) diff --git a/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md b/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md index 6463f93676d..fe58114879a 100644 --- a/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md +++ b/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md @@ -356,6 +356,13 @@ Allows to configure the zip export of metadata records and their attachments. - **Total size of attachments allowed in zip export (MB)** Maximum total size of attachments allowed in zip export (in MB). If the total size of attachments linked to the selected metadata is above this value, exporting as zip (with attachments) is not allowed. Leave empty for no limit. +## Metadata workflow + +The following settings control what metadata can be published when the metadata workflow is active. They are found under `Administration` --> `Settings` --> `Metadata Workflow`. + +- **Allow publication of invalid metadata** When disabled, only metadata that passes all required XSD and schematron validation rules can be published. When enabled, metadata can be published regardless of its validation status. Templates are always exempt from this check, as they are typically incomplete by design. +- **Allow publication of non-approved metadata** When disabled, only metadata with an `Approved` workflow status can be published. When enabled, metadata can be published regardless of its workflow status. Templates are always exempt from this check, as they are not expected to go through the approval workflow. + ## Harvesting *Allow editing on harvested records*: Enables/Disables editing of harvested records in the catalogue. By default, harvested records cannot be edited. diff --git a/services/src/main/java/org/fao/geonet/api/records/MetadataSharingApi.java b/services/src/main/java/org/fao/geonet/api/records/MetadataSharingApi.java index 2d13d9a53b3..5a8c6d7e30b 100644 --- a/services/src/main/java/org/fao/geonet/api/records/MetadataSharingApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/MetadataSharingApi.java @@ -1352,18 +1352,28 @@ private void updateOwnership(Integer groupIdentifier, } /** - * For privileges to {@link ReservedGroup#all} group, check if it's allowed or not to publish invalid metadata. + * For privileges to {@link ReservedGroup#all} group, check if it's allowed or not to publish invalid or + * non-approved metadata. Templates (including sub-templates and templates of sub-templates) are exempt from + * these checks, as they are typically incomplete and not expected to be valid or approved. * - * @param context - * @param messages - * @param metadata - * @param allowPublishInvalidMd - * @param allowPublishNonApprovedMd - * @throws Exception + * @param context the current service context + * @param messages the resource bundle for error messages + * @param metadata the metadata record being published + * @param allowPublishInvalidMd whether publishing invalid metadata is allowed (excluding templates) + * @param allowPublishNonApprovedMd whether publishing non-approved metadata is allowed (excluding templates) + * @throws Exception if the metadata is invalid or not approved and publishing is not allowed */ private void checkCanPublishToAllGroup(ServiceContext context, ResourceBundle messages, AbstractMetadata metadata, boolean allowPublishInvalidMd, boolean allowPublishNonApprovedMd) throws Exception { + // Templates are typically incomplete and should not be subject to validation or approval checks + MetadataType metadataType = metadata.getDataInfo().getType(); + if (metadataType == MetadataType.TEMPLATE + || metadataType == MetadataType.SUB_TEMPLATE + || metadataType == MetadataType.TEMPLATE_OF_SUB_TEMPLATE) { + return; + } + if (!allowPublishInvalidMd) { boolean hasValidation = (metadataValidationRepository.count(MetadataValidationSpecs.hasMetadataId(metadata.getId())) > 0); diff --git a/services/src/test/java/org/fao/geonet/api/records/MetadataSharingApiTest.java b/services/src/test/java/org/fao/geonet/api/records/MetadataSharingApiTest.java index e5ee8d9d129..abbddc721fc 100644 --- a/services/src/test/java/org/fao/geonet/api/records/MetadataSharingApiTest.java +++ b/services/src/test/java/org/fao/geonet/api/records/MetadataSharingApiTest.java @@ -31,7 +31,12 @@ import org.fao.geonet.api.records.model.SharingResponse; import org.fao.geonet.domain.Group; import org.fao.geonet.domain.GroupType; +import org.fao.geonet.domain.ISODate; import org.fao.geonet.domain.Metadata; +import org.fao.geonet.domain.MetadataType; +import org.fao.geonet.domain.MetadataValidation; +import org.fao.geonet.domain.MetadataValidationId; +import org.fao.geonet.domain.MetadataValidationStatus; import org.fao.geonet.domain.OperationAllowed; import org.fao.geonet.domain.Profile; import org.fao.geonet.domain.ReservedGroup; @@ -44,7 +49,9 @@ import org.fao.geonet.kernel.setting.Settings; import org.fao.geonet.repository.MetadataRepository; import org.fao.geonet.repository.MetadataStatusRepository; +import org.fao.geonet.repository.MetadataValidationRepository; import org.fao.geonet.repository.OperationAllowedRepository; +import org.fao.geonet.repository.StatusValueRepository; import org.fao.geonet.repository.UserRepositoryTest; import org.fao.geonet.services.AbstractServiceIntegrationTest; import org.junit.Before; @@ -87,6 +94,12 @@ public class MetadataSharingApiTest extends AbstractServiceIntegrationTest { @Autowired private MetadataStatusRepository metadataStatusRepository; + @Autowired + private MetadataValidationRepository metadataValidationRepository; + + @Autowired + private StatusValueRepository statusValueRepository; + @Autowired private SettingManager settingManager; @@ -643,4 +656,82 @@ private void grantUserAdminInNewGroup(User user, String groupName) { .setProfile(Profile.UserAdmin) .setUser(user)); } + + /** + * Verifies that publishing a TEMPLATE succeeds even when the metadata has a required invalid + * validation record and {@code allowPublishInvalidMd} is {@code false}. + * Templates are exempt from validation checks. + */ + @Test + public void publishInvalidTemplateSucceedsWhenPublishInvalidMdDisabled() throws Exception { + // Disable publishing of invalid metadata – the strict mode that triggered the bug. + settingManager.setValue(Settings.METADATA_WORKFLOW_ALLOW_PUBLISH_INVALID_MD, false); + + // Change the metadata type to TEMPLATE. + Metadata template = metadataRepository.findById(metadataId).get(); + template.getDataInfo().setType(MetadataType.TEMPLATE); + metadataRepository.save(template); + + // Persist a required, INVALID validation record so the check would fail for normal metadata. + MetadataValidation invalidValidation = new MetadataValidation() + .setId(new MetadataValidationId(metadataId, "xsd")) + .setStatus(MetadataValidationStatus.INVALID) + .setRequired(true) + .setNumTests(1) + .setNumFailures(1); + metadataValidationRepository.save(invalidValidation); + + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + MockHttpSession mockHttpSession = loginAs(reviewerUser); + + // Publishing the template must succeed (HTTP 204) despite the invalid validation record. + mockMvc.perform(put("/srv/api/records/" + metadataUuid + "/publish") + .session(mockHttpSession)) + .andExpect(status().isNoContent()); + + // Confirm publication privileges were actually granted. + List ops = operationAllowedRepository.findAllById_MetadataId(metadataId); + boolean published = ops.stream().anyMatch(op -> ReservedGroup.isReserved(op.getId().getGroupId())); + assertTrue("Template should be published even when it has an invalid validation record", published); + } + + /** + * Verifies that publishing a TEMPLATE succeeds even when its workflow status is DRAFT + * (non-approved) and {@code allowPublishNonApprovedMd} is {@code false}. + * Templates are exempt from approval status checks. + */ + @Test + public void publishNonApprovedTemplateSucceedsWhenPublishNonApprovedMdDisabled() throws Exception { + // Disable publishing of non-approved metadata – the strict mode that triggered the bug. + settingManager.setValue(Settings.METADATA_WORKFLOW_ALLOW_PUBLISH_NON_APPROVED_MD, false); + + // Change the metadata type to TEMPLATE. + Metadata template = metadataRepository.findById(metadataId).get(); + template.getDataInfo().setType(MetadataType.TEMPLATE); + metadataRepository.save(template); + + // Assign a DRAFT (non-approved) workflow status so the check would fail for normal metadata. + MetadataStatus draftStatus = new MetadataStatus(); + draftStatus.setMetadataId(metadataId); + draftStatus.setChangeDate(new ISODate()); + draftStatus.setUserId(1); + draftStatus.setChangeMessage("draft status for template publish test"); + draftStatus.setOwner(1); + draftStatus.setUuid(metadataUuid); + draftStatus.setStatusValue(statusValueRepository.findById(Integer.parseInt(StatusValue.Status.DRAFT)).get()); + metadataStatusRepository.save(draftStatus); + + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + MockHttpSession mockHttpSession = loginAs(reviewerUser); + + // Publishing the template must succeed (HTTP 204) despite the non-approved status. + mockMvc.perform(put("/srv/api/records/" + metadataUuid + "/publish") + .session(mockHttpSession)) + .andExpect(status().isNoContent()); + + // Confirm publication privileges were actually granted. + List ops = operationAllowedRepository.findAllById_MetadataId(metadataId); + boolean published = ops.stream().anyMatch(op -> ReservedGroup.isReserved(op.getId().getGroupId())); + assertTrue("Template should be published even when its workflow status is not approved", published); + } } diff --git a/web-ui/src/main/resources/catalog/locales/en-admin.json b/web-ui/src/main/resources/catalog/locales/en-admin.json index 67bf99e235d..7d88c4b1620 100644 --- a/web-ui/src/main/resources/catalog/locales/en-admin.json +++ b/web-ui/src/main/resources/catalog/locales/en-admin.json @@ -937,9 +937,9 @@ "metadata/workflow/allowSubmitApproveInvalidMd": "Allow submission/approval of invalid metadata", "metadata/workflow/allowSubmitApproveInvalidMd-help": "Allows the submission/approval of metadata that is not valid according to xsd or schematron rules.", "metadata/workflow/allowPublishNonApprovedMd": "Allow publication of non-approved metadata", - "metadata/workflow/allowPublishNonApprovedMd-help": "Allows the publication of metadata that is not approved.", + "metadata/workflow/allowPublishNonApprovedMd-help": "Allows the publication of metadata that is not approved. Excluding templates.", "metadata/workflow/allowPublishInvalidMd": "Allow publication of invalid metadata", - "metadata/workflow/allowPublishInvalidMd-help": "Allows the publication of metadata that is not valid according to xsd or schematron rules.", + "metadata/workflow/allowPublishInvalidMd-help": "Allows the publication of metadata that is not valid according to xsd or schematron rules. Excluding templates.", "metadata/workflow/draftWhenInGroup": "Activate workflow for record created in ", "metadata/workflow/enable": "Enable workflow", "draftWhenInGroup-all": "Any group", From 49a8fad7645764dff475a9b2c6703493878e92b7 Mon Sep 17 00:00:00 2001 From: tylerjmchugh Date: Thu, 16 Jul 2026 09:15:07 -0400 Subject: [PATCH 2/7] Apply suggestions from code review --- .../org/fao/geonet/domain/MetadataType.java | 22 ++++++++++++++----- .../api/records/MetadataSharingApi.java | 10 ++------- .../api/records/MetadataWorkflowApi.java | 6 +++-- .../records/editing/MetadataEditingApi.java | 3 ++- .../api/records/MetadataSharingApiTest.java | 4 ++-- 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/domain/src/main/java/org/fao/geonet/domain/MetadataType.java b/domain/src/main/java/org/fao/geonet/domain/MetadataType.java index 372c04c26a6..148dcda7757 100644 --- a/domain/src/main/java/org/fao/geonet/domain/MetadataType.java +++ b/domain/src/main/java/org/fao/geonet/domain/MetadataType.java @@ -34,22 +34,22 @@ public enum MetadataType { /** * Indicates the associated {@link Metadata} entity is a normal metadata. */ - METADATA('n'), + METADATA('n', true), /** * Indicates the associated {@link Metadata} entity is a template metadata. */ - TEMPLATE('y'), + TEMPLATE('y', false), /** * Indicates the associated {@link Metadata} entity is a sub-template metadata.

A * sub-template is a metadata fragment that can be inserted into another metadata. It can also * be shared as an xlink in multiple metadata to reduce duplication. */ - SUB_TEMPLATE('s'), + SUB_TEMPLATE('s', false), /** * Indicates the associated {@link Metadata} entity is a template of sub template. */ - TEMPLATE_OF_SUB_TEMPLATE('t'); + TEMPLATE_OF_SUB_TEMPLATE('t', false); /** * The code (for backwards compatibility) of the metadatatype. @@ -59,10 +59,22 @@ public enum MetadataType { * Same as {@link #code} expect as a string instead of a char. */ public final String codeString; + /** + * Indicates if the metadata type requires validation. + * Templates do not require validation as they are incomplete by design. + */ + public final boolean requiresValidation; - private MetadataType(final char code) { + /** + * Constructor for the enum. + * + * @param code the code of the metadata type. + * @param requiresValidation indicates if the metadata type requires validation. + */ + MetadataType(final char code, final boolean requiresValidation) { this.code = code; this.codeString = String.valueOf(code); + this.requiresValidation = requiresValidation; } @Nonnull diff --git a/services/src/main/java/org/fao/geonet/api/records/MetadataSharingApi.java b/services/src/main/java/org/fao/geonet/api/records/MetadataSharingApi.java index 5a8c6d7e30b..c3aa44a3a96 100644 --- a/services/src/main/java/org/fao/geonet/api/records/MetadataSharingApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/MetadataSharingApi.java @@ -1366,15 +1366,9 @@ private void updateOwnership(Integer groupIdentifier, private void checkCanPublishToAllGroup(ServiceContext context, ResourceBundle messages, AbstractMetadata metadata, boolean allowPublishInvalidMd, boolean allowPublishNonApprovedMd) throws Exception { - // Templates are typically incomplete and should not be subject to validation or approval checks - MetadataType metadataType = metadata.getDataInfo().getType(); - if (metadataType == MetadataType.TEMPLATE - || metadataType == MetadataType.SUB_TEMPLATE - || metadataType == MetadataType.TEMPLATE_OF_SUB_TEMPLATE) { - return; - } + boolean metadataTypeRequiresValidation = metadata.getDataInfo().getType().requiresValidation; - if (!allowPublishInvalidMd) { + if (!allowPublishInvalidMd && metadataTypeRequiresValidation) { boolean hasValidation = (metadataValidationRepository.count(MetadataValidationSpecs.hasMetadataId(metadata.getId())) > 0); diff --git a/services/src/main/java/org/fao/geonet/api/records/MetadataWorkflowApi.java b/services/src/main/java/org/fao/geonet/api/records/MetadataWorkflowApi.java index a91221be925..e283d57975a 100644 --- a/services/src/main/java/org/fao/geonet/api/records/MetadataWorkflowApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/MetadataWorkflowApi.java @@ -552,9 +552,10 @@ public Map setStatus(@Parameter(description = API_PAR boolean isAllowedSubmitApproveInvalidMd = settingManager .getValueAsBool(Settings.METADATA_WORKFLOW_ALLOW_SUBMIT_APPROVE_INVALID_MD); + boolean metadataTypeRequiresValidation = metadata.getDataInfo().getType().requiresValidation; if (((status.getStatus() == Integer.parseInt(StatusValue.Status.SUBMITTED)) || (status.getStatus() == Integer.parseInt(StatusValue.Status.APPROVED))) - && !isAllowedSubmitApproveInvalidMd) { + && !isAllowedSubmitApproveInvalidMd && metadataTypeRequiresValidation) { metadataValidator.doValidate(metadata, context.getLanguage()); boolean isInvalid = MetadataUtils.retrieveMetadataValidationStatus(metadata, context); @@ -1373,7 +1374,8 @@ private boolean isAllowedMetadataStatusChange(ServiceContext context, AbstractMe MetadataProcessingReport report) throws Exception { boolean isAllowedSubmitApproveInvalidMd = settingManager .getValueAsBool(Settings.METADATA_WORKFLOW_ALLOW_SUBMIT_APPROVE_INVALID_MD); - if (!isAllowedSubmitApproveInvalidMd) { + boolean metadataTypeRequiresValidation = metadata.getDataInfo().getType().requiresValidation; + if (!isAllowedSubmitApproveInvalidMd && metadataTypeRequiresValidation) { boolean isInvalid = MetadataUtils.retrieveMetadataValidationStatus(metadata, context); if (isInvalid) { diff --git a/services/src/main/java/org/fao/geonet/api/records/editing/MetadataEditingApi.java b/services/src/main/java/org/fao/geonet/api/records/editing/MetadataEditingApi.java index 326a64db857..abd821f8533 100644 --- a/services/src/main/java/org/fao/geonet/api/records/editing/MetadataEditingApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/editing/MetadataEditingApi.java @@ -390,9 +390,10 @@ public void saveEdits( // Automatically change the workflow state after save if (isEnabledWorkflow) { boolean isAllowedSubmitApproveInvalidMd = sm.getValueAsBool(Settings.METADATA_WORKFLOW_ALLOW_SUBMIT_APPROVE_INVALID_MD); + boolean metadataTypeRequiresValidation = metadata.getDataInfo().getType().requiresValidation; if (((status.equals(StatusValue.Status.SUBMITTED)) || (status.equals(StatusValue.Status.APPROVED))) - && !isAllowedSubmitApproveInvalidMd) { + && !isAllowedSubmitApproveInvalidMd && metadataTypeRequiresValidation) { if (!forceValidationOnMdSave) { validator.doValidate(metadata, context.getLanguage()); diff --git a/services/src/test/java/org/fao/geonet/api/records/MetadataSharingApiTest.java b/services/src/test/java/org/fao/geonet/api/records/MetadataSharingApiTest.java index abbddc721fc..866b79cb689 100644 --- a/services/src/test/java/org/fao/geonet/api/records/MetadataSharingApiTest.java +++ b/services/src/test/java/org/fao/geonet/api/records/MetadataSharingApiTest.java @@ -664,7 +664,7 @@ private void grantUserAdminInNewGroup(User user, String groupName) { */ @Test public void publishInvalidTemplateSucceedsWhenPublishInvalidMdDisabled() throws Exception { - // Disable publishing of invalid metadata – the strict mode that triggered the bug. + // Disable publishing of invalid metadata. settingManager.setValue(Settings.METADATA_WORKFLOW_ALLOW_PUBLISH_INVALID_MD, false); // Change the metadata type to TEMPLATE. @@ -702,7 +702,7 @@ public void publishInvalidTemplateSucceedsWhenPublishInvalidMdDisabled() throws */ @Test public void publishNonApprovedTemplateSucceedsWhenPublishNonApprovedMdDisabled() throws Exception { - // Disable publishing of non-approved metadata – the strict mode that triggered the bug. + // Disable publishing of non-approved metadata. settingManager.setValue(Settings.METADATA_WORKFLOW_ALLOW_PUBLISH_NON_APPROVED_MD, false); // Change the metadata type to TEMPLATE. From e6e0da3157c8915d04e6b2dd67baf34c8ef0b668 Mon Sep 17 00:00:00 2001 From: tylerjmchugh Date: Thu, 16 Jul 2026 10:24:12 -0400 Subject: [PATCH 3/7] Update docs and translation --- .../configuring-the-catalog/system-configuration.md | 4 +++- web-ui/src/main/resources/catalog/locales/en-admin.json | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md b/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md index fe58114879a..7c4f6119385 100644 --- a/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md +++ b/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md @@ -361,8 +361,10 @@ Allows to configure the zip export of metadata records and their attachments. The following settings control what metadata can be published when the metadata workflow is active. They are found under `Administration` --> `Settings` --> `Metadata Workflow`. - **Allow publication of invalid metadata** When disabled, only metadata that passes all required XSD and schematron validation rules can be published. When enabled, metadata can be published regardless of its validation status. Templates are always exempt from this check, as they are typically incomplete by design. -- **Allow publication of non-approved metadata** When disabled, only metadata with an `Approved` workflow status can be published. When enabled, metadata can be published regardless of its workflow status. Templates are always exempt from this check, as they are not expected to go through the approval workflow. +- **Allow submission/approval of invalid metadata** When disabled, records must be valid before they can move to `Submitted` or `Approved` workflow status. When enabled, records can be submitted or approved even if validation fails. Templates are always exempt from this check, as they are typically incomplete by design. +- **Allow publication of non-approved metadata** When disabled, only metadata with an `Approved` workflow status can be published. When enabled, metadata can be published regardless of its workflow status. ## Harvesting *Allow editing on harvested records*: Enables/Disables editing of harvested records in the catalogue. By default, harvested records cannot be edited. + diff --git a/web-ui/src/main/resources/catalog/locales/en-admin.json b/web-ui/src/main/resources/catalog/locales/en-admin.json index 7d88c4b1620..daac8aa45fe 100644 --- a/web-ui/src/main/resources/catalog/locales/en-admin.json +++ b/web-ui/src/main/resources/catalog/locales/en-admin.json @@ -935,9 +935,9 @@ "metadata/workflow/automaticUnpublishInvalidMd": "Automatic unpublication of invalid metadata", "metadata/workflow/automaticUnpublishInvalidMd-help": " Automatically unpublishes metadata that is edited that becomes not valid according to xsd or schematron rules.", "metadata/workflow/allowSubmitApproveInvalidMd": "Allow submission/approval of invalid metadata", - "metadata/workflow/allowSubmitApproveInvalidMd-help": "Allows the submission/approval of metadata that is not valid according to xsd or schematron rules.", + "metadata/workflow/allowSubmitApproveInvalidMd-help": "Allows the submission/approval of metadata that is not valid according to xsd or schematron rules. Excluding templates.", "metadata/workflow/allowPublishNonApprovedMd": "Allow publication of non-approved metadata", - "metadata/workflow/allowPublishNonApprovedMd-help": "Allows the publication of metadata that is not approved. Excluding templates.", + "metadata/workflow/allowPublishNonApprovedMd-help": "Allows the publication of metadata that is not approved.", "metadata/workflow/allowPublishInvalidMd": "Allow publication of invalid metadata", "metadata/workflow/allowPublishInvalidMd-help": "Allows the publication of metadata that is not valid according to xsd or schematron rules. Excluding templates.", "metadata/workflow/draftWhenInGroup": "Activate workflow for record created in ", From b15d7f5c9c5688f90f368db7df3c23df85f619d1 Mon Sep 17 00:00:00 2001 From: tylerjmchugh Date: Thu, 16 Jul 2026 10:36:56 -0400 Subject: [PATCH 4/7] Fix tests and javadocs --- .../system-configuration.md | 1 - .../api/records/MetadataSharingApi.java | 5 +- .../api/records/MetadataSharingApiTest.java | 68 +++++++++++-------- .../api/records/MetadataWorkflowApiTest.java | 53 +++++++++++++-- 4 files changed, 91 insertions(+), 36 deletions(-) diff --git a/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md b/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md index 7c4f6119385..d0a6dcef3bf 100644 --- a/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md +++ b/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md @@ -367,4 +367,3 @@ The following settings control what metadata can be published when the metadata ## Harvesting *Allow editing on harvested records*: Enables/Disables editing of harvested records in the catalogue. By default, harvested records cannot be edited. - diff --git a/services/src/main/java/org/fao/geonet/api/records/MetadataSharingApi.java b/services/src/main/java/org/fao/geonet/api/records/MetadataSharingApi.java index c3aa44a3a96..0ecdc5f7f2a 100644 --- a/services/src/main/java/org/fao/geonet/api/records/MetadataSharingApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/MetadataSharingApi.java @@ -1353,14 +1353,13 @@ private void updateOwnership(Integer groupIdentifier, /** * For privileges to {@link ReservedGroup#all} group, check if it's allowed or not to publish invalid or - * non-approved metadata. Templates (including sub-templates and templates of sub-templates) are exempt from - * these checks, as they are typically incomplete and not expected to be valid or approved. + * non-approved metadata. Templates are excluded from the validation check as they are incomplete by design. * * @param context the current service context * @param messages the resource bundle for error messages * @param metadata the metadata record being published * @param allowPublishInvalidMd whether publishing invalid metadata is allowed (excluding templates) - * @param allowPublishNonApprovedMd whether publishing non-approved metadata is allowed (excluding templates) + * @param allowPublishNonApprovedMd whether publishing non-approved metadata is allowed * @throws Exception if the metadata is invalid or not approved and publishing is not allowed */ private void checkCanPublishToAllGroup(ServiceContext context, ResourceBundle messages, AbstractMetadata metadata, diff --git a/services/src/test/java/org/fao/geonet/api/records/MetadataSharingApiTest.java b/services/src/test/java/org/fao/geonet/api/records/MetadataSharingApiTest.java index 866b79cb689..8cfcce9cad3 100644 --- a/services/src/test/java/org/fao/geonet/api/records/MetadataSharingApiTest.java +++ b/services/src/test/java/org/fao/geonet/api/records/MetadataSharingApiTest.java @@ -27,11 +27,11 @@ import jeeves.server.context.ServiceContext; import org.fao.geonet.api.records.model.GroupOperations; import org.fao.geonet.api.records.model.GroupPrivilege; +import org.fao.geonet.api.records.model.MetadataStatusParameter; import org.fao.geonet.api.records.model.SharingParameter; import org.fao.geonet.api.records.model.SharingResponse; import org.fao.geonet.domain.Group; import org.fao.geonet.domain.GroupType; -import org.fao.geonet.domain.ISODate; import org.fao.geonet.domain.Metadata; import org.fao.geonet.domain.MetadataType; import org.fao.geonet.domain.MetadataValidation; @@ -658,9 +658,8 @@ private void grantUserAdminInNewGroup(User user, String groupName) { } /** - * Verifies that publishing a TEMPLATE succeeds even when the metadata has a required invalid - * validation record and {@code allowPublishInvalidMd} is {@code false}. - * Templates are exempt from validation checks. + * Verifies that publishing a TEMPLATE succeeds even when invalid and {@code allowPublishInvalidMd} is {@code false}. + * Templates are exempt from validation checks during publish operations. */ @Test public void publishInvalidTemplateSucceedsWhenPublishInvalidMdDisabled() throws Exception { @@ -696,42 +695,55 @@ public void publishInvalidTemplateSucceedsWhenPublishInvalidMdDisabled() throws } /** - * Verifies that publishing a TEMPLATE succeeds even when its workflow status is DRAFT - * (non-approved) and {@code allowPublishNonApprovedMd} is {@code false}. - * Templates are exempt from approval status checks. + * Verifies that approving a TEMPLATE succeeds even when invalid and {@code allowSubmitApproveInvalidMd} is {@code false}. + * Templates are exempt from validation checks during submit/approve operations. */ @Test - public void publishNonApprovedTemplateSucceedsWhenPublishNonApprovedMdDisabled() throws Exception { - // Disable publishing of non-approved metadata. - settingManager.setValue(Settings.METADATA_WORKFLOW_ALLOW_PUBLISH_NON_APPROVED_MD, false); + public void approveInvalidTemplateSucceedsWhenAllowInvalidSubmissionApprovalDisabled() throws Exception { + // Disable submission/approval of invalid metadata. + settingManager.setValue(Settings.METADATA_WORKFLOW_ALLOW_SUBMIT_APPROVE_INVALID_MD, false); + // The /status workflow endpoint requires workflow to be enabled for the record group. + settingManager.setValue(Settings.METADATA_WORKFLOW_ENABLE, true); + settingManager.setValue(Settings.METADATA_WORKFLOW_DRAFT_WHEN_IN_GROUP, ".*"); // Change the metadata type to TEMPLATE. Metadata template = metadataRepository.findById(metadataId).get(); template.getDataInfo().setType(MetadataType.TEMPLATE); metadataRepository.save(template); - // Assign a DRAFT (non-approved) workflow status so the check would fail for normal metadata. - MetadataStatus draftStatus = new MetadataStatus(); - draftStatus.setMetadataId(metadataId); - draftStatus.setChangeDate(new ISODate()); - draftStatus.setUserId(1); - draftStatus.setChangeMessage("draft status for template publish test"); - draftStatus.setOwner(1); - draftStatus.setUuid(metadataUuid); - draftStatus.setStatusValue(statusValueRepository.findById(Integer.parseInt(StatusValue.Status.DRAFT)).get()); - metadataStatusRepository.save(draftStatus); + // Persist a required, INVALID validation record so the check would fail for normal metadata. + MetadataValidation invalidValidation = new MetadataValidation() + .setId(new MetadataValidationId(metadataId, "xsd")) + .setStatus(MetadataValidationStatus.INVALID) + .setRequired(true) + .setNumTests(1) + .setNumFailures(1); + metadataValidationRepository.save(invalidValidation); MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); MockHttpSession mockHttpSession = loginAs(reviewerUser); - // Publishing the template must succeed (HTTP 204) despite the non-approved status. - mockMvc.perform(put("/srv/api/records/" + metadataUuid + "/publish") - .session(mockHttpSession)) - .andExpect(status().isNoContent()); + // Create the status parameter with the approved status + MetadataStatusParameter statusParameter = new MetadataStatusParameter(); + statusParameter.setStatus(Integer.parseInt(StatusValue.Status.APPROVED)); - // Confirm publication privileges were actually granted. - List ops = operationAllowedRepository.findAllById_MetadataId(metadataId); - boolean published = ops.stream().anyMatch(op -> ReservedGroup.isReserved(op.getId().getGroupId())); - assertTrue("Template should be published even when its workflow status is not approved", published); + // Convert to JSON + Gson gson = new Gson(); + String json = gson.toJson(statusParameter); + + // Approving the template must succeed despite the invalid validation record. + mockMvc.perform(put("/srv/api/records/" + metadataUuid + "/status") + .session(mockHttpSession) + .content(json) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()); + + // Confirm the template was actually approved. + List statuses = metadataStatusRepository.findAllByMetadataId(metadataId, Sort.unsorted()); + boolean approved = statuses.stream() + .map(MetadataStatus::getStatusValue) + .map(StatusValue::getId) + .anyMatch(id -> id.equals(Integer.parseInt(StatusValue.Status.APPROVED))); + assertTrue("Template should be approved even when it has an invalid validation record", approved); } } diff --git a/services/src/test/java/org/fao/geonet/api/records/MetadataWorkflowApiTest.java b/services/src/test/java/org/fao/geonet/api/records/MetadataWorkflowApiTest.java index 87ec40f129a..b20dabfece4 100644 --- a/services/src/test/java/org/fao/geonet/api/records/MetadataWorkflowApiTest.java +++ b/services/src/test/java/org/fao/geonet/api/records/MetadataWorkflowApiTest.java @@ -5,10 +5,13 @@ import jeeves.server.context.ServiceContext; import org.fao.geonet.api.ApiUtils; import org.fao.geonet.api.records.model.MetadataBatchSubmitParameter; +import org.fao.geonet.api.records.model.MetadataStatusParameter; import org.fao.geonet.domain.*; import org.fao.geonet.kernel.SelectionManager; import org.fao.geonet.kernel.setting.SettingManager; import org.fao.geonet.kernel.setting.Settings; +import org.fao.geonet.repository.MetadataRepository; +import org.fao.geonet.repository.MetadataValidationRepository; import org.fao.geonet.repository.MetadataStatusRepository; import org.fao.geonet.repository.StatusValueRepository; import org.fao.geonet.services.AbstractServiceIntegrationTest; @@ -38,20 +41,22 @@ public class MetadataWorkflowApiTest extends AbstractServiceIntegrationTest { @Autowired MetadataStatusRepository metadataStatusRepo; @Autowired - StatusValueRepository statusValueRepo; - @Autowired private SettingManager settingManager; @Autowired + private MetadataRepository metadataRepository; + @Autowired + private MetadataValidationRepository metadataValidationRepository; + @Autowired private StatusValueRepository statusValueRepository; private String uuid; private String uuid2; - private ServiceContext context; + private int metadataId; @Before public void setUp() throws Exception { - context = createServiceContext(); + ServiceContext context = createServiceContext(); loginAsAdmin(context); settingManager.setValue(Settings.METADATA_WORKFLOW_ENABLE, true); settingManager.setValue(Settings.METADATA_WORKFLOW_DRAFT_WHEN_IN_GROUP, ".*"); @@ -59,6 +64,7 @@ public void setUp() throws Exception { AbstractMetadata metadata = injectMetadataInDb(getSampleMetadataXml(), context); injectStatusForMetadata(metadata, DRAFT); uuid = metadata.getUuid(); + metadataId = metadata.getId(); AbstractMetadata metadata2 = injectMetadataInDb(getSampleMetadataXml(), context); injectStatusForMetadata(metadata2, APPROVED); uuid2 = metadata2.getUuid(); @@ -107,6 +113,45 @@ public void testBatchSubmit() throws Exception { .andExpect(jsonPath("$.numberOfRecordsProcessed").value(1)); } + @Test + public void testApproveInvalidTemplateSucceedsWhenAllowInvalidSubmissionApprovalDisabled() throws Exception { + settingManager.setValue(Settings.METADATA_WORKFLOW_ALLOW_SUBMIT_APPROVE_INVALID_MD, false); + + Metadata templateMetadata = metadataRepository.findOneByUuid(uuid); + templateMetadata.getDataInfo().setType(MetadataType.TEMPLATE); + metadataRepository.save(templateMetadata); + + MetadataValidation invalidValidation = new MetadataValidation() + .setId(new MetadataValidationId(metadataId, "xsd")) + .setStatus(MetadataValidationStatus.INVALID) + .setRequired(true) + .setNumTests(1) + .setNumFailures(1); + metadataValidationRepository.save(invalidValidation); + + MetadataStatusParameter statusParameter = new MetadataStatusParameter(); + statusParameter.setStatus(Integer.parseInt(APPROVED)); + statusParameter.setChangeMessage("approve template in test"); + + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + MockHttpSession mockHttpSession = loginAsAdmin(); + Gson gson = new Gson(); + String json = gson.toJson(statusParameter); + + mockMvc.perform(put("/srv/api/records/" + uuid + "/status") + .content(json) + .contentType(API_JSON_EXPECTED_ENCODING) + .accept(MediaType.parseMediaType("application/json")) + .session(mockHttpSession)) + .andExpect(status().isCreated()); + + boolean approvedStatusCreated = metadataStatusRepo.findAllByMetadataId(metadataId, org.springframework.data.domain.Sort.unsorted()).stream() + .map(MetadataStatus::getStatusValue) + .map(StatusValue::getId) + .anyMatch(id -> id == Integer.parseInt(APPROVED)); + Assert.isTrue(approvedStatusCreated); + } + private void injectStatusForMetadata(AbstractMetadata metadata, String status) { MetadataStatus metadataStatus; Optional statusValue; From bcf95f2bc3ed6930bfe84443c91e77ff79eddee4 Mon Sep 17 00:00:00 2001 From: tylerjmchugh Date: Fri, 17 Jul 2026 15:01:07 -0400 Subject: [PATCH 5/7] Re-trigger checks From 41775a2a9f61abbe189ced457a7f45acfca4fde5 Mon Sep 17 00:00:00 2001 From: tylerjmchugh Date: Mon, 20 Jul 2026 08:01:09 -0400 Subject: [PATCH 6/7] Apply suggestions from code review --- .../org/fao/geonet/api/records/editing/MetadataEditingApi.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/src/main/java/org/fao/geonet/api/records/editing/MetadataEditingApi.java b/services/src/main/java/org/fao/geonet/api/records/editing/MetadataEditingApi.java index abd821f8533..0c6f0d4c857 100644 --- a/services/src/main/java/org/fao/geonet/api/records/editing/MetadataEditingApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/editing/MetadataEditingApi.java @@ -456,12 +456,13 @@ public void saveEdits( } boolean automaticUnpublishInvalidMd = sm.getValueAsBool(METADATA_WORKFLOW_AUTOMATIC_UNPUBLISH_INVALID_MD); + boolean metadataTypeRequiresValidation = metadata.getDataInfo().getType().requiresValidation; boolean isUnpublished = false; // Unpublish the metadata automatically if the setting // automaticUnpublishInvalidMd is enabled and // the metadata becomes invalid - if (automaticUnpublishInvalidMd) { + if (automaticUnpublishInvalidMd && metadataTypeRequiresValidation) { final OperationAllowedRepository operationAllowedRepo = context .getBean(OperationAllowedRepository.class); From ce2c97738cbd49e50c9891574cc24c4d5ede52aa Mon Sep 17 00:00:00 2001 From: tylerjmchugh Date: Mon, 20 Jul 2026 08:31:52 -0400 Subject: [PATCH 7/7] Remove extra test --- .../api/records/MetadataSharingApiTest.java | 53 ------------------- 1 file changed, 53 deletions(-) diff --git a/services/src/test/java/org/fao/geonet/api/records/MetadataSharingApiTest.java b/services/src/test/java/org/fao/geonet/api/records/MetadataSharingApiTest.java index 8cfcce9cad3..1d8ccc2edd0 100644 --- a/services/src/test/java/org/fao/geonet/api/records/MetadataSharingApiTest.java +++ b/services/src/test/java/org/fao/geonet/api/records/MetadataSharingApiTest.java @@ -693,57 +693,4 @@ public void publishInvalidTemplateSucceedsWhenPublishInvalidMdDisabled() throws boolean published = ops.stream().anyMatch(op -> ReservedGroup.isReserved(op.getId().getGroupId())); assertTrue("Template should be published even when it has an invalid validation record", published); } - - /** - * Verifies that approving a TEMPLATE succeeds even when invalid and {@code allowSubmitApproveInvalidMd} is {@code false}. - * Templates are exempt from validation checks during submit/approve operations. - */ - @Test - public void approveInvalidTemplateSucceedsWhenAllowInvalidSubmissionApprovalDisabled() throws Exception { - // Disable submission/approval of invalid metadata. - settingManager.setValue(Settings.METADATA_WORKFLOW_ALLOW_SUBMIT_APPROVE_INVALID_MD, false); - // The /status workflow endpoint requires workflow to be enabled for the record group. - settingManager.setValue(Settings.METADATA_WORKFLOW_ENABLE, true); - settingManager.setValue(Settings.METADATA_WORKFLOW_DRAFT_WHEN_IN_GROUP, ".*"); - - // Change the metadata type to TEMPLATE. - Metadata template = metadataRepository.findById(metadataId).get(); - template.getDataInfo().setType(MetadataType.TEMPLATE); - metadataRepository.save(template); - - // Persist a required, INVALID validation record so the check would fail for normal metadata. - MetadataValidation invalidValidation = new MetadataValidation() - .setId(new MetadataValidationId(metadataId, "xsd")) - .setStatus(MetadataValidationStatus.INVALID) - .setRequired(true) - .setNumTests(1) - .setNumFailures(1); - metadataValidationRepository.save(invalidValidation); - - MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); - MockHttpSession mockHttpSession = loginAs(reviewerUser); - - // Create the status parameter with the approved status - MetadataStatusParameter statusParameter = new MetadataStatusParameter(); - statusParameter.setStatus(Integer.parseInt(StatusValue.Status.APPROVED)); - - // Convert to JSON - Gson gson = new Gson(); - String json = gson.toJson(statusParameter); - - // Approving the template must succeed despite the invalid validation record. - mockMvc.perform(put("/srv/api/records/" + metadataUuid + "/status") - .session(mockHttpSession) - .content(json) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); - - // Confirm the template was actually approved. - List statuses = metadataStatusRepository.findAllByMetadataId(metadataId, Sort.unsorted()); - boolean approved = statuses.stream() - .map(MetadataStatus::getStatusValue) - .map(StatusValue::getId) - .anyMatch(id -> id.equals(Integer.parseInt(StatusValue.Status.APPROVED))); - assertTrue("Template should be approved even when it has an invalid validation record", approved); - } }