From 1592f7fa1d8745afc90eb13668a8ca28e9366572 Mon Sep 17 00:00:00 2001 From: Cezary Witkowski Date: Fri, 17 Jan 2025 11:16:31 +0100 Subject: [PATCH] [MNT-24807] repo event2 is exposing user password hash and salt (#3147) * [MNT-24807] Implemented PropertyReplacer that replaces values of sensitive properties (e.g. passwords) during creation of NodeResource for event2 * [MNT-24807] Fix failing tests * Revert "[MNT-24807] Fix failing tests" This reverts commit c118f713f283e48779f2760280691b7f052a10c9. * [MNT-24807] Fix failing tests without reformat Signed-off-by: cezary-witkowski * [MNT-24807] Introduced interface to keep convention Signed-off-by: cezary-witkowski * [MNT-24807] Added ability to configure property filter and mapper for user Signed-off-by: cezary-witkowski * [MNT-24807] Fixed npe and pmd issues Signed-off-by: cezary-witkowski * [MNT-24807] Fixed more pmd comments, applied pre-commit formatting Signed-off-by: cezary-witkowski * [MNT-24807] Renamed user configured properties to indicate what they do, added failsafe when userConfiguredReplacementText is not configured at all Signed-off-by: cezary-witkowski * [MNT-24807] Added unit tests Signed-off-by: cezary-witkowski * [MNT-24807] Additional config to disable property mapper entirely * [MNT-24807] PMD again * [MNT-24807] Updated year in licence for some files I missed Signed-off-by: cezary-witkowski --------- Signed-off-by: cezary-witkowski --- .../repo/event2/NodeResourceHelper.java | 112 ++++++++------- .../filter/AbstractNodeEventFilter.java | 136 ++---------------- .../event2/filter/NodePropertyFilter.java | 28 ++-- .../repo/event2/filter/NodeTypeFilter.java | 8 +- .../repo/event2/mapper/PropertyMapper.java | 37 +++++ .../event2/mapper/PropertyMapperFactory.java | 58 ++++++++ ...eplaceSensitivePropertyWithTextMapper.java | 72 ++++++++++ .../event2/shared/CSVStringToListParser.java | 52 +++++++ .../repo/event2/shared/QNameMatcher.java | 75 ++++++++++ .../repo/event2/shared/TypeDefExpander.java | 107 ++++++++++++++ .../resources/alfresco/events2-context.xml | 24 +++- .../resources/alfresco/repository.properties | 6 +- .../event2/CSVStringToListParserUnitTest.java | 48 +++++++ .../repo/event2/EventFilterUnitTest.java | 76 +++++----- .../repo/event2/PropertyMapperUnitTest.java | 81 +++++++++++ .../repo/event2/QNameMatcherUnitTest.java | 65 +++++++++ .../repo/event2/RepoEvent2UnitSuite.java | 19 +-- .../repo/event2/TypeDefExpanderUnitTest.java | 98 +++++++++++++ 18 files changed, 868 insertions(+), 234 deletions(-) create mode 100644 repository/src/main/java/org/alfresco/repo/event2/mapper/PropertyMapper.java create mode 100644 repository/src/main/java/org/alfresco/repo/event2/mapper/PropertyMapperFactory.java create mode 100644 repository/src/main/java/org/alfresco/repo/event2/mapper/ReplaceSensitivePropertyWithTextMapper.java create mode 100644 repository/src/main/java/org/alfresco/repo/event2/shared/CSVStringToListParser.java create mode 100644 repository/src/main/java/org/alfresco/repo/event2/shared/QNameMatcher.java create mode 100644 repository/src/main/java/org/alfresco/repo/event2/shared/TypeDefExpander.java create mode 100644 repository/src/test/java/org/alfresco/repo/event2/CSVStringToListParserUnitTest.java create mode 100644 repository/src/test/java/org/alfresco/repo/event2/PropertyMapperUnitTest.java create mode 100644 repository/src/test/java/org/alfresco/repo/event2/QNameMatcherUnitTest.java create mode 100644 repository/src/test/java/org/alfresco/repo/event2/TypeDefExpanderUnitTest.java diff --git a/repository/src/main/java/org/alfresco/repo/event2/NodeResourceHelper.java b/repository/src/main/java/org/alfresco/repo/event2/NodeResourceHelper.java index 76e7657ade..0b2f7c7bb2 100644 --- a/repository/src/main/java/org/alfresco/repo/event2/NodeResourceHelper.java +++ b/repository/src/main/java/org/alfresco/repo/event2/NodeResourceHelper.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2023 Alfresco Software Limited + * Copyright (C) 2005 - 2025 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -42,6 +42,9 @@ import java.util.Set; import java.util.stream.Collectors; import com.google.common.collect.Sets; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; import org.alfresco.model.ContentModel; import org.alfresco.repo.event.v1.model.ContentInfo; @@ -50,6 +53,7 @@ import org.alfresco.repo.event.v1.model.UserInfo; import org.alfresco.repo.event2.filter.EventFilterRegistry; import org.alfresco.repo.event2.filter.NodeAspectFilter; import org.alfresco.repo.event2.filter.NodePropertyFilter; +import org.alfresco.repo.event2.mapper.PropertyMapper; import org.alfresco.repo.node.MLPropertyInterceptor; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.permissions.AccessDeniedException; @@ -68,9 +72,6 @@ import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.util.PathUtil; import org.alfresco.util.PropertyCheck; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.factory.InitializingBean; /** * Helper for {@link NodeResource} objects. @@ -81,14 +82,15 @@ public class NodeResourceHelper implements InitializingBean { private static final Log LOGGER = LogFactory.getLog(NodeResourceHelper.class); - protected NodeService nodeService; - protected DictionaryService dictionaryService; - protected PersonService personService; + protected NodeService nodeService; + protected DictionaryService dictionaryService; + protected PersonService personService; protected EventFilterRegistry eventFilterRegistry; - protected NamespaceService namespaceService; - protected PermissionService permissionService; + protected NamespaceService namespaceService; + protected PermissionService permissionService; + protected PropertyMapper propertyMapper; - private NodeAspectFilter nodeAspectFilter; + private NodeAspectFilter nodeAspectFilter; private NodePropertyFilter nodePropertyFilter; @Override @@ -100,6 +102,7 @@ public class NodeResourceHelper implements InitializingBean PropertyCheck.mandatory(this, "eventFilterRegistry", eventFilterRegistry); PropertyCheck.mandatory(this, "namespaceService", namespaceService); PropertyCheck.mandatory(this, "permissionService", permissionService); + PropertyCheck.mandatory(this, "propertyMapper", propertyMapper); this.nodeAspectFilter = eventFilterRegistry.getNodeAspectFilter(); this.nodePropertyFilter = eventFilterRegistry.getNodePropertyFilter(); @@ -124,7 +127,7 @@ public class NodeResourceHelper implements InitializingBean { this.permissionService = permissionService; } - + // To make IntelliJ stop complaining about unused method! @SuppressWarnings("unused") public void setEventFilterRegistry(EventFilterRegistry eventFilterRegistry) @@ -137,6 +140,11 @@ public class NodeResourceHelper implements InitializingBean this.namespaceService = namespaceService; } + public void setPropertyMapper(PropertyMapper propertyMapper) + { + this.propertyMapper = propertyMapper; + } + public NodeResource.Builder createNodeResourceBuilder(NodeRef nodeRef) { final QName type = nodeService.getType(nodeRef); @@ -148,22 +156,22 @@ public class NodeResourceHelper implements InitializingBean Map mapUserCache = new HashMap<>(2); return NodeResource.builder() - .setId(nodeRef.getId()) - .setName((String) properties.get(ContentModel.PROP_NAME)) - .setNodeType(getQNamePrefixString(type)) - .setIsFile(isSubClass(type, ContentModel.TYPE_CONTENT)) - .setIsFolder(isSubClass(type, ContentModel.TYPE_FOLDER)) - .setCreatedByUser(getUserInfo((String) properties.get(ContentModel.PROP_CREATOR), mapUserCache)) - .setCreatedAt(getZonedDateTime((Date)properties.get(ContentModel.PROP_CREATED))) - .setModifiedByUser(getUserInfo((String) properties.get(ContentModel.PROP_MODIFIER), mapUserCache)) - .setModifiedAt(getZonedDateTime((Date)properties.get(ContentModel.PROP_MODIFIED))) - .setContent(getContentInfo(properties)) - .setPrimaryAssocQName(getPrimaryAssocQName(nodeRef)) - .setPrimaryHierarchy(PathUtil.getNodeIdsInReverse(path, false)) - .setProperties(mapToNodeProperties(properties)) - .setLocalizedProperties(mapToNodeLocalizedProperties(properties)) - .setAspectNames(getMappedAspects(nodeRef)) - .setSecondaryParents(getSecondaryParents(nodeRef)); + .setId(nodeRef.getId()) + .setName((String) properties.get(ContentModel.PROP_NAME)) + .setNodeType(getQNamePrefixString(type)) + .setIsFile(isSubClass(type, ContentModel.TYPE_CONTENT)) + .setIsFolder(isSubClass(type, ContentModel.TYPE_FOLDER)) + .setCreatedByUser(getUserInfo((String) properties.get(ContentModel.PROP_CREATOR), mapUserCache)) + .setCreatedAt(getZonedDateTime((Date) properties.get(ContentModel.PROP_CREATED))) + .setModifiedByUser(getUserInfo((String) properties.get(ContentModel.PROP_MODIFIER), mapUserCache)) + .setModifiedAt(getZonedDateTime((Date) properties.get(ContentModel.PROP_MODIFIED))) + .setContent(getContentInfo(properties)) + .setPrimaryAssocQName(getPrimaryAssocQName(nodeRef)) + .setPrimaryHierarchy(PathUtil.getNodeIdsInReverse(path, false)) + .setProperties(mapToNodeProperties(properties)) + .setLocalizedProperties(mapToNodeLocalizedProperties(properties)) + .setAspectNames(getMappedAspects(nodeRef)) + .setSecondaryParents(getSecondaryParents(nodeRef)); } private boolean isSubClass(QName className, QName ofClassQName) @@ -171,17 +179,18 @@ public class NodeResourceHelper implements InitializingBean return dictionaryService.isSubClass(className, ofClassQName); } - private String getPrimaryAssocQName(NodeRef nodeRef) + private String getPrimaryAssocQName(NodeRef nodeRef) { String result = null; - try + try { ChildAssociationRef primaryParent = nodeService.getPrimaryParent(nodeRef); - if(primaryParent != null && primaryParent.getQName() != null) + if (primaryParent != null && primaryParent.getQName() != null) { result = primaryParent.getQName().getPrefixedQName(namespaceService).getPrefixString(); } - } catch (NamespaceException namespaceException) + } + catch (NamespaceException namespaceException) { LOGGER.error("Cannot return a valid primary association QName: " + namespaceException.getMessage()); } @@ -215,8 +224,8 @@ public class NodeResourceHelper implements InitializingBean { v = ((MLText) v).getDefaultValue(); } - - filteredProps.put(getQNamePrefixString(k), v); + Serializable mappedValue = propertyMapper.map(k, v); + filteredProps.put(getQNamePrefixString(k), mappedValue); } }); @@ -232,7 +241,10 @@ public class NodeResourceHelper implements InitializingBean { final MLText mlTextValue = (MLText) v; final HashMap localizedValues = new HashMap<>(mlTextValue.size()); - mlTextValue.forEach((locale, text) -> localizedValues.put(locale.toString(), text)); + mlTextValue.forEach((locale, text) -> { + Serializable mappedValue = propertyMapper.map(k, text); + localizedValues.put(locale.toString(), (String) mappedValue); + }); filteredProps.put(getQNamePrefixString(k), localizedValues); } }); @@ -259,7 +271,7 @@ public class NodeResourceHelper implements InitializingBean { String sysUserName = AuthenticationUtil.getSystemUserName(); if (userName.equals(sysUserName) || (AuthenticationUtil.isMtEnabled() - && userName.startsWith(sysUserName + "@"))) + && userName.startsWith(sysUserName + "@"))) { userInfo = new UserInfo(userName, userName, ""); } @@ -306,11 +318,11 @@ public class NodeResourceHelper implements InitializingBean } /** - * Returns the QName in the format prefix:local, but in the exceptional case where there is no registered prefix - * returns it in the form {uri}local. + * Returns the QName in the format prefix:local, but in the exceptional case where there is no registered prefix returns it in the form {uri}local. * - * @param k QName - * @return a String representing the QName in the format prefix:local or {uri}local. + * @param k + * QName + * @return a String representing the QName in the format prefix:local or {uri}local. */ public String getQNamePrefixString(QName k) { @@ -342,7 +354,7 @@ public class NodeResourceHelper implements InitializingBean public QName getNodeType(NodeRef nodeRef) { - return nodeService.getType(nodeRef); + return nodeService.getType(nodeRef); } public Serializable getProperty(NodeRef nodeRef, QName qName) @@ -352,13 +364,14 @@ public class NodeResourceHelper implements InitializingBean public Map getProperties(NodeRef nodeRef) { - //We need to have full MLText properties here. This is why we are marking the current thread as MLAware + // We need to have full MLText properties here. This is why we are marking the current thread as MLAware final boolean toRestore = MLPropertyInterceptor.isMLAware(); MLPropertyInterceptor.setMLAware(true); try { return nodeService.getProperties(nodeRef); - } finally + } + finally { MLPropertyInterceptor.setMLAware(toRestore); } @@ -377,7 +390,7 @@ public class NodeResourceHelper implements InitializingBean } static Map> getLocalizedPropertiesBefore(Map> locPropsBefore, - Map> locPropsAfter) + Map> locPropsAfter) { final Map> result = new HashMap<>(locPropsBefore.size()); @@ -410,7 +423,7 @@ public class NodeResourceHelper implements InitializingBean { return mapToNodeAspects(nodeService.getAspects(nodeRef)); } - + public List getPrimaryHierarchy(NodeRef nodeRef, boolean showLeaf) { final Path path = nodeService.getPath(nodeRef); @@ -420,16 +433,17 @@ public class NodeResourceHelper implements InitializingBean /** * Gathers node's secondary parents. * - * @param nodeRef - node reference + * @param nodeRef + * - node reference * @return a list of node's secondary parents. */ public List getSecondaryParents(final NodeRef nodeRef) { return nodeService.getParentAssocs(nodeRef).stream() - .filter(not(ChildAssociationRef::isPrimary)) - .map(ChildAssociationRef::getParentRef) - .map(NodeRef::getId) - .collect(Collectors.toList()); + .filter(not(ChildAssociationRef::isPrimary)) + .map(ChildAssociationRef::getParentRef) + .map(NodeRef::getId) + .collect(Collectors.toList()); } public PermissionService getPermissionService() diff --git a/repository/src/main/java/org/alfresco/repo/event2/filter/AbstractNodeEventFilter.java b/repository/src/main/java/org/alfresco/repo/event2/filter/AbstractNodeEventFilter.java index 9b78cc9324..a2e241d852 100644 --- a/repository/src/main/java/org/alfresco/repo/event2/filter/AbstractNodeEventFilter.java +++ b/repository/src/main/java/org/alfresco/repo/event2/filter/AbstractNodeEventFilter.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2022 Alfresco Software Limited + * Copyright (C) 2005 - 2025 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -26,160 +26,50 @@ package org.alfresco.repo.event2.filter; import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Set; -import java.util.StringTokenizer; -import org.alfresco.service.cmr.dictionary.DictionaryService; -import org.alfresco.service.namespace.NamespaceException; -import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.repo.event2.shared.CSVStringToListParser; +import org.alfresco.repo.event2.shared.QNameMatcher; +import org.alfresco.repo.event2.shared.TypeDefExpander; import org.alfresco.service.namespace.QName; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** - * Abstract {@link EventFilter} implementation, containing common event filtering - * functionality for the {@link QName} type. + * Abstract {@link EventFilter} implementation, containing common event filtering functionality for the {@link QName} type. * * @author Jamal Kaabi-Mofrad */ public abstract class AbstractNodeEventFilter implements EventFilter { - private static final Logger LOGGER = LoggerFactory.getLogger(AbstractNodeEventFilter.class); + protected TypeDefExpander typeDefExpander; - private static final String MARKER_INCLUDE_SUBTYPES = "include_subtypes"; - private static final String WILDCARD = "*"; - - protected DictionaryService dictionaryService; - protected NamespaceService namespaceService; - - private Set excludedTypes; - private Set excludedNamespaceURI; - - public AbstractNodeEventFilter() - { - this.excludedTypes = new HashSet<>(); - this.excludedNamespaceURI = new HashSet<>(); - } + private QNameMatcher qNameMatcher; public final void init() { - preprocessExcludedTypes(getExcludedTypes()); + qNameMatcher = new QNameMatcher(getExcludedTypes()); } - public void setDictionaryService(DictionaryService dictionaryService) + public void setTypeDefExpander(TypeDefExpander typeDefExpander) { - this.dictionaryService = dictionaryService; - } - - public void setNamespaceService(NamespaceService namespaceService) - { - this.namespaceService = namespaceService; + this.typeDefExpander = typeDefExpander; } @Override public boolean isExcluded(QName qName) { - if (qName != null) - { - return excludedTypes.contains(qName) || excludedNamespaceURI.contains(qName.getNamespaceURI()); - } - return false; + return qNameMatcher.isMatching(qName); } protected abstract Set getExcludedTypes(); protected List parseFilterList(String unparsedFilterList) { - List list = new LinkedList<>(); - - StringTokenizer st = new StringTokenizer(unparsedFilterList, ","); - while (st.hasMoreTokens()) - { - String entry = st.nextToken().trim(); - if (!entry.isEmpty()) - { - if (!entry.equals("none") && !entry.contains("${")) - { - list.add(entry); - } - } - } - return list; - } - - /** - * Processes the user-defined list of types into valid QNames. It - * validates them against the dictionary and also supports wildcards - */ - private void preprocessExcludedTypes(Set excluded) - { - excluded.forEach(qName -> { - if (WILDCARD.equals(qName.getLocalName())) - { - //excludedPrefixes.add(getPrefix(qName)); - excludedNamespaceURI.add(qName.getNamespaceURI()); - } - else - { - excludedTypes.add(qName); - } - }); - - if (LOGGER.isDebugEnabled()) - { - LOGGER.debug("Excluded namespace URIs:" + excludedNamespaceURI); - LOGGER.debug("Excluded types:" + excludedTypes); - } - } - - private QName getQName(String type) - { - return QName.createQName(type, namespaceService); + return CSVStringToListParser.parse(unparsedFilterList); } protected Collection expandTypeDef(String typeDef) { - if ((typeDef == null) || typeDef.isEmpty() || typeDef.equals("none") || typeDef.contains("${")) - { - return Collections.emptyList(); - } - - if (typeDef.indexOf(' ') < 0) - { - return Collections.singleton(getQName(typeDef)); - } - - String[] typeDefParts = typeDef.split(" "); - if (typeDefParts.length != 2) - { - LOGGER.warn("Ignoring invalid blacklist type pattern: " + typeDef); - return Collections.emptyList(); - } - - if (typeDefParts[1].equals(MARKER_INCLUDE_SUBTYPES)) - { - if (typeDefParts[0].indexOf('*') >= 0) - { - LOGGER.warn("Ignoring invalid blacklist type pattern: " + typeDef); - return Collections.emptyList(); - } - QName baseType; - try - { - baseType = getQName(typeDefParts[0]); - } - catch (NamespaceException ne) - { - return Collections.emptyList(); - } - return dictionaryService.getSubTypes(baseType, true); - } - - LOGGER.warn("Ignoring invalid blacklist type pattern: " + typeDef); - return Collections.emptyList(); + return typeDefExpander.expand(typeDef); } } diff --git a/repository/src/main/java/org/alfresco/repo/event2/filter/NodePropertyFilter.java b/repository/src/main/java/org/alfresco/repo/event2/filter/NodePropertyFilter.java index d6a4d70bdf..3eefadc8b1 100644 --- a/repository/src/main/java/org/alfresco/repo/event2/filter/NodePropertyFilter.java +++ b/repository/src/main/java/org/alfresco/repo/event2/filter/NodePropertyFilter.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2023 Alfresco Software Limited + * Copyright (C) 2005 - 2025 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -25,6 +25,7 @@ */ package org.alfresco.repo.event2.filter; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -43,34 +44,37 @@ public class NodePropertyFilter extends AbstractNodeEventFilter // These properties are included as top-level info, // so exclude them from the properties object private static final Set EXCLUDED_TOP_LEVEL_PROPS = Set.of(ContentModel.PROP_NAME, - ContentModel.PROP_MODIFIER, - ContentModel.PROP_MODIFIED, - ContentModel.PROP_CREATOR, - ContentModel.PROP_CREATED, - ContentModel.PROP_CONTENT); + ContentModel.PROP_MODIFIER, + ContentModel.PROP_MODIFIED, + ContentModel.PROP_CREATOR, + ContentModel.PROP_CREATED, + ContentModel.PROP_CONTENT); // These properties should not be excluded from the properties object private static final Set ALLOWED_PROPERTIES = Set.of(ContentModel.PROP_CASCADE_TX, - ContentModel.PROP_CASCADE_CRC); + ContentModel.PROP_CASCADE_CRC); - private final List nodePropertiesBlackList; + private final List nodePropertiesBlackList = new ArrayList<>(); - public NodePropertyFilter() + public NodePropertyFilter(String userConfiguredProperties) { - this.nodePropertiesBlackList = parseFilterList(FILTERED_PROPERTIES); + super(); + nodePropertiesBlackList.addAll(parseFilterList(FILTERED_PROPERTIES)); + nodePropertiesBlackList.addAll(parseFilterList(userConfiguredProperties)); } @Override public Set getExcludedTypes() { Set result = new HashSet<>(EXCLUDED_TOP_LEVEL_PROPS); - nodePropertiesBlackList.forEach(nodeProperty-> result.addAll(expandTypeDef(nodeProperty))); + nodePropertiesBlackList.forEach(nodeProperty -> result.addAll(expandTypeDef(nodeProperty))); return result; } @Override public boolean isExcluded(QName qName) { - if(qName != null && ALLOWED_PROPERTIES.contains(qName)){ + if (qName != null && ALLOWED_PROPERTIES.contains(qName)) + { return false; } return super.isExcluded(qName); diff --git a/repository/src/main/java/org/alfresco/repo/event2/filter/NodeTypeFilter.java b/repository/src/main/java/org/alfresco/repo/event2/filter/NodeTypeFilter.java index f0c3779292..3b341b326a 100644 --- a/repository/src/main/java/org/alfresco/repo/event2/filter/NodeTypeFilter.java +++ b/repository/src/main/java/org/alfresco/repo/event2/filter/NodeTypeFilter.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2020 Alfresco Software Limited + * Copyright (C) 2005 - 2025 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -31,6 +31,7 @@ import java.util.List; import java.util.Set; import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.namespace.QName; /** @@ -41,10 +42,13 @@ import org.alfresco.service.namespace.QName; public class NodeTypeFilter extends AbstractNodeEventFilter { private final List nodeTypesBlackList; + private final DictionaryService dictionaryService; - public NodeTypeFilter(String filteredNodeTypes) + public NodeTypeFilter(String filteredNodeTypes, DictionaryService dictionaryService) { + super(); this.nodeTypesBlackList = parseFilterList(filteredNodeTypes); + this.dictionaryService = dictionaryService; } @Override diff --git a/repository/src/main/java/org/alfresco/repo/event2/mapper/PropertyMapper.java b/repository/src/main/java/org/alfresco/repo/event2/mapper/PropertyMapper.java new file mode 100644 index 0000000000..5ff1416b4b --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/event2/mapper/PropertyMapper.java @@ -0,0 +1,37 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2025 - 2025 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.event2.mapper; + +import java.io.Serializable; + +import org.alfresco.service.namespace.QName; + +public interface PropertyMapper +{ + PropertyMapper NO_OP = (propertyQName, value) -> value; + + Serializable map(QName propertyQName, Serializable value); +} diff --git a/repository/src/main/java/org/alfresco/repo/event2/mapper/PropertyMapperFactory.java b/repository/src/main/java/org/alfresco/repo/event2/mapper/PropertyMapperFactory.java new file mode 100644 index 0000000000..fd2a74bba9 --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/event2/mapper/PropertyMapperFactory.java @@ -0,0 +1,58 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2025 - 2025 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.event2.mapper; + +import org.alfresco.repo.event2.shared.CSVStringToListParser; +import org.alfresco.repo.event2.shared.TypeDefExpander; +import org.alfresco.service.namespace.QName; + +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +public class PropertyMapperFactory +{ + private final TypeDefExpander typeDefExpander; + + public PropertyMapperFactory(TypeDefExpander typeDefExpander) + { + this.typeDefExpander = typeDefExpander; + } + + public PropertyMapper createPropertyMapper(String enabled, String userConfiguredSensitiveProperties, String userConfiguredReplacementText) + { + if ("false".equalsIgnoreCase(enabled)) + { + return PropertyMapper.NO_OP; + } + Set sensitiveProperties = Optional.ofNullable(userConfiguredSensitiveProperties) + .filter(Predicate.not(String::isEmpty)) + .map(CSVStringToListParser::parse) + .map(typeDefExpander::expand) + .orElse(Set.of()); + return new ReplaceSensitivePropertyWithTextMapper(sensitiveProperties, userConfiguredReplacementText); + } +} diff --git a/repository/src/main/java/org/alfresco/repo/event2/mapper/ReplaceSensitivePropertyWithTextMapper.java b/repository/src/main/java/org/alfresco/repo/event2/mapper/ReplaceSensitivePropertyWithTextMapper.java new file mode 100644 index 0000000000..6a48d5edf2 --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/event2/mapper/ReplaceSensitivePropertyWithTextMapper.java @@ -0,0 +1,72 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2025 - 2025 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.event2.mapper; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.event2.shared.QNameMatcher; +import org.alfresco.repo.transfer.TransferModel; +import org.alfresco.service.namespace.QName; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +public class ReplaceSensitivePropertyWithTextMapper implements PropertyMapper +{ + private static final Set DEFAULT_SENSITIVE_PROPERTIES = Set.of( + ContentModel.PROP_PASSWORD, + ContentModel.PROP_SALT, + ContentModel.PROP_PASSWORD_HASH, + TransferModel.PROP_PASSWORD + ); + private static final String DEFAULT_REPLACEMENT_TEXT = "SENSITIVE_DATA_REMOVED"; + + private final QNameMatcher qNameMatcher; + private final String replacementText; + + public ReplaceSensitivePropertyWithTextMapper(Set userConfiguredSensitiveProperties, String userConfiguredReplacementText) + { + Set sensitiveProperties = Optional.ofNullable(userConfiguredSensitiveProperties) + .filter(Predicate.not(Collection::isEmpty)) + .orElse(DEFAULT_SENSITIVE_PROPERTIES); + qNameMatcher = new QNameMatcher(sensitiveProperties); + replacementText = Optional.ofNullable(userConfiguredReplacementText) + .filter(Predicate.not(String::isEmpty)) + .filter(userInput -> !userInput.contains("${")) + .orElse(DEFAULT_REPLACEMENT_TEXT); + } + @Override + public Serializable map(QName propertyQName, Serializable value) + { + if (qNameMatcher.isMatching(propertyQName)) + { + return replacementText; + } + return value; + } +} diff --git a/repository/src/main/java/org/alfresco/repo/event2/shared/CSVStringToListParser.java b/repository/src/main/java/org/alfresco/repo/event2/shared/CSVStringToListParser.java new file mode 100644 index 0000000000..1962f22991 --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/event2/shared/CSVStringToListParser.java @@ -0,0 +1,52 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2025 - 2025 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.event2.shared; + +import java.util.LinkedList; +import java.util.List; +import java.util.StringTokenizer; + +public final class CSVStringToListParser +{ + private CSVStringToListParser() + {} + + public static List parse(String userInputCSV) + { + List list = new LinkedList<>(); + + StringTokenizer st = new StringTokenizer(userInputCSV, ","); + while (st.hasMoreTokens()) + { + String entry = st.nextToken().trim(); + if (!entry.isEmpty() && !entry.equals("none") && !entry.contains("${")) + { + list.add(entry); + } + } + return list; + } +} diff --git a/repository/src/main/java/org/alfresco/repo/event2/shared/QNameMatcher.java b/repository/src/main/java/org/alfresco/repo/event2/shared/QNameMatcher.java new file mode 100644 index 0000000000..c5b241fd03 --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/event2/shared/QNameMatcher.java @@ -0,0 +1,75 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2025 - 2025 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.event2.shared; + +import java.util.HashSet; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.alfresco.service.namespace.QName; + +public class QNameMatcher +{ + private static final Logger LOGGER = LoggerFactory.getLogger(QNameMatcher.class); + private static final String WILDCARD = "*"; + + private final Set matchingTypes; + private final Set matchingNamespaceURIs; + + public QNameMatcher(Set qNamesToMatch) + { + matchingTypes = new HashSet<>(); + matchingNamespaceURIs = new HashSet<>(); + + qNamesToMatch.forEach(qName -> { + if (WILDCARD.equals(qName.getLocalName())) + { + matchingNamespaceURIs.add(qName.getNamespaceURI()); + } + else + { + matchingTypes.add(qName); + } + }); + + if (LOGGER.isDebugEnabled()) + { + LOGGER.debug("Matching namespace URIs:" + matchingNamespaceURIs); + LOGGER.debug("Matching types:" + matchingTypes); + } + } + + public boolean isMatching(QName qName) + { + if (qName != null) + { + return matchingTypes.contains(qName) || matchingNamespaceURIs.contains(qName.getNamespaceURI()); + } + return false; + } +} diff --git a/repository/src/main/java/org/alfresco/repo/event2/shared/TypeDefExpander.java b/repository/src/main/java/org/alfresco/repo/event2/shared/TypeDefExpander.java new file mode 100644 index 0000000000..30cdcd0d79 --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/event2/shared/TypeDefExpander.java @@ -0,0 +1,107 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2025 - 2025 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.event2.shared; + +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.namespace.NamespaceException; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class TypeDefExpander +{ + private static final Logger LOGGER = LoggerFactory.getLogger(TypeDefExpander.class); + private static final String MARKER_INCLUDE_SUBTYPES = "include_subtypes"; + + private final DictionaryService dictionaryService; + private final NamespaceService namespaceService; + + public TypeDefExpander(DictionaryService dictionaryService, NamespaceService namespaceService) + { + this.dictionaryService = dictionaryService; + this.namespaceService = namespaceService; + } + + public Set expand(Collection types){ + Set result = new HashSet<>(); + types.forEach(type -> result.addAll(expand(type))); + return result; + } + + public Collection expand(String typeDef) + { + if ((typeDef == null) || typeDef.isEmpty() || typeDef.equals("none") || typeDef.contains("${")) + { + return Collections.emptyList(); + } + + if (typeDef.indexOf(' ') < 0) + { + return Collections.singleton(getQName(typeDef)); + } + + String[] typeDefParts = typeDef.split(" "); + if (typeDefParts.length != 2) + { + LOGGER.warn("Ignoring invalid type pattern: " + typeDef); + return Collections.emptyList(); + } + + if (typeDefParts[1].equals(MARKER_INCLUDE_SUBTYPES)) + { + if (typeDefParts[0].indexOf('*') >= 0) + { + LOGGER.warn("Ignoring invalid type pattern: " + typeDef); + return Collections.emptyList(); + } + QName baseType; + try + { + baseType = getQName(typeDefParts[0]); + } + catch (NamespaceException ne) + { + return Collections.emptyList(); + } + return dictionaryService.getSubTypes(baseType, true); + } + + LOGGER.warn("Ignoring invalid type pattern: " + typeDef); + return Collections.emptyList(); + } + + private QName getQName(String type) + { + return QName.createQName(type, namespaceService); + } +} + diff --git a/repository/src/main/resources/alfresco/events2-context.xml b/repository/src/main/resources/alfresco/events2-context.xml index aa5d78d1ba..0864141aea 100644 --- a/repository/src/main/resources/alfresco/events2-context.xml +++ b/repository/src/main/resources/alfresco/events2-context.xml @@ -3,23 +3,30 @@ + + + + + - - + + - + + + @@ -31,6 +38,16 @@ + + + + + + + + + + @@ -53,6 +70,7 @@ + diff --git a/repository/src/main/resources/alfresco/repository.properties b/repository/src/main/resources/alfresco/repository.properties index f8a67e367c..24d9c81f19 100644 --- a/repository/src/main/resources/alfresco/repository.properties +++ b/repository/src/main/resources/alfresco/repository.properties @@ -1234,6 +1234,10 @@ repo.event2.filter.childAssocTypes=rn:rendition # Comma separated list of users which should be excluded # Note: username's case-sensitivity depends on the {user.name.caseSensitive} setting repo.event2.filter.users= +repo.event2.filter.nodeProperties= +repo.event2.mapper.enabled=true +repo.event2.mapper.overrideDefaultProperties= +repo.event2.mapper.overrideReplacementText= # Topic name repo.event2.topic.endpoint=amqp:topic:alfresco.repo.event2 # Specifies the strategy for sending the events @@ -1393,4 +1397,4 @@ default.async.folder.items=1000 # Default NodeSize Thread pool default.nodeSize.corePoolSize=5 default.nodeSize.maximumPoolSize=10 -default.nodeSize.workQueueSize=100 \ No newline at end of file +default.nodeSize.workQueueSize=100 diff --git a/repository/src/test/java/org/alfresco/repo/event2/CSVStringToListParserUnitTest.java b/repository/src/test/java/org/alfresco/repo/event2/CSVStringToListParserUnitTest.java new file mode 100644 index 0000000000..dcde1e6d07 --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/event2/CSVStringToListParserUnitTest.java @@ -0,0 +1,48 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2025 - 2025 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.event2; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.junit.Test; + +import org.alfresco.repo.event2.shared.CSVStringToListParser; + +public class CSVStringToListParserUnitTest +{ + @Test + public void shouldIgnoreEmptySpacesAndNoneValueAndTemplateStringsAndParseTheRest() + { + String userInputCSV = "a,,none,2,${test}, ,*"; + + List result = CSVStringToListParser.parse(userInputCSV); + + List expected = List.of("a", "2", "*"); + assertEquals(expected, result); + } +} diff --git a/repository/src/test/java/org/alfresco/repo/event2/EventFilterUnitTest.java b/repository/src/test/java/org/alfresco/repo/event2/EventFilterUnitTest.java index 25c6056781..00c3251755 100644 --- a/repository/src/test/java/org/alfresco/repo/event2/EventFilterUnitTest.java +++ b/repository/src/test/java/org/alfresco/repo/event2/EventFilterUnitTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2020 Alfresco Software Limited + * Copyright (C) 2005 - 2025 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -35,6 +35,10 @@ import static org.mockito.Mockito.when; import java.util.Collection; import java.util.Collections; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.stubbing.Answer; + import org.alfresco.model.ContentModel; import org.alfresco.model.ForumModel; import org.alfresco.model.RenditionModel; @@ -43,14 +47,12 @@ import org.alfresco.repo.event2.filter.EventUserFilter; import org.alfresco.repo.event2.filter.NodeAspectFilter; import org.alfresco.repo.event2.filter.NodePropertyFilter; import org.alfresco.repo.event2.filter.NodeTypeFilter; +import org.alfresco.repo.event2.shared.TypeDefExpander; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.namespace.NamespaceException; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.util.OneToManyHashBiMap; -import org.junit.BeforeClass; -import org.junit.Test; -import org.mockito.stubbing.Answer; /** * Tests event filters. @@ -78,32 +80,32 @@ public class EventFilterUnitTest namespaceService = new MockNamespaceServiceImpl(); namespaceService.registerNamespace(NamespaceService.SYSTEM_MODEL_PREFIX, - NamespaceService.SYSTEM_MODEL_1_0_URI); + NamespaceService.SYSTEM_MODEL_1_0_URI); namespaceService.registerNamespace(NamespaceService.CONTENT_MODEL_PREFIX, - NamespaceService.CONTENT_MODEL_1_0_URI); + NamespaceService.CONTENT_MODEL_1_0_URI); namespaceService.registerNamespace(NamespaceService.FORUMS_MODEL_PREFIX, - NamespaceService.FORUMS_MODEL_1_0_URI); + NamespaceService.FORUMS_MODEL_1_0_URI); namespaceService.registerNamespace(NamespaceService.RENDITION_MODEL_PREFIX, - NamespaceService.RENDITION_MODEL_1_0_URI); + NamespaceService.RENDITION_MODEL_1_0_URI); + namespaceService.registerNamespace(ContentModel.USER_MODEL_PREFIX, + ContentModel.USER_MODEL_URI); - propertyFilter = new NodePropertyFilter(); - propertyFilter.setNamespaceService(namespaceService); - propertyFilter.setDictionaryService(dictionaryService); + TypeDefExpander typeDefExpander = new TypeDefExpander(dictionaryService, namespaceService); + + propertyFilter = new NodePropertyFilter("usr:password"); + propertyFilter.setTypeDefExpander(typeDefExpander); propertyFilter.init(); - typeFilter = new NodeTypeFilter("sys:*, fm:*, cm:thumbnail"); - typeFilter.setNamespaceService(namespaceService); - typeFilter.setDictionaryService(dictionaryService); + typeFilter = new NodeTypeFilter("sys:*, fm:*, cm:thumbnail", dictionaryService); + typeFilter.setTypeDefExpander(typeDefExpander); typeFilter.init(); aspectFilter = new NodeAspectFilter("cm:workingcopy"); - aspectFilter.setNamespaceService(namespaceService); - aspectFilter.setDictionaryService(dictionaryService); + aspectFilter.setTypeDefExpander(typeDefExpander); aspectFilter.init(); childAssociationTypeFilter = new ChildAssociationTypeFilter("rn:rendition"); - childAssociationTypeFilter.setNamespaceService(namespaceService); - childAssociationTypeFilter.setDictionaryService(dictionaryService); + childAssociationTypeFilter.setTypeDefExpander(typeDefExpander); childAssociationTypeFilter.init(); caseInsensitive_userFilter = new EventUserFilter("System, john.doe, null", false); @@ -114,10 +116,12 @@ public class EventFilterUnitTest public void nodePropertyFilter() { assertTrue("System properties are excluded by default.", - propertyFilter.isExcluded(ContentModel.PROP_NODE_UUID)); + propertyFilter.isExcluded(ContentModel.PROP_NODE_UUID)); assertTrue("System properties are excluded by default.", - propertyFilter.isExcluded(ContentModel.PROP_NODE_DBID)); + propertyFilter.isExcluded(ContentModel.PROP_NODE_DBID)); + + assertTrue("User configured properties are excluded.", propertyFilter.isExcluded(ContentModel.PROP_PASSWORD)); assertFalse("Property cascadeTx is not excluded", propertyFilter.isExcluded(ContentModel.PROP_CASCADE_TX)); assertFalse("Property cascadeCRC is not excluded", propertyFilter.isExcluded(ContentModel.PROP_CASCADE_CRC)); @@ -129,16 +133,16 @@ public class EventFilterUnitTest public void nodeTypeFilter() { assertTrue("Thumbnail node type should have been filtered.", - typeFilter.isExcluded(ContentModel.TYPE_THUMBNAIL)); + typeFilter.isExcluded(ContentModel.TYPE_THUMBNAIL)); assertTrue("System folder node types are excluded by default.", - typeFilter.isExcluded(ContentModel.TYPE_SYSTEM_FOLDER)); + typeFilter.isExcluded(ContentModel.TYPE_SYSTEM_FOLDER)); assertTrue("System node type should have been filtered (sys:*).", - typeFilter.isExcluded(QName.createQName("sys:testSomeSystemType", namespaceService))); + typeFilter.isExcluded(QName.createQName("sys:testSomeSystemType", namespaceService))); assertTrue("Forum node type should have been filtered (fm:*).", - typeFilter.isExcluded(ForumModel.TYPE_POST)); + typeFilter.isExcluded(ForumModel.TYPE_POST)); assertFalse(typeFilter.isExcluded(ContentModel.TYPE_FOLDER)); } @@ -147,7 +151,7 @@ public class EventFilterUnitTest public void nodeAspectFilter() { assertTrue("Working copy aspect should have been filtered.", - aspectFilter.isExcluded(ContentModel.ASPECT_WORKING_COPY)); + aspectFilter.isExcluded(ContentModel.ASPECT_WORKING_COPY)); assertFalse(aspectFilter.isExcluded(ContentModel.ASPECT_TITLED)); } @@ -160,41 +164,41 @@ public class EventFilterUnitTest assertFalse(childAssociationTypeFilter.isExcluded(ContentModel.ASSOC_CONTAINS)); } - + @Test public void userFilter_case_insensitive() { assertTrue("System user should have been filtered.", - caseInsensitive_userFilter.isExcluded("System")); + caseInsensitive_userFilter.isExcluded("System")); assertTrue("System user should have been filtered (case-insensitive).", - caseInsensitive_userFilter.isExcluded("SYSTEM")); + caseInsensitive_userFilter.isExcluded("SYSTEM")); assertTrue("'null' user should have been filtered.", - caseInsensitive_userFilter.isExcluded("null")); + caseInsensitive_userFilter.isExcluded("null")); assertTrue("john.doe user should have been filtered.", - caseInsensitive_userFilter.isExcluded("John.Doe")); + caseInsensitive_userFilter.isExcluded("John.Doe")); assertFalse("'jane.doe' user should not have been filtered.", - caseInsensitive_userFilter.isExcluded("jane.doe")); + caseInsensitive_userFilter.isExcluded("jane.doe")); } @Test public void userFilter_case_sensitive() { assertFalse("'system' user should not have been filtered.", - caseSensitive_userFilter.isExcluded("system")); + caseSensitive_userFilter.isExcluded("system")); assertTrue("'System' user should have been filtered.", - caseSensitive_userFilter.isExcluded("System")); + caseSensitive_userFilter.isExcluded("System")); assertFalse("'John.Doe' user should not have been filtered.", - caseSensitive_userFilter.isExcluded("John.Doe")); + caseSensitive_userFilter.isExcluded("John.Doe")); assertTrue("'john.doe' user should have been filtered.", - caseSensitive_userFilter.isExcluded("john.doe")); + caseSensitive_userFilter.isExcluded("john.doe")); assertFalse("'jane.doe' user should not have been filtered.", - caseSensitive_userFilter.isExcluded("jane.doe")); + caseSensitive_userFilter.isExcluded("jane.doe")); } /** diff --git a/repository/src/test/java/org/alfresco/repo/event2/PropertyMapperUnitTest.java b/repository/src/test/java/org/alfresco/repo/event2/PropertyMapperUnitTest.java new file mode 100644 index 0000000000..20552beac3 --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/event2/PropertyMapperUnitTest.java @@ -0,0 +1,81 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2025 - 2025 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.event2; + +import static org.junit.Assert.assertEquals; + +import java.util.Set; +import java.util.UUID; + +import org.junit.Test; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.event2.mapper.PropertyMapper; +import org.alfresco.repo.event2.mapper.ReplaceSensitivePropertyWithTextMapper; +import org.alfresco.repo.transfer.TransferModel; + +public class PropertyMapperUnitTest +{ + private static final String DEFAULT_REPLACEMENT_TEXT = "SENSITIVE_DATA_REMOVED"; + private static final String USER_CONFIGURED_REPLACEMENT_TEXT = "HIDDEN_BY_SECURITY_CONFIG"; + + private final PropertyMapper defaultPropertyMapper = new ReplaceSensitivePropertyWithTextMapper(null,null); + private final PropertyMapper userConfiguredpropertyMapper = new ReplaceSensitivePropertyWithTextMapper( + Set.of(ContentModel.PROP_PASSWORD, TransferModel.PROP_PASSWORD), + USER_CONFIGURED_REPLACEMENT_TEXT + ); + + @Test + public void shouldReplacePropertyValueWhenItsOneOfTheDefaultSensitivePropertiesWhenUsingDefaultConfig() + { + assertEquals(DEFAULT_REPLACEMENT_TEXT, defaultPropertyMapper.map(ContentModel.PROP_PASSWORD, "test_pass")); + assertEquals(DEFAULT_REPLACEMENT_TEXT, defaultPropertyMapper.map(ContentModel.PROP_SALT, UUID.randomUUID().toString())); + assertEquals(DEFAULT_REPLACEMENT_TEXT, defaultPropertyMapper.map(ContentModel.PROP_PASSWORD_HASH, "r4nD0M_h4sH")); + assertEquals(DEFAULT_REPLACEMENT_TEXT, defaultPropertyMapper.map(TransferModel.PROP_PASSWORD, "pyramid")); + } + + @Test + public void shouldNotReplacePropertyValueWhenItsNotOneOfTheDefaultSensitivePropertiesWhenUsingDefaultConfig() + { + assertEquals("Bob", defaultPropertyMapper.map(ContentModel.PROP_USERNAME, "Bob")); + } + + @Test + public void shouldReplacePropertyValueWhenItsOneOfTheDefaultSensitivePropertiesWhenUsingUserConfig() + { + assertEquals(USER_CONFIGURED_REPLACEMENT_TEXT, userConfiguredpropertyMapper.map(ContentModel.PROP_PASSWORD, "test_pass")); + + assertEquals(USER_CONFIGURED_REPLACEMENT_TEXT, userConfiguredpropertyMapper.map(TransferModel.PROP_PASSWORD, "pyramid")); + } + + @Test + public void shouldNotReplacePropertyValueWhenItsNotOneOfTheDefaultSensitivePropertiesWhenUsingUserConfig() + { + String randomUuid = UUID.randomUUID().toString(); + assertEquals(randomUuid, userConfiguredpropertyMapper.map(ContentModel.PROP_SALT, randomUuid)); + assertEquals("r4nD0M_h4sH", userConfiguredpropertyMapper.map(ContentModel.PROP_PASSWORD_HASH, "r4nD0M_h4sH")); + } +} diff --git a/repository/src/test/java/org/alfresco/repo/event2/QNameMatcherUnitTest.java b/repository/src/test/java/org/alfresco/repo/event2/QNameMatcherUnitTest.java new file mode 100644 index 0000000000..b400b8ec4a --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/event2/QNameMatcherUnitTest.java @@ -0,0 +1,65 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2025 - 2025 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.event2; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Set; + +import org.junit.Test; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.event2.shared.QNameMatcher; +import org.alfresco.repo.transfer.TransferModel; +import org.alfresco.service.namespace.QName; + +public class QNameMatcherUnitTest +{ + @Test + public void shouldMatchOnlyQNamesFromUserModelURI() + { + QNameMatcher qNameMatcher = new QNameMatcher(Set.of(QName.createQName(ContentModel.USER_MODEL_URI, "*"))); + + assertTrue(qNameMatcher.isMatching(ContentModel.PROP_USER_USERNAME)); + assertTrue(qNameMatcher.isMatching(ContentModel.TYPE_USER)); + assertFalse(qNameMatcher.isMatching(ContentModel.PROP_TITLE)); + assertFalse(qNameMatcher.isMatching(TransferModel.PROP_USERNAME)); + assertFalse(qNameMatcher.isMatching(null)); + } + + @Test + public void shouldOnlyMatchSpecificQName() + { + QNameMatcher qNameMatcher = new QNameMatcher(Set.of(ContentModel.PROP_USER_USERNAME)); + + assertTrue(qNameMatcher.isMatching(ContentModel.PROP_USER_USERNAME)); + assertFalse(qNameMatcher.isMatching(ContentModel.PROP_NAME)); + assertFalse(qNameMatcher.isMatching(ContentModel.PROP_USERNAME)); + assertFalse(qNameMatcher.isMatching(TransferModel.PROP_USERNAME)); + assertFalse(qNameMatcher.isMatching(null)); + } +} diff --git a/repository/src/test/java/org/alfresco/repo/event2/RepoEvent2UnitSuite.java b/repository/src/test/java/org/alfresco/repo/event2/RepoEvent2UnitSuite.java index b012df9ff4..95e8ad8891 100644 --- a/repository/src/test/java/org/alfresco/repo/event2/RepoEvent2UnitSuite.java +++ b/repository/src/test/java/org/alfresco/repo/event2/RepoEvent2UnitSuite.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2023 Alfresco Software Limited + * Copyright (C) 2005 - 2025 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -30,12 +30,15 @@ import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; @RunWith(Suite.class) -@SuiteClasses({ EventFilterUnitTest.class, - EventConsolidatorUnitTest.class, - EventJSONSchemaUnitTest.class, - EnqueuingEventSenderUnitTest.class, - NodeResourceHelperUnitTest.class +@SuiteClasses({EventFilterUnitTest.class, + EventConsolidatorUnitTest.class, + EventJSONSchemaUnitTest.class, + EnqueuingEventSenderUnitTest.class, + NodeResourceHelperUnitTest.class, + PropertyMapperUnitTest.class, + QNameMatcherUnitTest.class, + CSVStringToListParserUnitTest.class, + TypeDefExpanderUnitTest.class }) public class RepoEvent2UnitSuite -{ -} +{} diff --git a/repository/src/test/java/org/alfresco/repo/event2/TypeDefExpanderUnitTest.java b/repository/src/test/java/org/alfresco/repo/event2/TypeDefExpanderUnitTest.java new file mode 100644 index 0000000000..e952e89a82 --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/event2/TypeDefExpanderUnitTest.java @@ -0,0 +1,98 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2025 - 2025 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.event2; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.Before; +import org.junit.Test; + +import org.alfresco.model.ContentModel; +import org.alfresco.model.DataListModel; +import org.alfresco.repo.event2.shared.TypeDefExpander; +import org.alfresco.repo.transfer.TransferModel; +import org.alfresco.repo.workflow.WorkflowModel; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +public class TypeDefExpanderUnitTest +{ + private DictionaryService dictionaryService; + private NamespaceService namespaceService; + private TypeDefExpander typeDefExpander; + + @Before + public void setUp() + { + dictionaryService = mock(DictionaryService.class); + namespaceService = mock(NamespaceService.class); + typeDefExpander = new TypeDefExpander(dictionaryService, namespaceService); + } + + @Test + public void testExpandWithValidType() + { + String input = "usr:username"; + when(namespaceService.getNamespaceURI("usr")).thenReturn(ContentModel.USER_MODEL_URI); + + Collection result = typeDefExpander.expand(input); + + QName expected = ContentModel.PROP_USER_USERNAME; + assertEquals(expected, result.iterator().next()); + } + + @Test + public void testExpandWithValidTypeIncludingSubtypes() + { + String input = "cm:content include_subtypes"; + when(namespaceService.getNamespaceURI("cm")).thenReturn(NamespaceService.CONTENT_MODEL_1_0_URI); + Set subtypes = Set.of(TransferModel.TYPE_TRANSFER_RECORD, DataListModel.TYPE_EVENT, WorkflowModel.TYPE_TASK); + when(dictionaryService.getSubTypes(ContentModel.TYPE_CONTENT, true)).thenReturn(subtypes); + + Collection result = typeDefExpander.expand(input); + + assertEquals(subtypes, result); + } + + @Test + public void testExpandWithInvalidTypes() + { + Set input = Stream.of(null, " ", "none", "${test.prop}").collect(Collectors.toSet()); + + Collection result = typeDefExpander.expand(input); + + assertTrue(result.isEmpty()); + } +}