129 lines
4.8 KiB
Python
129 lines
4.8 KiB
Python
"""SP26 — Docker-secrets file fallback in cyclone.secrets.get_secret().
|
|
|
|
SP25 added a plain env-var tier ahead of the macOS Keychain lookup.
|
|
SP26 adds a further tier above that: an `<env_name>_FILE` env var
|
|
pointing at a file on disk — the standard Docker-secrets pattern.
|
|
The file takes precedence over the plain env var (matches the
|
|
``auth/bootstrap.py:_read_secret`` convention).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
# A secret name we use throughout the unmapped cases. Because it is not
|
|
# in ``_ENV_NAME_FOR``, ``get_secret()`` will read it as
|
|
# ``os.environ.get("cyclone.test.file.password")`` directly (and the
|
|
# ``_FILE`` companion is the same name with ``_FILE`` appended). This
|
|
# mirrors SP25's ``test_secrets_envvar.py`` convention.
|
|
_UNMAPPED_NAME = "cyclone.test.file.password"
|
|
|
|
|
|
@pytest.fixture
|
|
def secrets_module(monkeypatch):
|
|
"""Reload cyclone.secrets with a clean keyring state per test."""
|
|
import cyclone.secrets as secrets_mod
|
|
importlib.reload(secrets_mod)
|
|
yield secrets_mod
|
|
importlib.reload(secrets_mod)
|
|
|
|
|
|
def _write_secret_file(tmp_path: Path, name: str, contents: str) -> Path:
|
|
f = tmp_path / name
|
|
f.write_text(contents)
|
|
return f
|
|
|
|
|
|
def test_file_env_var_returns_file_contents(
|
|
tmp_path: Path, monkeypatch, secrets_module,
|
|
) -> None:
|
|
"""_FILE set, file exists → returns file contents (no Keychain lookup)."""
|
|
f = _write_secret_file(tmp_path, "pw", "from-file\n")
|
|
monkeypatch.setenv(f"{_UNMAPPED_NAME}_FILE", str(f))
|
|
monkeypatch.delenv(_UNMAPPED_NAME, raising=False)
|
|
monkeypatch.setattr(
|
|
secrets_module.keyring, "get_password", lambda s, n: None,
|
|
)
|
|
assert secrets_module.get_secret(_UNMAPPED_NAME) == "from-file"
|
|
|
|
|
|
def test_file_env_var_strips_trailing_newline(
|
|
tmp_path: Path, monkeypatch, secrets_module,
|
|
) -> None:
|
|
"""Docker secret files commonly end with \\n — strip it."""
|
|
f = _write_secret_file(tmp_path, "pw", "supersecret\n")
|
|
monkeypatch.setenv(f"{_UNMAPPED_NAME}_FILE", str(f))
|
|
monkeypatch.delenv(_UNMAPPED_NAME, raising=False)
|
|
monkeypatch.setattr(
|
|
secrets_module.keyring, "get_password", lambda s, n: None,
|
|
)
|
|
assert secrets_module.get_secret(_UNMAPPED_NAME) == "supersecret"
|
|
|
|
|
|
def test_file_env_var_strips_leading_and_trailing_whitespace(
|
|
tmp_path: Path, monkeypatch, secrets_module,
|
|
) -> None:
|
|
f = _write_secret_file(tmp_path, "pw", " supersecret \n")
|
|
monkeypatch.setenv(f"{_UNMAPPED_NAME}_FILE", str(f))
|
|
monkeypatch.delenv(_UNMAPPED_NAME, raising=False)
|
|
monkeypatch.setattr(
|
|
secrets_module.keyring, "get_password", lambda s, n: None,
|
|
)
|
|
assert secrets_module.get_secret(_UNMAPPED_NAME) == "supersecret"
|
|
|
|
|
|
def test_file_env_var_wins_over_plain_env_var(
|
|
tmp_path: Path, monkeypatch, secrets_module,
|
|
) -> None:
|
|
"""When both _FILE and the plain env var are set, _FILE wins."""
|
|
f = _write_secret_file(tmp_path, "pw", "from-file\n")
|
|
monkeypatch.setenv(_UNMAPPED_NAME, "from-env")
|
|
monkeypatch.setenv(f"{_UNMAPPED_NAME}_FILE", str(f))
|
|
monkeypatch.setattr(
|
|
secrets_module.keyring, "get_password", lambda s, n: None,
|
|
)
|
|
assert secrets_module.get_secret(_UNMAPPED_NAME) == "from-file"
|
|
|
|
|
|
def test_file_env_var_missing_file_raises_runtime_error(
|
|
tmp_path: Path, monkeypatch, secrets_module,
|
|
) -> None:
|
|
"""If _FILE points at a non-existent path, raise RuntimeError."""
|
|
missing = tmp_path / "does-not-exist"
|
|
monkeypatch.setenv(f"{_UNMAPPED_NAME}_FILE", str(missing))
|
|
monkeypatch.delenv(_UNMAPPED_NAME, raising=False)
|
|
monkeypatch.setattr(
|
|
secrets_module.keyring, "get_password", lambda s, n: None,
|
|
)
|
|
with pytest.raises(RuntimeError, match=f"{_UNMAPPED_NAME}_FILE"):
|
|
secrets_module.get_secret(_UNMAPPED_NAME)
|
|
|
|
|
|
def test_file_env_var_empty_string_treated_as_unset(
|
|
monkeypatch, secrets_module,
|
|
) -> None:
|
|
"""An empty _FILE env var falls through to the plain env var (SP25 rule)."""
|
|
monkeypatch.setenv(f"{_UNMAPPED_NAME}_FILE", "")
|
|
monkeypatch.setenv(_UNMAPPED_NAME, "from-env")
|
|
monkeypatch.setattr(
|
|
secrets_module.keyring, "get_password", lambda s, n: None,
|
|
)
|
|
assert secrets_module.get_secret(_UNMAPPED_NAME) == "from-env"
|
|
|
|
|
|
def test_gainwell_file_env_var_full_chain(
|
|
tmp_path: Path, monkeypatch, secrets_module,
|
|
) -> None:
|
|
"""Setting CYCLONE_SFTP_PASSWORD_FILE makes get_secret('sftp.gainwell.password')
|
|
return the file's contents — the operator-visible chain works end-to-end."""
|
|
f = _write_secret_file(tmp_path, "sftp_pw", "real-mft-password\n")
|
|
monkeypatch.setenv("CYCLONE_SFTP_PASSWORD_FILE", str(f))
|
|
monkeypatch.delenv("CYCLONE_SFTP_PASSWORD", raising=False)
|
|
monkeypatch.setattr(
|
|
secrets_module.keyring, "get_password", lambda s, n: None,
|
|
)
|
|
assert secrets_module.get_secret("sftp.gainwell.password") == "real-mft-password"
|