This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new fa1110b08f issue #7182 : Hop Password Variable Resolver for encoded
passwords (#7496)
fa1110b08f is described below
commit fa1110b08f6d2a597970618f1d98c9fe7a428842
Author: Matt Casters <[email protected]>
AuthorDate: Wed Jul 15 14:23:15 2026 +0200
issue #7182 : Hop Password Variable Resolver for encoded passwords (#7496)
* issue #7182 : Hop Password Variable Resolver for encoded passwords
Add a technology-agnostic variable resolver that decrypts Hop-encoded
passwords (Encrypted / AES2) for use in expressions such as VFS URIs.
Supports resolving the argument as a variable name, optional fail-if-
undefined, documentation, unit tests, and integration-tests/resolver.
* issue #7182 : quote MessageFormat braces in password resolver i18n
* issue #7182 : properly quote MessageFormat literals in password resolver
i18n
Wrap #{...} expressions in single quotes so MessageFormat treats them as
literal text instead of format arguments.
* issue #7182 : re-bind Encr in password encoder tests to avoid surefire
pollution
HopClientEnvironment only initializes Encr once. Force Encr.init(AES/Hop)
in @BeforeEach so tests do not pick up the wrong encoder or AES key from a
previous test class when surefire reuses the JVM.
* issue #7182 : use Const import in password resolver test setup
---
docs/hop-user-manual/modules/ROOT/nav.adoc | 1 +
.../hop-password-variable-resolver.adoc | 88 +++
.../metadata-types/variable-resolver/index.adoc | 1 +
.../resolver/0001-pipeline-get-configurations.hpl | 200 ++++++
.../resolver/0001-pipeline-resolver-test.hpl | 705 +++++++++++++++++++++
.../resolver/0002-hop-password-variable.hpl | 94 +++
.../resolver/0003-hop-password-literal.hpl | 94 +++
.../0004-hop-password-missing-variable.hpl | 94 +++
.../datasets/0001-golden-pipeline-resolver.csv | 4 +
.../datasets/0002-golden-hop-password-variable.csv | 2 +
.../datasets/0003-golden-hop-password-literal.csv | 2 +
integration-tests/resolver/dev-env-config.json | 11 +
integration-tests/resolver/files/db1.properties | 23 +
integration-tests/resolver/files/db2.properties | 23 +
integration-tests/resolver/files/db3.properties | 24 +
integration-tests/resolver/hop-config.json | 290 +++++++++
.../resolver/main-0001-pipeline-resolver.hwf | 79 +++
.../resolver/main-0002-hop-password-variable.hwf | 79 +++
.../resolver/main-0003-hop-password-literal.hwf | 79 +++
.../main-0004-hop-password-missing-variable.hwf | 146 +++++
.../dataset/0001-golden-pipeline-resolver.json | 80 +++
.../dataset/0002-golden-hop-password-variable.json | 16 +
.../dataset/0003-golden-hop-password-literal.json | 16 +
.../metadata/pipeline-run-configuration/local.json | 20 +
.../0001-pipeline-resolver-test UNIT.json | 68 ++
.../unit-test/0002-hop-password-variable UNIT.json | 28 +
.../unit-test/0003-hop-password-literal UNIT.json | 28 +
.../resolver/metadata/variable-resolver/conf.json | 13 +
.../variable-resolver/hop-pwd-literal.json | 11 +
.../metadata/variable-resolver/hop-pwd-strict.json | 11 +
.../metadata/variable-resolver/hop-pwd.json | 11 +
.../metadata/workflow-run-configuration/local.json | 9 +
integration-tests/resolver/project-config.json | 15 +
.../resolver/HopPasswordVariableResolver.java | 139 ++++
.../resolver/messages/messages_en_US.properties | 21 +
.../resolver/HopPasswordVariableResolverTest.java | 141 +++++
36 files changed, 2666 insertions(+)
diff --git a/docs/hop-user-manual/modules/ROOT/nav.adoc
b/docs/hop-user-manual/modules/ROOT/nav.adoc
index 43c7a9a476..7afae79f25 100644
--- a/docs/hop-user-manual/modules/ROOT/nav.adoc
+++ b/docs/hop-user-manual/modules/ROOT/nav.adoc
@@ -466,6 +466,7 @@ under the License.
***
xref:metadata-types/variable-resolver/azure-key-vault-variable-resolver.adoc[Azure
Key Vault variable resolver]
***
xref:metadata-types/variable-resolver/google-secret-manager-variable-resolver.adoc[Google
Secret Manager variable resolver]
***
xref:metadata-types/variable-resolver/hashicorp-vault-variable-resolver.adoc[Hashicorp
Vault variable resolver]
+***
xref:metadata-types/variable-resolver/hop-password-variable-resolver.adoc[Hop
Password variable resolver]
***
xref:metadata-types/variable-resolver/openbao-variable-resolver.adoc[OpenBAO
variable resolver]
***
xref:metadata-types/variable-resolver/pipeline-variable-resolver.adoc[Pipeline
variable resolver]
** xref:metadata-types/static-schema-definition.adoc[Static Schema Definition]
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/variable-resolver/hop-password-variable-resolver.adoc
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/variable-resolver/hop-password-variable-resolver.adoc
new file mode 100644
index 0000000000..84e6b9be16
--- /dev/null
+++
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/variable-resolver/hop-password-variable-resolver.adoc
@@ -0,0 +1,88 @@
+////
+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.
+////
+[[HopPasswordVariableResolver]]
+:imagesdir: ../assets/images
+:openvar: #{
+:closevar: }
+:description: Decrypt Hop-encoded passwords in variable expressions (for
example VFS / SFTP URIs)
+
+== Functionality
+
+Some places in Hop accept a free-form string (for example a VFS URI with a
password in the userinfo section) and never call the password decoder
themselves.
+If you put a Hop-encoded value such as `Encrypted ...` or `AES2 ...` directly
in that string, the backend receives the encoded text instead of the real
password.
+
+The *Hop Password Variable Resolver* bridges that gap: it calls the same
decoder used for connection passwords
(`Encr.decryptPasswordOptionallyEncrypted`) so you can keep encoded secrets in
environment or project variables and still use them where only a plain string
is accepted.
+
+IMPORTANT: This resolver is an *interoperability* helper for trusted
operators. It is *not* a confidentiality feature. The default Hop password
encoder is obfuscation (reversible with a public seed). For real protection at
rest, use the xref:password/passwords/aespasswords.adoc[AES2 password encoder]
and/or a secrets-manager resolver (Vault, Azure Key Vault, Google Secret
Manager).
+
+== Options
+
+* *Resolve argument as a variable name* (recommended, enabled by default): the
text after the colon is a *variable name*. Hop looks up that variable and
decrypts its value.
+* When *Resolve argument as a variable name* is *disabled*, the text after the
colon is treated as a *literal* encoded password and decrypted as-is.
+* *Fail if variable is not defined* (disabled by default): only applies when
resolving a variable name. When enabled, an undefined variable causes an error
so the pipeline or workflow fails. When disabled, a missing variable leaves the
original expression unchanged.
+
+TIP: Enable *Fail if variable is not defined* in production environments where
a missing password variable should stop the run rather than silently leave
`#{hop-pwd:…}` unresolved.
+
+== Expression format
+
+Create a Variable Resolver metadata entry (for example named `hop-pwd`) of
type *Hop Password Variable Resolver*.
+
+With *Resolve argument as a variable name* enabled (cleaner expressions):
+
+[source]
+----
+{openvar}hop-pwd:SFTP_PASSWORD{closevar}
+----
+
+If variable `SFTP_PASSWORD` holds `Encrypted 2be98afc...` (or an `AES2 ...`
value when AES2 is configured), the expression resolves to the plaintext
password.
+
+With the option disabled (literal ciphertext in the expression):
+
+[source]
+----
+{openvar}hop-pwd:Encrypted 2be98afc86aa7f2e4cb79a369ce93bcc9{closevar}
+----
+
+Prefer the variable-name form so pipelines do not embed ciphertext.
+
+== SFTP / VFS example
+
+Store the encoded password in a project or environment variable, for example:
+
+[source,properties]
+----
+SFTP_USER=myuser
+SFTP_PASSWORD=Encrypted 2be98afc86aa7f2e4cb79a369ce93bcc9
+SFTP_HOST=myserver
+----
+
+Then use a URI such as:
+
+[source]
+----
+sftp://${SFTP_USER}:{openvar}hop-pwd:SFTP_PASSWORD{closevar}@${SFTP_HOST}/path/to/file
+----
+
+After resolution, VFS receives a normal URI with a plaintext password in
userinfo.
+Avoid logging resolved URIs at Detailed/Debug level if they may contain
credentials.
+
+== Security notes
+
+* Anyone who can author pipelines and configure variable resolvers is already
a trusted operator in Hop's threat model.
+* Decrypting default `Encrypted ...` values was always possible offline; this
resolver only makes intentional use practical.
+* With AES2, decryption still requires `HOP_AES_ENCODER_KEY` on the runtime;
without the key, resolution fails the same way as other password fields.
+* Prefer secrets managers when you can keep credentials out of Hop project
files entirely.
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/variable-resolver/index.adoc
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/variable-resolver/index.adoc
index c758199e3e..8a529cd2fd 100644
---
a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/variable-resolver/index.adoc
+++
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/variable-resolver/index.adoc
@@ -59,5 +59,6 @@ Here are the available variable resolver plugins:
*
xref:metadata-types/variable-resolver/azure-key-vault-variable-resolver.adoc[Azure
Key Vault variable resolver]
*
xref:metadata-types/variable-resolver/google-secret-manager-variable-resolver.adoc[Google
Secret Manager Variable Resolver]
*
xref:metadata-types/variable-resolver/hashicorp-vault-variable-resolver.adoc[Hashicorp
Vault variable resolver]
+*
xref:metadata-types/variable-resolver/hop-password-variable-resolver.adoc[Hop
Password variable resolver]
* xref:metadata-types/variable-resolver/openbao-variable-resolver.adoc[OpenBAO
variable resolver]
*
xref:metadata-types/variable-resolver/pipeline-variable-resolver.adoc[Pipeline
variable resolver]
diff --git a/integration-tests/resolver/0001-pipeline-get-configurations.hpl
b/integration-tests/resolver/0001-pipeline-get-configurations.hpl
new file mode 100644
index 0000000000..10653acfff
--- /dev/null
+++ b/integration-tests/resolver/0001-pipeline-get-configurations.hpl
@@ -0,0 +1,200 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+ <info>
+ <name>0001-pipeline-get-configurations</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2025/01/14 17:20:35.075</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2025/01/14 17:20:35.075</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>files/${TYPE}.properties</from>
+ <to>get values</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>get values</from>
+ <to>OUTPUT</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>OUTPUT</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>432</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>files/${TYPE}.properties</name>
+ <type>PropertyInput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <encoding>UTF-8</encoding>
+ <fields>
+ <field>
+ <column>key</column>
+ <length>-1</length>
+ <name>Key</name>
+ <precision>-1</precision>
+ <repeat>N</repeat>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ </field>
+ <field>
+ <column>value</column>
+ <length>-1</length>
+ <name>Value</name>
+ <precision>-1</precision>
+ <repeat>N</repeat>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ </field>
+ </fields>
+ <file>
+ <file_required>N</file_required>
+ <include_subfolders>N</include_subfolders>
+ <name>${PROJECT_HOME}/files/${TYPE}.properties</name>
+ </file>
+ <file_type>property</file_type>
+ <filefield>N</filefield>
+ <include>N</include>
+ <ini_section>N</ini_section>
+ <isaddresult>Y</isaddresult>
+ <limit>0</limit>
+ <resetrownumber>N</resetrownumber>
+ <resolvevaluevariable>N</resolvevaluevariable>
+ <rownum>N</rownum>
+ <attributes/>
+ <GUI>
+ <xloc>128</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>get values</name>
+ <type>Denormaliser</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <field_name>Value</field_name>
+ <key_value>hostname</key_value>
+ <target_aggregation_type>-</target_aggregation_type>
+ <target_length>-1</target_length>
+ <target_name>hostname</target_name>
+ <target_precision>-1</target_precision>
+ <target_type>String</target_type>
+ </field>
+ <field>
+ <field_name>Value</field_name>
+ <key_value>port</key_value>
+ <target_aggregation_type>-</target_aggregation_type>
+ <target_length>-1</target_length>
+ <target_name>port</target_name>
+ <target_precision>-1</target_precision>
+ <target_type>String</target_type>
+ </field>
+ <field>
+ <field_name>Value</field_name>
+ <key_value>db</key_value>
+ <target_aggregation_type>-</target_aggregation_type>
+ <target_length>-1</target_length>
+ <target_name>db</target_name>
+ <target_precision>-1</target_precision>
+ <target_type>String</target_type>
+ </field>
+ <field>
+ <field_name>Value</field_name>
+ <key_value>username</key_value>
+ <target_aggregation_type>-</target_aggregation_type>
+ <target_length>-1</target_length>
+ <target_name>username</target_name>
+ <target_precision>-1</target_precision>
+ <target_type>String</target_type>
+ </field>
+ <field>
+ <field_name>Value</field_name>
+ <key_value>password</key_value>
+ <target_aggregation_type>-</target_aggregation_type>
+ <target_length>-1</target_length>
+ <target_name>password</target_name>
+ <target_precision>-1</target_precision>
+ <target_type>String</target_type>
+ </field>
+ <field>
+ <field_name>Value</field_name>
+ <key_value>variable</key_value>
+ <target_aggregation_type>-</target_aggregation_type>
+ <target_length>-1</target_length>
+ <target_name>variable</target_name>
+ <target_precision>-1</target_precision>
+ <target_type>String</target_type>
+ </field>
+ </fields>
+ <group>
+</group>
+ <key_field>Key</key_field>
+ <attributes/>
+ <GUI>
+ <xloc>288</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/resolver/0001-pipeline-resolver-test.hpl
b/integration-tests/resolver/0001-pipeline-resolver-test.hpl
new file mode 100644
index 0000000000..9874637537
--- /dev/null
+++ b/integration-tests/resolver/0001-pipeline-resolver-test.hpl
@@ -0,0 +1,705 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+ <info>
+ <name>0001-pipeline-resolver-test</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2025/01/14 17:22:13.379</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2025/01/14 17:22:13.379</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>db1</from>
+ <to>Union</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>db2</from>
+ <to>Union</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>db3</from>
+ <to>Union</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Union</from>
+ <to>Validate</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>get db1 configuration</from>
+ <to>db1 string substitution test</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>db1 string substitution test</from>
+ <to>db1</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>get db2 configuration</from>
+ <to>db2 string substitution test</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>db2 string substitution test</from>
+ <to>db2</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>get db3 configuration</from>
+ <to>db3 string substitution test</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>db3 string substitution test</from>
+ <to>db3</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>Union</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>816</xloc>
+ <yloc>128</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Validate</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>928</xloc>
+ <yloc>128</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>db1</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>688</xloc>
+ <yloc>64</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>db1 string substitution test</name>
+ <type>Calculator</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <calculation>
+ <calc_type>CONSTANT</calc_type>
+ <conversion_mask/>
+ <currency_symbol/>
+ <decimal_symbol/>
+ <field_a>Hostname: #{conf:db1:hostname}; Port: #{conf:db1:port}</field_a>
+ <field_b/>
+ <field_c/>
+ <field_name>hostname_constant</field_name>
+ <grouping_symbol/>
+ <remove>Y</remove>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <value_type>String</value_type>
+ </calculation>
+ <calculation>
+ <calc_type>SUBSTITUTE_VARIABLE</calc_type>
+ <conversion_mask/>
+ <currency_symbol/>
+ <decimal_symbol/>
+ <field_a>hostname_constant</field_a>
+ <field_b/>
+ <field_c/>
+ <field_name>hostname_str</field_name>
+ <grouping_symbol/>
+ <remove>N</remove>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <value_type>String</value_type>
+ </calculation>
+ <calculation>
+ <calc_type>CONSTANT</calc_type>
+ <conversion_mask/>
+ <currency_symbol/>
+ <decimal_symbol/>
+ <field_a>{ "json": [ #{conf:db1} ] }</field_a>
+ <field_b/>
+ <field_c/>
+ <field_name>json_constant</field_name>
+ <grouping_symbol/>
+ <remove>Y</remove>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <value_type>String</value_type>
+ </calculation>
+ <calculation>
+ <calc_type>SUBSTITUTE_VARIABLE</calc_type>
+ <conversion_mask/>
+ <currency_symbol/>
+ <decimal_symbol/>
+ <field_a>json_constant</field_a>
+ <field_b/>
+ <field_c/>
+ <field_name>json_str</field_name>
+ <grouping_symbol/>
+ <remove>N</remove>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <value_type>String</value_type>
+ </calculation>
+ <calculation>
+ <calc_type>CONSTANT</calc_type>
+ <conversion_mask/>
+ <currency_symbol/>
+ <decimal_symbol/>
+ <field_a>#{conf:db1:hostname}</field_a>
+ <field_b/>
+ <field_c/>
+ <field_name>hostname_2_constant</field_name>
+ <grouping_symbol/>
+ <remove>Y</remove>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <value_type>String</value_type>
+ </calculation>
+ <calculation>
+ <calc_type>SUBSTITUTE_VARIABLE</calc_type>
+ <conversion_mask/>
+ <currency_symbol/>
+ <decimal_symbol/>
+ <field_a>hostname_2_constant</field_a>
+ <field_b/>
+ <field_c/>
+ <field_name>hostname_2</field_name>
+ <grouping_symbol/>
+ <remove>N</remove>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <value_type>String</value_type>
+ </calculation>
+ <failIfNoFile>Y</failIfNoFile>
+ <attributes/>
+ <GUI>
+ <xloc>400</xloc>
+ <yloc>64</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>db2</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>688</xloc>
+ <yloc>128</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>db2 string substitution test</name>
+ <type>Calculator</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <calculation>
+ <calc_type>CONSTANT</calc_type>
+ <conversion_mask/>
+ <currency_symbol/>
+ <decimal_symbol/>
+ <field_a>Hostname: #{conf:db2:hostname}; Port: #{conf:db2:port}</field_a>
+ <field_b/>
+ <field_c/>
+ <field_name>hostname_constant</field_name>
+ <grouping_symbol/>
+ <remove>Y</remove>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <value_type>String</value_type>
+ </calculation>
+ <calculation>
+ <calc_type>SUBSTITUTE_VARIABLE</calc_type>
+ <conversion_mask/>
+ <currency_symbol/>
+ <decimal_symbol/>
+ <field_a>hostname_constant</field_a>
+ <field_b/>
+ <field_c/>
+ <field_name>hostname_str</field_name>
+ <grouping_symbol/>
+ <remove>N</remove>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <value_type>String</value_type>
+ </calculation>
+ <calculation>
+ <calc_type>CONSTANT</calc_type>
+ <conversion_mask/>
+ <currency_symbol/>
+ <decimal_symbol/>
+ <field_a>{ "json": [ #{conf:db2} ] }</field_a>
+ <field_b/>
+ <field_c/>
+ <field_name>json_constant</field_name>
+ <grouping_symbol/>
+ <remove>Y</remove>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <value_type>String</value_type>
+ </calculation>
+ <calculation>
+ <calc_type>SUBSTITUTE_VARIABLE</calc_type>
+ <conversion_mask/>
+ <currency_symbol/>
+ <decimal_symbol/>
+ <field_a>json_constant</field_a>
+ <field_b/>
+ <field_c/>
+ <field_name>json_str</field_name>
+ <grouping_symbol/>
+ <remove>N</remove>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <value_type>String</value_type>
+ </calculation>
+ <calculation>
+ <calc_type>CONSTANT</calc_type>
+ <conversion_mask/>
+ <currency_symbol/>
+ <decimal_symbol/>
+ <field_a>#{conf:db2:hostname}</field_a>
+ <field_b/>
+ <field_c/>
+ <field_name>hostname_2_constant</field_name>
+ <grouping_symbol/>
+ <remove>Y</remove>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <value_type>String</value_type>
+ </calculation>
+ <calculation>
+ <calc_type>SUBSTITUTE_VARIABLE</calc_type>
+ <conversion_mask/>
+ <currency_symbol/>
+ <decimal_symbol/>
+ <field_a>hostname_2_constant</field_a>
+ <field_b/>
+ <field_c/>
+ <field_name>hostname_2</field_name>
+ <grouping_symbol/>
+ <remove>N</remove>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <value_type>String</value_type>
+ </calculation>
+ <failIfNoFile>Y</failIfNoFile>
+ <attributes/>
+ <GUI>
+ <xloc>400</xloc>
+ <yloc>128</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>db3</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>688</xloc>
+ <yloc>192</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>db3 string substitution test</name>
+ <type>Calculator</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <calculation>
+ <calc_type>CONSTANT</calc_type>
+ <conversion_mask/>
+ <currency_symbol/>
+ <decimal_symbol/>
+ <field_a>Hostname: #{conf:db3:hostname}; Port: #{conf:db3:port}</field_a>
+ <field_b/>
+ <field_c/>
+ <field_name>hostname_constant</field_name>
+ <grouping_symbol/>
+ <remove>Y</remove>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <value_type>String</value_type>
+ </calculation>
+ <calculation>
+ <calc_type>SUBSTITUTE_VARIABLE</calc_type>
+ <conversion_mask/>
+ <currency_symbol/>
+ <decimal_symbol/>
+ <field_a>hostname_constant</field_a>
+ <field_b/>
+ <field_c/>
+ <field_name>hostname_str</field_name>
+ <grouping_symbol/>
+ <remove>N</remove>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <value_type>String</value_type>
+ </calculation>
+ <calculation>
+ <calc_type>CONSTANT</calc_type>
+ <conversion_mask/>
+ <currency_symbol/>
+ <decimal_symbol/>
+ <field_a>{ "json": [ #{conf:db3} ] }</field_a>
+ <field_b/>
+ <field_c/>
+ <field_name>json_constant</field_name>
+ <grouping_symbol/>
+ <remove>Y</remove>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <value_type>String</value_type>
+ </calculation>
+ <calculation>
+ <calc_type>SUBSTITUTE_VARIABLE</calc_type>
+ <conversion_mask/>
+ <currency_symbol/>
+ <decimal_symbol/>
+ <field_a>json_constant</field_a>
+ <field_b/>
+ <field_c/>
+ <field_name>json_str</field_name>
+ <grouping_symbol/>
+ <remove>N</remove>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <value_type>String</value_type>
+ </calculation>
+ <calculation>
+ <calc_type>CONSTANT</calc_type>
+ <conversion_mask/>
+ <currency_symbol/>
+ <decimal_symbol/>
+ <field_a>#{conf:db3:hostname}</field_a>
+ <field_b/>
+ <field_c/>
+ <field_name>hostname_2_constant</field_name>
+ <grouping_symbol/>
+ <remove>Y</remove>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <value_type>String</value_type>
+ </calculation>
+ <calculation>
+ <calc_type>SUBSTITUTE_VARIABLE</calc_type>
+ <conversion_mask/>
+ <currency_symbol/>
+ <decimal_symbol/>
+ <field_a>hostname_2_constant</field_a>
+ <field_b/>
+ <field_c/>
+ <field_name>hostname_2</field_name>
+ <grouping_symbol/>
+ <remove>N</remove>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <value_type>String</value_type>
+ </calculation>
+ <failIfNoFile>Y</failIfNoFile>
+ <attributes/>
+ <GUI>
+ <xloc>400</xloc>
+ <yloc>192</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>get db1 configuration</name>
+ <type>GetVariable</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <length>-1</length>
+ <name>hostname</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{conf:db1:hostname}</variable>
+ </field>
+ <field>
+ <length>-1</length>
+ <name>port</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{conf:db1:port}</variable>
+ </field>
+ <field>
+ <length>-1</length>
+ <name>db</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{conf:db1:db}</variable>
+ </field>
+ <field>
+ <length>-1</length>
+ <name>username</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{conf:db1:username}</variable>
+ </field>
+ <field>
+ <length>-1</length>
+ <name>password</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{conf:db1:port}</variable>
+ </field>
+ <field>
+ <length>-1</length>
+ <name>variable</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{conf:db1:variable}</variable>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>144</xloc>
+ <yloc>64</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>get db2 configuration</name>
+ <type>GetVariable</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <length>-1</length>
+ <name>hostname</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{conf:db2:hostname}</variable>
+ </field>
+ <field>
+ <length>-1</length>
+ <name>port</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{conf:db2:port}</variable>
+ </field>
+ <field>
+ <length>-1</length>
+ <name>db</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{conf:db2:db}</variable>
+ </field>
+ <field>
+ <length>-1</length>
+ <name>username</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{conf:db2:username}</variable>
+ </field>
+ <field>
+ <length>-1</length>
+ <name>password</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{conf:db2:port}</variable>
+ </field>
+ <field>
+ <length>-1</length>
+ <name>variable</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{conf:db2:variable}</variable>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>144</xloc>
+ <yloc>128</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>get db3 configuration</name>
+ <type>GetVariable</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <length>-1</length>
+ <name>hostname</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{conf:db3:hostname}</variable>
+ </field>
+ <field>
+ <length>-1</length>
+ <name>port</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{conf:db3:port}</variable>
+ </field>
+ <field>
+ <length>-1</length>
+ <name>db</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{conf:db3:db}</variable>
+ </field>
+ <field>
+ <length>-1</length>
+ <name>username</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{conf:db3:username}</variable>
+ </field>
+ <field>
+ <length>-1</length>
+ <name>password</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{conf:db3:port}</variable>
+ </field>
+ <field>
+ <length>-1</length>
+ <name>variable</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{conf:db3:variable}</variable>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>144</xloc>
+ <yloc>192</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/resolver/0002-hop-password-variable.hpl
b/integration-tests/resolver/0002-hop-password-variable.hpl
new file mode 100644
index 0000000000..75c3e68324
--- /dev/null
+++ b/integration-tests/resolver/0002-hop-password-variable.hpl
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+ <info>
+ <name>0002-hop-password-variable</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Resolve a Hop-encoded password via variable name
(resolveAsVariable=true)</description>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2026/07/11 00:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/07/11 00:00:00.000</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>Get variables</from>
+ <to>Output</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>Get variables</name>
+ <type>GetVariable</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <length>-1</length>
+ <name>password</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{hop-pwd:HOP_PWD}</variable>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>64</xloc>
+ <yloc>48</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Output</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>288</xloc>
+ <yloc>48</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/resolver/0003-hop-password-literal.hpl
b/integration-tests/resolver/0003-hop-password-literal.hpl
new file mode 100644
index 0000000000..5f164615f7
--- /dev/null
+++ b/integration-tests/resolver/0003-hop-password-literal.hpl
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+ <info>
+ <name>0003-hop-password-literal</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Resolve a Hop-encoded password as a literal
(resolveAsVariable=false)</description>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2026/07/11 00:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/07/11 00:00:00.000</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>Get variables</from>
+ <to>Output</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>Get variables</name>
+ <type>GetVariable</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <length>-1</length>
+ <name>password</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{hop-pwd-literal:Encrypted
696e7467d9eacebc03c89cc9bf1cbd649382b8de}</variable>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>64</xloc>
+ <yloc>48</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Output</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>288</xloc>
+ <yloc>48</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/resolver/0004-hop-password-missing-variable.hpl
b/integration-tests/resolver/0004-hop-password-missing-variable.hpl
new file mode 100644
index 0000000000..40a2d53817
--- /dev/null
+++ b/integration-tests/resolver/0004-hop-password-missing-variable.hpl
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+ <info>
+ <name>0004-hop-password-missing-variable</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Expected to fail: hop-pwd-strict with
failIfVariableNotDefined and missing variable</description>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2026/07/11 00:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/07/11 00:00:00.000</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>Get variables</from>
+ <to>Output</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>Get variables</name>
+ <type>GetVariable</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <length>-1</length>
+ <name>password</name>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <type>String</type>
+ <variable>#{hop-pwd-strict:DOES_NOT_EXIST}</variable>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>64</xloc>
+ <yloc>48</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Output</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>288</xloc>
+ <yloc>48</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git
a/integration-tests/resolver/datasets/0001-golden-pipeline-resolver.csv
b/integration-tests/resolver/datasets/0001-golden-pipeline-resolver.csv
new file mode 100644
index 0000000000..379d2e2434
--- /dev/null
+++ b/integration-tests/resolver/datasets/0001-golden-pipeline-resolver.csv
@@ -0,0 +1,4 @@
+hostname,port,db,username,password,variable,hostname_str,json,hostname_2
+hostname1,port1,db1,user1,port1,value1,Hostname: hostname1; Port: port1,"{
""json"": [
{""hostname"":""hostname1"",""password"":""pwd1"",""port"":""port1"",""variable"":""value1"",""db"":""db1"",""username"":""user1""}
] }",hostname1
+hostname2,port2,db2,user2,port2,value2,Hostname: hostname2; Port: port2,"{
""json"": [
{""hostname"":""hostname2"",""password"":""pwd2"",""port"":""port2"",""variable"":""value2"",""db"":""db2"",""username"":""user2""}
] }",hostname2
+hostname3,port3,db3,user3,port3,value3,Hostname: hostname3; Port: port3,"{
""json"": [
{""hostname"":""hostname3"",""password"":""pwd3"",""port"":""port3"",""variable"":""value3"",""db"":""db3"",""username"":""user3""}
] }",hostname3
diff --git
a/integration-tests/resolver/datasets/0002-golden-hop-password-variable.csv
b/integration-tests/resolver/datasets/0002-golden-hop-password-variable.csv
new file mode 100644
index 0000000000..b592e0a8a1
--- /dev/null
+++ b/integration-tests/resolver/datasets/0002-golden-hop-password-variable.csv
@@ -0,0 +1,2 @@
+password
+integration-test-pwd
diff --git
a/integration-tests/resolver/datasets/0003-golden-hop-password-literal.csv
b/integration-tests/resolver/datasets/0003-golden-hop-password-literal.csv
new file mode 100644
index 0000000000..b592e0a8a1
--- /dev/null
+++ b/integration-tests/resolver/datasets/0003-golden-hop-password-literal.csv
@@ -0,0 +1,2 @@
+password
+integration-test-pwd
diff --git a/integration-tests/resolver/dev-env-config.json
b/integration-tests/resolver/dev-env-config.json
new file mode 100644
index 0000000000..e5833d836c
--- /dev/null
+++ b/integration-tests/resolver/dev-env-config.json
@@ -0,0 +1,11 @@
+{
+ "variables" : [ {
+ "name" : "HOP_PWD",
+ "value" : "Encrypted 696e7467d9eacebc03c89cc9bf1cbd649382b8de",
+ "description" : "Hop-encoded password for plaintext integration-test-pwd"
+ }, {
+ "name" : "HOP_PWD_PLAIN",
+ "value" : "integration-test-pwd",
+ "description" : "Expected plaintext after decrypt"
+ } ]
+}
diff --git a/integration-tests/resolver/files/db1.properties
b/integration-tests/resolver/files/db1.properties
new file mode 100644
index 0000000000..9d0d3cd862
--- /dev/null
+++ b/integration-tests/resolver/files/db1.properties
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+#
+username=user1
+password=pwd1
+hostname=hostname1
+port=port1
+db=db1
+variable=value1
diff --git a/integration-tests/resolver/files/db2.properties
b/integration-tests/resolver/files/db2.properties
new file mode 100644
index 0000000000..18e42138fa
--- /dev/null
+++ b/integration-tests/resolver/files/db2.properties
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+#
+username=user2
+password=pwd2
+hostname=hostname2
+port=port2
+db=db2
+variable=value2
diff --git a/integration-tests/resolver/files/db3.properties
b/integration-tests/resolver/files/db3.properties
new file mode 100644
index 0000000000..4f5974c77d
--- /dev/null
+++ b/integration-tests/resolver/files/db3.properties
@@ -0,0 +1,24 @@
+#
+# 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.
+#
+#
+username=user3
+password=pwd3
+hostname=hostname3
+port=port3
+db=db3
+variable=value3
+
diff --git a/integration-tests/resolver/hop-config.json
b/integration-tests/resolver/hop-config.json
new file mode 100644
index 0000000000..102ac981d5
--- /dev/null
+++ b/integration-tests/resolver/hop-config.json
@@ -0,0 +1,290 @@
+{
+ "variables": [
+ {
+ "name": "HOP_LENIENT_STRING_TO_NUMBER_CONVERSION",
+ "value": "N",
+ "description": "System wide flag to allow lenient string to number
conversion for backward compatibility. If this setting is set to \"Y\", an
string starting with digits will be converted successfully into a number.
(example: 192.168.1.1 will be converted into 192 or 192.168 or 192168 depending
on the decimal and grouping symbol). The default (N) will be to throw an error
if non-numeric symbols are found in the string."
+ },
+ {
+ "name": "HOP_COMPATIBILITY_DB_IGNORE_TIMEZONE",
+ "value": "N",
+ "description": "System wide flag to ignore timezone while writing
date/timestamp value to the database."
+ },
+ {
+ "name": "HOP_LOG_SIZE_LIMIT",
+ "value": "0",
+ "description": "The log size limit for all pipelines and workflows that
don't have the \"log size limit\" property set in their respective properties."
+ },
+ {
+ "name": "HOP_EMPTY_STRING_DIFFERS_FROM_NULL",
+ "value": "N",
+ "description": "NULL vs Empty String. If this setting is set to Y, an
empty string and null are different. Otherwise they are not."
+ },
+ {
+ "name": "HOP_MAX_LOG_SIZE_IN_LINES",
+ "value": "0",
+ "description": "The maximum number of log lines that are kept internally
by Hop. Set to 0 to keep all rows (default)"
+ },
+ {
+ "name": "HOP_MAX_LOG_TIMEOUT_IN_MINUTES",
+ "value": "1440",
+ "description": "The maximum age (in minutes) of a log line while being
kept internally by Hop. Set to 0 to keep all rows indefinitely (default)"
+ },
+ {
+ "name": "HOP_MAX_WORKFLOW_TRACKER_SIZE",
+ "value": "5000",
+ "description": "The maximum number of workflow trackers kept in memory"
+ },
+ {
+ "name": "HOP_MAX_ACTIONS_LOGGED",
+ "value": "5000",
+ "description": "The maximum number of action results kept in memory for
logging purposes."
+ },
+ {
+ "name": "HOP_MAX_LOGGING_REGISTRY_SIZE",
+ "value": "10000",
+ "description": "The maximum number of logging registry entries kept in
memory for logging purposes."
+ },
+ {
+ "name": "HOP_LOG_TAB_REFRESH_DELAY",
+ "value": "1000",
+ "description": "The hop log tab refresh delay."
+ },
+ {
+ "name": "HOP_LOG_TAB_REFRESH_PERIOD",
+ "value": "1000",
+ "description": "The hop log tab refresh period."
+ },
+ {
+ "name": "HOP_PLUGIN_CLASSES",
+ "value": null,
+ "description": "A comma delimited list of classes to scan for plugin
annotations"
+ },
+ {
+ "name": "HOP_PLUGIN_PACKAGES",
+ "value": null,
+ "description": "A comma delimited list of packages to scan for plugin
annotations (warning: slow!!)"
+ },
+ {
+ "name": "HOP_TRANSFORM_PERFORMANCE_SNAPSHOT_LIMIT",
+ "value": "0",
+ "description": "The maximum number of transform performance snapshots to
keep in memory. Set to 0 to keep all snapshots indefinitely (default)"
+ },
+ {
+ "name": "HOP_ROWSET_GET_TIMEOUT",
+ "value": "50",
+ "description": "The name of the variable that optionally contains an
alternative rowset get timeout (in ms). This only makes a difference for
extremely short lived pipelines."
+ },
+ {
+ "name": "HOP_ROWSET_PUT_TIMEOUT",
+ "value": "50",
+ "description": "The name of the variable that optionally contains an
alternative rowset put timeout (in ms). This only makes a difference for
extremely short lived pipelines."
+ },
+ {
+ "name": "HOP_CORE_TRANSFORMS_FILE",
+ "value": null,
+ "description": "The name of the project variable that will contain the
alternative location of the hop-transforms.xml file. You can use this to
customize the list of available internal transforms outside of the codebase."
+ },
+ {
+ "name": "HOP_CORE_WORKFLOW_ACTIONS_FILE",
+ "value": null,
+ "description": "The name of the project variable that will contain the
alternative location of the hop-workflow-actions.xml file."
+ },
+ {
+ "name": "HOP_SERVER_OBJECT_TIMEOUT_MINUTES",
+ "value": "1440",
+ "description": "This project variable will set a time-out after which
waiting, completed or stopped pipelines and workflows will be automatically
cleaned up. The default value is 1440 (one day)."
+ },
+ {
+ "name": "HOP_PIPELINE_PAN_JVM_EXIT_CODE",
+ "value": null,
+ "description": "Set this variable to an integer that will be returned as
the Pan JVM exit code."
+ },
+ {
+ "name": "HOP_DISABLE_CONSOLE_LOGGING",
+ "value": "N",
+ "description": "Set this variable to Y to disable standard Hop logging
to the console. (stdout)"
+ },
+ {
+ "name": "HOP_REDIRECT_STDERR",
+ "value": "N",
+ "description": "Set this variable to Y to redirect stderr to Hop
logging."
+ },
+ {
+ "name": "HOP_REDIRECT_STDOUT",
+ "value": "N",
+ "description": "Set this variable to Y to redirect stdout to Hop
logging."
+ },
+ {
+ "name": "HOP_DEFAULT_NUMBER_FORMAT",
+ "value": null,
+ "description": "The name of the variable containing an alternative
default number format"
+ },
+ {
+ "name": "HOP_DEFAULT_BIGNUMBER_FORMAT",
+ "value": null,
+ "description": "The name of the variable containing an alternative
default bignumber format"
+ },
+ {
+ "name": "HOP_DEFAULT_INTEGER_FORMAT",
+ "value": null,
+ "description": "The name of the variable containing an alternative
default integer format"
+ },
+ {
+ "name": "HOP_DEFAULT_DATE_FORMAT",
+ "value": null,
+ "description": "The name of the variable containing an alternative
default date format"
+ },
+ {
+ "name": "HOP_DEFAULT_TIMESTAMP_FORMAT",
+ "value": null,
+ "description": "The name of the variable containing an alternative
default timestamp format"
+ },
+ {
+ "name": "HOP_DEFAULT_SERVLET_ENCODING",
+ "value": null,
+ "description": "Defines the default encoding for servlets, leave it
empty to use Java default encoding"
+ },
+ {
+ "name": "HOP_FAIL_ON_LOGGING_ERROR",
+ "value": "N",
+ "description": "Set this variable to Y when you want the
workflow/pipeline fail with an error when the related logging process (e.g. to
a database) fails."
+ },
+ {
+ "name": "HOP_AGGREGATION_MIN_NULL_IS_VALUED",
+ "value": "N",
+ "description": "Set this variable to Y to set the minimum to NULL if
NULL is within an aggregate. Otherwise by default NULL is ignored by the MIN
aggregate and MIN is set to the minimum value that is not NULL. See also the
variable HOP_AGGREGATION_ALL_NULLS_ARE_ZERO."
+ },
+ {
+ "name": "HOP_AGGREGATION_ALL_NULLS_ARE_ZERO",
+ "value": "N",
+ "description": "Set this variable to Y to return 0 when all values
within an aggregate are NULL. Otherwise by default a NULL is returned when all
values are NULL."
+ },
+ {
+ "name": "HOP_COMPATIBILITY_TEXT_FILE_OUTPUT_APPEND_NO_HEADER",
+ "value": "N",
+ "description": "Set this variable to Y for backward compatibility for
the Text File Output transform. Setting this to Ywill add no header row at all
when the append option is enabled, regardless if the file is existing or not."
+ },
+ {
+ "name": "HOP_PASSWORD_ENCODER_PLUGIN",
+ "value": "Hop",
+ "description": "Specifies the password encoder plugin to use by ID (Hop
is the default)."
+ },
+ {
+ "name": "HOP_SYSTEM_HOSTNAME",
+ "value": null,
+ "description": "You can use this variable to speed up hostname lookup.
Hostname lookup is performed by Hop so that it is capable of logging the server
on which a workflow or pipeline is executed."
+ },
+ {
+ "name": "HOP_SERVER_JETTY_ACCEPTORS",
+ "value": null,
+ "description": "A variable to configure jetty option: acceptors for
Carte"
+ },
+ {
+ "name": "HOP_SERVER_JETTY_ACCEPT_QUEUE_SIZE",
+ "value": null,
+ "description": "A variable to configure jetty option: acceptQueueSize
for Carte"
+ },
+ {
+ "name": "HOP_SERVER_JETTY_RES_MAX_IDLE_TIME",
+ "value": null,
+ "description": "A variable to configure jetty option:
lowResourcesMaxIdleTime for Carte"
+ },
+ {
+ "name":
"HOP_COMPATIBILITY_MERGE_ROWS_USE_REFERENCE_STREAM_WHEN_IDENTICAL",
+ "value": "N",
+ "description": "Set this variable to Y for backward compatibility for
the Merge Rows (diff) transform. Setting this to Y will use the data from the
reference stream (instead of the comparison stream) in case the compared rows
are identical."
+ },
+ {
+ "name": "HOP_SPLIT_FIELDS_REMOVE_ENCLOSURE",
+ "value": "false",
+ "description": "Set this variable to false to preserve enclosure symbol
after splitting the string in the Split fields transform. Changing it to true
will remove first and last enclosure symbol from the resulting string chunks."
+ },
+ {
+ "name": "HOP_ALLOW_EMPTY_FIELD_NAMES_AND_TYPES",
+ "value": "false",
+ "description": "Set this variable to TRUE to allow your pipeline to pass
'null' fields and/or empty types."
+ },
+ {
+ "name": "HOP_GLOBAL_LOG_VARIABLES_CLEAR_ON_EXPORT",
+ "value": "false",
+ "description": "Set this variable to false to preserve global log
variables defined in pipeline / workflow Properties -> Log panel. Changing it
to true will clear it when export pipeline / workflow."
+ },
+ {
+ "name": "HOP_FILE_OUTPUT_MAX_STREAM_COUNT",
+ "value": "1024",
+ "description": "This project variable is used by the Text File Output
transform. It defines the max number of simultaneously open files within the
transform. The transform will close/reopen files as necessary to insure the max
is not exceeded"
+ },
+ {
+ "name": "HOP_FILE_OUTPUT_MAX_STREAM_LIFE",
+ "value": "0",
+ "description": "This project variable is used by the Text File Output
transform. It defines the max number of milliseconds between flushes of files
opened by the transform."
+ },
+ {
+ "name": "HOP_USE_NATIVE_FILE_DIALOG",
+ "value": "N",
+ "description": "Set this value to Y if you want to use the system file
open/save dialog when browsing files"
+ },
+ {
+ "name": "HOP_AUTO_CREATE_CONFIG",
+ "value": "Y",
+ "description": "Set this value to N if you don't want to automatically
create a hop configuration file (hop-config.json) when it's missing"
+ }
+ ],
+ "LocaleDefault": "en_BE",
+ "guiProperties": {
+ "FontFixedSize": "13",
+ "MaxUndo": "100",
+ "DarkMode": "Y",
+ "FontNoteSize": "13",
+ "ShowOSLook": "Y",
+ "FontFixedStyle": "0",
+ "FontNoteName": ".AppleSystemUIFont",
+ "FontFixedName": "Monospaced",
+ "FontGraphStyle": "0",
+ "FontDefaultSize": "13",
+ "GraphColorR": "255",
+ "FontGraphSize": "13",
+ "IconSize": "32",
+ "BackgroundColorB": "255",
+ "FontNoteStyle": "0",
+ "FontGraphName": ".AppleSystemUIFont",
+ "FontDefaultName": ".AppleSystemUIFont",
+ "GraphColorG": "255",
+ "UseGlobalFileBookmarks": "Y",
+ "FontDefaultStyle": "0",
+ "GraphColorB": "255",
+ "BackgroundColorR": "255",
+ "BackgroundColorG": "255",
+ "WorkflowDialogStyle": "RESIZE,MAX,MIN",
+ "LineWidth": "1",
+ "ContextDialogShowCategories": "Y"
+ },
+ "projectsConfig": {
+ "enabled": true,
+ "projectMandatory": true,
+ "environmentMandatory": true,
+ "defaultProject": "default",
+ "defaultEnvironment": null,
+ "standardParentProject": "default",
+ "standardProjectsFolder": null,
+ "projectConfigurations": [
+ {
+ "projectName": "default",
+ "projectHome": "${HOP_CONFIG_FOLDER}",
+ "configFilename": "project-config.json"
+ }
+ ],
+ "lifecycleEnvironments": [
+ {
+ "name": "dev",
+ "purpose": "Testing",
+ "projectName": "default",
+ "configurationFiles": [
+ "${PROJECT_HOME}/dev-env-config.json"
+ ]
+ }
+ ],
+ "projectLifecycles": []
+ }
+}
\ No newline at end of file
diff --git a/integration-tests/resolver/main-0001-pipeline-resolver.hwf
b/integration-tests/resolver/main-0001-pipeline-resolver.hwf
new file mode 100644
index 0000000000..3dfa918dbd
--- /dev/null
+++ b/integration-tests/resolver/main-0001-pipeline-resolver.hwf
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<workflow>
+ <name>main-0001-pipeline-resolver</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2025/01/14 17:21:30.278</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2025/01/14 17:21:30.278</modified_date>
+ <parameters>
+ </parameters>
+ <actions>
+ <action>
+ <name>Start</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <DayOfMonth>1</DayOfMonth>
+ <hour>12</hour>
+ <intervalMinutes>60</intervalMinutes>
+ <intervalSeconds>0</intervalSeconds>
+ <minutes>0</minutes>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <weekDay>1</weekDay>
+ <parallel>N</parallel>
+ <xloc>96</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Run Pipeline Unit Tests</name>
+ <description/>
+ <type>RunPipelineTests</type>
+ <attributes/>
+ <test_names>
+ <test_name>
+ <name>0001-pipeline-resolver-test UNIT</name>
+ </test_name>
+ </test_names>
+ <parallel>N</parallel>
+ <xloc>256</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>Run Pipeline Unit Tests</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
diff --git a/integration-tests/resolver/main-0002-hop-password-variable.hwf
b/integration-tests/resolver/main-0002-hop-password-variable.hwf
new file mode 100644
index 0000000000..d69e79fde2
--- /dev/null
+++ b/integration-tests/resolver/main-0002-hop-password-variable.hwf
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<workflow>
+ <name>main-0002-hop-password-variable</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2026/07/11 00:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/07/11 00:00:00.000</modified_date>
+ <parameters>
+ </parameters>
+ <actions>
+ <action>
+ <name>Start</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <DayOfMonth>1</DayOfMonth>
+ <hour>12</hour>
+ <intervalMinutes>60</intervalMinutes>
+ <intervalSeconds>0</intervalSeconds>
+ <minutes>0</minutes>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <weekDay>1</weekDay>
+ <parallel>N</parallel>
+ <xloc>96</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Run Pipeline Unit Tests</name>
+ <description/>
+ <type>RunPipelineTests</type>
+ <attributes/>
+ <test_names>
+ <test_name>
+ <name>0002-hop-password-variable UNIT</name>
+ </test_name>
+ </test_names>
+ <parallel>N</parallel>
+ <xloc>256</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>Run Pipeline Unit Tests</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
diff --git a/integration-tests/resolver/main-0003-hop-password-literal.hwf
b/integration-tests/resolver/main-0003-hop-password-literal.hwf
new file mode 100644
index 0000000000..b1d4d39414
--- /dev/null
+++ b/integration-tests/resolver/main-0003-hop-password-literal.hwf
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<workflow>
+ <name>main-0003-hop-password-literal</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2026/07/11 00:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/07/11 00:00:00.000</modified_date>
+ <parameters>
+ </parameters>
+ <actions>
+ <action>
+ <name>Start</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <DayOfMonth>1</DayOfMonth>
+ <hour>12</hour>
+ <intervalMinutes>60</intervalMinutes>
+ <intervalSeconds>0</intervalSeconds>
+ <minutes>0</minutes>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <weekDay>1</weekDay>
+ <parallel>N</parallel>
+ <xloc>96</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Run Pipeline Unit Tests</name>
+ <description/>
+ <type>RunPipelineTests</type>
+ <attributes/>
+ <test_names>
+ <test_name>
+ <name>0003-hop-password-literal UNIT</name>
+ </test_name>
+ </test_names>
+ <parallel>N</parallel>
+ <xloc>256</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>Run Pipeline Unit Tests</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
diff --git
a/integration-tests/resolver/main-0004-hop-password-missing-variable.hwf
b/integration-tests/resolver/main-0004-hop-password-missing-variable.hwf
new file mode 100644
index 0000000000..ded4e91171
--- /dev/null
+++ b/integration-tests/resolver/main-0004-hop-password-missing-variable.hwf
@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<workflow>
+ <name>main-0004-hop-password-missing-variable</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Expect pipeline to fail when hop-pwd-strict cannot resolve a
missing variable</description>
+ <extended_description/>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2026/07/11 00:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/07/11 00:00:00.000</modified_date>
+ <parameters>
+ </parameters>
+ <actions>
+ <action>
+ <name>Start</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <DayOfMonth>1</DayOfMonth>
+ <hour>12</hour>
+ <intervalMinutes>60</intervalMinutes>
+ <intervalSeconds>0</intervalSeconds>
+ <minutes>0</minutes>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <weekDay>1</weekDay>
+ <parallel>N</parallel>
+ <xloc>96</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>0004-hop-password-missing-variable.hpl</name>
+ <description/>
+ <type>PIPELINE</type>
+ <attributes/>
+
<filename>${PROJECT_HOME}/0004-hop-password-missing-variable.hpl</filename>
+ <params_from_previous>N</params_from_previous>
+ <exec_per_row>N</exec_per_row>
+ <clear_rows>N</clear_rows>
+ <clear_files>N</clear_files>
+ <set_logfile>N</set_logfile>
+ <logfile/>
+ <logext/>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <loglevel>Basic</loglevel>
+ <set_append_logfile>N</set_append_logfile>
+ <wait_until_finished>Y</wait_until_finished>
+ <create_parent_folder>N</create_parent_folder>
+ <run_configuration>local</run_configuration>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <parallel>N</parallel>
+ <xloc>336</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Check pipeline log</name>
+ <description/>
+ <type>EVAL</type>
+ <attributes/>
+ <script>
+var txt = previous_result.getLogText();
+
+var ok = true;
+
+var expectedValues = [
+ "DOES_NOT_EXIST",
+ "Password variable",
+ ];
+
+for (var i = 0 ; i<expectedValues.length ; i++) {
+ var expectedValue = expectedValues[i];
+ if ( !txt.contains(expectedValue)) {
+ ok = false;
+ log.logError("Value logged as '"+expectedValue+"' was not logged at least
once");
+ }
+}
+
+ok;</script>
+ <parallel>N</parallel>
+ <xloc>576</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Fail</name>
+ <description/>
+ <type>ABORT</type>
+ <attributes/>
+ <message>This pipeline should fail when the password variable is not
defined</message>
+ <parallel>N</parallel>
+ <xloc>576</xloc>
+ <yloc>176</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>0004-hop-password-missing-variable.hpl</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ <hop>
+ <from>0004-hop-password-missing-variable.hpl</from>
+ <to>Check pipeline log</to>
+ <enabled>Y</enabled>
+ <evaluation>N</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>0004-hop-password-missing-variable.hpl</from>
+ <to>Fail</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
diff --git
a/integration-tests/resolver/metadata/dataset/0001-golden-pipeline-resolver.json
b/integration-tests/resolver/metadata/dataset/0001-golden-pipeline-resolver.json
new file mode 100644
index 0000000000..a96760571b
--- /dev/null
+++
b/integration-tests/resolver/metadata/dataset/0001-golden-pipeline-resolver.json
@@ -0,0 +1,80 @@
+{
+ "base_filename": "0001-golden-pipeline-resolver.csv",
+ "name": "0001-golden-pipeline-resolver",
+ "description": "",
+ "dataset_fields": [
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "hostname",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "port",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "db",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "username",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "password",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "variable",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "hostname_str",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "json",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "hostname_2",
+ "field_format": ""
+ }
+ ],
+ "folder_name": ""
+}
diff --git
a/integration-tests/resolver/metadata/dataset/0002-golden-hop-password-variable.json
b/integration-tests/resolver/metadata/dataset/0002-golden-hop-password-variable.json
new file mode 100644
index 0000000000..d0af433582
--- /dev/null
+++
b/integration-tests/resolver/metadata/dataset/0002-golden-hop-password-variable.json
@@ -0,0 +1,16 @@
+{
+ "base_filename": "0002-golden-hop-password-variable.csv",
+ "name": "0002-golden-hop-password-variable",
+ "description": "",
+ "dataset_fields": [
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "password",
+ "field_format": ""
+ }
+ ],
+ "folder_name": ""
+}
diff --git
a/integration-tests/resolver/metadata/dataset/0003-golden-hop-password-literal.json
b/integration-tests/resolver/metadata/dataset/0003-golden-hop-password-literal.json
new file mode 100644
index 0000000000..ff906dc04a
--- /dev/null
+++
b/integration-tests/resolver/metadata/dataset/0003-golden-hop-password-literal.json
@@ -0,0 +1,16 @@
+{
+ "base_filename": "0003-golden-hop-password-literal.csv",
+ "name": "0003-golden-hop-password-literal",
+ "description": "",
+ "dataset_fields": [
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "password",
+ "field_format": ""
+ }
+ ],
+ "folder_name": ""
+}
diff --git
a/integration-tests/resolver/metadata/pipeline-run-configuration/local.json
b/integration-tests/resolver/metadata/pipeline-run-configuration/local.json
new file mode 100644
index 0000000000..d6b7ca4eed
--- /dev/null
+++ b/integration-tests/resolver/metadata/pipeline-run-configuration/local.json
@@ -0,0 +1,20 @@
+{
+ "engineRunConfiguration": {
+ "Local": {
+ "feedback_size": "50000",
+ "sample_size": "100",
+ "sample_type_in_gui": "Last",
+ "wait_time": "20",
+ "rowset_size": "10000",
+ "safe_mode": false,
+ "show_feedback": false,
+ "topo_sort": false,
+ "gather_metrics": false,
+ "transactional": false
+ }
+ },
+ "defaultSelection": true,
+ "configurationVariables": [],
+ "name": "local",
+ "description": "Runs your pipelines locally with the standard local Hop
pipeline engine"
+}
\ No newline at end of file
diff --git
a/integration-tests/resolver/metadata/unit-test/0001-pipeline-resolver-test
UNIT.json
b/integration-tests/resolver/metadata/unit-test/0001-pipeline-resolver-test
UNIT.json
new file mode 100644
index 0000000000..55d1816370
--- /dev/null
+++ b/integration-tests/resolver/metadata/unit-test/0001-pipeline-resolver-test
UNIT.json
@@ -0,0 +1,68 @@
+{
+ "database_replacements": [],
+ "autoOpening": true,
+ "description": "",
+ "persist_filename": "",
+ "test_type": "UNIT_TEST",
+ "variableValues": [],
+ "basePath": "",
+ "golden_data_sets": [
+ {
+ "field_mappings": [
+ {
+ "transform_field": "hostname",
+ "data_set_field": "hostname"
+ },
+ {
+ "transform_field": "port",
+ "data_set_field": "port"
+ },
+ {
+ "transform_field": "db",
+ "data_set_field": "db"
+ },
+ {
+ "transform_field": "username",
+ "data_set_field": "username"
+ },
+ {
+ "transform_field": "password",
+ "data_set_field": "password"
+ },
+ {
+ "transform_field": "variable",
+ "data_set_field": "variable"
+ },
+ {
+ "transform_field": "hostname_str",
+ "data_set_field": "hostname_str"
+ },
+ {
+ "transform_field": "json_str",
+ "data_set_field": "json"
+ },
+ {
+ "transform_field": "hostname_2",
+ "data_set_field": "hostname_2"
+ }
+ ],
+ "field_order": [
+ "hostname",
+ "port",
+ "db",
+ "username",
+ "password",
+ "variable",
+ "hostname_str",
+ "json",
+ "hostname_2"
+ ],
+ "data_set_name": "0001-golden-pipeline-resolver",
+ "transform_name": "Validate"
+ }
+ ],
+ "input_data_sets": [],
+ "name": "0001-pipeline-resolver-test UNIT",
+ "trans_test_tweaks": [],
+ "pipeline_filename": "./0001-pipeline-resolver-test.hpl"
+}
\ No newline at end of file
diff --git
a/integration-tests/resolver/metadata/unit-test/0002-hop-password-variable
UNIT.json
b/integration-tests/resolver/metadata/unit-test/0002-hop-password-variable
UNIT.json
new file mode 100644
index 0000000000..9156dd85c4
--- /dev/null
+++ b/integration-tests/resolver/metadata/unit-test/0002-hop-password-variable
UNIT.json
@@ -0,0 +1,28 @@
+{
+ "database_replacements": [],
+ "autoOpening": true,
+ "description": "",
+ "persist_filename": "",
+ "test_type": "UNIT_TEST",
+ "variableValues": [],
+ "basePath": "",
+ "golden_data_sets": [
+ {
+ "field_mappings": [
+ {
+ "transform_field": "password",
+ "data_set_field": "password"
+ }
+ ],
+ "field_order": [
+ "password"
+ ],
+ "data_set_name": "0002-golden-hop-password-variable",
+ "transform_name": "Output"
+ }
+ ],
+ "input_data_sets": [],
+ "name": "0002-hop-password-variable UNIT",
+ "trans_test_tweaks": [],
+ "pipeline_filename": "./0002-hop-password-variable.hpl"
+}
diff --git
a/integration-tests/resolver/metadata/unit-test/0003-hop-password-literal
UNIT.json
b/integration-tests/resolver/metadata/unit-test/0003-hop-password-literal
UNIT.json
new file mode 100644
index 0000000000..feadde6bf3
--- /dev/null
+++ b/integration-tests/resolver/metadata/unit-test/0003-hop-password-literal
UNIT.json
@@ -0,0 +1,28 @@
+{
+ "database_replacements": [],
+ "autoOpening": true,
+ "description": "",
+ "persist_filename": "",
+ "test_type": "UNIT_TEST",
+ "variableValues": [],
+ "basePath": "",
+ "golden_data_sets": [
+ {
+ "field_mappings": [
+ {
+ "transform_field": "password",
+ "data_set_field": "password"
+ }
+ ],
+ "field_order": [
+ "password"
+ ],
+ "data_set_name": "0003-golden-hop-password-literal",
+ "transform_name": "Output"
+ }
+ ],
+ "input_data_sets": [],
+ "name": "0003-hop-password-literal UNIT",
+ "trans_test_tweaks": [],
+ "pipeline_filename": "./0003-hop-password-literal.hpl"
+}
diff --git a/integration-tests/resolver/metadata/variable-resolver/conf.json
b/integration-tests/resolver/metadata/variable-resolver/conf.json
new file mode 100644
index 0000000000..bd95082169
--- /dev/null
+++ b/integration-tests/resolver/metadata/variable-resolver/conf.json
@@ -0,0 +1,13 @@
+{
+ "virtualPath": "",
+ "name": "conf",
+ "description": "Uses a pipeline to read configuration files using variable
expressions",
+ "variable-resolver": {
+ "Variable-Resolver-Pipeline": {
+ "outputTransformName": "OUTPUT",
+ "filename": "${PROJECT_HOME}/0001-pipeline-get-configurations.hpl",
+ "expressionVariableName": "TYPE",
+ "runConfigurationName": "local"
+ }
+ }
+}
\ No newline at end of file
diff --git
a/integration-tests/resolver/metadata/variable-resolver/hop-pwd-literal.json
b/integration-tests/resolver/metadata/variable-resolver/hop-pwd-literal.json
new file mode 100644
index 0000000000..a96568e0a1
--- /dev/null
+++ b/integration-tests/resolver/metadata/variable-resolver/hop-pwd-literal.json
@@ -0,0 +1,11 @@
+{
+ "virtualPath": "",
+ "name": "hop-pwd-literal",
+ "description": "Decrypt Hop passwords; argument is literal Encrypted/AES2
text",
+ "variable-resolver": {
+ "HopPassword": {
+ "resolveAsVariable": false,
+ "failIfVariableNotDefined": false
+ }
+ }
+}
diff --git
a/integration-tests/resolver/metadata/variable-resolver/hop-pwd-strict.json
b/integration-tests/resolver/metadata/variable-resolver/hop-pwd-strict.json
new file mode 100644
index 0000000000..3677cdc2a6
--- /dev/null
+++ b/integration-tests/resolver/metadata/variable-resolver/hop-pwd-strict.json
@@ -0,0 +1,11 @@
+{
+ "virtualPath": "",
+ "name": "hop-pwd-strict",
+ "description": "Decrypt Hop passwords from variable names; fail if the
variable is not defined",
+ "variable-resolver": {
+ "HopPassword": {
+ "resolveAsVariable": true,
+ "failIfVariableNotDefined": true
+ }
+ }
+}
diff --git a/integration-tests/resolver/metadata/variable-resolver/hop-pwd.json
b/integration-tests/resolver/metadata/variable-resolver/hop-pwd.json
new file mode 100644
index 0000000000..60dd2fda4b
--- /dev/null
+++ b/integration-tests/resolver/metadata/variable-resolver/hop-pwd.json
@@ -0,0 +1,11 @@
+{
+ "virtualPath": "",
+ "name": "hop-pwd",
+ "description": "Decrypt Hop passwords; argument is a variable name",
+ "variable-resolver": {
+ "HopPassword": {
+ "resolveAsVariable": true,
+ "failIfVariableNotDefined": false
+ }
+ }
+}
diff --git
a/integration-tests/resolver/metadata/workflow-run-configuration/local.json
b/integration-tests/resolver/metadata/workflow-run-configuration/local.json
new file mode 100644
index 0000000000..e37a93039a
--- /dev/null
+++ b/integration-tests/resolver/metadata/workflow-run-configuration/local.json
@@ -0,0 +1,9 @@
+{
+ "engineRunConfiguration": {
+ "Local": {
+ "safe_mode": false
+ }
+ },
+ "name": "local",
+ "description": "Runs your workflows locally with the standard local Hop
workflow engine"
+}
\ No newline at end of file
diff --git a/integration-tests/resolver/project-config.json
b/integration-tests/resolver/project-config.json
new file mode 100644
index 0000000000..899c6c1092
--- /dev/null
+++ b/integration-tests/resolver/project-config.json
@@ -0,0 +1,15 @@
+{
+ "metadataBaseFolder": "${PROJECT_HOME}/metadata",
+ "unitTestsBasePath": "${PROJECT_HOME}",
+ "dataSetsCsvFolder": "${PROJECT_HOME}/datasets",
+ "enforcingExecutionInHome": true,
+ "config": {
+ "variables": [
+ {
+ "name": "HOP_LICENSE_HEADER_FILE",
+ "value": "${PROJECT_HOME}/../asf-header.txt",
+ "description": "This will automatically serialize the ASF license
header into pipelines and workflows in the integration test projects"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git
a/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/resolver/HopPasswordVariableResolver.java
b/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/resolver/HopPasswordVariableResolver.java
new file mode 100644
index 0000000000..6f628d8960
--- /dev/null
+++
b/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/resolver/HopPasswordVariableResolver.java
@@ -0,0 +1,139 @@
+/*
+ * 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.hop.passwords.resolver;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.encryption.Encr;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.gui.plugin.GuiElementType;
+import org.apache.hop.core.gui.plugin.GuiPlugin;
+import org.apache.hop.core.gui.plugin.GuiWidgetElement;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.variables.resolver.IVariableResolver;
+import org.apache.hop.core.variables.resolver.VariableResolver;
+import org.apache.hop.core.variables.resolver.VariableResolverPlugin;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+
+/**
+ * Resolves Hop-encoded (or optionally encrypted) password values for use in
places that do not call
+ * {@link Encr} themselves, such as VFS URIs (SFTP, etc.).
+ *
+ * <p>Expression format: {@code #{resolver-name:argument}}
+ *
+ * <ul>
+ * <li>With {@link #resolveAsVariable} enabled (recommended): {@code
#{hop-pwd:SFTP_PASSWORD}}
+ * looks up variable {@code SFTP_PASSWORD}, then decrypts its value.
+ * <li>With {@link #resolveAsVariable} disabled: {@code #{hop-pwd:Encrypted
abcd...}} decrypts the
+ * literal argument.
+ * </ul>
+ *
+ * <p>This is an interoperability helper for trusted operators. The default
Hop password encoder is
+ * obfuscation, not strong encryption; use AES2 or a secrets manager for real
confidentiality.
+ */
+@Getter
+@Setter
+@GuiPlugin
+@VariableResolverPlugin(
+ id = "HopPassword",
+ name = "Hop Password Variable Resolver",
+ description =
+ "Decrypt Hop-encoded passwords (Encrypted / AES2) for use in
expressions such as VFS URIs",
+ documentationUrl =
"/metadata-types/variable-resolver/hop-password-variable-resolver.html")
+public class HopPasswordVariableResolver implements IVariableResolver {
+
+ /**
+ * When true, the expression argument after the first colon is treated as a
variable name whose
+ * value is looked up and then decrypted. When false, the argument is
decrypted as a literal
+ * encoded password string.
+ */
+ @GuiWidgetElement(
+ id = "resolveAsVariable",
+ order = "01",
+ label = "i18n::HopPasswordVariableResolver.label.resolveAsVariable",
+ toolTip = "i18n::HopPasswordVariableResolver.tooltip.resolveAsVariable",
+ type = GuiElementType.CHECKBOX,
+ parentId = VariableResolver.GUI_PLUGIN_ELEMENT_PARENT_ID)
+ @HopMetadataProperty
+ private boolean resolveAsVariable = true;
+
+ /**
+ * When true and {@link #resolveAsVariable} is enabled, throw if the named
variable is not
+ * defined. When false (default), a missing variable yields a null/empty
resolution (expression
+ * left unchanged by the variable system).
+ */
+ @GuiWidgetElement(
+ id = "failIfVariableNotDefined",
+ order = "02",
+ label =
"i18n::HopPasswordVariableResolver.label.failIfVariableNotDefined",
+ toolTip =
"i18n::HopPasswordVariableResolver.tooltip.failIfVariableNotDefined",
+ type = GuiElementType.CHECKBOX,
+ parentId = VariableResolver.GUI_PLUGIN_ELEMENT_PARENT_ID)
+ @HopMetadataProperty
+ private boolean failIfVariableNotDefined = false;
+
+ @Override
+ public void init() {
+ // Nothing to initialize.
+ }
+
+ @Override
+ public String getPluginId() {
+ return "HopPassword";
+ }
+
+ @Override
+ public String getPluginName() {
+ return "Hop Password Variable Resolver";
+ }
+
+ @Override
+ public String resolve(String secretPath, IVariables variables) throws
HopException {
+ if (StringUtils.isEmpty(secretPath)) {
+ return secretPath;
+ }
+
+ String encodedValue;
+ if (resolveAsVariable) {
+ String variableName = secretPath.trim();
+ // Allow optional ${VAR} form if someone passes it literally.
+ if (variableName.startsWith("${")
+ && variableName.endsWith("}")
+ && variableName.length() > 3) {
+ variableName = variableName.substring(2, variableName.length() - 1);
+ }
+ encodedValue = variables.getVariable(variableName);
+ if (encodedValue == null) {
+ if (failIfVariableNotDefined) {
+ throw new HopException("Password variable '" + variableName + "' is
not defined");
+ }
+ return null;
+ }
+ } else {
+ encodedValue = secretPath;
+ }
+
+ if (StringUtils.isEmpty(encodedValue)) {
+ return encodedValue;
+ }
+
+ return Encr.decryptPasswordOptionallyEncrypted(encodedValue);
+ }
+}
diff --git
a/plugins/misc/passwords/src/main/resources/org/apache/hop/passwords/resolver/messages/messages_en_US.properties
b/plugins/misc/passwords/src/main/resources/org/apache/hop/passwords/resolver/messages/messages_en_US.properties
new file mode 100644
index 0000000000..32106edc49
--- /dev/null
+++
b/plugins/misc/passwords/src/main/resources/org/apache/hop/passwords/resolver/messages/messages_en_US.properties
@@ -0,0 +1,21 @@
+#
+# 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.
+#
+#
+HopPasswordVariableResolver.label.resolveAsVariable = Resolve argument as a
variable name
+HopPasswordVariableResolver.tooltip.resolveAsVariable = When enabled, the
value after the colon is treated as a variable name. That variable is looked up
and its value is decrypted. When disabled, the argument is decrypted as a
literal (for example Encrypted \u2026 or AES2 \u2026). Prefer the variable-name
form for cleaner expressions such as '#{hop-pwd:SFTP_PASSWORD}'.
+HopPasswordVariableResolver.label.failIfVariableNotDefined = Fail if variable
is not defined
+HopPasswordVariableResolver.tooltip.failIfVariableNotDefined = When enabled
together with "Resolve argument as a variable name", an undefined variable name
causes an error (pipeline/workflow fails). When disabled, a missing variable is
ignored and the original expression is left unchanged.
diff --git
a/plugins/misc/passwords/src/test/java/org/apache/hop/passwords/resolver/HopPasswordVariableResolverTest.java
b/plugins/misc/passwords/src/test/java/org/apache/hop/passwords/resolver/HopPasswordVariableResolverTest.java
new file mode 100644
index 0000000000..0bf68838bb
--- /dev/null
+++
b/plugins/misc/passwords/src/test/java/org/apache/hop/passwords/resolver/HopPasswordVariableResolverTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.hop.passwords.resolver;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hop.core.Const;
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.encryption.Encr;
+import org.apache.hop.core.encryption.HopTwoWayPasswordEncoder;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.variables.Variables;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class HopPasswordVariableResolverTest {
+
+ private HopPasswordVariableResolver resolver;
+ private Variables variables;
+
+ @BeforeAll
+ static void initEnvironment() throws Exception {
+ HopClientEnvironment.init();
+ }
+
+ @BeforeEach
+ void setUp() throws Exception {
+ // Re-bind the default Hop encoder so this class is not polluted by AES
encoder tests that
+ // share the JVM (surefire reuseForks).
+ System.setProperty(Const.HOP_PASSWORD_ENCODER_PLUGIN, "Hop");
+ Encr.init("Hop");
+ resolver = new HopPasswordVariableResolver();
+ variables = new Variables();
+ variables.initializeFrom(null);
+ }
+
+ @Test
+ void pluginMetadata() {
+ assertEquals("HopPassword", resolver.getPluginId());
+ assertEquals("Hop Password Variable Resolver", resolver.getPluginName());
+ assertTrue(resolver.isResolveAsVariable());
+ assertFalse(resolver.isFailIfVariableNotDefined());
+ }
+
+ @Test
+ void resolveLiteralEncryptedPassword() throws Exception {
+ resolver.setResolveAsVariable(false);
+ String plain = "mypass";
+ String encoded = Encr.encryptPasswordIfNotUsingVariables(plain);
+
+ assertEquals(plain, resolver.resolve(encoded, variables));
+ }
+
+ @Test
+ void resolveLiteralPlaintextPassthrough() throws Exception {
+ resolver.setResolveAsVariable(false);
+ assertEquals("already-plain", resolver.resolve("already-plain",
variables));
+ }
+
+ @Test
+ void resolveVariableNameToEncryptedValue() throws Exception {
+ resolver.setResolveAsVariable(true);
+ String plain = "sftp-secret";
+ String encoded = Encr.encryptPasswordIfNotUsingVariables(plain);
+ variables.setVariable("SFTP_PASSWORD", encoded);
+
+ assertEquals(plain, resolver.resolve("SFTP_PASSWORD", variables));
+ }
+
+ @Test
+ void resolveVariableNameWithWhitespace() throws Exception {
+ resolver.setResolveAsVariable(true);
+ String plain = "trim-me";
+ String encoded = Encr.encryptPasswordIfNotUsingVariables(plain);
+ variables.setVariable("PWD", encoded);
+
+ assertEquals(plain, resolver.resolve(" PWD ", variables));
+ }
+
+ @Test
+ void resolveMissingVariableReturnsNullByDefault() throws Exception {
+ resolver.setResolveAsVariable(true);
+ resolver.setFailIfVariableNotDefined(false);
+ assertNull(resolver.resolve("DOES_NOT_EXIST", variables));
+ }
+
+ @Test
+ void resolveMissingVariableThrowsWhenFailEnabled() {
+ resolver.setResolveAsVariable(true);
+ resolver.setFailIfVariableNotDefined(true);
+ HopException ex =
+ assertThrows(HopException.class, () ->
resolver.resolve("DOES_NOT_EXIST", variables));
+ assertTrue(ex.getMessage().contains("DOES_NOT_EXIST"));
+ }
+
+ @Test
+ void resolveOptionalDollarBraceVariableForm() throws Exception {
+ resolver.setResolveAsVariable(true);
+ String plain = "brace-form";
+ String encoded = Encr.encryptPasswordIfNotUsingVariables(plain);
+ variables.setVariable("SFTP_PASSWORD", encoded);
+
+ assertEquals(plain, resolver.resolve("${SFTP_PASSWORD}", variables));
+ }
+
+ @Test
+ void resolveEmptyArgument() throws Exception {
+ assertNull(resolver.resolve(null, variables));
+ assertEquals("", resolver.resolve("", variables));
+ }
+
+ @Test
+ void resolveUsesHopEncryptedPrefix() throws Exception {
+ resolver.setResolveAsVariable(false);
+ HopTwoWayPasswordEncoder encoder = new HopTwoWayPasswordEncoder();
+ String encoded = encoder.encode("abcd", true);
+
assertTrue(encoded.startsWith(HopTwoWayPasswordEncoder.PASSWORD_ENCRYPTED_PREFIX));
+ assertEquals("abcd", resolver.resolve(encoded, variables));
+ }
+}