diff --git a/DETAIL.md b/DETAIL.md index 9daa94a7..16c144d3 100644 --- a/DETAIL.md +++ b/DETAIL.md @@ -696,6 +696,7 @@ Arguments: - `--enable-ssh`: enable SSH via ACP if SSH is closed - `--no-enable-ssh`: fail instead of enabling SSH via ACP if SSH is closed - `--json`: emit a machine-readable result; requires `--no-input` +- `--no-persist-password`: write the config without saving `TC_PASSWORD`; later commands then prompt or read the password from `--password-env`/`--password-stdin` Hidden advanced arguments: - `--internal-share-use-disk-root`: writes `TC_INTERNAL_SHARE_USE_DISK_ROOT=true`; internal disks use the disk root instead of the safer `ShareRoot` diff --git a/src/timecapsulesmb/cli/configure.py b/src/timecapsulesmb/cli/configure.py index 6cb11e1e..e1565433 100644 --- a/src/timecapsulesmb/cli/configure.py +++ b/src/timecapsulesmb/cli/configure.py @@ -283,6 +283,11 @@ def main(argv: Optional[list[str]] = None) -> int: ssh_group.add_argument("--enable-ssh", action="store_true", help="Enable SSH via ACP if SSH is closed") ssh_group.add_argument("--no-enable-ssh", action="store_true", help="Fail instead of enabling SSH via ACP if SSH is closed") parser.add_argument("--json", action="store_true", help="Output a machine-readable configure result") + parser.add_argument( + "--no-persist-password", + action="store_true", + help="Write the config without saving the device password to .env", + ) parser.add_argument("--internal-share-use-disk-root", action="store_true", help=argparse.SUPPRESS) smb_bind_group = parser.add_mutually_exclusive_group() smb_bind_group.add_argument("--smb-bind-lan-only", action="store_true", help=argparse.SUPPRESS) @@ -303,6 +308,7 @@ def main(argv: Optional[list[str]] = None) -> int: args = parser.parse_args(argv) if args.json and not no_input_enabled(args): parser.error("--json requires --no-input") + persist_password = not args.no_persist_password ensure_install_id() env_path = resolve_app_paths(config_path=args.config).config_path @@ -515,7 +521,7 @@ def save_without_authentication(probed_state: ProbedDeviceState) -> bool: password=values["TC_PASSWORD"], ssh_opts=ssh_opts, configure_id=configure_id, - persist_password=True, + persist_password=persist_password, discovered_airport_syap=target.discovered_airport_syap, enable_ssh=True, verbose_wait=not args.json, @@ -537,7 +543,7 @@ def save_without_authentication(probed_state: ProbedDeviceState) -> bool: ata_idle_seconds=args.ata_idle_seconds, ata_standby=args.ata_standby, probe=probe_for_context, - write_env=lambda path, output: write_configure_env_file(path, output, persist_password=True), + write_env=lambda path, output: write_configure_env_file(path, output, persist_password=persist_password), infer_model_from_syap=infer_mdns_device_model_from_airport_syap, ), callbacks=OperationCallbacks( diff --git a/tests/test_cli.py b/tests/test_cli.py index dbb0f5d1..bf273791 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2076,6 +2076,24 @@ def test_configure_no_input_uses_explicit_host_and_password_env_without_prompts( self.assertEqual(result.values["TC_PASSWORD"], "pw") result.mocks.discover_snapshot_merged_detailed.assert_not_called() + def test_configure_no_persist_password_omits_password_from_env(self) -> None: + with mock.patch.dict(os.environ, {"TCAPSULE_TEST_PASSWORD": "pw"}): + result = self.run_configure_cli( + [ + "--no-input", + "--host", + "root@10.0.0.2", + "--password-env", + "TCAPSULE_TEST_PASSWORD", + "--no-persist-password", + ], + probe_state=self.make_probe_state(self.make_probe_result_netbsd6()), + ) + + self.assertEqual(result.rc, 0) + self.assertEqual(result.values["TC_HOST"], "root@10.0.0.2") + self.assertNotIn("TC_PASSWORD", result.values) + def test_configure_no_input_requires_password_before_probe_or_write(self) -> None: result = self.run_configure_cli( ["--no-input", "--host", "root@10.0.0.2"],