Source code for uniqc.gateway.redaction

"""Redaction helpers for gateway API responses.

Task ``metadata`` / ``result`` payloads are free-form: callers may pass
backend credentials, proxy configuration or other sensitive values at
submission time, and they end up persisted on the task record. The
gateway must never echo those back over the REST API, so API responses
pass through :func:`redact_sensitive` before serialization. Only the
response is scrubbed — the stored task record keeps the original data.
"""

from __future__ import annotations

from typing import Any

REDACTED = "[REDACTED]"

# Keys whose values are scrubbed from API responses. Matching is
# case-insensitive and requires the token to appear as a full
# alphanumeric word, so ``api_key`` / ``QUARK_API_KEY`` match while
# ``monkey`` or ``keyboard`` do not.
_SENSITIVE_WORDS = ("token", "key", "secret", "password", "passwd", "credential", "proxy", "auth")


def _is_sensitive_key(key: object) -> bool:
    if not isinstance(key, str):
        return False
    lowered = key.lower()
    return any(
        lowered == word
        or lowered.startswith(word + "_")
        or lowered.startswith(word + "-")
        or lowered.endswith("_" + word)
        or lowered.endswith("-" + word)
        or f"_{word}_" in lowered
        or f"-{word}-" in lowered
        for word in _SENSITIVE_WORDS
    )


[docs] def redact_sensitive(value: Any) -> Any: """Return a copy of *value* with sensitive dict entries redacted. Recurses into nested dicts and lists; any dict entry whose key names a credential (token, api key, password, proxy, ...) is replaced with ``REDACTED``. Falsy values are left untouched so redaction never fabricates non-empty data. """ if isinstance(value, dict): return { key: REDACTED if _is_sensitive_key(key) and nested else redact_sensitive(nested) for key, nested in value.items() } if isinstance(value, list): return [redact_sensitive(item) for item in value] return value