diff --git a/core/src/main/java/org/fao/geonet/api/records/attachments/AbstractStore.java b/core/src/main/java/org/fao/geonet/api/records/attachments/AbstractStore.java index 168e4e2a63c..35d1b2867a7 100644 --- a/core/src/main/java/org/fao/geonet/api/records/attachments/AbstractStore.java +++ b/core/src/main/java/org/fao/geonet/api/records/attachments/AbstractStore.java @@ -286,7 +286,7 @@ public String toString() { private String escapeResourceManagementExternalProperties(String value) { return value.replace(RESOURCE_MANAGEMENT_EXTERNAL_PROPERTIES_SEPARATOR, RESOURCE_MANAGEMENT_EXTERNAL_PROPERTIES_ESCAPED_SEPARATOR); -} + } /** * Create an encoded base 64 object id contains the following fields to uniquely identify the resource diff --git a/core/src/main/java/org/fao/geonet/kernel/SelectionManager.java b/core/src/main/java/org/fao/geonet/kernel/SelectionManager.java index a42a9e982e9..230bf390ff6 100644 --- a/core/src/main/java/org/fao/geonet/kernel/SelectionManager.java +++ b/core/src/main/java/org/fao/geonet/kernel/SelectionManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2016 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -51,9 +51,9 @@ * Manage objects selection for a user session. */ public class SelectionManager { - public static final String SELECTION_METADATA = "metadata"; - public static final String SELECTION_BUCKET = "bucket"; + // Bucket name used in the search UI to store the selected the metadata + public static final String SELECTION_BUCKET = "s101"; // used to limit select all if get system setting maxrecords fails or contains value we can't parse public static final int DEFAULT_MAXHITS = 1000; public static final String ADD_ALL_SELECTED = "add-all"; @@ -61,20 +61,20 @@ public class SelectionManager { public static final String ADD_SELECTED = "add"; public static final String REMOVE_SELECTED = "remove"; public static final String CLEAR_ADD_SELECTED = "clear-add"; - private Hashtable> selections = null; + private Hashtable> selections; private SelectionManager() { - selections = new Hashtable>(0); + selections = new Hashtable<>(0); Set MDSelection = Collections - .synchronizedSet(new HashSet(0)); + .synchronizedSet(new HashSet<>(0)); selections.put(SELECTION_METADATA, MDSelection); } public Map getSelectionsAndSize() { return selections.entrySet().stream().collect(Collectors.toMap( - e -> e.getKey(), + Map.Entry::getKey, e -> e.getValue().size() )); } @@ -183,7 +183,7 @@ public int updateSelection(String type, // Get the selection manager or create it Set selection = this.getSelection(type); if (selection == null) { - selection = Collections.synchronizedSet(new HashSet()); + selection = Collections.synchronizedSet(new HashSet<>()); this.selections.put(type, selection); } @@ -192,30 +192,21 @@ public int updateSelection(String type, this.selectAll(type, context, session); else if (selected.equals(REMOVE_ALL_SELECTED)) this.close(type); - else if (selected.equals(ADD_SELECTED) && listOfIdentifiers.size() > 0) { + else if (selected.equals(ADD_SELECTED) && !listOfIdentifiers.isEmpty()) { // TODO ? Should we check that the element exist first ? - for (String paramid : listOfIdentifiers) { - selection.add(paramid); - } - } else if (selected.equals(REMOVE_SELECTED) && listOfIdentifiers.size() > 0) { + selection.addAll(listOfIdentifiers); + } else if (selected.equals(REMOVE_SELECTED) && !listOfIdentifiers.isEmpty()) { for (String paramid : listOfIdentifiers) { selection.remove(paramid); } - } else if (selected.equals(CLEAR_ADD_SELECTED) && listOfIdentifiers.size() > 0) { + } else if (selected.equals(CLEAR_ADD_SELECTED) && !listOfIdentifiers.isEmpty()) { this.close(type); - for (String paramid : listOfIdentifiers) { - selection.add(paramid); - } + selection.addAll(listOfIdentifiers); } } // Remove empty/null element from the selection - Iterator iter = selection.iterator(); - while (iter.hasNext()) { - Object element = iter.next(); - if (element == null) - iter.remove(); - } + selection.removeIf(Objects::isNull); return selection.size(); } @@ -241,14 +232,12 @@ public void selectAll(String type, ServiceContext context, UserSession session) if (StringUtils.isNotEmpty(type)) { JsonNode request = (JsonNode) session.getProperty(Geonet.Session.SEARCH_REQUEST + type); - if (request == null) { - return; - } else { + if (request != null) { final SearchResponse searchResponse; try { EsSearchManager searchManager = context.getBean(EsSearchManager.class); searchResponse = searchManager.query(request.get("query"), FIELDLIST_UUID, 0, maxhits); - List uuidList = new ArrayList(); + List uuidList = new ArrayList<>(); ObjectMapper objectMapper = new ObjectMapper(); for (Hit h : (List) searchResponse.hits().hits()) { uuidList.add((String) objectMapper.convertValue(h.source(), Map.class).get(Geonet.IndexFieldNames.UUID)); @@ -293,7 +282,7 @@ public Set getSelection(String type) { Set sel = selections.get(type); if (sel == null) { Set MDSelection = Collections - .synchronizedSet(new HashSet(0)); + .synchronizedSet(new HashSet<>(0)); selections.put(type, MDSelection); } return selections.get(type); diff --git a/datastorages/cmis/src/main/java/org/fao/geonet/api/records/attachments/CMISStore.java b/datastorages/cmis/src/main/java/org/fao/geonet/api/records/attachments/CMISStore.java index 989dc719fc4..5957cc4c3c8 100644 --- a/datastorages/cmis/src/main/java/org/fao/geonet/api/records/attachments/CMISStore.java +++ b/datastorages/cmis/src/main/java/org/fao/geonet/api/records/attachments/CMISStore.java @@ -779,14 +779,14 @@ public String toString() { } protected static class ResourceHolderImpl implements ResourceHolder { - private CmisObject cmisObject; + private final CmisObject cmisObject; private Path tempFolderPath; private Path path; private final MetadataResource metadataResource; public ResourceHolderImpl(final CmisObject cmisObject, MetadataResource metadataResource) throws IOException { // Preserve filename by putting the files into a temporary folder and using the same filename. - tempFolderPath = Files.createTempDirectory("gn-meta-res-" + String.valueOf(metadataResource.getMetadataId() + "-")); + tempFolderPath = Files.createTempDirectory("gn-meta-res-" + metadataResource.getMetadataId() + "-"); tempFolderPath.toFile().deleteOnExit(); path = tempFolderPath.resolve(getFilename(cmisObject.getName())); this.metadataResource = metadataResource; @@ -817,11 +817,5 @@ public void close() throws IOException { path=null; tempFolderPath = null; } - - @Override - protected void finalize() throws Throwable { - close(); - super.finalize(); - } } } diff --git a/datastorages/jcloud/src/main/java/org/fao/geonet/api/records/attachments/JCloudStore.java b/datastorages/jcloud/src/main/java/org/fao/geonet/api/records/attachments/JCloudStore.java index e4016a72e37..579704aff45 100644 --- a/datastorages/jcloud/src/main/java/org/fao/geonet/api/records/attachments/JCloudStore.java +++ b/datastorages/jcloud/src/main/java/org/fao/geonet/api/records/attachments/JCloudStore.java @@ -24,13 +24,11 @@ */ package org.fao.geonet.api.records.attachments; - import static org.jclouds.blobstore.options.PutOptions.Builder.multipart; import jeeves.server.context.ServiceContext; import org.apache.commons.collections.MapUtils; -import org.apache.commons.lang.StringUtils; import org.fao.geonet.ApplicationContextHolder; import org.fao.geonet.api.exception.ResourceNotFoundException; import org.fao.geonet.constants.Geonet; @@ -50,6 +48,7 @@ import org.jclouds.blobstore.options.CopyOptions; import org.jclouds.blobstore.options.ListContainerOptions; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.StringUtils; import java.io.File; import java.io.IOException; @@ -60,6 +59,7 @@ import java.nio.file.PathMatcher; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -67,6 +67,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nullable; @@ -75,8 +77,17 @@ public class JCloudStore extends AbstractStore { + private static final ConcurrentHashMap locks = new ConcurrentHashMap<>(); + + private static final String FIRST_VERSION = "1"; + + private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + static { + DATE_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC")); + } + // For azure Blob ADSL hdi_isfolder property name used to identify folders - private final static String AZURE_BLOB_IS_FOLDER_PROPERTY_NAME="hdi_isfolder"; + private static final String AZURE_BLOB_IS_FOLDER_PROPERTY_NAME="hdi_isfolder"; private Path baseMetadataDir = null; @@ -102,7 +113,7 @@ public List getResources(final ServiceContext context, final S FileSystems.getDefault().getPathMatcher("glob:" + filter); ListContainerOptions opts = new ListContainerOptions(); - opts.delimiter(jCloudConfiguration.getFolderDelimiter()).prefix(resourceTypeDir);; + opts.delimiter(jCloudConfiguration.getFolderDelimiter()).prefix(resourceTypeDir); // Page through the data String marker = null; @@ -114,7 +125,7 @@ public List getResources(final ServiceContext context, final S PageSet page = jCloudConfiguration.getClient().getBlobStore().list(jCloudConfiguration.getContainerName(), opts); for (StorageMetadata storageMetadata : page) { - // Only add to the list if it is a blob and it matches the filter. + // Only add to the list if it is a blob, and it matches the filter. Path keyPath = new File(storageMetadata.getName()).toPath().getFileName(); if (storageMetadata.getType() == StorageType.BLOB && matcher.matches(keyPath)){ final String filename = getFilename(storageMetadata.getName()); @@ -136,28 +147,56 @@ private MetadataResource createResourceDescription(final ServiceContext context, StorageMetadata storageMetadata, int metadataId, boolean approved) { String filename = getFilename(metadataUuid, resourceId); + Date changedDate; + String changedDatePropertyName = jCloudConfiguration.getExternalResourceManagementChangedDatePropertyName(); + if (storageMetadata.getUserMetadata().containsKey(changedDatePropertyName)) { + String changedDateValue = storageMetadata.getUserMetadata().get(changedDatePropertyName); + try { + changedDate = DATE_FORMATTER.parse(changedDateValue); + } catch (ParseException e) { + Log.warning(Geonet.RESOURCES, String.format("Unable to parse date '%s' into format pattern '%s' on resource '%s' for metadata %d(%s). Will use resource last modified date", + changedDateValue, DATE_FORMATTER.toPattern(), resourceId, metadataId, metadataUuid), e); + changedDate = storageMetadata.getLastModified(); + } + } else { + changedDate = storageMetadata.getLastModified(); + } + + String versionValue = null; if (jCloudConfiguration.isVersioningEnabled()) { - versionValue = storageMetadata.getETag(); // ETAG is cryptic may need some other value? + String versionPropertyName = jCloudConfiguration.getExternalResourceManagementVersionPropertyName(); + if (StringUtils.hasLength(versionPropertyName)) { + if (storageMetadata.getUserMetadata().containsKey(versionPropertyName)) { + versionValue = storageMetadata.getUserMetadata().get(versionPropertyName); + } else { + Log.warning(Geonet.RESOURCES, String.format("Expecting property '%s' on resource '%s' for metadata %d(%s) but the property was not found.", + versionPropertyName, resourceId, metadataId, metadataUuid)); + versionValue = ""; + } + } else { + versionValue = storageMetadata.getETag(); + } } MetadataResourceExternalManagementProperties.ValidationStatus validationStatus = MetadataResourceExternalManagementProperties.ValidationStatus.UNKNOWN; - if (!StringUtils.isEmpty(jCloudConfiguration.getExternalResourceManagementValidationStatusPropertyName())) { + if (StringUtils.hasLength(jCloudConfiguration.getExternalResourceManagementValidationStatusPropertyName())) { String validationStatusPropertyName = jCloudConfiguration.getExternalResourceManagementValidationStatusPropertyName(); String propertyValue = null; if (storageMetadata.getUserMetadata().containsKey(validationStatusPropertyName)) { propertyValue = storageMetadata.getUserMetadata().get(validationStatusPropertyName); } - if (StringUtils.isNotEmpty(propertyValue)) { + if (StringUtils.hasLength(propertyValue)) { validationStatus = MetadataResourceExternalManagementProperties.ValidationStatus.fromValue(Integer.parseInt(propertyValue)); } } MetadataResourceExternalManagementProperties metadataResourceExternalManagementProperties = - getMetadataResourceExternalManagementProperties(context, metadataId, metadataUuid, visibility, resourceId, filename, storageMetadata.getETag(), storageMetadata.getType(), validationStatus); + getMetadataResourceExternalManagementProperties(context, metadataId, metadataUuid, visibility, resourceId, filename, storageMetadata.getETag(), storageMetadata.getType(), + validationStatus); return new FilesystemStoreResource(metadataUuid, metadataId, filename, - settingManager.getNodeURL() + "api/records/", visibility, storageMetadata.getSize(), storageMetadata.getLastModified(), versionValue, metadataResourceExternalManagementProperties, approved); + settingManager.getNodeURL() + "api/records/", visibility, storageMetadata.getSize(), changedDate, versionValue, metadataResourceExternalManagementProperties, approved); } protected static String getFilename(final String key) { @@ -217,50 +256,70 @@ protected String getKey(final ServiceContext context, String metadataUuid, int m @Override public MetadataResource putResource(final ServiceContext context, final String metadataUuid, final String filename, - final InputStream is, @Nullable final Date changeDate, final MetadataResourceVisibility visibility, Boolean approved) + final InputStream is, @Nullable final Date changeDate, final MetadataResourceVisibility visibility, final Boolean approved) throws Exception { return putResource(context, metadataUuid, filename, is, changeDate, visibility, approved, null); } protected MetadataResource putResource(final ServiceContext context, final String metadataUuid, final String filename, - final InputStream is, @Nullable final Date changeDate, final MetadataResourceVisibility visibility, Boolean approved, Map additionalProperties) + final InputStream is, @Nullable final Date changeDate, final MetadataResourceVisibility visibility, final Boolean approved, + Map additionalProperties) throws Exception { final int metadataId = canEdit(context, metadataUuid, approved); String key = getKey(context, metadataUuid, metadataId, visibility, filename); - Map properties = null; + // Get or create a lock object + Object lock = locks.computeIfAbsent(key, k -> new Object()); - try { - StorageMetadata storageMetadata = jCloudConfiguration.getClient().getBlobStore().blobMetadata(jCloudConfiguration.getContainerName(), key); - if (storageMetadata != null) { - properties = storageMetadata.getUserMetadata(); - } - } catch (ContainerNotFoundException ignored) { - // ignored - } + // Avoid multiple updates on the same file at the same time. otherwise the properties could get messed up. + // Especially the version number. + synchronized (lock) { + try { + Map properties = null; + boolean isNewResource = true; + try { + StorageMetadata storageMetadata = jCloudConfiguration.getClient().getBlobStore().blobMetadata(jCloudConfiguration.getContainerName(), key); + if (storageMetadata != null) { + isNewResource = false; - if (properties == null) { - properties = new HashMap<>(); - } + // Copy existing properties + properties = new HashMap<>(storageMetadata.getUserMetadata()); + } + } catch (ContainerNotFoundException ignored) { + // ignored + } + + if (properties == null) { + properties = new HashMap<>(); + } - addProperties(metadataUuid, properties, changeDate, additionalProperties); + setProperties(properties, metadataUuid, changeDate, additionalProperties); - Blob blob = jCloudConfiguration.getClient().getBlobStore().blobBuilder(key) - .payload(is) - .contentLength(is.available()) - .userMetadata(properties) - .build(); - // Upload the Blob in multiple chunks to supports large files. - jCloudConfiguration.getClient().getBlobStore().putBlob(jCloudConfiguration.getContainerName(), blob, multipart()); - Blob blobResults = jCloudConfiguration.getClient().getBlobStore().getBlob(jCloudConfiguration.getContainerName(), key); + // Update/set version + setPropertiesVersion(context, properties, isNewResource, metadataUuid, metadataId, visibility, approved, filename); + Blob blob = jCloudConfiguration.getClient().getBlobStore().blobBuilder(key) + .payload(is) + .contentLength(is.available()) + .userMetadata(properties) + .build(); - return createResourceDescription(context, metadataUuid, visibility, filename, blobResults.getMetadata(), metadataId, approved); + Log.info(Geonet.RESOURCES, + String.format("Put(2) blob '%s' with version label '%s'.", key, properties.get(jCloudConfiguration.getExternalResourceManagementVersionPropertyName()))); + // Upload the Blob in multiple chunks to supports large files. + jCloudConfiguration.getClient().getBlobStore().putBlob(jCloudConfiguration.getContainerName(), blob, multipart()); + Blob blobResults = jCloudConfiguration.getClient().getBlobStore().getBlob(jCloudConfiguration.getContainerName(), key); + + return createResourceDescription(context, metadataUuid, visibility, filename, blobResults.getMetadata(), metadataId, approved); + } finally { + locks.remove(key); + } + } } - protected void addProperties(String metadataUuid, Map properties, Date changeDate, Map additionalProperties) { + protected void setProperties(Map properties, String metadataUuid, Date changeDate, Map additionalProperties) { // Add additional properties if exists. if (MapUtils.isNotEmpty(additionalProperties)) { @@ -268,42 +327,137 @@ protected void addProperties(String metadataUuid, Map properties } // now update metadata uuid and status and change date . - setMetadataUUID(properties, metadataUuid); - // JCloud does not allow changing the last modified date. So the change date will be put in defined changed date field if supplied. - setExternalResourceManagementChangedDate(properties, changeDate); + // JCloud does not allow changing the last modified date or creation date. So the change date/created date will be put in defined changed date/created date field if supplied. + setExternalResourceManagementDates(properties, changeDate); // If it is a new record so set the default status value property if it does not already exist as an additional property. - if (!StringUtils.isEmpty(jCloudConfiguration.getExternalResourceManagementValidationStatusPropertyName()) && + if (StringUtils.hasLength(jCloudConfiguration.getExternalResourceManagementValidationStatusPropertyName()) && !properties.containsKey(jCloudConfiguration.getExternalResourceManagementValidationStatusPropertyName())) { setExternalManagementResourceValidationStatus(properties, jCloudConfiguration.getValidationStatusDefaultValue()); } - } protected void setMetadataUUID(Map properties, String metadataUuid) { // Don't allow users metadata uuid to be supplied as a property so let's overwrite any value that may exist. - if (!StringUtils.isEmpty(jCloudConfiguration.getMetadataUUIDPropertyName())) { - setProperty(properties, jCloudConfiguration.getMetadataUUIDPropertyName(), metadataUuid); + if (StringUtils.hasLength(jCloudConfiguration.getMetadataUUIDPropertyName())) { + setPropertyValue(properties, jCloudConfiguration.getMetadataUUIDPropertyName(), metadataUuid); } } - protected void setExternalResourceManagementChangedDate(Map properties, Date changeDate) { - // Don't allow change date to be supplied as a property so let's overwrite any value that may exist. - if (changeDate != null && !StringUtils.isEmpty(jCloudConfiguration.getExternalResourceManagementChangedDatePropertyName())) { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - properties.put(jCloudConfiguration.getExternalResourceManagementChangedDatePropertyName(), dateFormat.format(changeDate)); + protected void setExternalResourceManagementDates(Map properties, Date changeDate) { + // If changeDate was not supplied then default to now. + if (changeDate == null) { + changeDate = new Date(); + } + + // JCloud does not allow created date to be set so we may supply the value we want as a property so assign the value. + // Only assign the value if we currently don't have a creation date, and we don't have a version assigned either because if either of these exists then + // it will indicate that this is not the first version. + String createdDatePropertyName = jCloudConfiguration.getExternalResourceManagementCreatedDatePropertyName(); + String versionPropertyName = jCloudConfiguration.getExternalResourceManagementVersionPropertyName(); + if (StringUtils.hasLength(createdDatePropertyName) && + !properties.containsKey(createdDatePropertyName) && + (!StringUtils.hasLength(versionPropertyName) || (!properties.containsKey(versionPropertyName))) + ) { + properties.put(jCloudConfiguration.getExternalResourceManagementCreatedDatePropertyName(), DATE_FORMATTER.format(changeDate)); + } + + // JCloud does not allow last modified date to be changed so we may supply the value we want as a property so let's overwrite any value that may exist. + if (StringUtils.hasLength(jCloudConfiguration.getExternalResourceManagementChangedDatePropertyName())) { + properties.put(jCloudConfiguration.getExternalResourceManagementChangedDatePropertyName(), DATE_FORMATTER.format(changeDate)); } } protected void setExternalManagementResourceValidationStatus(Map properties, MetadataResourceExternalManagementProperties.ValidationStatus status) { - if (!StringUtils.isEmpty(jCloudConfiguration.getExternalResourceManagementValidationStatusPropertyName())) { - setProperty(properties, jCloudConfiguration.getExternalResourceManagementValidationStatusPropertyName(), String.valueOf(status.getValue())); + if (StringUtils.hasLength(jCloudConfiguration.getExternalResourceManagementValidationStatusPropertyName())) { + setPropertyValue(properties, jCloudConfiguration.getExternalResourceManagementValidationStatusPropertyName(), String.valueOf(status.getValue())); } } - protected void setProperty(Map properties, String propertyName, String value) { - if (!StringUtils.isEmpty(propertyName)) { + /** + * Set the new version if this is a new record and if updating then bump the version up by 1. + * @param context need to get metadata if metadata id is a working copy. + * @param properties containing all the properties. The version field should be in the properties map. + * @param isNewResource flag to indicate that this is a new resource of if updating existing resource. + * @param metadataUuid uuid of the related metadata record that contains the resource being versioned. + * @param metadataId id of the related metadata record that contains the resource being versioned. + * @param visibility of the resource being versioned. + * @param approved status of the approved record. + * @param filename or resource of the resource being versioned. + * @throws Exception if there are errors. + */ + protected void setPropertiesVersion(final ServiceContext context, final Map properties, boolean isNewResource, String metadataUuid, int metadataId, + final MetadataResourceVisibility visibility, final Boolean approved, final String filename) throws Exception { + if (StringUtils.hasLength(jCloudConfiguration.getExternalResourceManagementVersionPropertyName())) { + String versionPropertyName = jCloudConfiguration.getExternalResourceManagementVersionPropertyName(); + + final int approvedMetadataId = Boolean.TRUE.equals(approved) ? metadataId : canEdit(context, metadataUuid, true); + // if the current record id equal to the approved record id then it has not been approved and is a draft otherwise we are editing a working copy + final boolean draft = (metadataId == approvedMetadataId); + + String newVersionLabel = null; + if (!isNewResource && !draft && + (jCloudConfiguration.getVersioningStrategy().equals(JCloudConfiguration.VersioningStrategy.DRAFT) || + jCloudConfiguration.getVersioningStrategy().equals(JCloudConfiguration.VersioningStrategy.APPROVED))) { + String approveKey = getKey(context, metadataUuid, approvedMetadataId, visibility, filename); + + try { + StorageMetadata storageMetadata = jCloudConfiguration.getClient().getBlobStore().blobMetadata(jCloudConfiguration.getContainerName(), approveKey); + if (storageMetadata != null) { + if (storageMetadata.getUserMetadata().containsKey(versionPropertyName)) { + newVersionLabel = bumpVersion(storageMetadata.getUserMetadata().get(versionPropertyName)); + } + } + } catch (ContainerNotFoundException ignored) { + // ignored + } + if (newVersionLabel == null) { + newVersionLabel = FIRST_VERSION; + } + } + + if (properties.containsKey(versionPropertyName)) { + if (isNewResource) { + throw new RuntimeException(String.format("Found property '%s' while adding new resource '%s' for metadata %d(%s). This is unexpected.", + versionPropertyName, filename, metadataId, metadataUuid)); + } + if (newVersionLabel == null) { + if (jCloudConfiguration.getVersioningStrategy().equals(JCloudConfiguration.VersioningStrategy.DRAFT) || + jCloudConfiguration.getVersioningStrategy().equals(JCloudConfiguration.VersioningStrategy.ALL)) { + newVersionLabel = bumpVersion(properties.get(versionPropertyName)); + } else { + newVersionLabel = properties.get(versionPropertyName); + } + } + } else { + if (!isNewResource) { + // If the version was not found then it means that it will be starting from version 1 when there could be previous versions. + // This could be a data problem and should be investigated. + Log.error(Geonet.RESOURCES, + String.format("Expecting property '%s' while modifying existing resource '%s' for metadata %d(%s) but the property was not found. Version being set to '%s'", + versionPropertyName, filename, metadataId, metadataUuid, FIRST_VERSION)); + } + newVersionLabel = FIRST_VERSION; + } + + setPropertyValue(properties, versionPropertyName, newVersionLabel); + } + } + + /** + * Bump the version string up one version. + * @param currentVersionLabel to be increased + * @return new version label + */ + protected String bumpVersion(String currentVersionLabel) { + int majorVersion = Integer.parseInt(currentVersionLabel); + majorVersion++; + return String.valueOf(majorVersion); + } + + protected void setPropertyValue(Map properties, String propertyName, String value) { + if (StringUtils.hasLength(propertyName)) { properties.put(propertyName, value); } } @@ -313,7 +467,7 @@ public MetadataResource patchResourceStatus(final ServiceContext context, final int metadataId = canEdit(context, metadataUuid, approved); String sourceKey = null; - StorageMetadata storageMetadata = null; + StorageMetadata storageMetadata; for (MetadataResourceVisibility sourceVisibility : MetadataResourceVisibility.values()) { final String key = getKey(context, metadataUuid, metadataId, sourceVisibility, resourceId); try { @@ -332,12 +486,12 @@ public MetadataResource patchResourceStatus(final ServiceContext context, final } } if (sourceKey != null) { - final String destKey = getKey(context, metadataUuid, metadataId, visibility, resourceId); + final String targetKey = getKey(context, metadataUuid, metadataId, visibility, resourceId); - jCloudConfiguration.getClient().getBlobStore().copyBlob(jCloudConfiguration.getContainerName(), sourceKey, jCloudConfiguration.getContainerName(), destKey, CopyOptions.NONE); + jCloudConfiguration.getClient().getBlobStore().copyBlob(jCloudConfiguration.getContainerName(), sourceKey, jCloudConfiguration.getContainerName(), targetKey, CopyOptions.NONE); jCloudConfiguration.getClient().getBlobStore().removeBlob(jCloudConfiguration.getContainerName(), sourceKey); - Blob blobResults = jCloudConfiguration.getClient().getBlobStore().getBlob(jCloudConfiguration.getContainerName(), destKey); + Blob blobResults = jCloudConfiguration.getClient().getBlobStore().getBlob(jCloudConfiguration.getContainerName(), targetKey); return createResourceDescription(context, metadataUuid, visibility, resourceId, blobResults.getMetadata(), metadataId, approved); } else { @@ -391,6 +545,8 @@ public String delResource(final ServiceContext context, final String metadataUui return String.format("Metadata resource '%s' removed.", resourceId); } } + Log.info(Geonet.RESOURCES, + String.format("Unable to remove resource '%s'.", resourceId)); return String.format("Unable to remove resource '%s'.", resourceId); } @@ -401,6 +557,8 @@ public String delResource(final ServiceContext context, final String metadataUui if (tryDelResource(context, metadataUuid, metadataId, visibility, resourceId)) { return String.format("Metadata resource '%s' removed.", resourceId); } + Log.info(Geonet.RESOURCES, + String.format("Unable to remove resource '%s'.", resourceId)); return String.format("Unable to remove resource '%s'.", resourceId); } @@ -419,6 +577,117 @@ protected boolean tryDelResource(final ServiceContext context, final String meta return false; } + @Override + public void copyResources(ServiceContext context, String sourceUuid, String targetUuid, MetadataResourceVisibility metadataResourceVisibility, boolean sourceApproved, boolean targetApproved) throws Exception { + final int sourceMetadataId = canEdit(context, sourceUuid, metadataResourceVisibility, sourceApproved); + final int targetMetadataId = canEdit(context, targetUuid, metadataResourceVisibility, targetApproved); + final String sourceResourceTypeDir = getMetadataDir(context, sourceMetadataId) + jCloudConfiguration.getFolderDelimiter() + metadataResourceVisibility + jCloudConfiguration.getFolderDelimiter(); + final String targetResourceTypeDir = getMetadataDir(context, targetMetadataId) + jCloudConfiguration.getFolderDelimiter() + metadataResourceVisibility + jCloudConfiguration.getFolderDelimiter(); + + Log.debug(Geonet.RESOURCES, String.format("Copying resources from '%s' (approved=%s) to '%s' (approved=%s)", + sourceResourceTypeDir, sourceApproved, targetResourceTypeDir, targetApproved)); + + String versionPropertyName = null; + if (jCloudConfiguration.isVersioningEnabled()) { + versionPropertyName = jCloudConfiguration.getExternalResourceManagementVersionPropertyName(); + } + + try { + ListContainerOptions opts = new ListContainerOptions(); + opts.prefix(sourceResourceTypeDir).recursive(); + + // Page through the data + String marker = null; + do { + if (marker != null) { + opts.afterMarker(marker); + } + + PageSet page = jCloudConfiguration.getClient().getBlobStore().list(jCloudConfiguration.getContainerName(), opts); + + for (StorageMetadata sourceStorageMetadata : page) { + if (!isFolder(sourceStorageMetadata)) { + String sourceBlobName = sourceStorageMetadata.getName(); + String targetBlobName = targetResourceTypeDir + sourceBlobName.substring(sourceResourceTypeDir.length()); + + Blob sourceBlob = jCloudConfiguration.getClient().getBlobStore().getBlob(jCloudConfiguration.getContainerName(), sourceBlobName); + + // Copy existing properties. + Map targetProperties = new HashMap<>(sourceBlob.getMetadata().getUserMetadata()); + + // Check if target exists. + StorageMetadata targetStorageMetadata = null; + + try { + targetStorageMetadata = jCloudConfiguration.getClient().getBlobStore().blobMetadata(jCloudConfiguration.getContainerName(), targetBlobName); + + } catch (ContainerNotFoundException ignored) { + // ignored + } + + Log.debug(Geonet.RESOURCES, String.format("Copying resource from '%s' to '%s' (new=%s)", sourceBlobName, targetBlobName, targetStorageMetadata==null)); + + if (jCloudConfiguration.isVersioningEnabled() && StringUtils.hasLength(versionPropertyName)) { + if (targetStorageMetadata != null && + targetProperties.containsKey(versionPropertyName) && + targetStorageMetadata.getUserMetadata().containsKey(versionPropertyName) && + !targetProperties.get(versionPropertyName).equals(targetStorageMetadata.getUserMetadata().get(versionPropertyName))) { + + String targetVersionCurrentLabel; + if (jCloudConfiguration.getVersioningStrategy().equals(JCloudConfiguration.VersioningStrategy.DRAFT) || + jCloudConfiguration.getVersioningStrategy().equals(JCloudConfiguration.VersioningStrategy.APPROVED)) { + // If draft or approved, then we only bump the target version up by one version only. + targetVersionCurrentLabel = targetStorageMetadata.getUserMetadata().get(versionPropertyName); + if (StringUtils.hasLength(targetVersionCurrentLabel)) { + targetVersionCurrentLabel = bumpVersion(targetVersionCurrentLabel); + } else { + targetVersionCurrentLabel = FIRST_VERSION; + // Log warning as this could be an issue if the version property is being lost. + Log.warning(Geonet.RESOURCES, String.format("Target version for resource '%s' was empty. Setting version to '%s'", targetBlobName, targetVersionCurrentLabel)); + } + } else { + // If versioning all then we will use the current version. + targetVersionCurrentLabel = targetProperties.get(versionPropertyName); + Log.debug(Geonet.RESOURCES, String.format("Keeping version '%s' for source for resource '%s'", targetVersionCurrentLabel, targetBlobName)); + if (!StringUtils.hasLength(targetVersionCurrentLabel)) { + targetVersionCurrentLabel = FIRST_VERSION; + // Log warning as this could be an issue if the version property is being lost. + Log.warning(Geonet.RESOURCES, String.format("Version resource '%s' was empty. Setting version to '%s'", targetBlobName, targetVersionCurrentLabel)); + } + } + targetProperties.put(versionPropertyName, targetVersionCurrentLabel); + } else if (targetApproved && (targetStorageMetadata == null || !targetStorageMetadata.getUserMetadata().containsKey(versionPropertyName))) { + // If the targetApproved is true then it is a new draft so if target resource did not exist + // then this will be added as a first version item. Otherwise, we keep the version unchanged from the approved copy. + targetProperties.put(versionPropertyName, FIRST_VERSION); + } + + // If version is still not set then lets set it. + if (!targetProperties.containsKey(versionPropertyName) || !StringUtils.hasLength(targetProperties.get(versionPropertyName))) { + targetProperties.put(versionPropertyName, FIRST_VERSION); + // There seems to have been an issue detecting the version so log a warning + Log.warning(Geonet.RESOURCES, String.format("Version was not set for resource '%s'. Setting version to '%s'", targetBlobName, + targetProperties.get(versionPropertyName))); + } + } + Blob targetblob = jCloudConfiguration.getClient().getBlobStore().blobBuilder(targetBlobName) + .payload(sourceBlob.getPayload()) + .contentLength(sourceBlob.getMetadata().getContentMetadata().getContentLength()) + .userMetadata(targetProperties) + .build(); + + // Upload the Blob in multiple chunks to supports large files. + jCloudConfiguration.getClient().getBlobStore().putBlob(jCloudConfiguration.getContainerName(), targetblob, multipart()); + } + } + marker = page.getNextMarker(); + } while (marker != null); + } catch (ContainerNotFoundException e) { + Log.warning(Geonet.RESOURCES, + String.format("Unable to located metadata '%s' directory to be copied.", sourceMetadataId)); + } + } + @Override public MetadataResource getResourceDescription(final ServiceContext context, final String metadataUuid, final MetadataResourceVisibility visibility, final String filename, Boolean approved) throws Exception { @@ -441,13 +710,6 @@ public MetadataResource getResourceDescription(final ServiceContext context, fin public MetadataResourceContainer getResourceContainerDescription(final ServiceContext context, final String metadataUuid, Boolean approved) throws Exception { int metadataId = getAndCheckMetadataId(metadataUuid, approved); - final String key = getMetadataDir(context, metadataId); - - - String folderRoot = jCloudConfiguration.getExternalResourceManagementFolderRoot(); - if (folderRoot == null) { - folderRoot = ""; - } MetadataResourceExternalManagementProperties metadataResourceExternalManagementProperties = getMetadataResourceExternalManagementProperties(context, metadataId, metadataUuid, null, String.valueOf(metadataId), null, null, StorageType.FOLDER, MetadataResourceExternalManagementProperties.ValidationStatus.UNKNOWN); @@ -468,7 +730,7 @@ private String getMetadataDir(ServiceContext context, final int metadataId) { } String key; - // For windows it may be "\" in which case we need to change it to folderDelimiter which is normally "/" + // For windows, it may be "\" in which case we need to change it to folderDelimiter which is normally "/" if (metadataDir.getFileSystem().getSeparator().equals(jCloudConfiguration.getFolderDelimiter())) { key = metadataDir.toString(); } else { @@ -490,23 +752,23 @@ private String getMetadataDir(ServiceContext context, final int metadataId) { protected Path getBaseMetadataDir(ServiceContext context, Path metadataFullDir) { //If we not already figured out the base metadata dir then lets figure it out. - if (baseMetadataDir == null) { + if (this.baseMetadataDir == null) { Path systemFullDir = getDataDirectory(context).getSystemDataDir(); // If the metadata full dir is relative from the system dir then use system dir as the base dir. if (metadataFullDir.toString().startsWith(systemFullDir.toString())) { - baseMetadataDir = systemFullDir; + this.baseMetadataDir = systemFullDir; } else { // If the metadata full dir is an absolute folder then use that as the base dir. if (getDataDirectory(context).getMetadataDataDir().isAbsolute()) { - baseMetadataDir = metadataFullDir.getRoot(); + this.baseMetadataDir = metadataFullDir.getRoot(); } else { // use it as a relative url. - baseMetadataDir = Paths.get("."); + this.baseMetadataDir = Paths.get("."); } } } - return baseMetadataDir; + return this.baseMetadataDir; } private GeonetworkDataDirectory getDataDirectory(ServiceContext context) { @@ -549,7 +811,7 @@ private MetadataResourceExternalManagementProperties getMetadataResourceExternal String metadataResourceExternalManagementPropertiesUrl = jCloudConfiguration.getExternalResourceManagementUrl(); String objectId = getResourceManagementExternalPropertiesObjectId((type == null ? "document" : (StorageType.FOLDER.equals(type) ? "folder" : "document")), visibility, metadataId, version, resourceId); - if (!StringUtils.isEmpty(metadataResourceExternalManagementPropertiesUrl)) { + if (StringUtils.hasLength(metadataResourceExternalManagementPropertiesUrl)) { // {objectid} objectId // It will be the type:visibility:metadataId:version:resourceId in base64 // i.e. folder::100::100 # Folder in resource 100 // i.e. document:public:100:v1:sample.jpg # public document 100 version v1 name sample.jpg @@ -611,10 +873,7 @@ private MetadataResourceExternalManagementProperties getMetadataResourceExternal } } - MetadataResourceExternalManagementProperties metadataResourceExternalManagementProperties - = new MetadataResourceExternalManagementProperties(objectId, metadataResourceExternalManagementPropertiesUrl, validationStatus); - - return metadataResourceExternalManagementProperties; + return new MetadataResourceExternalManagementProperties(objectId, metadataResourceExternalManagementPropertiesUrl, validationStatus); } public ResourceManagementExternalProperties getResourceManagementExternalProperties() { @@ -622,7 +881,7 @@ public ResourceManagementExternalProperties getResourceManagementExternalPropert @Override public boolean isEnabled() { // Return true if we have an external management url - return !StringUtils.isEmpty(jCloudConfiguration.getExternalResourceManagementUrl()); + return StringUtils.hasLength(jCloudConfiguration.getExternalResourceManagementUrl()); } @Override @@ -658,7 +917,7 @@ protected static class ResourceHolderImpl implements ResourceHolder { public ResourceHolderImpl(final Blob object, MetadataResource metadataResource) throws IOException { // Preserve filename by putting the files into a temporary folder and using the same filename. - tempFolderPath = Files.createTempDirectory("gn-meta-res-" + String.valueOf(metadataResource.getMetadataId() + "-")); + tempFolderPath = Files.createTempDirectory("gn-meta-res-" + metadataResource.getMetadataId() + "-"); tempFolderPath.toFile().deleteOnExit(); path = tempFolderPath.resolve(getFilename(object.getMetadata().getName())); this.metadataResource = metadataResource; @@ -681,14 +940,8 @@ public MetadataResource getMetadata() { public void close() throws IOException { // Delete temporary file and folder. IO.deleteFileOrDirectory(tempFolderPath, true); - path=null; + path = null; tempFolderPath = null; } - - @Override - protected void finalize() throws Throwable { - close(); - super.finalize(); - } } } diff --git a/datastorages/jcloud/src/main/java/org/fao/geonet/resources/JCloudConfiguration.java b/datastorages/jcloud/src/main/java/org/fao/geonet/resources/JCloudConfiguration.java index 5971855c8a2..236ef078375 100644 --- a/datastorages/jcloud/src/main/java/org/fao/geonet/resources/JCloudConfiguration.java +++ b/datastorages/jcloud/src/main/java/org/fao/geonet/resources/JCloudConfiguration.java @@ -68,6 +68,11 @@ public class JCloudConfiguration { */ private String externalResourceManagementChangedDatePropertyName; + /** + * Property name for storing the creation date of the record. + */ + private String externalResourceManagementCreatedDatePropertyName; + /** * Property name for validation status that is expected to be an integer with values of null, 0, 1, 2 * (See MetadataResourceExternalManagementProperties.ValidationStatus for code meaning) @@ -86,7 +91,39 @@ public class JCloudConfiguration { * Enable option to add versioning in the link to the resource. */ private Boolean versioningEnabled; + /** + * Property name for storing the version information JCloud does not support versioning. + */ + private String externalResourceManagementVersionPropertyName; + /** + * Property to identify the version strategy to be used. + */ + public enum VersioningStrategy { + /** + * Each new resource change should generate a new version + * i.e. All new uploads will increase the version including draft and working copy. + * For workflow, this could cause confusion on working copies which would increase the version in the working copy + * but when merged only the last version would be merged and could make it look like there are missing versions. + */ + ALL, + /** + * Each new resource change should generate a new version, But working copies will only increase by one version. + * This will avoid working copy version increases more than one to avoid the issues from ALL (lost versions on merge) + * This option may be preferred to ALL when workflow is enabled. + */ + DRAFT, + /** + * Add a new version each time a metadata is approved. + * i.e. draft will remain as version 1 until approved and working copy will only increase by 1 which is what would be used once approved. + */ + APPROVED + } + + /** + * Version strategy to use when generating new versions + */ + private VersioningStrategy versioningStrategy = VersioningStrategy.ALL; public void setProvider(String provider) { this.provider = provider; @@ -225,6 +262,24 @@ public void setVersioningEnabled(String versioningEnabled) { this.versioningEnabled = BooleanUtils.toBooleanObject(versioningEnabled); } + public String getExternalResourceManagementVersionPropertyName() { + return externalResourceManagementVersionPropertyName; + } + + public void setExternalResourceManagementVersionPropertyName(String externalResourceManagementVersionPropertyName) { + this.externalResourceManagementVersionPropertyName = externalResourceManagementVersionPropertyName; + } + + public VersioningStrategy getVersioningStrategy() { + return versioningStrategy; + } + + public void setVersioningStrategy(String versioningStrategy) { + if (StringUtils.hasLength(versioningStrategy)) { + this.versioningStrategy = VersioningStrategy.valueOf(versioningStrategy); + } + } + public String getMetadataUUIDPropertyName() { return metadataUUIDPropertyName; } @@ -240,6 +295,15 @@ public String getExternalResourceManagementChangedDatePropertyName() { public void setExternalResourceManagementChangedDatePropertyName(String externalResourceManagementChangedDatePropertyName) { this.externalResourceManagementChangedDatePropertyName = externalResourceManagementChangedDatePropertyName; } + + public String getExternalResourceManagementCreatedDatePropertyName() { + return externalResourceManagementCreatedDatePropertyName; + } + + public void setExternalResourceManagementCreatedDatePropertyName(String externalResourceManagementCreatedDatePropertyName) { + this.externalResourceManagementCreatedDatePropertyName = externalResourceManagementCreatedDatePropertyName; + } + public String getExternalResourceManagementValidationStatusPropertyName() { return externalResourceManagementValidationStatusPropertyName; } @@ -311,7 +375,9 @@ private void validateMetadataPropertyNames() throws IllegalArgumentException { String[] names = { getMetadataUUIDPropertyName(), getExternalResourceManagementChangedDatePropertyName(), - getExternalResourceManagementValidationStatusPropertyName() + getExternalResourceManagementValidationStatusPropertyName(), + getExternalResourceManagementCreatedDatePropertyName(), + getExternalResourceManagementVersionPropertyName() }; JCloudMetadataNameValidator.validateMetadataNamesForProvider(provider, names); diff --git a/datastorages/jcloud/src/main/resources/config-store/config-jcloud-overrides.properties b/datastorages/jcloud/src/main/resources/config-store/config-jcloud-overrides.properties index 4a2bafe5b34..bbb966a00af 100644 --- a/datastorages/jcloud/src/main/resources/config-store/config-jcloud-overrides.properties +++ b/datastorages/jcloud/src/main/resources/config-store/config-jcloud-overrides.properties @@ -16,7 +16,10 @@ jcloud.external.resource.management.validation.status.property.name=${JCLOUD_EXT jcloud.external.resource.management.validation.status.default.value=${JCLOUD_EXTERNAL_RESOURCE_MANAGEMENT_VALIDATION_STATUS_DEFAULT_VALUE:#{null}} jcloud.external.resource.management.changed.date.property.name=${JCLOUD_EXTERNAL_RESOURCE_MANAGEMENT_CHANGE_DATE_PROPERTY_NAME:#{null}} +jcloud.external.resource.management.created.date.property.name=${JCLOUD_EXTERNAL_RESOURCE_MANAGEMENT_CREATED_DATE_PROPERTY_NAME:#{null}} jcloud.versioning.enabled=${JCLOUD_VERSIONING_ENABLED:#{null}} +jcloud.versioning.strategy=${JCLOUD_VERSIONING_STRATEGY:#{null}} +jcloud.external.resource.management.version.property.name=${JCLOUD_EXTERNAL_RESOURCE_MANAGEMENT_VERSION_PROPERTY_NAME:#{null}} jcloud.metadata.uuid.property.name=${JCLOUD_METADATA_UUID_PROPERTY_NAME:#{null}} diff --git a/datastorages/jcloud/src/main/resources/config-store/config-jcloud.xml b/datastorages/jcloud/src/main/resources/config-store/config-jcloud.xml index e6a43b63a16..427437dd29e 100644 --- a/datastorages/jcloud/src/main/resources/config-store/config-jcloud.xml +++ b/datastorages/jcloud/src/main/resources/config-store/config-jcloud.xml @@ -53,8 +53,12 @@ + + + + diff --git a/datastorages/s3/src/main/java/org/fao/geonet/api/records/attachments/S3Store.java b/datastorages/s3/src/main/java/org/fao/geonet/api/records/attachments/S3Store.java index 061f566aab3..2114f8f5d20 100644 --- a/datastorages/s3/src/main/java/org/fao/geonet/api/records/attachments/S3Store.java +++ b/datastorages/s3/src/main/java/org/fao/geonet/api/records/attachments/S3Store.java @@ -295,11 +295,5 @@ public void close() throws IOException { path = null; } } - - @Override - protected void finalize() throws Throwable { - close(); - super.finalize(); - } } } diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/creating-user.md b/docs/manual/docs/administrator-guide/managing-users-and-groups/creating-user.md index 240fac6944b..e1d35ba75eb 100644 --- a/docs/manual/docs/administrator-guide/managing-users-and-groups/creating-user.md +++ b/docs/manual/docs/administrator-guide/managing-users-and-groups/creating-user.md @@ -3,8 +3,11 @@ To add a new user to the GeoNetwork system, please do the following: 1. Select the *Administration* button in the menu. On the Administration page, select *User management*. -2. Click the button *Add a new user*; -3. Provide the *information* required for the new user; -4. Assign the correct *profile* (see [Users, Groups and Roles](index.md#user_profiles)); -5. Assign the user to a *group* (see [Creating group](creating-group.md)); +2. Click the button *Add a new user*. +3. Provide the *information* required for the new user. +4. Assign the correct *profile* (see [Users, Groups and Roles](index.md#user_profiles)). +5. Assign the user to a *group* (see [Creating group](creating-group.md)). 6. Click *Save*. + +!!! note + Usernames are not case sensitive. The application does not allow to create different users with the same username in different cases. diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/img/password-forgot.png b/docs/manual/docs/administrator-guide/managing-users-and-groups/img/password-forgot.png index d1bc512667d..bdccc9830b2 100644 Binary files a/docs/manual/docs/administrator-guide/managing-users-and-groups/img/password-forgot.png and b/docs/manual/docs/administrator-guide/managing-users-and-groups/img/password-forgot.png differ diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/img/selfregistration-start.png b/docs/manual/docs/administrator-guide/managing-users-and-groups/img/selfregistration-start.png index 7e9a6f8084f..1c617a5d007 100644 Binary files a/docs/manual/docs/administrator-guide/managing-users-and-groups/img/selfregistration-start.png and b/docs/manual/docs/administrator-guide/managing-users-and-groups/img/selfregistration-start.png differ diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/index.md b/docs/manual/docs/administrator-guide/managing-users-and-groups/index.md index aa0408ce3f4..c35bb17f71b 100644 --- a/docs/manual/docs/administrator-guide/managing-users-and-groups/index.md +++ b/docs/manual/docs/administrator-guide/managing-users-and-groups/index.md @@ -3,6 +3,7 @@ - [Creating group](creating-group.md) - [Creating user](creating-user.md) - [User Self-Registration](user-self-registration.md) +- [User reset password](user-reset-password.md) - [Authentication mode](authentication-mode.md) ## Default user {#user-defaults} diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/user-reset-password.md b/docs/manual/docs/administrator-guide/managing-users-and-groups/user-reset-password.md new file mode 100644 index 00000000000..2eb887c85d5 --- /dev/null +++ b/docs/manual/docs/administrator-guide/managing-users-and-groups/user-reset-password.md @@ -0,0 +1,36 @@ +# User 'Forgot your password?' function {#user_forgot_password} + +!!! note + This function requires an email server configured. See [System configuration](../configuring-the-catalog/system-configuration.md#system-config-feedback). + +This function allows users who have forgotten their password to request a new one. Go to the sign in page to access the form: + +![](img/password-forgot.png) + +If a user takes this option they will receive an email inviting them to change their password as follows: + + You have requested to change your Greenhouse GeoNetwork Site password. + + You can change your password using the following link: + + http://localhost:8080/geonetwork/srv/en/password.change.form?username=dubya.shrub@greenhouse.gov&changeKey=635d6c84ddda782a9b6ca9dda0f568b011bb7733 + + This link is valid for today only. + + Greenhouse GeoNetwork Site + +The catalog has generated a changeKey from the forgotten password and the current date and emailed that to the user as part of a link to a change password form. + +If you want to change the content of this email, you should modify `xslt/service/account/password-forgotten-email.xsl`. + +When the user clicks on the link, a change password form is displayed in their browser and a new password can be entered. When that form is submitted, the changeKey is regenerated and checked with the changeKey supplied in the link, if they match then the password is changed to the new password supplied by the user. + +The final step in this process is a verification email sent to the email address of the user confirming that a change of password has taken place: + + Your Greenhouse GeoNetwork Site password has been changed. + + If you did not change this password contact the Greenhouse GeoNetwork Site helpdesk + + The Greenhouse GeoNetwork Site team + +If you want to change the content of this email, you should modify `xslt/service/account/password-changed-email.xsl`. diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/user-self-registration.md b/docs/manual/docs/administrator-guide/managing-users-and-groups/user-self-registration.md index fe3cb2d0142..aa7fdbb254b 100644 --- a/docs/manual/docs/administrator-guide/managing-users-and-groups/user-self-registration.md +++ b/docs/manual/docs/administrator-guide/managing-users-and-groups/user-self-registration.md @@ -1,5 +1,9 @@ # User Self-Registration {#user_self_registration} +!!! note + This function requires an email server configured. See [System configuration](../configuring-the-catalog/system-configuration.md#system-config-feedback). + + To enable the self-registration functions, see [System configuration](../configuring-the-catalog/system-configuration.md). When self-registration is enabled, for users that are not logged in, an additional link is shown on the login page: ![](img/selfregistration-start.png) @@ -15,8 +19,8 @@ The fields in this form are self-explanatory except for the following: - the user will still be given the `Registered User` profile - an email will be sent to the Email address nominated in the Feedback section of the 'System Administration' menu, informing them of the request for a more privileged profile - **Requested group**: By default, self-registered users are not assigned to any group. If a group is selected: - - the user will still not be assigned to any group - - an email will be sent to the Email address nominated in the Feedback section of the 'System Administration' menu, informing them of the requested group. + - the user will still not be assigned to any group + - an email will be sent to the Email address nominated in the Feedback section of the 'System Administration' menu, informing them of the requested group. ## What happens when a user self-registers? @@ -72,39 +76,3 @@ If you want to change the content of this email, you should modify `xslt/service The Greenhouse GeoNetwork Site If you want to change the content of this email, you should modify `xslt/service/account/registration-prof-email.xsl`. - -## The 'Forgot your password?' function - -This function allows users who have forgotten their password to request a new one. Go to the sign in page to access the form: - -![](img/password-forgot.png) - -For security reasons, only users that have the `Registered User` profile can request a new password. - -If a user takes this option they will receive an email inviting them to change their password as follows: - - You have requested to change your Greenhouse GeoNetwork Site password. - - You can change your password using the following link: - - http://localhost:8080/geonetwork/srv/en/password.change.form?username=dubya.shrub@greenhouse.gov&changeKey=635d6c84ddda782a9b6ca9dda0f568b011bb7733 - - This link is valid for today only. - - Greenhouse GeoNetwork Site - -The catalog has generated a changeKey from the forgotten password and the current date and emailed that to the user as part of a link to a change password form. - -If you want to change the content of this email, you should modify `xslt/service/account/password-forgotten-email.xsl`. - -When the user clicks on the link, a change password form is displayed in their browser and a new password can be entered. When that form is submitted, the changeKey is regenerated and checked with the changeKey supplied in the link, if they match then the password is changed to the new password supplied by the user. - -The final step in this process is a verification email sent to the email address of the user confirming that a change of password has taken place: - - Your Greenhouse GeoNetwork Site password has been changed. - - If you did not change this password contact the Greenhouse GeoNetwork Site helpdesk - - The Greenhouse GeoNetwork Site team - -If you want to change the content of this email, you should modify `xslt/service/account/password-changed-email.xsl`. diff --git a/docs/manual/docs/overview/authors.md b/docs/manual/docs/overview/authors.md index 302e339d85e..106fba51a02 100644 --- a/docs/manual/docs/overview/authors.md +++ b/docs/manual/docs/overview/authors.md @@ -9,7 +9,6 @@ In brief the committee votes on proposals on the geonetwork-dev mailinglist. Pro ### Members of the Project Steering Committee - Jeroen Ticheler (jeroen ticheler * geocat net) [GeoCat](https://www.geocat.net) - Chair -- Francois Prunayre [Titellus](https://titellus.net) - Simon Pigot [CSIRO](https://www.csiro.au) - Florent Gravin [CamptoCamp](https://camptocamp.com) - Jose Garcia [GeoCat](https://www.geocat.net) @@ -20,6 +19,7 @@ In brief the committee votes on proposals on the geonetwork-dev mailinglist. Pro - Jo Cook [Astun Technology](https://www.astuntechnology.com) - Patrizia Monteduro (Patrizia Monteduro * fao org) [FAO-UN](https://www.fao.org) - Emanuele Tajariol (e tajariol * mclink it - GeoSolutions) +- Francois Prunayre - Jesse Eichar - Andrea Carboni (acarboni * crisalis-tech com - Independent consultant) - Archie Warnock (warnock * awcubed com) [A/WWW Enterprises](https://www.awcubed.com) diff --git a/docs/manual/mkdocs.yml b/docs/manual/mkdocs.yml index f25d6a2ca22..cc3c1920116 100644 --- a/docs/manual/mkdocs.yml +++ b/docs/manual/mkdocs.yml @@ -323,6 +323,7 @@ nav: - administrator-guide/managing-users-and-groups/creating-group.md - administrator-guide/managing-users-and-groups/creating-user.md - administrator-guide/managing-users-and-groups/user-self-registration.md + - administrator-guide/managing-users-and-groups/user-reset-password.md - 'Classification Systems': - administrator-guide/managing-classification-systems/index.md - administrator-guide/managing-classification-systems/managing-categories.md diff --git a/domain/src/main/java/org/fao/geonet/repository/UserRepository.java b/domain/src/main/java/org/fao/geonet/repository/UserRepository.java index feaf720afb6..b5ac5138653 100644 --- a/domain/src/main/java/org/fao/geonet/repository/UserRepository.java +++ b/domain/src/main/java/org/fao/geonet/repository/UserRepository.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2016 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -45,6 +45,10 @@ public interface UserRepository extends GeonetRepository, JpaSpec /** * Find all users identified by the provided username ignoring the case. + * + * Old versions allowed to create users with the same username with different case. + * New versions do not allow this. + * * @param username the username. * @return all users with username equals ignore case the provided username. */ diff --git a/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustom.java b/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustom.java index 65e3162a22e..21148980e14 100644 --- a/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustom.java +++ b/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustom.java @@ -61,7 +61,7 @@ public interface UserRepositoryCustom { */ @Nonnull List> findAllByGroupOwnerNameAndProfile(@Nonnull Collection metadataIds, - @Nullable Profile profil); + @Nullable Profile profile); /** * Find all the users that own at least one metadata element. diff --git a/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustomImpl.java b/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustomImpl.java index e5f1efa1166..4585548d9fe 100644 --- a/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustomImpl.java +++ b/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustomImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2016 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -25,7 +25,6 @@ import org.fao.geonet.domain.*; import org.fao.geonet.utils.Log; -import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; import javax.annotation.Nonnull; @@ -48,66 +47,83 @@ public class UserRepositoryCustomImpl implements UserRepositoryCustom { @PersistenceContext - private EntityManager _entityManager; + private EntityManager entityManager; @Override public User findOne(final String userId) { - return _entityManager.find(User.class, Integer.valueOf(userId)); + return entityManager.find(User.class, Integer.valueOf(userId)); } @Override - public User findOneByEmail(final String email) { - CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + public User findOneByEmail(@Nonnull final String email) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(User.class); Root root = query.from(User.class); + Join joinedEmailAddresses = root.join(User_.emailAddresses); - query.where(cb.isMember(email, root.get(User_.emailAddresses))); - final List resultList = _entityManager.createQuery(query).getResultList(); + // Case in-sensitive email search + query.where(cb.equal(cb.lower(joinedEmailAddresses), email.toLowerCase())); + query.orderBy(cb.asc(root.get(User_.username))); + final List resultList = entityManager.createQuery(query).getResultList(); if (resultList.isEmpty()) { return null; } if (resultList.size() > 1) { - Log.error(Constants.DOMAIN_LOG_MODULE, "The database is inconsistent. There are multiple users with the email address: " + - email); + Log.error(Constants.DOMAIN_LOG_MODULE, String.format("The database is inconsistent. There are multiple users with the email address: %s", + email)); } return resultList.get(0); } @Override - public User findOneByEmailAndSecurityAuthTypeIsNullOrEmpty(final String email) { - CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + public User findOneByEmailAndSecurityAuthTypeIsNullOrEmpty(@Nonnull final String email) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(User.class); Root root = query.from(User.class); + Join joinedEmailAddresses = root.join(User_.emailAddresses); final Path authTypePath = root.get(User_.security).get(UserSecurity_.authType); query.where(cb.and( - cb.isMember(email, root.get(User_.emailAddresses)), - cb.or(cb.isNull(authTypePath), cb.equal(cb.trim(authTypePath), "")))); - List results = _entityManager.createQuery(query).getResultList(); + // Case in-sensitive email search + cb.equal(cb.lower(joinedEmailAddresses), email.toLowerCase()), + cb.or(cb.isNull(authTypePath), cb.equal(cb.trim(authTypePath), ""))) + ).orderBy(cb.asc(root.get(User_.username))); + List results = entityManager.createQuery(query).getResultList(); if (results.isEmpty()) { return null; } else { + if (results.size() > 1) { + Log.error(Constants.DOMAIN_LOG_MODULE, String.format("The database is inconsistent. There are multiple users with the email address: %s", + email)); + } return results.get(0); } } @Override - public User findOneByUsernameAndSecurityAuthTypeIsNullOrEmpty(final String username) { - CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + public User findOneByUsernameAndSecurityAuthTypeIsNullOrEmpty(@Nonnull final String username) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(User.class); Root root = query.from(User.class); final Path authTypePath = root.get(User_.security).get(UserSecurity_.authType); final Path usernamePath = root.get(User_.username); - query.where(cb.and(cb.equal(usernamePath, username), cb.or(cb.isNull(authTypePath), cb.equal(cb.trim(authTypePath), "")))); - List results = _entityManager.createQuery(query).getResultList(); - + // Case in-sensitive username search + query.where(cb.and( + cb.equal(cb.lower(usernamePath), username.toLowerCase()), + cb.or(cb.isNull(authTypePath), cb.equal(cb.trim(authTypePath), ""))) + ).orderBy(cb.asc(root.get(User_.username))); + List results = entityManager.createQuery(query).getResultList(); if (results.isEmpty()) { return null; } else { + if (results.size() > 1) { + Log.error(Constants.DOMAIN_LOG_MODULE, String.format("The database is inconsistent. There are multiple users with username: %s", + username)); + } return results.get(0); } } @@ -115,7 +131,7 @@ public User findOneByUsernameAndSecurityAuthTypeIsNullOrEmpty(final String usern @Nonnull @Override public List findDuplicatedUsernamesCaseInsensitive() { - CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(String.class); Root userRoot = query.from(User.class); @@ -123,14 +139,14 @@ public List findDuplicatedUsernamesCaseInsensitive() { query.groupBy(cb.lower(userRoot.get(User_.username))); query.having(cb.gt(cb.count(userRoot), 1)); - return _entityManager.createQuery(query).getResultList(); + return entityManager.createQuery(query).getResultList(); } @Override @Nonnull public List> findAllByGroupOwnerNameAndProfile(@Nonnull final Collection metadataIds, @Nullable final Profile profile) { - List> results = new ArrayList>(); + List> results = new ArrayList<>(); results.addAll(findAllByGroupOwnerNameAndProfileInternal(metadataIds, profile, false)); results.addAll(findAllByGroupOwnerNameAndProfileInternal(metadataIds, profile, true)); @@ -139,8 +155,8 @@ public List> findAllByGroupOwnerNameAndProfile(@Nonnull fina } private List> findAllByGroupOwnerNameAndProfileInternal(@Nonnull final Collection metadataIds, - @Nullable final Profile profile, boolean draft) { - CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + @Nullable final Profile profile, boolean draft) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(Tuple.class); Root userRoot = query.from(User.class); @@ -148,22 +164,20 @@ private List> findAllByGroupOwnerNameAndProfileInternal(@Non Predicate metadataPredicate; Predicate ownerPredicate; - Root metadataRoot = null; - Root metadataDraftRoot = null; if (!draft) { - metadataRoot = query.from(Metadata.class); + Root metadataRoot = query.from(Metadata.class); query.multiselect(metadataRoot.get(Metadata_.id), userRoot); metadataPredicate = metadataRoot.get(Metadata_.id).in(metadataIds); ownerPredicate = cb.equal(metadataRoot.get(Metadata_.sourceInfo).get(MetadataSourceInfo_.groupOwner), userGroupRoot.get(UserGroup_.id).get(UserGroupId_.groupId)); } else { - metadataDraftRoot = query.from(MetadataDraft.class); - query.multiselect(metadataDraftRoot.get(MetadataDraft_.id), userRoot); - metadataPredicate = metadataDraftRoot.get(Metadata_.id).in(metadataIds); + Root metadataRoot = query.from(MetadataDraft.class); + query.multiselect(metadataRoot.get(MetadataDraft_.id), userRoot); + metadataPredicate = metadataRoot.get(MetadataDraft_.id).in(metadataIds); - ownerPredicate = cb.equal(metadataDraftRoot.get(Metadata_.sourceInfo).get(MetadataSourceInfo_.groupOwner), + ownerPredicate = cb.equal(metadataRoot.get(MetadataDraft_.sourceInfo).get(MetadataSourceInfo_.groupOwner), userGroupRoot.get(UserGroup_.id).get(UserGroupId_.groupId)); } @@ -180,9 +194,9 @@ private List> findAllByGroupOwnerNameAndProfileInternal(@Non query.distinct(true); - List> results = new ArrayList>(); + List> results = new ArrayList<>(); - for (Tuple result : _entityManager.createQuery(query).getResultList()) { + for (Tuple result : entityManager.createQuery(query).getResultList()) { Integer mdId = (Integer) result.get(0); User user = (User) result.get(1); results.add(Pair.read(mdId, user)); @@ -193,7 +207,7 @@ private List> findAllByGroupOwnerNameAndProfileInternal(@Non @Nonnull @Override public List findAllUsersThatOwnMetadata() { - final CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); final CriteriaQuery query = cb.createQuery(User.class); final Root metadataRoot = query.from(Metadata.class); @@ -206,13 +220,13 @@ public List findAllUsersThatOwnMetadata() { query.where(ownerExpression); query.distinct(true); - return _entityManager.createQuery(query).getResultList(); + return entityManager.createQuery(query).getResultList(); } @Nonnull @Override public List findAllUsersInUserGroups(@Nonnull final Specification userGroupSpec) { - final CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); final CriteriaQuery query = cb.createQuery(User.class); final Root userGroupRoot = query.from(UserGroup.class); @@ -225,7 +239,7 @@ public List findAllUsersInUserGroups(@Nonnull final Specification> found = _userRepo.findAllByGroupOwnerNameAndProfile(Arrays.asList(md1.getId()), null); - Collections.sort(found, Comparator.comparing(s -> s.two().getName())); + List> found = userRepo.findAllByGroupOwnerNameAndProfile(List.of(md1.getId()), null); + found.sort(Comparator.comparing(s -> s.two().getName())); assertEquals(2, found.size()); assertEquals(md1.getId(), found.get(0).one().intValue()); @@ -203,9 +248,9 @@ public void testFindAllByGroupOwnerNameAndProfile() { assertEquals(editUser, found.get(0).two()); assertEquals(reviewerUser, found.get(1).two()); - found = _userRepo.findAllByGroupOwnerNameAndProfile(Arrays.asList(md1.getId()), null); + found = userRepo.findAllByGroupOwnerNameAndProfile(List.of(md1.getId()), null); // Sort by user name descending - Collections.sort(found, Comparator.comparing(s -> s.two().getName(), Comparator.reverseOrder())); + found.sort(Comparator.comparing(s -> s.two().getName(), Comparator.reverseOrder())); assertEquals(2, found.size()); assertEquals(md1.getId(), found.get(0).one().intValue()); @@ -214,13 +259,13 @@ public void testFindAllByGroupOwnerNameAndProfile() { assertEquals(reviewerUser, found.get(0).two()); - found = _userRepo.findAllByGroupOwnerNameAndProfile(Arrays.asList(md1.getId(), md2.getId()), null); + found = userRepo.findAllByGroupOwnerNameAndProfile(Arrays.asList(md1.getId(), md2.getId()), null); assertEquals(4, found.size()); int md1Found = 0; int md2Found = 0; - for (Pair record : found) { - if (record.one() == md1.getId()) { + for (Pair info : found) { + if (info.one() == md1.getId()) { md1Found++; } else { md2Found++; @@ -232,21 +277,21 @@ public void testFindAllByGroupOwnerNameAndProfile() { @Test public void testFindAllUsersInUserGroups() { - Group group1 = _groupRepo.save(GroupRepositoryTest.newGroup(_inc)); - Group group2 = _groupRepo.save(GroupRepositoryTest.newGroup(_inc)); + Group group1 = groupRepo.save(GroupRepositoryTest.newGroup(_inc)); + Group group2 = groupRepo.save(GroupRepositoryTest.newGroup(_inc)); - User editUser = _userRepo.save(newUser().setProfile(Profile.Editor)); - User reviewerUser = _userRepo.save(newUser().setProfile(Profile.Reviewer)); - User registeredUser = _userRepo.save(newUser().setProfile(Profile.RegisteredUser)); - _userRepo.save(newUser().setProfile(Profile.Administrator)); + User editUser = userRepo.save(newUser().setProfile(Profile.Editor)); + User reviewerUser = userRepo.save(newUser().setProfile(Profile.Reviewer)); + User registeredUser = userRepo.save(newUser().setProfile(Profile.RegisteredUser)); + userRepo.save(newUser().setProfile(Profile.Administrator)); - _userGroupRepository.save(new UserGroup().setGroup(group1).setUser(editUser).setProfile(Profile.Editor)); - _userGroupRepository.save(new UserGroup().setGroup(group2).setUser(registeredUser).setProfile(Profile.RegisteredUser)); - _userGroupRepository.save(new UserGroup().setGroup(group2).setUser(reviewerUser).setProfile(Profile.Editor)); - _userGroupRepository.save(new UserGroup().setGroup(group1).setUser(reviewerUser).setProfile(Profile.Reviewer)); + userGroupRepository.save(new UserGroup().setGroup(group1).setUser(editUser).setProfile(Profile.Editor)); + userGroupRepository.save(new UserGroup().setGroup(group2).setUser(registeredUser).setProfile(Profile.RegisteredUser)); + userGroupRepository.save(new UserGroup().setGroup(group2).setUser(reviewerUser).setProfile(Profile.Editor)); + userGroupRepository.save(new UserGroup().setGroup(group1).setUser(reviewerUser).setProfile(Profile.Reviewer)); - List found = Lists.transform(_userRepo.findAllUsersInUserGroups(UserGroupSpecs.hasGroupId(group1.getId())), - new Function() { + List found = Lists.transform(userRepo.findAllUsersInUserGroups(UserGroupSpecs.hasGroupId(group1.getId())), + new Function<>() { @Nullable @Override @@ -259,7 +304,7 @@ public Integer apply(@Nullable User input) { assertTrue(found.contains(editUser.getId())); assertTrue(found.contains(reviewerUser.getId())); - found = Lists.transform(_userRepo.findAllUsersInUserGroups(Specification.not(UserGroupSpecs.hasProfile(Profile.RegisteredUser) + found = Lists.transform(userRepo.findAllUsersInUserGroups(Specification.not(UserGroupSpecs.hasProfile(Profile.RegisteredUser) )), new Function() { @Nullable @@ -278,21 +323,20 @@ public Integer apply(@Nullable User input) { @Test public void testFindAllUsersThatOwnMetadata() { - - User editUser = _userRepo.save(newUser().setProfile(Profile.Editor)); - User reviewerUser = _userRepo.save(newUser().setProfile(Profile.Reviewer)); - _userRepo.save(newUser().setProfile(Profile.RegisteredUser)); - _userRepo.save(newUser().setProfile(Profile.Administrator)); + User editUser = userRepo.save(newUser().setProfile(Profile.Editor)); + User reviewerUser = userRepo.save(newUser().setProfile(Profile.Reviewer)); + userRepo.save(newUser().setProfile(Profile.RegisteredUser)); + userRepo.save(newUser().setProfile(Profile.Administrator)); Metadata md1 = MetadataRepositoryTest.newMetadata(_inc); md1.getSourceInfo().setOwner(editUser.getId()); - _metadataRepo.save(md1); + metadataRepo.save(md1); Metadata md2 = MetadataRepositoryTest.newMetadata(_inc); md2.getSourceInfo().setOwner(reviewerUser.getId()); - _metadataRepo.save(md2); + metadataRepo.save(md2); - List found = _userRepo.findAllUsersThatOwnMetadata(); + List found = userRepo.findAllUsersThatOwnMetadata(); assertEquals(2, found.size()); boolean editUserFound = false; @@ -318,20 +362,18 @@ public void testFindDuplicatedUsernamesCaseInsensitive() { User userNonDuplicated1 = newUser(); usernameDuplicated1.setUsername("userNamE1"); usernameDuplicated2.setUsername("usERNAME1"); - _userRepo.save(usernameDuplicated1); - _userRepo.save(usernameDuplicated2); - _userRepo.save(userNonDuplicated1); + userRepo.save(usernameDuplicated1); + userRepo.save(usernameDuplicated2); + userRepo.save(userNonDuplicated1); - List duplicatedUsernames = _userRepo.findDuplicatedUsernamesCaseInsensitive(); - assertThat("Duplicated usernames don't match the expected ones", + List duplicatedUsernames = userRepo.findDuplicatedUsernamesCaseInsensitive(); + MatcherAssert.assertThat("Duplicated usernames don't match the expected ones", duplicatedUsernames, CoreMatchers.is(Lists.newArrayList("username1"))); assertEquals(1, duplicatedUsernames.size()); } private User newUser() { - User user = newUser(_inc); - return user; + return newUser(_inc); } - } diff --git a/services/src/main/java/org/fao/geonet/api/es/EsHTTPProxy.java b/services/src/main/java/org/fao/geonet/api/es/EsHTTPProxy.java index caf3c9ea8ad..dca972556b7 100644 --- a/services/src/main/java/org/fao/geonet/api/es/EsHTTPProxy.java +++ b/services/src/main/java/org/fao/geonet/api/es/EsHTTPProxy.java @@ -301,7 +301,7 @@ private static boolean hasOperation(ObjectNode doc, ReservedGroup group, Reserve @ResponseStatus(value = HttpStatus.OK) @ResponseBody public void search( - @RequestParam(defaultValue = SelectionManager.SELECTION_METADATA) + @RequestParam(defaultValue = SelectionManager.SELECTION_BUCKET) String bucket, @Parameter(description = "Type of related resource. If none, no associated resource returned.", required = false @@ -387,7 +387,7 @@ public void msearch( @PreAuthorize("hasAuthority('Administrator')") @ResponseBody public void call( - @RequestParam(defaultValue = SelectionManager.SELECTION_METADATA) + @RequestParam(defaultValue = SelectionManager.SELECTION_BUCKET) String bucket, @Parameter(description = "'_search' for search service.") @PathVariable String endPoint, diff --git a/services/src/main/java/org/fao/geonet/api/users/PasswordApi.java b/services/src/main/java/org/fao/geonet/api/users/PasswordApi.java index 13dcce6d877..00e4010dad8 100644 --- a/services/src/main/java/org/fao/geonet/api/users/PasswordApi.java +++ b/services/src/main/java/org/fao/geonet/api/users/PasswordApi.java @@ -1,5 +1,5 @@ //============================================================================= -//=== Copyright (C) 2001-2007 Food and Agriculture Organization of the +//=== Copyright (C) 2001-2024 Food and Agriculture Organization of the //=== United Nations (FAO-UN), United Nations World Food Programme (WFP) //=== and United Nations Environment Programme (UNEP) //=== @@ -27,7 +27,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jeeves.server.context.ServiceContext; import org.fao.geonet.ApplicationContextHolder; -import org.fao.geonet.api.API; import org.fao.geonet.api.ApiUtils; import org.fao.geonet.api.tools.i18n.LanguageUtils; import org.fao.geonet.constants.Geonet; @@ -57,6 +56,7 @@ import javax.servlet.http.HttpServletRequest; import java.text.SimpleDateFormat; import java.util.Calendar; +import java.util.List; import java.util.Locale; import java.util.ResourceBundle; @@ -76,6 +76,7 @@ public class PasswordApi { public static final String LOGGER = Geonet.GEONETWORK + ".api.user"; public static final String DATE_FORMAT = "yyyy-MM-dd"; + public static final String USER_PASSWORD_SENT = "user_password_sent"; @Autowired LanguageUtils languageUtils; @Autowired @@ -85,14 +86,13 @@ public class PasswordApi { @Autowired FeedbackLanguages feedbackLanguages; - @Autowired(required=false) + @Autowired(required = false) SecurityProviderConfiguration securityProviderConfiguration; @io.swagger.v3.oas.annotations.Operation(summary = "Update user password", description = "Get a valid changekey by email first and then update your password.") - @RequestMapping( + @PatchMapping( value = "/{username}", - method = RequestMethod.PATCH, produces = MediaType.TEXT_PLAIN_VALUE) @ResponseStatus(value = HttpStatus.CREATED) @ResponseBody @@ -100,13 +100,12 @@ public ResponseEntity updatePassword( @Parameter(description = "The user name", required = true) @PathVariable - String username, + String username, @Parameter(description = "The new password and a valid change key", required = true) @RequestBody - PasswordUpdateParameter passwordAndChangeKey, - HttpServletRequest request) - throws Exception { + PasswordUpdateParameter passwordAndChangeKey, + HttpServletRequest request) { Locale locale = languageUtils.parseAcceptLanguage(request.getLocales()); ResourceBundle messages = ResourceBundle.getBundle("org.fao.geonet.api.Messages", locale); Locale[] feedbackLocales = feedbackLanguages.getLocales(locale); @@ -117,8 +116,9 @@ public ResponseEntity updatePassword( ServiceContext context = ApiUtils.createServiceContext(request); - User user = userRepository.findOneByUsername(username); - if (user == null) { + List existingUsers = userRepository.findByUsernameIgnoreCase(username); + + if (existingUsers.isEmpty()) { Log.warning(LOGGER, String.format("User update password. Can't find user '%s'", username)); @@ -128,6 +128,9 @@ public ResponseEntity updatePassword( XslUtil.encodeForJavaScript(username) ), HttpStatus.PRECONDITION_FAILED); } + + User user = existingUsers.get(0); + if (LDAPConstants.LDAP_FLAG.equals(user.getSecurity().getAuthType())) { Log.warning(LOGGER, String.format("User '%s' is authenticated using LDAP. Password can't be sent by email.", username)); @@ -183,14 +186,16 @@ public ResponseEntity updatePassword( String content = localizedEmail.getParsedMessage(feedbackLocales); // send change link via email with admin in CC - if (!MailUtil.sendMail(user.getEmail(), + Boolean mailSent = MailUtil.sendMail(user.getEmail(), subject, content, null, sm, - adminEmail, "")) { + adminEmail, ""); + if (Boolean.FALSE.equals(mailSent)) { return new ResponseEntity<>(String.format( messages.getString("mail_error")), HttpStatus.PRECONDITION_FAILED); } + return new ResponseEntity<>(String.format( messages.getString("user_password_changed"), XslUtil.encodeForJavaScript(username) @@ -202,9 +207,8 @@ public ResponseEntity updatePassword( "reset his password. User MUST have an email to get the link. " + "LDAP users will not be able to retrieve their password " + "using this service.") - @RequestMapping( + @PutMapping( value = "/actions/forgot-password", - method = RequestMethod.PUT, produces = MediaType.TEXT_PLAIN_VALUE) @ResponseStatus(value = HttpStatus.CREATED) @ResponseBody @@ -212,9 +216,8 @@ public ResponseEntity sendPasswordByEmail( @Parameter(description = "The user name", required = true) @RequestParam - String username, - HttpServletRequest request) - throws Exception { + String username, + HttpServletRequest request) { Locale locale = languageUtils.parseAcceptLanguage(request.getLocales()); ResourceBundle messages = ResourceBundle.getBundle("org.fao.geonet.api.Messages", locale); Locale[] feedbackLocales = feedbackLanguages.getLocales(locale); @@ -225,17 +228,19 @@ public ResponseEntity sendPasswordByEmail( ServiceContext serviceContext = ApiUtils.createServiceContext(request); - final User user = userRepository.findOneByUsername(username); - if (user == null) { + List existingUsers = userRepository.findByUsernameIgnoreCase(username); + + if (existingUsers.isEmpty()) { Log.warning(LOGGER, String.format("User reset password. Can't find user '%s'", username)); // Return response not providing details about the issue, that should be logged. return new ResponseEntity<>(String.format( - messages.getString("user_password_sent"), + messages.getString(USER_PASSWORD_SENT), XslUtil.encodeForJavaScript(username) ), HttpStatus.CREATED); } + User user = existingUsers.get(0); if (LDAPConstants.LDAP_FLAG.equals(user.getSecurity().getAuthType())) { Log.warning(LOGGER, String.format("User '%s' is authenticated using LDAP. Password can't be sent by email.", @@ -243,19 +248,19 @@ public ResponseEntity sendPasswordByEmail( // Return response not providing details about the issue, that should be logged. return new ResponseEntity<>(String.format( - messages.getString("user_password_sent"), + messages.getString(USER_PASSWORD_SENT), XslUtil.encodeForJavaScript(username) ), HttpStatus.CREATED); } String email = user.getEmail(); - if (StringUtils.isEmpty(email)) { + if (!StringUtils.hasLength(email)) { Log.warning(LOGGER, String.format("User reset password. User '%s' has no email", username)); // Return response not providing details about the issue, that should be logged. return new ResponseEntity<>(String.format( - messages.getString("user_password_sent"), + messages.getString(USER_PASSWORD_SENT), XslUtil.encodeForJavaScript(username) ), HttpStatus.CREATED); } @@ -298,16 +303,18 @@ public ResponseEntity sendPasswordByEmail( String content = localizedEmail.getParsedMessage(feedbackLocales); // send change link via email with admin in CC - if (!MailUtil.sendMail(email, + Boolean mailSent = MailUtil.sendMail(email, subject, content, null, sm, - adminEmail, "")) { + adminEmail, ""); + if (Boolean.FALSE.equals(mailSent)) { return new ResponseEntity<>(String.format( messages.getString("mail_error")), HttpStatus.PRECONDITION_FAILED); } + return new ResponseEntity<>(String.format( - messages.getString("user_password_sent"), + messages.getString(USER_PASSWORD_SENT), XslUtil.encodeForJavaScript(username) ), HttpStatus.CREATED); } diff --git a/web-ui/src/main/resources/catalog/components/viewer/layermanager/partials/layermanageritem.html b/web-ui/src/main/resources/catalog/components/viewer/layermanager/partials/layermanageritem.html index b40b9faa08b..5a1e1fee2ff 100644 --- a/web-ui/src/main/resources/catalog/components/viewer/layermanager/partials/layermanageritem.html +++ b/web-ui/src/main/resources/catalog/components/viewer/layermanager/partials/layermanageritem.html @@ -29,7 +29,6 @@
- -
diff --git a/web-ui/src/main/resources/catalog/js/LoginController.js b/web-ui/src/main/resources/catalog/js/LoginController.js index 15a0a862c6e..d4c71283d6f 100644 --- a/web-ui/src/main/resources/catalog/js/LoginController.js +++ b/web-ui/src/main/resources/catalog/js/LoginController.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2016 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -89,7 +89,13 @@ gnConfig["system.security.passwordEnforcement.maxLength"], 6 ); - $scope.passwordPattern = gnConfig["system.security.passwordEnforcement.pattern"]; + + $scope.usePattern = gnConfig["system.security.passwordEnforcement.usePattern"]; + + if ($scope.usePattern) { + $scope.passwordPattern = + gnConfig["system.security.passwordEnforcement.pattern"]; + } }); $scope.resolveRecaptcha = false; diff --git a/web-ui/src/main/resources/catalog/style/gn_viewer.less b/web-ui/src/main/resources/catalog/style/gn_viewer.less index 2ca082d6b22..e2e1deddcc9 100644 --- a/web-ui/src/main/resources/catalog/style/gn_viewer.less +++ b/web-ui/src/main/resources/catalog/style/gn_viewer.less @@ -308,7 +308,6 @@ } } .dropdown-left { - @toggleWidth: 32px; @toggleHeight: 32px; @@ -339,10 +338,7 @@ } } } - } - - } .gn-searchlayer-list { diff --git a/web/src/main/webapp/WEB-INF/classes/ESAPI.properties b/web/src/main/webapp/WEB-INF/classes/ESAPI.properties index 68f7366c711..d943542e9b4 100644 --- a/web/src/main/webapp/WEB-INF/classes/ESAPI.properties +++ b/web/src/main/webapp/WEB-INF/classes/ESAPI.properties @@ -68,11 +68,12 @@ ESAPI.HTTPUtilities=org.owasp.esapi.reference.DefaultHTTPUtilities ESAPI.IntrusionDetector=org.owasp.esapi.reference.DefaultIntrusionDetector # Log4JFactory Requires log4j.xml or log4j.properties in classpath - http://www.laliluna.de/log4j-tutorial.html # Note that this is now considered deprecated! -ESAPI.Logger=org.owasp.esapi.logging.log4j.Log4JLogFactory +#ESAPI.Logger=org.owasp.esapi.logging.log4j.Log4JLogFactory #ESAPI.Logger=org.owasp.esapi.logging.java.JavaLogFactory # To use the new SLF4J logger in ESAPI (see GitHub issue #129), set # ESAPI.Logger=org.owasp.esapi.logging.slf4j.Slf4JLogFactory # and do whatever other normal SLF4J configuration that you normally would do for your application. +ESAPI.Logger=org.owasp.esapi.logging.slf4j.Slf4JLogFactory ESAPI.Randomizer=org.owasp.esapi.reference.DefaultRandomizer ESAPI.Validator=org.owasp.esapi.reference.DefaultValidator