Files
local-ci-cd-system/src/ci_orchestrator/credentials.py
T
2026-05-21 00:11:20 +02:00

72 lines
2.4 KiB
Python

"""Credential store abstraction.
Wraps the ``keyring`` library so callers do not depend on it directly.
Phase B note: the same interface is used on both Windows (Credential
Manager) and Linux (Secret Service / file-based vault); see
``plans/implementation-plan-A-B.md`` step B3.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Protocol
@dataclass(frozen=True)
class Credential:
"""Username + secret pair retrieved from a credential store."""
username: str
password: str
class CredentialStore(Protocol):
"""Read-only credential lookup."""
def get(self, target: str) -> Credential:
"""Return the credential associated with ``target``.
Raises :class:`KeyError` if the target is unknown.
"""
...
class KeyringCredentialStore:
"""Default :class:`CredentialStore` backed by the ``keyring`` library."""
def __init__(self, service: str = "ci") -> None:
self._service = service
def get(self, target: str) -> Credential:
try:
import keyring
except ImportError as exc: # pragma: no cover - dep declared
raise RuntimeError(
"keyring is not installed; run `pip install keyring`."
) from exc
# 1. Native lookup — works on Windows Credential Manager and backends
# that implement get_credential(service, username=None).
cred = keyring.get_credential(target, None)
if cred is not None:
return Credential(username=cred.username, password=cred.password)
# 2. Two-entry scheme for file backends (PlaintextKeyring, etc.) where
# get_credential(target, None) is a no-op. The username is stored
# separately under service="{target}:meta", key="username".
username = keyring.get_password(f"{target}:meta", "username")
if username is not None:
password = keyring.get_password(target, username)
if password is not None:
return Credential(username=username, password=password)
# 3. Legacy fallback: password stored under service=self._service.
password = keyring.get_password(self._service, target)
if password is not None:
return Credential(username=target, password=password)
raise KeyError(f"Credential '{target}' not found in keyring.")
__all__ = ["Credential", "CredentialStore", "KeyringCredentialStore"]