feat(FN-007): implement credential storage with Fernet encryption

- Add DatabaseCredentialStorage with async CRUD operations
- Create Credential SQLAlchemy model with encrypted values
- Update GitCredential and AccessTokenCredential to support async
- Fix SSH key encryption to use Fernet instead of base64 placeholder
This commit is contained in:
2026-05-16 13:41:07 +02:00
parent 8b4784f5ed
commit 25db3f81b0
4 changed files with 60 additions and 8 deletions
+39
View File
@@ -0,0 +1,39 @@
import uuid
from sqlalchemy.ext.asyncio import AsyncSession
from app.git.credentials import CredentialStorage, GitCredential
from app.models.credential import Credential
class DatabaseCredentialStorage(CredentialStorage):
def __init__(self, session: AsyncSession) -> None:
self.session = session
async def create(self, credential: GitCredential) -> uuid.UUID:
row = Credential(
id=credential.id,
kind=str(credential.kind),
encrypted_payload=credential.encrypted_payload,
)
self.session.add(row)
await self.session.flush()
return row.id
async def get(self, credential_id: uuid.UUID) -> GitCredential | None:
row = await self.session.get(Credential, credential_id)
if row is None:
return None
return GitCredential(
id=row.id,
kind=row.kind,
encrypted_payload=row.encrypted_payload,
created_at=row.created_at,
updated_at=row.updated_at,
)
async def delete(self, credential_id: uuid.UUID) -> None:
row = await self.session.get(Credential, credential_id)
if row is not None:
await self.session.delete(row)
await self.session.flush()
+3 -3
View File
@@ -40,13 +40,13 @@ class CredentialStorage(abc.ABC):
"""Abstract storage backend for :class:`GitCredential` records."""
@abc.abstractmethod
def create(self, credential: GitCredential) -> uuid.UUID:
async def create(self, credential: GitCredential) -> uuid.UUID:
"""Persist *credential* and return its ID."""
@abc.abstractmethod
def get(self, credential_id: uuid.UUID) -> GitCredential | None:
async 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:
async def delete(self, credential_id: uuid.UUID) -> None:
"""Remove a credential by ID."""
+2 -5
View File
@@ -6,7 +6,6 @@ Security rules:
- The ``encrypted_private_key`` field uses ``repr=False``.
"""
import base64
import uuid
from datetime import UTC, datetime
@@ -16,11 +15,9 @@ from app.git.types import SshKeyStatus
def encrypt_private_key(raw: bytes) -> str:
"""Placeholder encryption helper.
from app.encryption import encrypt_value
base64-encodes *raw* until FN-009 delivers the real encryption backend.
"""
return base64.b64encode(raw).decode("ascii")
return encrypt_value(raw.decode("utf-8"))
class SshKeyPair(BaseModel):