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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ 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.8] - Unreleased
## [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] - 2026-07-08

### Fixed
- #1192: Fix dependency resolution dropping incompatibilities and along with that fix a bug where
Expand Down
2 changes: 1 addition & 1 deletion module.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<Document name="ZPM.ZPM">
<Module>
<Name>ZPM</Name>
<Version>0.10.8-SNAPSHOT</Version>
<Version>0.10.9-SNAPSHOT</Version>
<ExternalName>IPM</ExternalName>
<Description>InterSystems Package Manager (IPM) provides development tools and
infrastructure for defining, building, distributing, and installing modules and
Expand Down
30 changes: 29 additions & 1 deletion src/cls/IPM/Repo/Definition.cls
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,12 @@ ClassMethod Configure(
set tInstance.ReadOnly = 0
}

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."))
}
if $data(pModifiers("password-stdin")) {
// prompt for password using secret input terminal mode
set ioDevice = $io
Expand All @@ -199,8 +205,30 @@ 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.")
}

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) {
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
} elseif $data(pModifiers("token")) {
write !, $$$FormattedLine($$$Yellow, "WARNING: Using -token via the CLI is insecure. Use -token-env.")
}

$$$ThrowOnError(..OnConfigure(tInstance,pInteractive,.pModifiers,.pData))
Expand Down
8 changes: 5 additions & 3 deletions src/cls/IPM/Repo/Http/Definition.cls
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ XData Commands [ XMLNamespace = "http://www.intersystems.com/PackageManager/CLI"
<modifier name="username" aliases="user" value="true" description="For HTTP(s) repositories, specifies the username to use when connecting." />
<modifier name="password" aliases="pass" value="true" description="For HTTP(s) repositories, specifies the password to use when connecting." />
<modifier name="password-stdin" aliases="pass-stdin" description="For HTTP(s) repositories, specifies the password to use when connecting, but prompt user in secret input terminal mode." />
<modifier name="password-env" aliases="pass-env" value="true" description="For HTTP(s) repositories, reads the password from the named environment variable." />
<modifier name="token" value="true" description="For HTTP(s) repositories, specifies the key to use when connecting." />
<modifier name="token-env" value="true" description="For HTTP(s) repositories, reads the token from the named environment variable." />
<modifier name="token-auth-method" value="true" description="Enum of { basic, bearer, apiKey }. Defaults to apiKey. If set to 'basic', token will be included as 'Authorization: Basic &lt;token&gt;'. If set to 'bearer', token will be included as 'Authorization: Bearer &lt;token&gt;'. If set to 'apiKey', token will be included as 'apiKey: &lt;token&gt;'" />
<modifier name="sslconfig" aliases="ssl" value="true" description="For HTTP(s) repositories, specifies the SSL configuration name to use when connecting." />
</group>
Expand All @@ -52,9 +54,9 @@ XData Commands [ XMLNamespace = "http://www.intersystems.com/PackageManager/CLI"
/// This callback method is invoked by the <METHOD>%Save</METHOD> method to
/// provide notification that the object is being saved. It is called before
/// any data is written to disk.
///
///
/// <P><VAR>insert</VAR> will be set to 1 if this object is being saved for the first time.
///
///
/// <P>If this method returns an error then the call to <METHOD>%Save</METHOD> will fail.
Method %OnBeforeSave(insert As %Boolean) As %Status [ Private, ServerOnly = 1 ]
{
Expand All @@ -66,7 +68,7 @@ Method %OnBeforeSave(insert As %Boolean) As %Status [ Private, ServerOnly = 1 ]

/// This callback method is invoked by the <METHOD>%ValidateObject</METHOD> method to
/// provide notification that the current object is being validated.
///
///
/// <P>If this method returns an error then <METHOD>%ValidateObject</METHOD> will fail.
Method %OnValidateObject() As %Status [ Private, ServerOnly = 1 ]
{
Expand Down
60 changes: 60 additions & 0 deletions tests/unit_tests/Test/PM/Unit/CLI.cls
Original file line number Diff line number Diff line change
Expand Up @@ -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)=
Expand Down Expand Up @@ -221,6 +229,58 @@ 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")

// 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
// 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")

// 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", "")

// 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()
{
do ..CompareModifiers("-export-deps 1", "reload","test-module")
Expand Down
Loading