[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 c118f713f2.

* [MNT-24807] Fix failing tests without reformat

Signed-off-by: cezary-witkowski <cezary.witkowski@hyland.com>

* [MNT-24807] Introduced interface to keep convention

Signed-off-by: cezary-witkowski <cezary.witkowski@hyland.com>

* [MNT-24807] Added ability to configure property filter and mapper for user

Signed-off-by: cezary-witkowski <cezary.witkowski@hyland.com>

* [MNT-24807] Fixed npe and pmd issues

Signed-off-by: cezary-witkowski <cezary.witkowski@hyland.com>

* [MNT-24807] Fixed more pmd comments, applied pre-commit formatting

Signed-off-by: cezary-witkowski <cezary.witkowski@hyland.com>

* [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 <cezary.witkowski@hyland.com>

* [MNT-24807] Added unit tests

Signed-off-by: cezary-witkowski <cezary.witkowski@hyland.com>

* [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 <cezary.witkowski@hyland.com>

---------

Signed-off-by: cezary-witkowski <cezary.witkowski@hyland.com>
This commit is contained in:
Cezary Witkowski
2025-01-17 11:16:31 +01:00
committed by GitHub
parent ea36e77775
commit 1592f7fa1d
18 changed files with 868 additions and 234 deletions

View File

@@ -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<String, UserInfo> 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<String, String> 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<QName, Serializable> 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<String, Map<String, String>> getLocalizedPropertiesBefore(Map<String, Map<String, String>> locPropsBefore,
Map<String, Map<String, String>> locPropsAfter)
Map<String, Map<String, String>> locPropsAfter)
{
final Map<String, Map<String, String>> result = new HashMap<>(locPropsBefore.size());
@@ -410,7 +423,7 @@ public class NodeResourceHelper implements InitializingBean
{
return mapToNodeAspects(nodeService.getAspects(nodeRef));
}
public List<String> 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<String> 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()

View File

@@ -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<QName>
{
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<QName> excludedTypes;
private Set<String> 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<QName> getExcludedTypes();
protected List<String> parseFilterList(String unparsedFilterList)
{
List<String> 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<QName> 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<QName> 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);
}
}

View File

@@ -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<QName> 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<QName> ALLOWED_PROPERTIES = Set.of(ContentModel.PROP_CASCADE_TX,
ContentModel.PROP_CASCADE_CRC);
ContentModel.PROP_CASCADE_CRC);
private final List<String> nodePropertiesBlackList;
private final List<String> 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<QName> getExcludedTypes()
{
Set<QName> 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);

View File

@@ -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<String> nodeTypesBlackList;
private final DictionaryService dictionaryService;
public NodeTypeFilter(String filteredNodeTypes)
public NodeTypeFilter(String filteredNodeTypes, DictionaryService dictionaryService)
{
super();
this.nodeTypesBlackList = parseFilterList(filteredNodeTypes);
this.dictionaryService = dictionaryService;
}
@Override

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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);
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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<QName> sensitiveProperties = Optional.ofNullable(userConfiguredSensitiveProperties)
.filter(Predicate.not(String::isEmpty))
.map(CSVStringToListParser::parse)
.map(typeDefExpander::expand)
.orElse(Set.of());
return new ReplaceSensitivePropertyWithTextMapper(sensitiveProperties, userConfiguredReplacementText);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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<QName> 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<QName> userConfiguredSensitiveProperties, String userConfiguredReplacementText)
{
Set<QName> 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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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<String> parse(String userInputCSV)
{
List<String> 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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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<QName> matchingTypes;
private final Set<String> matchingNamespaceURIs;
public QNameMatcher(Set<QName> 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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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<QName> expand(Collection<String> types){
Set<QName> result = new HashSet<>();
types.forEach(type -> result.addAll(expand(type)));
return result;
}
public Collection<QName> 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);
}
}

View File

@@ -3,23 +3,30 @@
<beans>
<bean id="event2TypeDefExpander" class="org.alfresco.repo.event2.shared.TypeDefExpander">
<constructor-arg ref="dictionaryService"/>
<constructor-arg ref="namespaceService"/>
</bean>
<!-- Event2 Filters -->
<bean id="event2FilterRegistry" class="org.alfresco.repo.event2.filter.EventFilterRegistry"/>
<bean id="abstractNodeEventFilter" class="org.alfresco.repo.event2.filter.AbstractNodeEventFilter" abstract="true" init-method="init">
<property name="dictionaryService" ref="dictionaryService"/>
<property name="namespaceService" ref="namespaceService"/>
<property name="typeDefExpander" ref="event2TypeDefExpander"/>
</bean>
<bean id="event2NodeTypeFilter" class="org.alfresco.repo.event2.filter.NodeTypeFilter" parent="abstractNodeEventFilter">
<constructor-arg value="${repo.event2.filter.nodeTypes}"/>
<constructor-arg ref="dictionaryService"/>
</bean>
<bean id="event2NodeAspectFilter" class="org.alfresco.repo.event2.filter.NodeAspectFilter" parent="abstractNodeEventFilter">
<constructor-arg value="${repo.event2.filter.nodeAspects}"/>
</bean>
<bean id="event2NodePropertyFilter" class="org.alfresco.repo.event2.filter.NodePropertyFilter" parent="abstractNodeEventFilter"/>
<bean id="event2NodePropertyFilter" class="org.alfresco.repo.event2.filter.NodePropertyFilter" parent="abstractNodeEventFilter">
<constructor-arg value="${repo.event2.filter.nodeProperties}"/>
</bean>
<bean id="event2UserFilter" class="org.alfresco.repo.event2.filter.EventUserFilter">
<constructor-arg value="${repo.event2.filter.users}"/>
@@ -31,6 +38,16 @@
</bean>
<!-- End of Event2 Filters -->
<bean id="event2PropertyMapperFactory" class="org.alfresco.repo.event2.mapper.PropertyMapperFactory">
<constructor-arg ref="event2TypeDefExpander"/>
</bean>
<bean id="event2PropertyMapper" factory-bean="event2PropertyMapperFactory" factory-method="createPropertyMapper">
<constructor-arg value="${repo.event2.mapper.enabled}"/>
<constructor-arg value="${repo.event2.mapper.overrideDefaultProperties}"/>
<constructor-arg value="${repo.event2.mapper.overrideReplacementText}"/>
</bean>
<bean id="baseEventGeneratorV2" abstract="true">
<property name="policyComponent" ref="policyComponent"/>
<property name="nodeService" ref="nodeService"/>
@@ -53,6 +70,7 @@
<property name="eventFilterRegistry" ref="event2FilterRegistry"/>
<property name="namespaceService" ref="namespaceService"/>
<property name="permissionService" ref="permissionService"/>
<property name="propertyMapper" ref="event2PropertyMapper"/>
</bean>
<bean id="baseEventSender" abstract="true">

View File

@@ -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
default.nodeSize.workQueueSize=100

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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<String> result = CSVStringToListParser.parse(userInputCSV);
List<String> expected = List.of("a", "2", "*");
assertEquals(expected, result);
}
}

View File

@@ -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"));
}
/**

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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"));
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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));
}
}

View File

@@ -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
{
}
{}

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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<QName> 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<QName> subtypes = Set.of(TransferModel.TYPE_TRANSFER_RECORD, DataListModel.TYPE_EVENT, WorkflowModel.TYPE_TASK);
when(dictionaryService.getSubTypes(ContentModel.TYPE_CONTENT, true)).thenReturn(subtypes);
Collection<QName> result = typeDefExpander.expand(input);
assertEquals(subtypes, result);
}
@Test
public void testExpandWithInvalidTypes()
{
Set<String> input = Stream.of(null, " ", "none", "${test.prop}").collect(Collectors.toSet());
Collection<QName> result = typeDefExpander.expand(input);
assertTrue(result.isEmpty());
}
}