From fbd7f5872d833b0f5dac8797b28af4a8ae29b594 Mon Sep 17 00:00:00 2001 From: isc-dchui Date: Wed, 8 Jul 2026 10:01:59 -0400 Subject: [PATCH 1/4] Add option to read repo password and token from environment variables --- CHANGELOG.md | 5 +++ src/cls/IPM/Repo/Definition.cls | 18 ++++++++++- src/cls/IPM/Repo/Http/Definition.cls | 8 +++-- tests/unit_tests/Test/PM/Unit/CLI.cls | 44 +++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3398d631..96ef81fb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.10.9] - Unreleased + +### Added +- #1178: Add `-password-env` and `-token-env` modifiers to the `repo` command to read the password/token from a named environment variable (secure alternatives to `-password` and `token`). + ## [0.10.8] - Unreleased ### Fixed diff --git a/src/cls/IPM/Repo/Definition.cls b/src/cls/IPM/Repo/Definition.cls index e55c5a830..c23aaeee5 100644 --- a/src/cls/IPM/Repo/Definition.cls +++ b/src/cls/IPM/Repo/Definition.cls @@ -199,8 +199,24 @@ ClassMethod Configure( // Restore normal echo mode — must happen even on Ctrl+C or error open ioDevice:(:"") $$$ThrowOnError(readSC) + } elseif $data(pModifiers("password-env"), passwordEnvVar) { + set pwd = $system.Util.GetEnviron(passwordEnvVar) + if (pwd = "") { + set msg = "Environment variable '"_passwordEnvVar_"' (from -password-env) is not set or is empty." + $$$ThrowStatus($$$ERROR($$$GeneralError, msg)) + } + set pModifiers("password") = pwd } elseif $data(pModifiers("password")) { - write !, $$$FormattedLine($$$Yellow, "WARNING: Using --password via the CLI is insecure. Use --password-stdin.") + write !, $$$FormattedLine($$$Yellow, "WARNING: Using --password via the CLI is insecure. Use --password-stdin or --password-env.") + } + + if $data(pModifiers("token-env"), tokenEnvVar) { + set tkn = $system.Util.GetEnviron(tokenEnvVar) + if (tkn = "") { + set msg = "Environment variable '"_tokenEnvVar_"' (from -token-env) is not set or is empty." + $$$ThrowStatus($$$ERROR($$$GeneralError, msg)) + } + set pModifiers("token") = tkn } $$$ThrowOnError(..OnConfigure(tInstance,pInteractive,.pModifiers,.pData)) diff --git a/src/cls/IPM/Repo/Http/Definition.cls b/src/cls/IPM/Repo/Http/Definition.cls index da6a0d390..5c44d61ff 100644 --- a/src/cls/IPM/Repo/Http/Definition.cls +++ b/src/cls/IPM/Repo/Http/Definition.cls @@ -41,7 +41,9 @@ XData Commands [ XMLNamespace = "http://www.intersystems.com/PackageManager/CLI" + + @@ -52,9 +54,9 @@ XData Commands [ XMLNamespace = "http://www.intersystems.com/PackageManager/CLI" /// This callback method is invoked by the %Save method to /// provide notification that the object is being saved. It is called before /// any data is written to disk. -/// +/// ///

insert will be set to 1 if this object is being saved for the first time. -/// +/// ///

If this method returns an error then the call to %Save will fail. Method %OnBeforeSave(insert As %Boolean) As %Status [ Private, ServerOnly = 1 ] { @@ -66,7 +68,7 @@ Method %OnBeforeSave(insert As %Boolean) As %Status [ Private, ServerOnly = 1 ] /// This callback method is invoked by the %ValidateObject method to /// provide notification that the current object is being validated. -/// +/// ///

If this method returns an error then %ValidateObject will fail. Method %OnValidateObject() As %Status [ Private, ServerOnly = 1 ] { diff --git a/tests/unit_tests/Test/PM/Unit/CLI.cls b/tests/unit_tests/Test/PM/Unit/CLI.cls index 563ab134c..9941d74f2 100644 --- a/tests/unit_tests/Test/PM/Unit/CLI.cls +++ b/tests/unit_tests/Test/PM/Unit/CLI.cls @@ -74,6 +74,14 @@ Method TestParser() set tResults(tCommands,"modifiers","username")="user" set tResults(tCommands,"modifiers","password")="pass" + set tCommands($increment(tCommands)) = "repo -r -n registry -url http://x/ -password-env MY_PW -token-env MY_TOK" + set tResults(tCommands)="repo" + set tResults(tCommands,"modifiers","name")="registry" + set tResults(tCommands,"modifiers","remote")="" + set tResults(tCommands,"modifiers","url")="http://x/" + set tResults(tCommands,"modifiers","password-env")="MY_PW" + set tResults(tCommands,"modifiers","token-env")="MY_TOK" + /* Set tCommands($i(tCommands)) = Set tResults(tCommands)= @@ -221,6 +229,42 @@ Method TestRepository() do ..RunCommand("repo -r -name registry -reset-defaults") } +Method TestRepositoryEnvVars() +{ + set os = ##class(%SYS.Python).Import("os") + + // Test positive path: password-env with valid env var + do os.putenv("IPM_TEST_PW", "secret") + do ..AssertNoException("repo -r -n registry-pw-test -url http://127.0.0.1:9999/ -username u -password-env IPM_TEST_PW") + + set repoDef = ##class(%IPM.Repo.Remote.Definition).ServerDefinitionKeyOpen("registry-pw-test") + do $$$AssertTrue($isobject(repoDef), "Repository registry-pw-test should exist") + do $$$AssertEquals(repoDef.Password, "secret", "Password should be set from env var") + + // Cleanup + do ..RunCommand("repo -n registry-pw-test -delete") + do os.putenv("IPM_TEST_PW", "") + + // Test negative path: password-env with unset env var + set sc = ..RunCommand("repo -r -n registry-pw-fail -url http://127.0.0.1:9999/ -username u -password-env IPM_TEST_PW") + do $$$AssertStatusNotOK(sc, "Command should fail when env var is not set") + + // Cleanup failed repo attempt if it somehow got created + do ..RunCommand("repo -n registry-pw-fail -delete") + + // Test token-env with valid env var + do os.putenv("IPM_TEST_TOK", "mytoken123") + do ..AssertNoException("repo -r -n registry-tok-test -url http://127.0.0.1:9999/ -token-env IPM_TEST_TOK") + + set repoDef = ##class(%IPM.Repo.Remote.Definition).ServerDefinitionKeyOpen("registry-tok-test") + do $$$AssertTrue($isobject(repoDef), "Repository registry-tok-test should exist") + do $$$AssertEquals(repoDef.Token, "mytoken123", "Token should be set from env var") + + // Cleanup + do ..RunCommand("repo -n registry-tok-test -delete") + do os.putenv("IPM_TEST_TOK", "") +} + Method TestModifiers() { do ..CompareModifiers("-export-deps 1", "reload","test-module") From 26265416123ddbe777ac87fc4128537e10b2c919 Mon Sep 17 00:00:00 2001 From: isc-dchui Date: Wed, 8 Jul 2026 10:29:55 -0400 Subject: [PATCH 2/4] Add some extra conflict checking --- CHANGELOG.md | 2 +- src/cls/IPM/Repo/Definition.cls | 9 +++++++++ tests/unit_tests/Test/PM/Unit/CLI.cls | 17 ++++++++++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96ef81fb0..49f4b372b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.10.9] - Unreleased ### Added -- #1178: Add `-password-env` and `-token-env` modifiers to the `repo` command to read the password/token from a named environment variable (secure alternatives to `-password` and `token`). +- #1178: Add `-password-env` and `-token-env` modifiers to the `repo` command to read the password/token from a named environment variable (secure alternatives to `-password` and `-token`). ## [0.10.8] - Unreleased diff --git a/src/cls/IPM/Repo/Definition.cls b/src/cls/IPM/Repo/Definition.cls index c23aaeee5..60fc46bb3 100644 --- a/src/cls/IPM/Repo/Definition.cls +++ b/src/cls/IPM/Repo/Definition.cls @@ -184,6 +184,10 @@ ClassMethod Configure( set tInstance.ReadOnly = 0 } + set passwordFlagsSet = $data(pModifiers("password-stdin")) + $data(pModifiers("password-env")) + $data(pModifiers("password")) + if (passwordFlagsSet > 1) { + $$$ThrowStatus($$$ERROR($$$GeneralError, "Only one of -password, -password-stdin, or -password-env may be specified.")) + } if $data(pModifiers("password-stdin")) { // prompt for password using secret input terminal mode set ioDevice = $io @@ -210,6 +214,9 @@ ClassMethod Configure( write !, $$$FormattedLine($$$Yellow, "WARNING: Using --password via the CLI is insecure. Use --password-stdin or --password-env.") } + if ($data(pModifiers("token-env")) && $data(pModifiers("token"))) { + $$$ThrowStatus($$$ERROR($$$GeneralError, "Only one of -token or -token-env may be specified.")) + } if $data(pModifiers("token-env"), tokenEnvVar) { set tkn = $system.Util.GetEnviron(tokenEnvVar) if (tkn = "") { @@ -217,6 +224,8 @@ ClassMethod Configure( $$$ThrowStatus($$$ERROR($$$GeneralError, msg)) } set pModifiers("token") = tkn + } elseif $data(pModifiers("token")) { + write !, $$$FormattedLine($$$Yellow, "WARNING: Using --token via the CLI is insecure. Use --token-env.") } $$$ThrowOnError(..OnConfigure(tInstance,pInteractive,.pModifiers,.pData)) diff --git a/tests/unit_tests/Test/PM/Unit/CLI.cls b/tests/unit_tests/Test/PM/Unit/CLI.cls index 9941d74f2..17181131c 100644 --- a/tests/unit_tests/Test/PM/Unit/CLI.cls +++ b/tests/unit_tests/Test/PM/Unit/CLI.cls @@ -249,9 +249,14 @@ Method TestRepositoryEnvVars() set sc = ..RunCommand("repo -r -n registry-pw-fail -url http://127.0.0.1:9999/ -username u -password-env IPM_TEST_PW") do $$$AssertStatusNotOK(sc, "Command should fail when env var is not set") - // Cleanup failed repo attempt if it somehow got created + // Defensive cleanup in case the repo was somehow created before the failure do ..RunCommand("repo -n registry-pw-fail -delete") + // Test negative path: password-env conflict with password + set sc = ..RunCommand("repo -r -n registry-pw-conflict -url http://127.0.0.1:9999/ -username u -password-env IPM_TEST_PW -password plaintext") + do $$$AssertStatusNotOK(sc, "Command should fail when both -password-env and -password are specified") + do ..RunCommand("repo -n registry-pw-conflict -delete") + // Test token-env with valid env var do os.putenv("IPM_TEST_TOK", "mytoken123") do ..AssertNoException("repo -r -n registry-tok-test -url http://127.0.0.1:9999/ -token-env IPM_TEST_TOK") @@ -263,6 +268,16 @@ Method TestRepositoryEnvVars() // Cleanup do ..RunCommand("repo -n registry-tok-test -delete") do os.putenv("IPM_TEST_TOK", "") + + // Test negative path: token-env with unset env var + set sc = ..RunCommand("repo -r -n registry-tok-fail -url http://127.0.0.1:9999/ -token-env IPM_TEST_TOK") + do $$$AssertStatusNotOK(sc, "Command should fail when token env var is not set") + do ..RunCommand("repo -n registry-tok-fail -delete") + + // Test negative path: token-env conflict with token + set sc = ..RunCommand("repo -r -n registry-tok-conflict -url http://127.0.0.1:9999/ -token-env IPM_TEST_TOK -token hardcoded") + do $$$AssertStatusNotOK(sc, "Command should fail when both -token-env and -token are specified") + do ..RunCommand("repo -n registry-tok-conflict -delete") } Method TestModifiers() From 99f33ba7b7f41072b432aa7fd672db2b89176a90 Mon Sep 17 00:00:00 2001 From: isc-dchui Date: Wed, 8 Jul 2026 10:53:32 -0400 Subject: [PATCH 3/4] Refactor minor issues --- src/cls/IPM/Repo/Definition.cls | 11 +++++++---- tests/unit_tests/Test/PM/Unit/CLI.cls | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/cls/IPM/Repo/Definition.cls b/src/cls/IPM/Repo/Definition.cls index 60fc46bb3..b57933c60 100644 --- a/src/cls/IPM/Repo/Definition.cls +++ b/src/cls/IPM/Repo/Definition.cls @@ -184,7 +184,9 @@ ClassMethod Configure( set tInstance.ReadOnly = 0 } - set passwordFlagsSet = $data(pModifiers("password-stdin")) + $data(pModifiers("password-env")) + $data(pModifiers("password")) + set passwordFlagsSet = ($data(pModifiers("password-stdin"))'=0) + + ($data(pModifiers("password-env"))'=0) + + ($data(pModifiers("password"))'=0) if (passwordFlagsSet > 1) { $$$ThrowStatus($$$ERROR($$$GeneralError, "Only one of -password, -password-stdin, or -password-env may be specified.")) } @@ -211,10 +213,11 @@ ClassMethod Configure( } set pModifiers("password") = pwd } elseif $data(pModifiers("password")) { - write !, $$$FormattedLine($$$Yellow, "WARNING: Using --password via the CLI is insecure. Use --password-stdin or --password-env.") + write !, $$$FormattedLine($$$Yellow, "WARNING: Using -password via the CLI is insecure. Use -password-stdin or -password-env.") } - if ($data(pModifiers("token-env")) && $data(pModifiers("token"))) { + set tokenFlagsSet = ($data(pModifiers("token-env"))'=0) + ($data(pModifiers("token"))'=0) + if (tokenFlagsSet > 1) { $$$ThrowStatus($$$ERROR($$$GeneralError, "Only one of -token or -token-env may be specified.")) } if $data(pModifiers("token-env"), tokenEnvVar) { @@ -225,7 +228,7 @@ ClassMethod Configure( } set pModifiers("token") = tkn } elseif $data(pModifiers("token")) { - write !, $$$FormattedLine($$$Yellow, "WARNING: Using --token via the CLI is insecure. Use --token-env.") + write !, $$$FormattedLine($$$Yellow, "WARNING: Using -token via the CLI is insecure. Use -token-env.") } $$$ThrowOnError(..OnConfigure(tInstance,pInteractive,.pModifiers,.pData)) diff --git a/tests/unit_tests/Test/PM/Unit/CLI.cls b/tests/unit_tests/Test/PM/Unit/CLI.cls index 17181131c..04f45ed38 100644 --- a/tests/unit_tests/Test/PM/Unit/CLI.cls +++ b/tests/unit_tests/Test/PM/Unit/CLI.cls @@ -253,6 +253,7 @@ Method TestRepositoryEnvVars() do ..RunCommand("repo -n registry-pw-fail -delete") // Test negative path: password-env conflict with password + // Note: -password-stdin conflict is not tested here because it requires interactive terminal input set sc = ..RunCommand("repo -r -n registry-pw-conflict -url http://127.0.0.1:9999/ -username u -password-env IPM_TEST_PW -password plaintext") do $$$AssertStatusNotOK(sc, "Command should fail when both -password-env and -password are specified") do ..RunCommand("repo -n registry-pw-conflict -delete") From b040b9d23ff4c8e8116a8c7a540c9d4916e39a29 Mon Sep 17 00:00:00 2001 From: isc-dchui Date: Wed, 8 Jul 2026 14:55:14 -0400 Subject: [PATCH 4/4] Update to new IPM version --- CHANGELOG.md | 2 +- module.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49f4b372b..b5c4c8a7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - #1178: Add `-password-env` and `-token-env` modifiers to the `repo` command to read the password/token from a named environment variable (secure alternatives to `-password` and `-token`). -## [0.10.8] - Unreleased +## [0.10.8] - 2026-07-08 ### Fixed - #1192: Fix dependency resolution dropping incompatibilities and along with that fix a bug where diff --git a/module.xml b/module.xml index 0a82daf0b..b0c901f31 100644 --- a/module.xml +++ b/module.xml @@ -3,7 +3,7 @@ ZPM - 0.10.8-SNAPSHOT + 0.10.9-SNAPSHOT IPM InterSystems Package Manager (IPM) provides development tools and infrastructure for defining, building, distributing, and installing modules and