atiaomar1978-hub commented on code in PR #25551:
URL: https://github.com/apache/camel/pull/25551#discussion_r3816912361
##########
components/camel-python3/src/main/docs/python3-language.adoc:
##########
@@ -0,0 +1,112 @@
+= Python 3 Language
+:doctitle: Python 3
+:shortname: python3
+:artifactid: camel-python3
+:description: Evaluates a Python 3 expression
+:since: 4.23
+:supportlevel: Preview
+:tabs-sync-option:
+
+*Since Camel {since}*
+
+Camel allows https://www.graalvm.org/python/[Python 3] (GraalPy) to be
+used as an xref:manual::expression.adoc[Expression] or
xref:manual::predicate.adoc[Predicate]
+in Camel routes.
+
+This language is distinct from xref:python-language.adoc[Python], which uses
Jython and is limited to Python 2.7.
+
+For example, you can use Python 3 in a xref:manual::predicate.adoc[Predicate]
+with the xref:eips:choice-eip.adoc[Content-Based Router] EIP.
+
+== Python 3 Options
+
+// language options: START
+include::partial$language-options.adoc[]
+// language options: END
+
+== Variables
+
+[width="100%",cols="10%,10%,80%",options="header",]
+|=======================================================================
+|Variable |Type |Description
+|body |Object |the message body
+|headers |Map |the message headers
+|properties |Map |the exchange properties
+|exchangeId |String |the exchange id
+|message |Message |the message
Review Comment:
**Blocking — variables table contradicts default behaviour**
This table lists `message`, `exchange`, and `context` as available
variables, but default `Python3Language` intentionally does **not** bind them —
scripts get `NameError` (see
`Python3LanguageSecurityTest.defaultDoesNotBindExchangeMessageOrContext`).
Only a registry-installed `Python3Language.createWithHostAccess()` instance
exposes those three.
Please either:
- remove them from the default table and document them under the
trusted/host-access section, or
- add a column such as "Default / opt-in" marking `message` / `exchange` /
`context` as **opt-in only**.
##########
components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3LanguageSecurityTest.java:
##########
@@ -0,0 +1,173 @@
+/*
+ * 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.language.python3;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.Language;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchThrowable;
+
+@DisabledIfSystemProperty(named = "os.arch", matches = "(?i)(s390x|ppc64le)")
+class Python3LanguageSecurityTest {
+
+ static CamelContext context;
+ static Python3Language trusted;
+
+ @BeforeAll
+ static void startContext() {
+ context = new DefaultCamelContext();
+ context.start();
+ trusted = Python3Language.createWithHostAccess();
+ trusted.setCamelContext(context);
+ trusted.start();
+ }
+
+ @AfterAll
+ static void stopContext() {
+ if (trusted != null) {
+ trusted.stop();
+ }
+ context.stop();
+ }
+
+ static Language defaultLanguage() {
+ return context.resolveLanguage("python3");
+ }
+
+ static Exchange sampleExchange() {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getIn().setBody(new Person("Ada", 36));
+ exchange.getIn().setHeader("foo", "bar");
+ exchange.setProperty("color", "red");
+ return exchange;
+ }
+
+ @Test
+ void defaultAllowsDataBindings() {
+ Language language = defaultLanguage();
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getIn().setBody("hello");
+ exchange.getIn().setHeader("foo", "bar");
+ exchange.setProperty("color", "red");
+ assertThat(language.createExpression("body").evaluate(exchange,
String.class)).isEqualTo("hello");
+
assertThat(language.createExpression("headers['foo']").evaluate(exchange,
String.class)).isEqualTo("bar");
+ assertThat(language.createExpression("headers['written'] =
'yes'\nheaders['written']").evaluate(exchange,
+ String.class)).isEqualTo("yes");
+ assertThat(exchange.getIn().getHeader("written")).isEqualTo("yes");
+
assertThat(language.createExpression("properties['color']").evaluate(exchange,
String.class)).isEqualTo("red");
+ assertThat(language.createExpression("exchangeId").evaluate(exchange,
String.class))
+ .isEqualTo(exchange.getExchangeId());
+ }
+
+ @Test
Review Comment:
**Excellent security test coverage**
This is exactly the test matrix a scripting language needs: default data
bindings work, Camel host objects are unbound, Java method invocation is
denied, class lookup/IO/process creation blocked, and trusted mode is verified
separately without widening the sandbox.
Strong work — sets the bar for the component.
##########
components/camel-python3/src/main/java/org/apache/camel/language/python3/Python3Language.java:
##########
@@ -0,0 +1,337 @@
+/*
+ * 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.language.python3;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.Expression;
+import org.apache.camel.ExpressionEvaluationException;
+import org.apache.camel.ExpressionIllegalSyntaxException;
+import org.apache.camel.Predicate;
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.Service;
+import org.apache.camel.spi.ScriptingLanguage;
+import org.apache.camel.spi.annotations.Language;
+import org.apache.camel.support.LRUCacheFactory;
+import org.apache.camel.support.TypedLanguageSupport;
+import org.graalvm.polyglot.Context;
+import org.graalvm.polyglot.Engine;
+import org.graalvm.polyglot.HostAccess;
+import org.graalvm.polyglot.PolyglotException;
+import org.graalvm.polyglot.Source;
+import org.graalvm.polyglot.Value;
+
+/**
+ * Camel expression language for Python 3 via <a
href="https://www.graalvm.org/python/">GraalPy</a>.
+ *
+ * <p>
+ * Default scripts see only data bindings: {@code body}, {@code headers},
{@code properties}, and {@code exchangeId}.
+ * {@code exchange}, {@code message}, and {@code context} are intentionally
absent so they resolve as Python
+ * {@code NameError} rather than opaque objects with no usable API. Binding
them by default would also become a
+ * privilege escalation if host access were later widened.
+ * </p>
+ *
+ * <p>
+ * By default, Python may index Java maps and lists (so {@code headers['foo']}
works) but cannot invoke methods on host
+ * objects. To allow host method calls on Exchange/Message/CamelContext, bind
a language created with
+ * {@link #createWithHostAccess()} before first use:
+ * </p>
+ *
+ * <pre>
+ * Python3Language python3 = Python3Language.createWithHostAccess();
+ * camelContext.getRegistry().bind("python3", python3);
+ * </pre>
+ */
+@Language("python3")
+public class Python3Language extends TypedLanguageSupport implements
ScriptingLanguage, Service {
+
+ private final HostAccess hostAccess;
+ /**
+ * When true, also bind {@code exchange}, {@code message}, and {@code
context}. Only {@link #createWithHostAccess()}
+ * sets this; default mode keeps those names undefined.
+ */
+ private final boolean bindCamelHostObjects;
+ private final Map<String, Source> sourceCache =
LRUCacheFactory.newLRUSoftCache(16, 1000, true);
+ private final Lock engineLock = new ReentrantLock();
+ private volatile Engine engine;
+
+ public Python3Language() {
+ this(Python3Helper.defaultHostAccess(), false);
+ }
+
+ public Python3Language(HostAccess hostAccess) {
+ this(hostAccess, false);
+ }
+
+ private Python3Language(HostAccess hostAccess, boolean
bindCamelHostObjects) {
+ this.hostAccess = hostAccess;
+ this.bindCamelHostObjects = bindCamelHostObjects;
+ }
+
+ /**
+ * Creates a separate language instance for trusted scripts. Uses {@link
HostAccess#ALL} so Python may call public
+ * methods and fields on bound host objects, and additionally exposes
{@code exchange}, {@code message}, and
+ * {@code context}.
+ * <p>
+ * This is an explicit opt-in: {@code HostAccess.ALL} is not a sandbox. It
does not enable {@code allowAllAccess},
+ * Java class lookup, host IO, or process creation. Use only when you
trust the scripts.
+ * </p>
+ */
+ public static Python3Language createWithHostAccess() {
+ return new Python3Language(HostAccess.ALL, true);
+ }
+
+ /**
+ * Helper for use in the Java route DSL, e.g. {@code
.filter(Python3Language.python3("body == 'Hello'"))}.
+ */
+ public static Python3Expression python3(String script) {
+ return new Python3Expression(script);
+ }
+
+ @Override
+ public void start() {
+ engine();
+ }
+
+ @Override
+ public void stop() {
+ sourceCache.clear();
+ Engine toClose;
+ engineLock.lock();
+ try {
+ toClose = engine;
+ engine = null;
+ } finally {
+ engineLock.unlock();
+ }
+ if (toClose != null) {
+ toClose.close();
+ }
+ }
+
+ @Override
+ public Predicate createPredicate(String expression) {
+ return createPython3Expression(expression);
+ }
+
+ @Override
+ public Expression createExpression(String expression) {
+ return createPython3Expression(expression);
+ }
+
+ private Python3Expression createPython3Expression(String expression) {
+ return new Python3Expression(loadResource(expression), this);
+ }
+
+ @Override
+ public <T> T evaluate(String script, Map<String, Object> bindings,
Class<T> resultType) {
+ script = loadResource(script);
+ try (Context cx = Python3Helper.newContext(engine(), hostAccess)) {
+ if (bindings != null) {
+ Value b = cx.getBindings("python");
+ bindings.forEach(b::putMember);
+ }
+ Value value = cx.eval(source(script));
+ return convert(value, resultType, getCamelContext(), null);
+ } catch (Exception e) {
+ throw wrapFailure(script, null, e);
+ }
+ }
+
+ Object evaluateExpression(String script, Exchange exchange) {
+ try (Context cx = Python3Helper.newContext(engine(), hostAccess)) {
+ Value b = cx.getBindings("python");
+ // Default: data only. Do not bind exchange/message/context — they
are undefined (NameError)
+ // unless createWithHostAccess() opted into trusted host-object
bindings.
+ b.putMember("exchangeId", exchange.getExchangeId());
+ b.putMember("headers", exchange.getMessage().getHeaders());
+ b.putMember("properties", exchange.getAllProperties());
+ b.putMember("body", exchange.getMessage().getBody());
+ if (bindCamelHostObjects) {
+ b.putMember("exchange", exchange);
+ b.putMember("message", exchange.getMessage());
+ b.putMember("context", exchange.getContext());
+ }
+ Value value = cx.eval(source(script));
+ return convert(value, Object.class, exchange.getContext(),
exchange);
+ } catch (Exception e) {
+ throw wrapFailure(script, exchange, e);
+ }
+ }
+
+ /**
+ * Resource load failures and Python parse errors are syntax problems.
Runtime errors (including host-access
+ * denials) are evaluation failures, matching groovy/javascript rather
than wrapping everything as illegal syntax.
+ */
+ static RuntimeCamelException wrapFailure(String script, Exchange exchange,
Exception e) {
+ if (e instanceof ExpressionIllegalSyntaxException ise) {
+ return ise;
+ }
+ if (e instanceof ExpressionEvaluationException eee) {
+ return eee;
+ }
+ if (isSyntaxError(e)) {
+ return new ExpressionIllegalSyntaxException(script, e);
+ }
+ return new ExpressionEvaluationException(null, exchange, e);
+ }
+
+ static boolean isSyntaxError(Throwable thrown) {
+ for (Throwable current = thrown; current != null; current =
current.getCause()) {
+ if (current instanceof PolyglotException pe && pe.isSyntaxError())
{
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private Engine engine() {
+ Engine existing = engine;
+ if (existing != null) {
+ return existing;
+ }
+ engineLock.lock();
+ try {
+ if (engine == null) {
+ engine = Python3Helper.newEngine();
+ }
+ return engine;
+ } finally {
+ engineLock.unlock();
+ }
+ }
+
+ private Source source(String script) {
+ Source cached = sourceCache.get(script);
+ if (cached != null) {
+ return cached;
+ }
+ Source created = Source.newBuilder("python", script,
"camel-python3").buildLiteral();
+ sourceCache.put(script, created);
+ return created;
+ }
+
+ /**
+ * Converts a GraalPy {@link Value} to a Context-independent Java object
while the Context is still open, then
+ * applies Camel type conversion for {@code resultType}.
+ */
+ @SuppressWarnings("unchecked")
+ static <T> T convert(Value value, Class<T> resultType, CamelContext
camelContext, Exchange exchange) {
+ Object obj = materialize(value, new HashMap<>());
+ if (resultType == null || resultType == Object.class) {
+ return (T) obj;
+ }
+ if (obj == null) {
+ return null;
+ }
+ if (resultType.isInstance(obj)) {
+ return resultType.cast(obj);
+ }
+ if (camelContext != null) {
+ if (exchange != null) {
+ return camelContext.getTypeConverter().convertTo(resultType,
exchange, obj);
+ }
+ return camelContext.getTypeConverter().convertTo(resultType, obj);
+ }
+ return resultType.cast(obj);
+ }
+
+ /**
+ * Copies guest values into ordinary Java types so the result remains
usable after {@link Context#close()}.
+ * <p>
+ * GraalPy returns the {@code __main__} module (not {@link
Value#isNull()}) for {@code None}, {@code pass}, and
+ * assignment-only scripts; that is treated as Java {@code null}.
+ */
+ private static Object materialize(Value value, Map<Value, Object> seen) {
+ if (value == null || value.isNull()) {
+ return null;
+ }
+ Object existing = seen.get(value);
+ if (existing != null) {
+ return existing;
+ }
+ if (value.isBoolean()) {
+ return value.asBoolean();
+ }
+ if (value.isNumber()) {
+ if (value.fitsInInt()) {
+ return value.asInt();
+ }
+ if (value.fitsInLong()) {
+ return value.asLong();
+ }
+ if (value.fitsInDouble()) {
+ return value.asDouble();
+ }
+ return value.as(Object.class);
+ }
+ if (value.isString()) {
+ return value.asString();
+ }
+ if (value.isHostObject()) {
+ return value.asHostObject();
+ }
+ if (isMainModule(value)) {
+ return null;
+ }
+ if (value.hasArrayElements()) {
+ int size = Math.toIntExact(value.getArraySize());
+ List<Object> list = new ArrayList<>(size);
+ seen.put(value, list);
+ for (int i = 0; i < size; i++) {
+ list.add(materialize(value.getArrayElement(i), seen));
+ }
+ return list;
+ }
+ if (value.hasHashEntries()) {
+ Map<Object, Object> map = new LinkedHashMap<>();
+ seen.put(value, map);
+ Value entries = value.getHashEntriesIterator();
+ while (entries.hasIteratorNextElement()) {
+ Value entry = entries.getIteratorNextElement();
+ Object key = materialize(entry.getArrayElement(0), seen);
+ Object val = materialize(entry.getArrayElement(1), seen);
+ map.put(key, val);
+ }
+ return map;
+ }
+ return value.as(Object.class);
Review Comment:
**Suggestion — guest object materialization**
`materialize` copies lists/dicts/primitives into plain Java types (well
tested in `Python3LanguageEvalTest`), but other guest values fall through to
`value.as(Object.class)` here. Python `set` / custom objects may remain
Polyglot-backed and fail after `Context#close()`.
Consider materializing sets (or documenting the limitation) and adding a
test for `{1, 2}` / tuple returns.
##########
components/camel-python3/src/main/java/org/apache/camel/language/python3/Python3Helper.java:
##########
@@ -0,0 +1,66 @@
+/*
+ * 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.language.python3;
+
+import org.graalvm.polyglot.Context;
+import org.graalvm.polyglot.Engine;
+import org.graalvm.polyglot.HostAccess;
+import org.graalvm.polyglot.PolyglotAccess;
+
+/**
+ * Factory for GraalPy {@link Engine} and {@link Context} used by the Python 3
language.
+ */
+public final class Python3Helper {
+
+ private Python3Helper() {
+ }
+
+ /**
+ * Host access that lets Python index Java {@link java.util.Map} and
{@link java.util.List} values (headers,
+ * properties, body collections) without allowing arbitrary host method
invocation. Does not use
+ * {@link HostAccess#ALL} or {@code allowAllAccess}.
+ */
+ public static HostAccess defaultHostAccess() {
+ return HostAccess.newBuilder()
+ .allowMapAccess(true)
+ .allowListAccess(true)
+ .allowArrayAccess(true)
+ .allowIterableAccess(true)
+ .allowIteratorAccess(true)
+ .build();
+ }
+
+ public static Engine newEngine() {
+ return Engine.newBuilder("python")
+ .option("engine.WarnInterpreterOnly", "false")
+ .build();
+ }
+
+ /**
+ * Builds a per-eval context. Intentionally does not call {@code
allowAllAccess}, {@code allowHostClassLookup},
+ * {@code allowIO}, or {@code allowCreateProcess}. {@code HostAccess.ALL}
(trusted mode) only unlocks public members
+ * of already-bound host objects; it is not a sandbox and does not grant
class lookup or IO.
+ */
+ public static Context newContext(Engine engine, HostAccess hostAccess) {
Review Comment:
**Good security defaults**
No `allowAllAccess`, no `allowHostClassLookup`, no `allowIO`, no
`allowCreateProcess`, `PolyglotAccess.NONE` — paired with map/list-only
`HostAccess` in default mode. Matches Camel's trust model for scripting
languages.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]