99 lines
3.4 KiB
Python
99 lines
3.4 KiB
Python
"""ResolutionFlow system service account.
|
|
|
|
This module manages the platform-level service account used as the author
|
|
for system/default content (seeded trees, synced step library entries, etc.).
|
|
|
|
The service account ID is resolved once at startup and cached on app.state
|
|
so that sync operations can use it without a DB query per request.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
import logging
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.admin_database import _admin_session_factory
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SERVICE_ACCOUNT_EMAIL = "noreply@resolutionflow.com"
|
|
SERVICE_ACCOUNT_NAME = "ResolutionFlow"
|
|
|
|
# Well-known UUID for the platform account — owns all default/global content.
|
|
# Created by migration 3a40fe11b427_create_global_content_tables.
|
|
PLATFORM_ACCOUNT_ID = uuid.UUID("00000000-0000-0000-0000-000000000001")
|
|
SYSTEM_ACCOUNT_NAME = "ResolutionFlow System"
|
|
SYSTEM_ACCOUNT_DISPLAY_CODE = "RF-SYS-1"
|
|
|
|
|
|
async def _ensure_system_account(db: AsyncSession) -> uuid.UUID:
|
|
"""Get or create the ResolutionFlow system account. Returns its ID."""
|
|
from app.models.account import Account
|
|
from sqlalchemy import text
|
|
|
|
result = await db.execute(
|
|
select(Account).where(Account.display_code == SYSTEM_ACCOUNT_DISPLAY_CODE)
|
|
)
|
|
account = result.scalar_one_or_none()
|
|
if account is not None:
|
|
return account.id
|
|
|
|
new_account = Account(
|
|
id=uuid.uuid4(),
|
|
name=SYSTEM_ACCOUNT_NAME,
|
|
display_code=SYSTEM_ACCOUNT_DISPLAY_CODE,
|
|
)
|
|
db.add(new_account)
|
|
await db.flush()
|
|
logger.info(f"[service_account] Created system account (id={new_account.id})")
|
|
return new_account.id
|
|
|
|
|
|
async def ensure_service_account(db: AsyncSession) -> uuid.UUID:
|
|
"""Ensure the ResolutionFlow service account exists and return its ID.
|
|
|
|
Idempotent — safe to call on every startup. This lookup must bypass RLS
|
|
because startup runs before any request-scoped tenant context exists and
|
|
the users table is tenant-isolated in Phase 4. The service account is
|
|
normally created by Alembic migration 1490781700bc; the runtime create path
|
|
remains as a self-healing fallback for environments that predate that seed.
|
|
"""
|
|
_ = db # Retained for call-site compatibility in app lifespan startup.
|
|
|
|
from app.models.user import User
|
|
|
|
async with _admin_session_factory() as admin_db:
|
|
result = await admin_db.execute(
|
|
select(User).where(User.email == SERVICE_ACCOUNT_EMAIL)
|
|
)
|
|
user = result.scalar_one_or_none()
|
|
|
|
if user is not None:
|
|
if not user.is_service_account:
|
|
user.is_service_account = True
|
|
await admin_db.commit()
|
|
return user.id
|
|
|
|
account_id = await _ensure_system_account(admin_db)
|
|
|
|
new_user = User(
|
|
id=uuid.uuid4(),
|
|
email=SERVICE_ACCOUNT_EMAIL,
|
|
name=SERVICE_ACCOUNT_NAME,
|
|
password_hash="!service-account-no-login", # bcrypt can't produce this prefix
|
|
role="engineer",
|
|
is_super_admin=False,
|
|
is_team_admin=False,
|
|
is_active=True,
|
|
is_service_account=True,
|
|
must_change_password=False,
|
|
account_id=account_id,
|
|
account_role="engineer",
|
|
)
|
|
admin_db.add(new_user)
|
|
await admin_db.commit()
|
|
logger.info(f"[service_account] Created service account (id={new_user.id})")
|
|
return new_user.id
|