Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -833,19 +833,22 @@ public List<PrivilegeStatusChange> reservedGroupsPrivilegesStatusChanges(Sharing
}

/**
* 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 are excluded from the validation check as they are incomplete by design.
*
* @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
* @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 {

if (!allowPublishInvalidMd) {
boolean metadataTypeRequiresValidation = metadata.getDataInfo().getType().requiresValidation;

if (!allowPublishInvalidMd && metadataTypeRequiresValidation) {
boolean hasValidation =
(metadataValidationRepository.count(MetadataValidationSpecs.hasMetadataId(metadata.getId())) > 0);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,22 @@
import jeeves.server.UserSession;
import jeeves.server.context.ServiceContext;
import org.fao.geonet.api.exception.NotAllowedException;
import org.fao.geonet.domain.AbstractMetadata;
import org.fao.geonet.domain.MetadataSourceInfo;
import org.fao.geonet.domain.Profile;
import org.fao.geonet.domain.*;
import org.fao.geonet.kernel.AccessManager;
import org.fao.geonet.kernel.setting.SettingManager;
import org.fao.geonet.kernel.setting.Settings;
import org.fao.geonet.repository.MetadataValidationRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import org.springframework.data.jpa.domain.Specification;

import java.lang.reflect.Method;
import java.util.ResourceBundle;

import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
Expand All @@ -61,6 +65,10 @@ public class MetadataPublicationServiceTest {
private static final String METADATA_UUID = "uuid-123";
private static final int GROUP_OWNER = 12;

@Mock
private MetadataValidationRepository metadataValidationRepository;


@Mock
private AccessManager accessManager;

Expand All @@ -70,6 +78,8 @@ public class MetadataPublicationServiceTest {
@InjectMocks
private MetadataPublicationService service;



private AbstractMetadata mockMetadata() {
AbstractMetadata metadata = mock(AbstractMetadata.class);
when(metadata.getId()).thenReturn(METADATA_ID);
Expand Down Expand Up @@ -149,4 +159,33 @@ public void allowsAdministratorWithoutCheckingPublicationProfile() throws Except
// Administrators bypass the configured publication-profile check.
verify(accessManager, never()).isProfileOnGroup(any(), any(), anyInt());
}



/**
* Templates have {@code MetadataType.requiresValidation == false}, so the
* {@code checkCanPublishToAllGroup} gate must skip the validation repository entirely
* even when {@code allowPublishInvalidMd} is {@code false}.
*/
@Test
public void templateIsExemptFromValidationCheckWhenPublishInvalidMdDisabled() throws Exception {
AbstractMetadata template = mock(AbstractMetadata.class);
MetadataDataInfo dataInfo = mock(MetadataDataInfo.class);
when(dataInfo.getType()).thenReturn(MetadataType.TEMPLATE);
when(template.getDataInfo()).thenReturn(dataInfo);

ServiceContext context = mock(ServiceContext.class);
ResourceBundle messages = mock(ResourceBundle.class);

Method method = MetadataPublicationService.class.getDeclaredMethod(
"checkCanPublishToAllGroup",
ServiceContext.class, ResourceBundle.class, AbstractMetadata.class, boolean.class, boolean.class);
method.setAccessible(true);

// Must not throw – templates are exempt from validation checks.
method.invoke(service, context, messages, template, false, true);

// The validation repository must never be consulted for templates.
verify(metadataValidationRepository, never()).count(any(Specification.class));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,22 @@ 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 enabled, metadata records can be published regardless of its validation status.
When disabled, only records that passe all required XSD and schematron validation rules can be published.
*This setting applies to metadata records only, metadata templates exempt from this check, as they are incomplete by design.*
- **Allow submission/approval of invalid metadata**
When enabled, records can be submitted or approved even when validation fails.
When disabled, records must be valid before they can move to `Submitted` or `Approved` workflow status.
*This setting applies to metadata records only, metadata templates exempt from this check, as they are incomplete by design.*
- **Allow publication of non-approved metadata**
When enabled, metadata records and templates can be published regardless of its workflow status.
When disabled, only metadata records and templates with an `Approved` workflow status can be published.

## Harvesting

*Allow editing on harvested records*: Enables/Disables editing of harvested records in the catalogue. By default, harvested records cannot be edited.
22 changes: 17 additions & 5 deletions domain/src/main/java/org/fao/geonet/domain/MetadataType.java
Original file line number Diff line number Diff line change
Expand Up @@ -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. <p></p> 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.
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -575,9 +575,10 @@ public Map<Integer, StatusChangeType> 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);
Expand Down Expand Up @@ -1396,7 +1397,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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -455,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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
import org.fao.geonet.domain.Group;
import org.fao.geonet.domain.GroupType;
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;
Expand All @@ -44,7 +48,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;
Expand Down Expand Up @@ -87,6 +93,12 @@ public class MetadataSharingApiTest extends AbstractServiceIntegrationTest {
@Autowired
private MetadataStatusRepository metadataStatusRepository;

@Autowired
private MetadataValidationRepository metadataValidationRepository;

@Autowired
private StatusValueRepository statusValueRepository;

@Autowired
private SettingManager settingManager;

Expand Down Expand Up @@ -643,4 +655,41 @@ private void grantUserAdminInNewGroup(User user, String groupName) {
.setProfile(Profile.UserAdmin)
.setUser(user));
}

/**
* 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 {
// Disable publishing of invalid metadata.
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<OperationAllowed> 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);
}
}
Loading
Loading