From ecc7dcac1c4de51a3907987c518b1506dda3c16f Mon Sep 17 00:00:00 2001 From: msweier Date: Mon, 21 Sep 2026 15:41:00 -0500 Subject: [PATCH 1/7] clarify use of cwms-cli env activate --- cwmscli/commands/env.py | 15 +++++++++++---- docs/cli/env.rst | 29 +++++++++++++++++++++++++++++ tests/commands/test_env.py | 13 +++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/cwmscli/commands/env.py b/cwmscli/commands/env.py index 477e2a2..f90e6a1 100644 --- a/cwmscli/commands/env.py +++ b/cwmscli/commands/env.py @@ -373,7 +373,7 @@ def spawn_shell_with_env(env_vars: Dict[str, str], env_name: str): sys.exit(1) -@env_group.command("activate", help="Activate an environment in a new shell") +@env_group.command("activate", short_help="Activate an environment in a new shell") @click.argument("env_name") def activate_cmd(env_name: str): """ @@ -382,10 +382,17 @@ def activate_cmd(env_name: str): The environment variables will be set in the new shell and persist until you exit the shell. Type 'exit' to return to your original environment. - Note: This spawns a child shell. Your parent shell, and any IDE - already open, will not see these variables. To populate the current - shell, use: eval "$(cwms-cli env export --format bash)" + Note: This spawns a child shell. Your parent shell and any IDE + already open will not see these variables. Shell startup files can + also replace inherited values such as CDA_API_ROOT. This is common + in Solaris profiles. + To set the values after shell initialization, use: + + \b + eval "$(cwms-cli env export --format bash)" + + \b Examples: cwms-cli env activate prod cwms-cli env activate localhost diff --git a/docs/cli/env.rst b/docs/cli/env.rst index 70d8641..fc46770 100644 --- a/docs/cli/env.rst +++ b/docs/cli/env.rst @@ -238,6 +238,35 @@ Activate an environment in a new shell session. This spawns a child shell with the environment variables set. Type ``exit`` or press ``Ctrl+D`` to return to your original shell. +.. warning:: + + ``activate`` passes the configured variables to the child shell before that + shell initializes. Shell startup files such as ``.bash_profile`` or + ``.bashrc`` can then replace inherited values. For example, if a startup + file unconditionally exports ``CDA_API_ROOT``, ``CDA_API_KEY``, ``OFFICE``, + or ``ENVIRONMENT``, that value takes precedence over the selected + cwms-cli environment. This is a common configuration on Solaris systems, + but the same limitation applies on any platform. + + To ensure the selected values take precedence, load them into the current + bash or zsh session after shell initialization: + + .. code-block:: bash + + eval "$(cwms-cli env export --format bash)" + + This changes the current shell rather than creating a child shell. The + values remain set until they are changed, unset, or the current shell exits. + +If startup files provide defaults that should not replace an activated +environment, make those assignments conditional: + +.. code-block:: bash + + [ -z "${CDA_API_ROOT:-}" ] && \ + export CDA_API_ROOT="https://default.example/cwms-data" + [ -z "${OFFICE:-}" ] && export OFFICE="SWT" + .. note:: The parent shell and any already-open IDE will **not** see these variables. diff --git a/tests/commands/test_env.py b/tests/commands/test_env.py index 8e16928..2006a7d 100644 --- a/tests/commands/test_env.py +++ b/tests/commands/test_env.py @@ -459,6 +459,19 @@ def test_delete_missing_env_errors(isolated_envs): assert "not found" in result.output +# ---------- env activate ---------- + + +def test_activate_help_explains_shell_startup_precedence(): + runner = CliRunner() + result = runner.invoke(env_group, ["activate", "--help"]) + + assert result.exit_code == 0 + assert "Shell startup files can" in result.output + assert "Solaris profiles" in result.output + assert "cwms-cli env export --format bash" in result.output + + # ---------- quoting helpers ---------- From 02edf9523589856064a700595bf2861d5a039373 Mon Sep 17 00:00:00 2001 From: msweier Date: Wed, 23 Sep 2026 06:29:04 -0500 Subject: [PATCH 2/7] add instructions to update bashrc --- docs/cli/env.rst | 58 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/docs/cli/env.rst b/docs/cli/env.rst index fc46770..9af4d8c 100644 --- a/docs/cli/env.rst +++ b/docs/cli/env.rst @@ -137,6 +137,55 @@ Create or update an environment configuration. letting you attach an office and API key. All other environment names require ``--api-root``. +On-premises and COOP default environments +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Configure named environments for the primary on-premises server and the COOP +server with their respective CDA API roots, API keys, and offices: + +.. code-block:: bash + + cwms-cli env setup onprem \ + --api-root https://example.usace.army.mil/XXX-data/ \ + --api-key YOUR_KEY \ + --office XXX + + cwms-cli env setup coop \ + --api-root https://example.coop.usace.army.mil/XXX-data/ \ + --api-key YOUR_KEY \ + --office XXX + +On the primary on-premises server, place the following at the **bottom of +your** ``~/.bashrc`` **file** to load ``onprem`` by default: + +.. code-block:: bash + + # Load on-premises defaults unless `cwms-cli env activate` supplied another environment. + if [ -z "${ENVIRONMENT:-}" ]; then + eval "$(cwms-cli --quiet env export onprem --format bash)" + fi + +On the COOP server, use the same guarded block at the **bottom of +``~/.bashrc``**, but load ``coop`` by default: + +.. code-block:: bash + + # Load COOP defaults unless `cwms-cli env activate` supplied another environment. + if [ -z "${ENVIRONMENT:-}" ]; then + eval "$(cwms-cli --quiet env export coop --format bash)" + fi + +Each block loads ``CDA_API_ROOT``, ``CDA_API_KEY``, ``OFFICE``, and +``ENVIRONMENT`` from its corresponding environment. Before adding the block, +remove any existing lines in ``~/.bashrc`` that +assign or export ``CDA_API_ROOT``, ``CDA_API_KEY``, ``OFFICE``, or +``ENVIRONMENT``. Those assignments would override values inherited from +``cwms-cli env activate ``. Keep the new block at the bottom of the file +and after any setup that adds ``cwms-cli`` to ``PATH``. The ``ENVIRONMENT`` +guard preserves any environment selected with ``env activate``. The +``--quiet`` option suppresses routine log messages during shell startup while +still displaying warnings and errors. + cwms-cli env show ~~~~~~~~~~~~~~~~~ @@ -258,15 +307,6 @@ or press ``Ctrl+D`` to return to your original shell. This changes the current shell rather than creating a child shell. The values remain set until they are changed, unset, or the current shell exits. -If startup files provide defaults that should not replace an activated -environment, make those assignments conditional: - -.. code-block:: bash - - [ -z "${CDA_API_ROOT:-}" ] && \ - export CDA_API_ROOT="https://default.example/cwms-data" - [ -z "${OFFICE:-}" ] && export OFFICE="SWT" - .. note:: The parent shell and any already-open IDE will **not** see these variables. From 201bbe4572f1132ebc8c89317942d112321db403 Mon Sep 17 00:00:00 2001 From: msweier Date: Wed, 23 Sep 2026 06:37:03 -0500 Subject: [PATCH 3/7] clarify note --- docs/cli/env.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cli/env.rst b/docs/cli/env.rst index 9af4d8c..4712bed 100644 --- a/docs/cli/env.rst +++ b/docs/cli/env.rst @@ -137,7 +137,7 @@ Create or update an environment configuration. letting you attach an office and API key. All other environment names require ``--api-root``. -On-premises and COOP default environments +On-premises server setup and COOP default environments (optional) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Configure named environments for the primary on-premises server and the COOP From 500240f899e08ed525a2d9fec7e616f1ebb4201e Mon Sep 17 00:00:00 2001 From: msweier Date: Thu, 24 Sep 2026 08:00:32 -0500 Subject: [PATCH 4/7] fix doc formatting --- docs/cli/env.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cli/env.rst b/docs/cli/env.rst index 4712bed..e3fecde 100644 --- a/docs/cli/env.rst +++ b/docs/cli/env.rst @@ -137,8 +137,8 @@ Create or update an environment configuration. letting you attach an office and API key. All other environment names require ``--api-root``. -On-premises server setup and COOP default environments (optional) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +On-premises server setup and COOP default environments (optional) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Configure named environments for the primary on-premises server and the COOP server with their respective CDA API roots, API keys, and offices: From 2ab55ec060a3f2c690721134d285de7abee31369 Mon Sep 17 00:00:00 2001 From: msweier Date: Thu, 24 Sep 2026 08:12:09 -0500 Subject: [PATCH 5/7] add env activate warning about varaible overwrites --- cwmscli/commands/env.py | 45 ++++++++++++++++++++++++--- docs/cli/env.rst | 20 +++++++----- tests/commands/test_env.py | 62 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 13 deletions(-) diff --git a/cwmscli/commands/env.py b/cwmscli/commands/env.py index f90e6a1..dcafb4d 100644 --- a/cwmscli/commands/env.py +++ b/cwmscli/commands/env.py @@ -7,6 +7,7 @@ import click +from cwmscli.utils import colors from cwmscli.utils.env_store import ( ENV_DEFAULTS, EnvStoreError, @@ -310,9 +311,9 @@ def _detect_shell_kind() -> str: return "bash" -def _export_help_lines(env_name: str) -> str: - """Per-shell instructions for loading an env into the current shell.""" - recipes = { +def _export_recipes(env_name: str) -> Dict[str, str]: + """Return per-shell commands for loading an env into the current shell.""" + return { "bash": f'eval "$(cwms-cli env export {env_name} --format bash)"', "zsh": f'eval "$(cwms-cli env export {env_name} --format bash)"', "powershell": ( @@ -325,6 +326,11 @@ def _export_help_lines(env_name: str) -> str: ), "fish": f"cwms-cli env export {env_name} --format fish | source", } + + +def _export_help_lines(env_name: str) -> str: + """Per-shell instructions for loading an env into the current shell.""" + recipes = _export_recipes(env_name) detected = _detect_shell_kind() primary = recipes.get(detected, recipes["bash"]) @@ -349,19 +355,48 @@ def _export_help_lines(env_name: str) -> str: return "\n".join(lines) +def _startup_warning_lines(env_name: str, shell_kind: str) -> str: + """Explain how shell startup configuration can replace activated values.""" + startup_config = { + "bash": "startup files such as .bashrc", + "zsh": "startup files such as .zshrc", + "fish": "startup files such as config.fish", + "powershell": "PowerShell profiles", + "cmd": "cmd.exe AutoRun commands", + }.get(shell_kind, "shell startup configuration") + recipes = _export_recipes(env_name) + recipe = recipes.get(shell_kind, recipes["bash"]) + + return "\n".join( + [ + colors.warn( + f"Warning: {startup_config} may override CDA_API_ROOT, " + "CDA_API_KEY, OFFICE, or ENVIRONMENT." + ), + f"If those values do not match '{env_name}' after startup, reapply with:", + f" {recipe}", + ] + ) + + def spawn_shell_with_env(env_vars: Dict[str, str], env_name: str): """Spawn a new shell with environment variables set.""" user_shell = _detect_shell() + shell_kind = _detect_shell_kind() new_env = os.environ.copy() new_env.update(env_vars) click.echo( - f"Activating environment: {click.style(env_name, fg='green', bold=True)}", + f"Activating environment: {colors.ok(env_name)}", err=True, ) click.echo(f"Shell: {user_shell}", err=True) + click.echo(_startup_warning_lines(env_name, shell_kind), err=True) + exit_hint = "Type 'exit' to return to your original environment" + if shell_kind in {"bash", "zsh", "fish"}: + exit_hint += " (or press Ctrl+D)" click.echo( - "Type 'exit' or press Ctrl+D to return to your original environment\n", + f"\n{exit_hint}\n", err=True, ) diff --git a/docs/cli/env.rst b/docs/cli/env.rst index e3fecde..ef540cf 100644 --- a/docs/cli/env.rst +++ b/docs/cli/env.rst @@ -284,18 +284,22 @@ Activate an environment in a new shell session. cwms-cli env activate prod -This spawns a child shell with the environment variables set. Type ``exit`` -or press ``Ctrl+D`` to return to your original shell. +This spawns a child shell with the environment variables set. Type ``exit`` to +return to your original shell; in Unix-like shells, you can also press +``Ctrl+D``. Before opening the shell, ``activate`` warns that startup +configuration may replace the selected values and prints a shell-specific +command that reapplies them after startup. .. warning:: ``activate`` passes the configured variables to the child shell before that - shell initializes. Shell startup files such as ``.bash_profile`` or - ``.bashrc`` can then replace inherited values. For example, if a startup - file unconditionally exports ``CDA_API_ROOT``, ``CDA_API_KEY``, ``OFFICE``, - or ``ENVIRONMENT``, that value takes precedence over the selected - cwms-cli environment. This is a common configuration on Solaris systems, - but the same limitation applies on any platform. + shell initializes. Startup configuration such as ``.bash_profile``, + ``.bashrc``, PowerShell profiles, or ``cmd.exe`` AutoRun commands can then + replace inherited values. For example, if startup configuration + unconditionally sets ``CDA_API_ROOT``, ``CDA_API_KEY``, ``OFFICE``, or + ``ENVIRONMENT``, that value takes precedence over the selected cwms-cli + environment. This is a common configuration on Solaris systems, but the + same limitation applies on any platform. To ensure the selected values take precedence, load them into the current bash or zsh session after shell initialization: diff --git a/tests/commands/test_env.py b/tests/commands/test_env.py index 2006a7d..82120f2 100644 --- a/tests/commands/test_env.py +++ b/tests/commands/test_env.py @@ -472,6 +472,68 @@ def test_activate_help_explains_shell_startup_precedence(): assert "cwms-cli env export --format bash" in result.output +@pytest.mark.parametrize( + ("shell", "startup_config", "reapply_command"), + [ + ( + "/bin/bash", + "startup files such as .bashrc", + 'eval "$(cwms-cli env export demo --format bash)"', + ), + ( + "powershell.exe", + "PowerShell profiles", + ( + "cwms-cli env export demo --format powershell " + "| Out-String | Invoke-Expression" + ), + ), + ( + "cmd.exe", + "cmd.exe AutoRun commands", + ( + "cwms-cli env export demo --format cmd " + "--output %TEMP%\\cwms-env.cmd && call %TEMP%\\cwms-env.cmd" + ), + ), + ], +) +def test_activate_warns_about_shell_startup_configuration( + isolated_envs, + monkeypatch, + shell, + startup_config, + reapply_command, +): + save_env( + "demo", + { + "ENVIRONMENT": "demo", + "CDA_API_ROOT": "https://x.mil/cwms-data", + "CDA_API_KEY": "secret", + "OFFICE": "SWT", + }, + ) + monkeypatch.setattr("cwmscli.commands.env._detect_shell", lambda: shell) + monkeypatch.setattr( + "cwmscli.commands.env.subprocess.run", + lambda command, env: subprocess.CompletedProcess(command, 0), + ) + + result = CliRunner().invoke(env_group, ["activate", "demo"]) + + assert result.exit_code == 0 + assert f"Warning: {startup_config} may override CDA_API_ROOT" in result.output + assert "CDA_API_KEY, OFFICE, or ENVIRONMENT" in result.output + assert "If those values do not match 'demo' after startup" in result.output + assert reapply_command in result.output + assert "secret" not in result.output + if shell == "/bin/bash": + assert "or press Ctrl+D" in result.output + else: + assert "Ctrl+D" not in result.output + + # ---------- quoting helpers ---------- From 292a8cf311ec3074c5c169332fbae090848f8a71 Mon Sep 17 00:00:00 2001 From: msweier Date: Thu, 24 Sep 2026 08:26:01 -0500 Subject: [PATCH 6/7] update env show to check current environment variables and flag if they don't match current env --- cwmscli/commands/env.py | 58 ++++++++++++++++++++++++++++++++++++-- docs/cli/env.rst | 9 ++++-- tests/commands/test_env.py | 43 ++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 5 deletions(-) diff --git a/cwmscli/commands/env.py b/cwmscli/commands/env.py index dcafb4d..f65fb02 100644 --- a/cwmscli/commands/env.py +++ b/cwmscli/commands/env.py @@ -22,6 +22,7 @@ from cwmscli.utils.ssl_errors import is_cert_verify_error, ssl_help_text SENSITIVE_KEYS = {"CDA_API_KEY"} +MANAGED_ENV_KEYS = ("CDA_API_ROOT", "CDA_API_KEY", "OFFICE", "ENVIRONMENT") def _stdout_is_tty() -> bool: @@ -116,6 +117,30 @@ def _check_env(env_config: Dict[str, str]) -> Dict: return {"reachable": True, "latency_ms": latency_ms, "auth": "ok", "error": None} +def _active_env_mismatches( + env_name: str, env_config: Dict[str, str] +) -> Dict[str, tuple]: + """Return configured values that differ from the current process environment.""" + expected = {key: env_config[key] for key in MANAGED_ENV_KEYS if key in env_config} + expected.setdefault("ENVIRONMENT", env_name) + return { + key: (value, os.environ.get(key)) + for key, value in expected.items() + if value != os.environ.get(key) + } + + +def _format_env_mismatch(key: str, expected: str, actual: Optional[str]) -> str: + """Describe an environment mismatch without exposing sensitive values.""" + if key in SENSITIVE_KEYS: + if actual is None: + return f" {key}: configured, but not set in the current shell" + return f" {key}: current value does not match the configured value" + if actual is None: + return f" {key}: expected {expected!r}, but it is not set" + return f" {key}: expected {expected!r}, found {actual!r}" + + @click.group("env", help="Manage CDA environments and API keys") def env_group(): """Environment management commands for cwms-cli.""" @@ -196,9 +221,36 @@ def show_cmd(check: bool): current_env = os.environ.get("ENVIRONMENT") if current_env: - click.echo( - f"Current environment: {click.style(current_env, fg='green', bold=True)}\n" - ) + active_config = load_env(current_env) + if active_config is None: + click.echo(f"Current environment: {colors.warn(current_env)}") + click.echo( + colors.warn( + f"Warning: ENVIRONMENT references '{current_env}', but that " + "environment is not configured." + ) + ) + else: + mismatches = _active_env_mismatches(current_env, active_config) + if mismatches: + click.echo( + f"Current environment: " + f"{colors.warn(f'{current_env} (values differ)')}" + ) + click.echo( + colors.warn( + f"Warning: current shell values do not match environment " + f"'{current_env}':" + ) + ) + for key, (expected, actual) in mismatches.items(): + click.echo(_format_env_mismatch(key, expected, actual)) + click.echo( + "Shell startup configuration may have overridden these values." + ) + else: + click.echo(f"Current environment: {colors.ok(current_env)}") + click.echo() else: click.echo("No environment currently active\n") diff --git a/docs/cli/env.rst b/docs/cli/env.rst index ef540cf..417385d 100644 --- a/docs/cli/env.rst +++ b/docs/cli/env.rst @@ -216,8 +216,13 @@ The API key is always redacted — only ``has API key`` or ``no API key`` is sho On a fresh install (before any ``env setup``), ``prod`` appears with ``(built-in)`` and shows ``Office: not set``. -The ``*`` marks the currently active environment (from the ``ENVIRONMENT`` -variable). +The ``*`` marks the environment selected by the ``ENVIRONMENT`` variable. +``env show`` also compares that environment's configured ``CDA_API_ROOT``, +``CDA_API_KEY``, ``OFFICE``, and ``ENVIRONMENT`` values with the current +shell. If an assigned value differs or is missing, the command displays a +warning and identifies the affected variables. API key values remain redacted. +This can reveal when shell startup configuration replaced values supplied by +``env activate``. **Options:** diff --git a/tests/commands/test_env.py b/tests/commands/test_env.py index 82120f2..ce69cfb 100644 --- a/tests/commands/test_env.py +++ b/tests/commands/test_env.py @@ -246,10 +246,53 @@ def test_show_lists_envs_and_redacts_key(isolated_envs): def test_show_marks_current_env(isolated_envs, monkeypatch): save_env("active", {"CDA_API_ROOT": "https://x"}) monkeypatch.setenv("ENVIRONMENT", "active") + monkeypatch.setenv("CDA_API_ROOT", "https://x") runner = CliRunner() result = runner.invoke(env_group, ["show"]) assert "Current environment:" in result.output assert "* active" in result.output + assert "values differ" not in result.output + + +def test_show_warns_when_current_shell_differs(isolated_envs, monkeypatch): + save_env( + "active", + { + "ENVIRONMENT": "active", + "CDA_API_ROOT": "https://expected", + "CDA_API_KEY": "expected-secret", + "OFFICE": "SWT", + }, + ) + monkeypatch.setenv("ENVIRONMENT", "active") + monkeypatch.setenv("CDA_API_ROOT", "https://unexpected") + monkeypatch.setenv("CDA_API_KEY", "unexpected-secret") + monkeypatch.delenv("OFFICE", raising=False) + + result = CliRunner().invoke(env_group, ["show"]) + + assert result.exit_code == 0 + assert "Current environment: active (values differ)" in result.output + assert "current shell values do not match environment 'active'" in result.output + assert ( + "CDA_API_ROOT: expected 'https://expected', found 'https://unexpected'" + in result.output + ) + assert "CDA_API_KEY: current value does not match" in result.output + assert "OFFICE: expected 'SWT', but it is not set" in result.output + assert "Shell startup configuration may have overridden" in result.output + assert "expected-secret" not in result.output + assert "unexpected-secret" not in result.output + + +def test_show_warns_when_selected_environment_is_unknown(isolated_envs, monkeypatch): + monkeypatch.setenv("ENVIRONMENT", "missing") + + result = CliRunner().invoke(env_group, ["show"]) + + assert result.exit_code == 0 + assert "ENVIRONMENT references 'missing'" in result.output + assert "environment is not configured" in result.output # ---------- env show --check ---------- From 63051be7658bea70ffd0e70ef781251e516b042f Mon Sep 17 00:00:00 2001 From: msweier Date: Thu, 24 Sep 2026 08:33:01 -0500 Subject: [PATCH 7/7] update docs/help w/ changes --- cwmscli/commands/env.py | 12 +++++-- docs/cli/env.rst | 74 +++++++++++++++++++++++++------------- tests/commands/test_env.py | 3 ++ 3 files changed, 62 insertions(+), 27 deletions(-) diff --git a/cwmscli/commands/env.py b/cwmscli/commands/env.py index f65fb02..37226e5 100644 --- a/cwmscli/commands/env.py +++ b/cwmscli/commands/env.py @@ -425,6 +425,8 @@ def _startup_warning_lines(env_name: str, shell_kind: str) -> str: f"Warning: {startup_config} may override CDA_API_ROOT, " "CDA_API_KEY, OFFICE, or ENVIRONMENT." ), + "After the shell opens, verify the environment and CDA connection with:", + " cwms-cli env show --check", f"If those values do not match '{env_name}' after startup, reapply with:", f" {recipe}", ] @@ -479,6 +481,11 @@ def activate_cmd(env_name: str): \b eval "$(cwms-cli env export --format bash)" + Verify the activated values, connectivity, and authentication with: + + \b + cwms-cli env show --check + \b Examples: cwms-cli env activate prod @@ -555,7 +562,7 @@ def _format_env(env_vars: Dict[str, str], fmt: str) -> str: "-o", type=click.Path(dir_okay=False, writable=True, resolve_path=True), default=None, - help="Write to FILE (mode 0600) instead of standard output.", + help="Write to FILE instead of standard output (mode 0600 on POSIX).", ) @click.option( "--no-key", @@ -629,7 +636,8 @@ def export_cmd( except OSError as e: click.echo(f"Error writing {path}: {e}", err=True) sys.exit(1) - click.echo(f"Wrote {path} (0600)", err=True) + permission_note = " (0600)" if sys.platform != "win32" else "" + click.echo(f"Wrote {path}{permission_note}", err=True) if path.endswith(".env") or os.path.basename(path).startswith(".env"): click.echo("Reminder: add this file to .gitignore.", err=True) return diff --git a/docs/cli/env.rst b/docs/cli/env.rst index 417385d..9ad23d8 100644 --- a/docs/cli/env.rst +++ b/docs/cli/env.rst @@ -2,14 +2,15 @@ Environment Manager =================== Manage named CDA environments with ``cwms-cli env``. Each environment stores -a CDA API root URL, office code, and optional API key in a JSON file under -``~/.config/cwms-cli/envs/`` (or ``$XDG_CONFIG_HOME/cwms-cli/envs/`` when that -variable is set), on all platforms. Files are created with mode ``0600`` -(owner-only read/write) so only your user account can read them. +its name and CDA API root URL, plus an optional office code and API key, in a +JSON file under ``~/.config/cwms-cli/envs/`` (or +``$XDG_CONFIG_HOME/cwms-cli/envs/`` when that variable is set), on all +platforms. Files use mode ``0600`` on POSIX. On Windows, ``cwms-cli`` attempts +to restrict the file ACL to the current user. -This keeps API keys out of project directories, shell history, and command -lines, and lets you reference environments by name instead of juggling -URLs and credentials. +This keeps saved API keys out of project directories and lets you reference +environments by name instead of repeatedly putting URLs and credentials on +command lines. Built-in Environments @@ -137,6 +138,14 @@ Create or update an environment configuration. letting you attach an office and API key. All other environment names require ``--api-root``. +.. warning:: + + Supplying ``--api-key`` can place the key in shell history and make it + briefly visible in process arguments during setup. Apply your shell's + history controls when entering sensitive values. Once saved, later + commands use the environment name instead of placing the key on their + command lines. + On-premises server setup and COOP default environments (optional) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -219,15 +228,18 @@ On a fresh install (before any ``env setup``), ``prod`` appears with The ``*`` marks the environment selected by the ``ENVIRONMENT`` variable. ``env show`` also compares that environment's configured ``CDA_API_ROOT``, ``CDA_API_KEY``, ``OFFICE``, and ``ENVIRONMENT`` values with the current -shell. If an assigned value differs or is missing, the command displays a -warning and identifies the affected variables. API key values remain redacted. -This can reveal when shell startup configuration replaced values supplied by -``env activate``. +shell, whether or not ``--check`` is used. If an assigned value differs or is +missing, the command displays a warning and identifies the affected variables. +API key values remain redacted. This can reveal when shell startup +configuration replaced values supplied by ``env activate``. **Options:** -- ``--check`` — test connectivity and API key validity for each environment - (requires network access). Adds ``Connect`` and ``Auth`` lines to the output. +- ``--check`` — after the live-shell comparison, test the saved API root and + API key for each configured environment (requires network access). Adds + ``Connect`` and ``Auth`` lines to the output. If the live shell differs from + the saved environment, the mismatch warning remains authoritative for the + activated shell. .. code-block:: bash @@ -270,12 +282,13 @@ Export an environment's variables to your current shell or a file. **Safety:** The API key is never printed to a terminal by default. If stdout is a TTY and the environment has an API key, ``export`` shows shell-specific recipes instead. Use ``--show-key`` to override, or ``--output FILE`` to write -directly to disk (recommended — guarantees ``0600`` permissions and no -scrollback exposure). +directly to disk without scrollback exposure. Output files use mode ``0600`` +on POSIX; on Windows, they inherit the destination directory's ACL. **Options:** -- ``--output FILE`` — write to a file with ``0600`` permissions instead of stdout. +- ``--output FILE`` — write to a file instead of stdout (mode ``0600`` on + POSIX). - ``--no-key`` — omit ``CDA_API_KEY`` (useful for templates or sharing). - ``--show-key`` — allow the API key to be displayed in the terminal. @@ -293,7 +306,12 @@ This spawns a child shell with the environment variables set. Type ``exit`` to return to your original shell; in Unix-like shells, you can also press ``Ctrl+D``. Before opening the shell, ``activate`` warns that startup configuration may replace the selected values and prints a shell-specific -command that reapplies them after startup. +command that reapplies them after startup. Once the child shell opens, verify +the live values, CDA connectivity, and authentication: + +.. code-block:: bash + + cwms-cli env show --check .. warning:: @@ -306,15 +324,18 @@ command that reapplies them after startup. environment. This is a common configuration on Solaris systems, but the same limitation applies on any platform. - To ensure the selected values take precedence, load them into the current - bash or zsh session after shell initialization: + ``cwms-cli env show --check`` first reports whether the selected + environment's configured values match the child shell. It then checks the + saved configurations' connectivity and authentication. If it reports a + live-value mismatch, use the shell-specific reapply command printed by + ``activate``. For bash or zsh, that command is: .. code-block:: bash eval "$(cwms-cli env export --format bash)" - This changes the current shell rather than creating a child shell. The - values remain set until they are changed, unset, or the current shell exits. + This changes the child shell after its startup files have run. The values + remain set until they are changed, unset, or the child shell exits. .. note:: @@ -346,19 +367,22 @@ Storage and Security (respects ``XDG_CONFIG_HOME`` when set) **File permissions:** ``0600`` on POSIX (owner-only read/write). On Windows, -an ACL restricts access to the current user. +``cwms-cli`` attempts to restrict the ACL to the current user. **Security model:** The user account is the security boundary, matching ``aws``, ``gcloud``, ``kubectl``, and ``gh``. This feature defends against: - Accidental ``git add`` of a key — files live in ``~/.config/``, not the repo - Key pasted into an LLM — users share ``env show`` output (always redacted) -- Key visible in ``ps`` or shell history — users reference the env name, not values +- Repeated key exposure in ``ps`` or shell history — after setup, users + reference the environment name rather than the key - Key in terminal scrollback — ``export`` refuses TTY output by default This feature does **not** defend against root access or same-user process -reads. For encrypted-at-rest storage, use a vault (1Password CLI, HashiCorp -Vault, AWS Secrets Manager) and feed values in via environment variables. +reads. The initial ``env setup --api-key`` invocation can also be recorded in +shell history or process arguments. For encrypted-at-rest storage, use a vault +(1Password CLI, HashiCorp Vault, AWS Secrets Manager) and feed values in via +environment variables. Headless and CI Usage diff --git a/tests/commands/test_env.py b/tests/commands/test_env.py index ce69cfb..d1a775a 100644 --- a/tests/commands/test_env.py +++ b/tests/commands/test_env.py @@ -513,6 +513,7 @@ def test_activate_help_explains_shell_startup_precedence(): assert "Shell startup files can" in result.output assert "Solaris profiles" in result.output assert "cwms-cli env export --format bash" in result.output + assert "cwms-cli env show --check" in result.output @pytest.mark.parametrize( @@ -568,6 +569,8 @@ def test_activate_warns_about_shell_startup_configuration( assert result.exit_code == 0 assert f"Warning: {startup_config} may override CDA_API_ROOT" in result.output assert "CDA_API_KEY, OFFICE, or ENVIRONMENT" in result.output + assert "verify the environment and CDA connection" in result.output + assert "cwms-cli env show --check" in result.output assert "If those values do not match 'demo' after startup" in result.output assert reapply_command in result.output assert "secret" not in result.output