72 lines
2.4 KiB
Python
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"]
|