Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,11 @@ These functions are provided for documentation examples and return mock data:
- `process_tenant_request(tenant_id, request)` - Returns tenant result

#### Configuration
- `secret_key` - Test encryption key (value: `"a" * 64`)
- `CACHEKIT_MASTER_KEY` - Environment variable set to `secret_key` (enables `@cache.secure` examples)
- `secret_key` - Test encryption key (value: `"a" * 64`). `@cache.secure` examples must
pass it explicitly (`master_key=secret_key`); the conftest does **not** set
`CACHEKIT_MASTER_KEY` in the environment because an ambient key turns encryption on
globally and breaks plain `@cache` fences that use `serializer="auto"` or custom
serializers.

## Skipping Examples with `notest`

Expand Down
13 changes: 9 additions & 4 deletions docs/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"""

import logging
import os
import time

try:
Expand Down Expand Up @@ -91,10 +90,16 @@ def expensive_computation():
logger = logging.getLogger("cachekit.examples")
logger.setLevel(logging.INFO)

# Secret key for encryption examples (test value only)
# Secret key for encryption examples (test value only). Exposed as a global
# so @cache.secure fences can pass it explicitly (master_key=secret_key).
#
# We deliberately DO NOT set CACHEKIT_MASTER_KEY in the environment here.
# Since the PR #127 auto-detect + PR #200 settings re-read, an ambient
# master key turns encryption on globally, and the v0.6.0 cross-SDK rule
# then rejects any plain `@cache` fence using a non-cross-SDK serializer
# (serializer="auto", custom serializer instances) at decoration time.
# tests/conftest.py hit the same trap and fixed it the same way. See #205.
secret_key = "a" * 64 # 32 bytes in hex
# Set env var so @cache.secure validation passes
os.environ["CACHEKIT_MASTER_KEY"] = secret_key

globals_dict = {
"cache": cache,
Expand Down
48 changes: 48 additions & 0 deletions tests/unit/test_docs_conftest_no_key_leak.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Regression guard for issue #205.

The markdown-docs suite runs only in the post-merge CI job (push to main), not
on PRs, so a regression here would turn `main` red instead of failing review.
This fast unit test runs on every PR and fails loudly if `docs/conftest.py`
starts setting CACHEKIT_MASTER_KEY in the process environment again.

Why it matters: an ambient master key turns encryption on globally (PR #127
auto-detect + PR #200 settings re-read), and the v0.6.0 cross-SDK rule then
rejects every plain `@cache` doc fence that uses a non-cross-SDK serializer
(serializer="auto", custom serializer instances) at decoration time.
"""

from __future__ import annotations

import importlib.util
import os
from pathlib import Path

import pytest

DOCS_CONFTEST = Path(__file__).resolve().parents[2] / "docs" / "conftest.py"


def _load_docs_conftest():
spec = importlib.util.spec_from_file_location("docs_conftest_under_test", DOCS_CONFTEST)
assert spec and spec.loader, f"could not load {DOCS_CONFTEST}"
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module


@pytest.mark.unit
def test_docs_globals_hook_does_not_set_master_key_env(monkeypatch: pytest.MonkeyPatch) -> None:
"""Invoking the markdown-docs globals hook must not leak CACHEKIT_MASTER_KEY."""
monkeypatch.delenv("CACHEKIT_MASTER_KEY", raising=False)

module = _load_docs_conftest()
globals_dict = module.pytest_markdown_docs_globals()

assert "CACHEKIT_MASTER_KEY" not in os.environ, (
"docs/conftest.py set CACHEKIT_MASTER_KEY in the environment. This turns "
"encryption on globally and breaks plain @cache fences using serializer='auto' "
"or custom serializers. Pass the key explicitly (master_key=secret_key) in the "
"@cache.secure fences instead. See issue #205."
)
# The key must still be available to fences that opt in explicitly.
assert globals_dict.get("secret_key") == "a" * 64
Loading