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."""