Merge branch 'fix/cli-45-credential-envvar'
ci / java (push) Successful in 2m51s
ci / windows-x86 (push) Successful in 1m21s
ci / nightly-windev (push) Has been skipped
ci / portable (push) Successful in 9m41s

# Conflicts:
#	archreview/2026-07-12/remediation/00-tracking.md
This commit is contained in:
Joseph Doherty
2026-08-07 06:09:03 -04:00
26 changed files with 773 additions and 53 deletions
+5 -1
View File
@@ -183,7 +183,11 @@ parity holds: a `WriteSecured` issued without a matching prior `AuthenticateUser
and supervisory advise fails natively, and that failure is surfaced unchanged
rather than pre-empted. The CLI exposes `authenticate-user` (credential via
`-password-env`, default `MXGATEWAY_VERIFY_PASSWORD`, or `-password`) and
`write-secured`.
`write-secured`. The credential is required: a missing or empty resolved value is
a usage error naming the flag and the variable, so the CLI fails before dialing
instead of authenticating with an empty password. `MXGATEWAY_VERIFY_PASSWORD` is
the canonical variable across all five client CLIs — see
[Cross-Language Smoke Matrix](../../docs/CrossLanguageSmokeMatrix.md).
### Array writes replace the whole array
+18 -3
View File
@@ -446,6 +446,11 @@ func runWriteSecured(ctx context.Context, args []string, stdout, stderr io.Write
return writeCommandOutput(stdout, *jsonOutput, "write-secured", options, reply, err)
}
// defaultVerifyPasswordEnv is the canonical CLI credential environment variable,
// shared by every official client CLI (CLI-45) so one exported variable drives
// the same operator workflow in all five languages.
const defaultVerifyPasswordEnv = "MXGATEWAY_VERIFY_PASSWORD"
func runAuthenticateUser(ctx context.Context, args []string, stdout, stderr io.Writer) error {
flags := flag.NewFlagSet("authenticate-user", flag.ContinueOnError)
flags.SetOutput(stderr)
@@ -458,7 +463,7 @@ func runAuthenticateUser(ctx context.Context, args []string, stdout, stderr io.W
// prefer the environment variable so it stays out of shell history and the
// process table. The -password flag remains for non-interactive scripting.
password := flags.String("password", "", "verify-user password (prefer -password-env)")
passwordEnv := flags.String("password-env", "MXGATEWAY_VERIFY_PASSWORD", "environment variable containing the verify-user password")
passwordEnv := flags.String("password-env", defaultVerifyPasswordEnv, "environment variable containing the verify-user password")
if err := flags.Parse(args); err != nil {
return err
@@ -471,8 +476,18 @@ func runAuthenticateUser(ctx context.Context, args []string, stdout, stderr io.W
}
resolvedPassword := *password
if resolvedPassword == "" && *passwordEnv != "" {
resolvedPassword = os.Getenv(*passwordEnv)
envName := *passwordEnv
if envName == "" {
envName = defaultVerifyPasswordEnv
}
if resolvedPassword == "" {
resolvedPassword = os.Getenv(envName)
}
// Fail fast rather than dialing: an unset or empty variable must not become a
// real MXAccess authentication attempt with an empty credential. The message
// names only the flag and the variable — never the resolved value.
if resolvedPassword == "" {
return fmt.Errorf("a password is required via -password or the %s environment variable", envName)
}
client, options, err := dialForCommand(ctx, common)
+63
View File
@@ -598,6 +598,69 @@ func TestRunAuthenticateUserRequiresVerifyUser(t *testing.T) {
}
}
// TestRunAuthenticateUserRejectsEmptyPassword pins the CLI-45 fail-fast contract:
// an unresolved credential must abort before dialing rather than authenticating
// with an empty password, and the usage error must name both -password and the
// canonical environment variable without echoing any value.
func TestRunAuthenticateUserRejectsEmptyPassword(t *testing.T) {
t.Setenv("MXGATEWAY_VERIFY_PASSWORD", "")
var stdout, stderr bytes.Buffer
err := runWithIO(t.Context(), []string{
"authenticate-user",
"-session-id", "s1",
"-verify-user", "operator",
"-plaintext",
"-api-key", "test",
}, &stdout, &stderr)
if err == nil {
t.Fatal("authenticate-user without a credential must fail before dialing")
}
if !strings.Contains(err.Error(), "-password") {
t.Fatalf("error must name the -password flag: %v", err)
}
if !strings.Contains(err.Error(), "MXGATEWAY_VERIFY_PASSWORD") {
t.Fatalf("error must name the canonical environment variable: %v", err)
}
}
// TestRunAuthenticateUserReadsPasswordFromCanonicalEnv pins that the default
// -password-env is MXGATEWAY_VERIFY_PASSWORD: with it set the credential guard
// passes and the command proceeds past it to the dial, which fails against an
// unused port under a short context — proving the guard was cleared without
// needing a live gateway.
func TestRunAuthenticateUserReadsPasswordFromCanonicalEnv(t *testing.T) {
t.Setenv("MXGATEWAY_VERIFY_PASSWORD", "env-sourced-credential")
ctx, cancel := context.WithTimeout(t.Context(), 200*time.Millisecond)
defer cancel()
var stdout, stderr bytes.Buffer
err := runWithIO(ctx, []string{
"authenticate-user",
"-session-id", "s1",
"-verify-user", "operator",
"-endpoint", "127.0.0.1:1",
"-plaintext",
"-api-key", "test",
"-call-timeout", "1s",
}, &stdout, &stderr)
if err == nil {
t.Fatal("expected the dial/RPC to fail against an unused port")
}
if strings.Contains(err.Error(), "flag provided but not defined") {
t.Fatalf("test invoked an unknown flag, so it never reached the guard: %v", err)
}
if strings.Contains(err.Error(), "a password is required") {
t.Fatalf("credential guard must be satisfied from %s: %v", "MXGATEWAY_VERIFY_PASSWORD", err)
}
if strings.Contains(err.Error(), "env-sourced-credential") ||
strings.Contains(stdout.String(), "env-sourced-credential") ||
strings.Contains(stderr.String(), "env-sourced-credential") {
t.Fatal("the resolved credential must never be echoed")
}
}
// TestRunWriteBulkVariantRejectsMismatchedHandlesAndValues pins the len-mismatch
// guard so a write-bulk with unequal item-handles / values counts fails fast
// before any dial.