Merge branch 'fix/cli-45-credential-envvar'
# Conflicts: # archreview/2026-07-12/remediation/00-tracking.md
This commit is contained in:
@@ -172,8 +172,13 @@ session.write(serverHandle, itemHandle, value, userId);
|
||||
native failure is surfaced, not papered over.
|
||||
|
||||
The CLI exposes `advise-supervisory`, `write-secured`, and `authenticate-user`
|
||||
(credential via `--password` or `--password-env`, never echoed), and `write` /
|
||||
`write2` take `--user-id`.
|
||||
(credential via `--password` or the variable named by `--password-env`, default
|
||||
`MXGATEWAY_VERIFY_PASSWORD`, never echoed), and `write` / `write2` take
|
||||
`--user-id`. The credential is required: a missing or empty resolved value is a
|
||||
picocli usage error naming the option and the variable, so the CLI fails before
|
||||
connecting 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
|
||||
|
||||
|
||||
+21
-4
@@ -70,6 +70,7 @@ import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Mixin;
|
||||
import picocli.CommandLine.Model.CommandSpec;
|
||||
import picocli.CommandLine.Option;
|
||||
import picocli.CommandLine.ParameterException;
|
||||
import picocli.CommandLine.Spec;
|
||||
|
||||
/**
|
||||
@@ -182,6 +183,13 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
/** Sentinel written to stdout after every command result in batch mode. */
|
||||
static final String BATCH_EOR = "__MXGW_BATCH_EOR__";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
static final String DEFAULT_VERIFY_PASSWORD_ENV = "MXGATEWAY_VERIFY_PASSWORD";
|
||||
|
||||
/** Sentinel queued by {@code stream-alarms} to mark a clean end of the alarm feed. */
|
||||
private static final Object ALARM_FEED_END = new Object();
|
||||
|
||||
@@ -1139,7 +1147,7 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
|
||||
@Option(
|
||||
names = "--password-env",
|
||||
defaultValue = "MXGATEWAY_VERIFY_PASSWORD",
|
||||
defaultValue = DEFAULT_VERIFY_PASSWORD_ENV,
|
||||
description = "Environment variable holding the password when --password is omitted.")
|
||||
String passwordEnv;
|
||||
|
||||
@@ -1151,11 +1159,20 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
public Integer call() {
|
||||
// Resolve the credential from the flag or environment. It flows only
|
||||
// into the request; it is never written to output, logs, or errors.
|
||||
String environmentName =
|
||||
passwordEnv == null || passwordEnv.isBlank() ? DEFAULT_VERIFY_PASSWORD_ENV : passwordEnv;
|
||||
String resolvedPassword = password == null || password.isBlank()
|
||||
? System.getenv(passwordEnv)
|
||||
? System.getenv(environmentName)
|
||||
: password;
|
||||
if (resolvedPassword == null) {
|
||||
resolvedPassword = "";
|
||||
if (resolvedPassword == null || resolvedPassword.isBlank()) {
|
||||
// Fail fast instead of dialing: a misconfigured environment must not
|
||||
// become a real MXAccess authentication attempt with an empty
|
||||
// credential (CLI-45). The message names the option and the variable
|
||||
// only — never the value.
|
||||
throw new ParameterException(
|
||||
common.spec.commandLine(),
|
||||
"a password is required via --password or the " + environmentName
|
||||
+ " environment variable");
|
||||
}
|
||||
try (MxGatewayCliClient client = clientFactory.connect(common.resolved())) {
|
||||
int userId = client.session(sessionId)
|
||||
|
||||
+70
@@ -2,7 +2,10 @@ package com.zb.mom.ww.mxgateway.cli;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
||||
|
||||
import com.zb.mom.ww.mxgateway.client.MxGatewayAlarmFeedSubscription;
|
||||
import com.zb.mom.ww.mxgateway.client.MxGatewayClientOptions;
|
||||
@@ -211,6 +214,73 @@ final class MxGatewayCliTests {
|
||||
assertFalse(run.errors().contains("super-secret-pw"), "password must never be echoed to stderr");
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI-45: an unresolved credential must abort with a picocli usage error
|
||||
* before the CLI dials, instead of authenticating with an empty password.
|
||||
* The message names the option and the variable, never a value.
|
||||
*/
|
||||
@Test
|
||||
void authenticateUserRejectsMissingCredentialWithUsageError() {
|
||||
FakeClientFactory factory = new FakeClientFactory();
|
||||
CliRun run = execute(
|
||||
factory,
|
||||
"authenticate-user",
|
||||
"--session-id", "session-cli",
|
||||
"--server-handle", "3",
|
||||
"--verify-user", "operator",
|
||||
"--password-env", "MXGW_CLI45_ABSENT_PASSWORD_VAR",
|
||||
"--json");
|
||||
|
||||
assertNotEquals(0, run.exitCode(), "a missing credential must fail");
|
||||
assertTrue(run.errors().contains("--password"), run.errors());
|
||||
assertTrue(run.errors().contains("MXGW_CLI45_ABSENT_PASSWORD_VAR"), run.errors());
|
||||
assertNull(factory.client, "the CLI must not connect without a credential");
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI-45: a blank {@code --password} is treated as missing — the CLI never
|
||||
* sends a fabricated empty credential to the wire.
|
||||
*/
|
||||
@Test
|
||||
void authenticateUserRejectsBlankPasswordValue() {
|
||||
FakeClientFactory factory = new FakeClientFactory();
|
||||
CliRun run = execute(
|
||||
factory,
|
||||
"authenticate-user",
|
||||
"--session-id", "session-cli",
|
||||
"--server-handle", "3",
|
||||
"--verify-user", "operator",
|
||||
"--password", "",
|
||||
"--password-env", "MXGW_CLI45_ABSENT_PASSWORD_VAR",
|
||||
"--json");
|
||||
|
||||
assertNotEquals(0, run.exitCode(), "a blank credential must fail");
|
||||
assertNull(factory.client, "the CLI must not connect without a credential");
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI-45: {@code --password-env} defaults to the canonical
|
||||
* {@code MXGATEWAY_VERIFY_PASSWORD}, so the usage error names it when no
|
||||
* explicit variable is given. Skipped if the canonical variable happens to be
|
||||
* exported in the running environment (which would satisfy the credential).
|
||||
*/
|
||||
@Test
|
||||
void authenticateUserDefaultsToCanonicalPasswordEnvName() {
|
||||
assumeTrue(System.getenv(MxGatewayCli.DEFAULT_VERIFY_PASSWORD_ENV) == null);
|
||||
|
||||
FakeClientFactory factory = new FakeClientFactory();
|
||||
CliRun run = execute(
|
||||
factory,
|
||||
"authenticate-user",
|
||||
"--session-id", "session-cli",
|
||||
"--server-handle", "3",
|
||||
"--verify-user", "operator",
|
||||
"--json");
|
||||
|
||||
assertNotEquals(0, run.exitCode());
|
||||
assertTrue(run.errors().contains("MXGATEWAY_VERIFY_PASSWORD"), run.errors());
|
||||
}
|
||||
|
||||
// ---- ping subcommand (D4) ----
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user