Merge branch 'fix/cli-45-credential-envvar'
# Conflicts: # archreview/2026-07-12/remediation/00-tracking.md
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user