Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import com.fasterxml.jackson.core.type.TypeReference;
Expand Down Expand Up @@ -57,6 +58,10 @@ public class Neo4jProducer extends DefaultProducer {
private static final TypeReference<Map<String, Object>> MAP_TYPE_REF = new TypeReference<>() {
};

// Only property values are passed as bound parameters; the property name is spliced into the
// Cypher query text, so it must be restricted to a safe identifier pattern.
private static final Pattern VALID_PROPERTY_NAME = Pattern.compile("^[A-Za-z_][A-Za-z0-9_]*$");

private Driver driver;

public Neo4jProducer(Neo4jEndpoint endpoint) {
Expand Down Expand Up @@ -135,6 +140,19 @@ private void createNode(Exchange exchange) throws InvalidPayloadException {
executeWriteQuery(exchange, query, properties, databaseName, Neo4Operation.CREATE_NODE);
}

/**
* Validates a Neo4j property name before it is spliced into a Cypher query. Property values are passed as bound
* parameters, but the property name is inserted into the query text, so it must be a safe identifier. Names that do
* not match are rejected with a clear error instead of producing a malformed or unintended query.
*/
static void validatePropertyName(String name) {
if (name == null || !VALID_PROPERTY_NAME.matcher(name).matches()) {
throw new IllegalArgumentException(
"Invalid Neo4j property name: '" + name + "'. Property names must match "
+ VALID_PROPERTY_NAME.pattern());
}
}

private void retrieveNodes(Exchange exchange) throws NoSuchHeaderException {
final String label = getEndpoint().getConfiguration().getLabel();
ObjectHelper.notNull(label, "label");
Expand Down Expand Up @@ -166,6 +184,7 @@ private void retrieveNodes(Exchange exchange) throws NoSuchHeaderException {
if (paramIndex > 0) {
whereClause.append(" AND ");
}
validatePropertyName(entry.getKey());
String paramName = "param" + paramIndex;
whereClause.append(alias).append(".").append(entry.getKey())
.append(" = $").append(paramName);
Expand All @@ -179,6 +198,9 @@ private void retrieveNodes(Exchange exchange) throws NoSuchHeaderException {
// Empty map, match all nodes
query = String.format("MATCH (%s:%s) RETURN %s", alias, label, alias);
}
} catch (IllegalArgumentException iae) {
exchange.setException(new Neo4jOperationException(RETRIEVE_NODES, iae));
return;
} catch (Exception e) {
exchange.setException(
new Neo4jOperationException(
Expand Down Expand Up @@ -264,6 +286,7 @@ private void deleteNode(Exchange exchange) throws NoSuchHeaderException {
if (paramIndex > 0) {
whereClause.append(" AND ");
}
validatePropertyName(entry.getKey());
String paramName = "param" + paramIndex;
whereClause.append(alias).append(".").append(entry.getKey())
.append(" = $").append(paramName);
Expand All @@ -277,6 +300,9 @@ private void deleteNode(Exchange exchange) throws NoSuchHeaderException {
// Empty map, delete all nodes of this label
query = String.format("MATCH (%s:%s) %s DELETE %s", alias, label, detached, alias);
}
} catch (IllegalArgumentException iae) {
exchange.setException(new Neo4jOperationException(Neo4Operation.DELETE_NODE, iae));
return;
} catch (Exception e) {
exchange.setException(
new Neo4jOperationException(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.component.neo4j;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;

class Neo4jPropertyNameValidationTest {

@Test
void acceptsValidPropertyNames() {
assertDoesNotThrow(() -> Neo4jProducer.validatePropertyName("name"));
assertDoesNotThrow(() -> Neo4jProducer.validatePropertyName("_internal"));
assertDoesNotThrow(() -> Neo4jProducer.validatePropertyName("firstName2"));
assertDoesNotThrow(() -> Neo4jProducer.validatePropertyName("A_B_1"));
}

@Test
void rejectsInvalidPropertyNames() {
assertThrows(IllegalArgumentException.class, () -> Neo4jProducer.validatePropertyName(null));
assertThrows(IllegalArgumentException.class, () -> Neo4jProducer.validatePropertyName(""));
assertThrows(IllegalArgumentException.class, () -> Neo4jProducer.validatePropertyName("first name"));
assertThrows(IllegalArgumentException.class, () -> Neo4jProducer.validatePropertyName("name-1"));
assertThrows(IllegalArgumentException.class, () -> Neo4jProducer.validatePropertyName("name.sub"));
assertThrows(IllegalArgumentException.class, () -> Neo4jProducer.validatePropertyName("1name"));
// A property name that would otherwise change the structure of the generated query.
assertThrows(IllegalArgumentException.class,
() -> Neo4jProducer.validatePropertyName("x) RETURN n UNION MATCH (m) RETURN m //"));
}
}