Write new secret manager using existing RSA logic

This commit is contained in:
2025-06-22 17:17:56 +02:00
parent 5985a726e3
commit 82ec7fabb4
34 changed files with 2042 additions and 640 deletions

View File

@ -2,6 +2,7 @@
# pyright: reportUnusedFunction=false
#
from collections.abc import AsyncGenerator
import logging
import os
from contextlib import asynccontextmanager
@ -12,15 +13,15 @@ from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlalchemy.ext.asyncio import AsyncSession
from sshecret_backend.db import DatabaseSessionManager
from starlette.middleware.sessions import SessionMiddleware
from sshecret_admin import api, frontend
from sshecret_admin.auth.models import PasswordDB, init_db
from sshecret_admin.auth.models import Base
from sshecret_admin.core.db import setup_database
from sshecret_admin.frontend.exceptions import RedirectException
from sshecret_admin.services.master_password import setup_master_password
from sshecret_admin.services.secret_manager import setup_private_key
from .dependencies import BaseDependencies
from .settings import AdminServerSettings
@ -40,44 +41,28 @@ def setup_frontend(app: FastAPI, dependencies: BaseDependencies) -> None:
def create_admin_app(
settings: AdminServerSettings, with_frontend: bool = True
settings: AdminServerSettings,
with_frontend: bool = True,
create_db: bool = False,
) -> FastAPI:
"""Create admin app."""
engine, get_db_session = setup_database(settings.admin_db)
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
"""Get async session."""
session_manager = DatabaseSessionManager(settings.async_db_url)
async with session_manager.session() as session:
yield session
def setup_password_manager() -> None:
"""Setup password manager."""
encr_master_password = setup_master_password(
settings=settings, regenerate=False
)
with Session(engine) as session:
existing_password = session.scalars(
select(PasswordDB).where(PasswordDB.id == 1)
).first()
if not encr_master_password:
if existing_password:
LOG.info("Master password already defined.")
return
# Looks like we have to regenerate it
LOG.warning(
"Master password was set, but not saved to the database. Regenerating it."
)
encr_master_password = setup_master_password(
settings=settings, regenerate=True
)
assert encr_master_password is not None
with Session(engine) as session:
pwdb = PasswordDB(id=1, encrypted_password=encr_master_password)
session.add(pwdb)
session.commit()
setup_private_key(settings, regenerate=False)
@asynccontextmanager
async def lifespan(_app: FastAPI):
"""Create database before starting the server."""
init_db(engine)
if create_db:
Base.metadata.create_all(engine)
setup_password_manager()
yield
@ -109,7 +94,7 @@ def create_admin_app(
status_code=status.HTTP_200_OK, content=jsonable_encoder({"status": "LIVE"})
)
dependencies = BaseDependencies(settings, get_db_session)
dependencies = BaseDependencies(settings, get_db_session, get_async_session)
app.include_router(api.create_api_router(dependencies))
if with_frontend:

View File

@ -12,7 +12,7 @@ from pydantic import ValidationError
from sqlalchemy import select, create_engine
from sqlalchemy.orm import Session
from sshecret_admin.auth.authentication import hash_password
from sshecret_admin.auth.models import AuthProvider, PasswordDB, User, init_db
from sshecret_admin.auth.models import AuthProvider, PasswordDB, User
from sshecret_admin.core.settings import AdminServerSettings
from sshecret_admin.services.admin_backend import AdminBackend
@ -72,7 +72,6 @@ def cli_create_user(
"""Create user."""
settings = cast(AdminServerSettings, ctx.obj)
engine = create_engine(settings.admin_db)
init_db(engine)
with Session(engine) as session:
create_user(session, username, email, password)
@ -87,7 +86,6 @@ def cli_change_user_passwd(ctx: click.Context, username: str, password: str) ->
"""Change password on user."""
settings = cast(AdminServerSettings, ctx.obj)
engine = create_engine(settings.admin_db)
init_db(engine)
with Session(engine) as session:
user = session.scalars(select(User).where(User.username == username)).first()
if not user:
@ -107,7 +105,6 @@ def cli_delete_user(ctx: click.Context, username: str) -> None:
"""Remove a user."""
settings = cast(AdminServerSettings, ctx.obj)
engine = create_engine(settings.admin_db)
init_db(engine)
with Session(engine) as session:
user = session.scalars(select(User).where(User.username == username)).first()
if not user:
@ -149,7 +146,6 @@ def cli_repl(ctx: click.Context) -> None:
"""Run an interactive console."""
settings = cast(AdminServerSettings, ctx.obj)
engine = create_engine(settings.admin_db)
init_db(engine)
with Session(engine) as session:
password_db = session.scalars(
select(PasswordDB).where(PasswordDB.id == 1)
@ -165,7 +161,7 @@ def cli_repl(ctx: click.Context) -> None:
loop = asyncio.get_event_loop()
return loop.run_until_complete(func)
admin = AdminBackend(settings, password_db.encrypted_password)
admin = AdminBackend(settings, )
locals = {
"run": run,
"admin": admin,

View File

@ -1,12 +1,13 @@
"""Database setup."""
import sqlite3
from contextlib import asynccontextmanager
from collections.abc import AsyncIterator, Generator, Callable
from sqlalchemy.orm import Session
from sqlalchemy.engine import URL
from sqlalchemy import create_engine, Engine
from sqlalchemy import create_engine, Engine, event
from sqlalchemy.ext.asyncio import (
AsyncConnection,
@ -18,11 +19,20 @@ from sqlalchemy.ext.asyncio import (
def setup_database(
db_url: URL | str,
db_url: URL,
) -> tuple[Engine, Callable[[], Generator[Session, None, None]]]:
"""Setup database."""
engine = create_engine(db_url, echo=True, future=True)
if db_url.drivername.startswith("sqlite"):
@event.listens_for(engine, "connect")
def set_sqlite_pragma(
dbapi_connection: sqlite3.Connection, _connection_record: object
) -> None:
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
def get_db_session() -> Generator[Session, None, None]:
"""Get DB Session."""
@ -33,8 +43,18 @@ def setup_database(
class DatabaseSessionManager:
def __init__(self, host: URL | str, **engine_kwargs: str):
def __init__(self, host: URL, **engine_kwargs: str):
self._engine: AsyncEngine | None = create_async_engine(host, **engine_kwargs)
if host.drivername.startswith("sqlite+"):
@event.listens_for(self._engine.sync_engine, "connect")
def set_sqlite_pragma(
dbapi_connection: sqlite3.Connection, _connection_record: object
) -> None:
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
self._sessionmaker: async_sessionmaker[AsyncSession] | None = (
async_sessionmaker(
autocommit=False, bind=self._engine, expire_on_commit=False

View File

@ -4,6 +4,8 @@ from collections.abc import AsyncGenerator, Awaitable, Callable, Generator
from dataclasses import dataclass
from typing import Self
from fastapi import Request
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from sshecret_admin.auth import User
from sshecret_admin.services import AdminBackend
@ -11,8 +13,9 @@ from sshecret_admin.core.settings import AdminServerSettings
DBSessionDep = Callable[[], Generator[Session, None, None]]
AsyncSessionDep = Callable[[], AsyncGenerator[AsyncSession, None]]
AdminDep = Callable[[Session], AsyncGenerator[AdminBackend, None]]
AdminDep = Callable[[Request, Session], AsyncGenerator[AdminBackend, None]]
GetUserDep = Callable[[User], Awaitable[User]]
@ -23,6 +26,8 @@ class BaseDependencies:
settings: AdminServerSettings
get_db_session: DBSessionDep
get_async_session: AsyncSessionDep
@dataclass
@ -43,6 +48,7 @@ class AdminDependencies(BaseDependencies):
return cls(
settings=deps.settings,
get_db_session=deps.get_db_session,
get_async_session=deps.get_async_session,
get_admin_backend=get_admin_backend,
get_current_active_user=get_current_active_user,
)