From 2037359e8d9b5f6edc56c37c7aa42ee3acb9ce9e Mon Sep 17 00:00:00 2001 From: Fusion Date: Thu, 14 May 2026 07:29:19 +0200 Subject: [PATCH] =?UTF-8?q?feat(FN-011):=20complete=20Step=202=20=E2=80=94?= =?UTF-8?q?=20credential=20model=20and=20storage=20interface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fusion-Task-Id: FN-011 Fusion-Task-Lineage: 4a9aca6f-9d91-43aa-8d2a-d59657c1541a --- apps/api/app/git/__init__.py | 4 +++ apps/api/app/git/credentials.py | 56 +++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 apps/api/app/git/credentials.py diff --git a/apps/api/app/git/__init__.py b/apps/api/app/git/__init__.py index b711b92..8ef66ce 100644 --- a/apps/api/app/git/__init__.py +++ b/apps/api/app/git/__init__.py @@ -1,11 +1,15 @@ """Git provider abstraction, credentials, SSH keys, and operations.""" +from app.git.credentials import AccessTokenCredential, CredentialStorage, GitCredential from app.git.provider import GitProvider from app.git.types import ConnectionStatus, CredentialKind, ProviderKind, SshKeyStatus __all__ = [ + "AccessTokenCredential", "ConnectionStatus", "CredentialKind", + "CredentialStorage", + "GitCredential", "GitProvider", "ProviderKind", "SshKeyStatus", diff --git a/apps/api/app/git/credentials.py b/apps/api/app/git/credentials.py new file mode 100644 index 0000000..55b561c --- /dev/null +++ b/apps/api/app/git/credentials.py @@ -0,0 +1,56 @@ +"""Credential models and storage interface. + +Security rules: +- No plaintext ``private_key`` or ``token`` fields exist on any model class. +- The ``encrypted_payload`` field is opaque bytes encoded as a string. +""" + +import abc +import uuid +from datetime import datetime, timezone + +from pydantic import BaseModel, ConfigDict, Field + +from app.git.types import CredentialKind + + +class GitCredential(BaseModel): + """Base credential model. + + Never stores plaintext secrets. The ``encrypted_payload`` field holds + opaque encrypted data. + """ + + model_config = ConfigDict(extra="forbid") + + id: uuid.UUID = Field(default_factory=uuid.uuid4) + kind: CredentialKind + encrypted_payload: str = Field(repr=False) + created_at: datetime = Field( + default_factory=lambda: datetime.now(timezone.utc) + ) + updated_at: datetime = Field( + default_factory=lambda: datetime.now(timezone.utc) + ) + + +class AccessTokenCredential(GitCredential): + """Access-token credential discriminated by ``kind``.""" + + kind: CredentialKind = CredentialKind.access_token + + +class CredentialStorage(abc.ABC): + """Abstract storage backend for :class:`GitCredential` records.""" + + @abc.abstractmethod + def create(self, credential: GitCredential) -> uuid.UUID: + """Persist *credential* and return its ID.""" + + @abc.abstractmethod + def get(self, credential_id: uuid.UUID) -> GitCredential | None: + """Retrieve a credential by ID, or ``None`` if not found.""" + + @abc.abstractmethod + def delete(self, credential_id: uuid.UUID) -> None: + """Remove a credential by ID."""