632554bf28
Vorher: seed_admin.py und docker-compose.yaml enthielten einen festen
Passwort-Fallback (Admin123!) — ein frisches Volume erzeugte ein
nutzbares Konto mit bekanntem Zugang. Auch die laufende Produktion
nutzte diesen Default (im Container verifiziert).
Fix:
- seed_admin.py: Bei NEUER Admin-Anlage ohne gesetztes ADMIN_PASSWORD
bricht der Start in Produktion AB (vor Benutzeranlage); in Dev wird
ein einmaliges Zufallspasswort generiert und ausgegeben. Bestehende
Admin-Accounts werden uebersprungen (kein Passwortgebrauch) — der
naechste Deploy laeuft also auch ohne gesetzte Variable weiter.
- docker-compose.yaml: ${ADMIN_PASSWORD:-Admin123!} -> required
(${ADMIN_PASSWORD:?...}) — kein Default mehr.
- .env.example/.env.docker.example: Default durch CHANGE_ME-Hinweis
ersetzt.
Abnahme (Astra): Ein frisches Volume ohne gesetztes Geheimnis erzeugt
kein nutzbares Konto mit festem Standardpasswort — erfuellt.
146 lines
5.3 KiB
Python
146 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Seed a default tenant and admin user for LeoCRM.
|
|
|
|
Usage: python scripts/seed_admin.py
|
|
|
|
Creates:
|
|
- Tenant: "Default Org" (slug: default)
|
|
- Admin role with full permissions
|
|
- Admin user: admin@media-on.de / Admin123!
|
|
- UserTenant link with admin role
|
|
|
|
If tenant or user already exists, skips creation.
|
|
|
|
Note: Uses the migration engine (crm_migration) to bypass RLS
|
|
for initial bootstrap on a fresh database.
|
|
"""
|
|
|
|
import asyncio
|
|
import os
|
|
import sys
|
|
|
|
# Ensure app is importable
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
|
|
|
from app.core.auth import hash_password
|
|
from app.core.db import get_migration_engine, set_tenant_context
|
|
from app.models.role import Role
|
|
from app.models.tenant import Tenant
|
|
from app.models.user import User, UserTenant
|
|
|
|
|
|
async def seed():
|
|
# Use migration engine to bypass RLS for bootstrap
|
|
engine = get_migration_engine()
|
|
async_session = async_sessionmaker(engine, expire_on_commit=False)
|
|
|
|
async with async_session() as db:
|
|
# Check if default tenant exists
|
|
result = await db.execute(select(Tenant).where(Tenant.slug == "default"))
|
|
tenant = result.scalar_one_or_none()
|
|
|
|
if tenant is None:
|
|
tenant = Tenant(name="Default Org", slug="default")
|
|
db.add(tenant)
|
|
await db.flush()
|
|
print(f"Created tenant: {tenant.name} (slug: {tenant.slug}, id: {tenant.id})")
|
|
else:
|
|
print(f"Tenant exists: {tenant.name} (id: {tenant.id})")
|
|
|
|
# Set tenant context for RLS
|
|
await set_tenant_context(db, tenant.id)
|
|
|
|
# Create admin role if not exists
|
|
result = await db.execute(select(Role).where(Role.name == "admin", Role.tenant_id == tenant.id))
|
|
role = result.scalar_one_or_none()
|
|
|
|
if role is None:
|
|
role = Role(
|
|
tenant_id=tenant.id,
|
|
name="admin",
|
|
permissions={"*:*": True},
|
|
)
|
|
db.add(role)
|
|
await db.flush()
|
|
print(f"Created admin role: {role.id}")
|
|
else:
|
|
print(f"Admin role exists: {role.id}")
|
|
|
|
# Check if admin user exists
|
|
result = await db.execute(select(User).where(User.email == os.environ.get("ADMIN_EMAIL", "admin@media-on.de")))
|
|
user = result.scalar_one_or_none()
|
|
|
|
if user is None:
|
|
# F30 (Astra P1): no known default password on fresh installs.
|
|
# - production: refuse to create an admin account with a guessable
|
|
# password — the operator must set ADMIN_PASSWORD.
|
|
# - non-production (dev/testing): generate a strong random
|
|
# password ONCE and print it; never fall back to a constant.
|
|
admin_password = os.environ.get("ADMIN_PASSWORD", "").strip()
|
|
from app.config import get_settings
|
|
|
|
if not admin_password:
|
|
if get_settings().environment == "production":
|
|
print(
|
|
"ERROR: ADMIN_PASSWORD is not set. Refusing to create an admin "
|
|
"account with a known default password in production. Set the "
|
|
"ADMIN_PASSWORD environment variable and restart.",
|
|
file=sys.stderr,
|
|
)
|
|
raise SystemExit(1)
|
|
import secrets
|
|
|
|
admin_password = secrets.token_urlsafe(16)
|
|
print(
|
|
"NOTICE: ADMIN_PASSWORD not set — generated a one-time random "
|
|
f"password for the new admin account: {admin_password}",
|
|
)
|
|
user = User(
|
|
email=os.environ.get("ADMIN_EMAIL", "admin@media-on.de"),
|
|
name="Administrator",
|
|
password_hash=hash_password(admin_password),
|
|
is_active=True,
|
|
is_system_admin=True,
|
|
preferences={},
|
|
)
|
|
db.add(user)
|
|
await db.flush()
|
|
print(f"Created user: {user.email} (id: {user.id})")
|
|
|
|
# Link user to tenant with admin role
|
|
ut = UserTenant(
|
|
user_id=user.id,
|
|
tenant_id=tenant.id,
|
|
role_id=role.id,
|
|
role="admin",
|
|
is_default=True,
|
|
)
|
|
db.add(ut)
|
|
await db.flush()
|
|
print("Created user_tenant link with admin role")
|
|
else:
|
|
print(f"User exists: {user.email} (id: {user.id})")
|
|
# Ensure existing admin has is_system_admin=True
|
|
if not user.is_system_admin:
|
|
user.is_system_admin = True
|
|
await db.flush()
|
|
print(f"Updated is_system_admin=True for {user.email}")
|
|
|
|
# Seed default workspace if none exists for this tenant
|
|
from app.services.workspace_service import seed_default_workspace
|
|
ws_result = await seed_default_workspace(db, tenant.id, user.id)
|
|
if ws_result:
|
|
print(f"Created default workspace: {ws_result['name']} (id: {ws_result['id']})")
|
|
else:
|
|
print("Workspace already exists — skipping workspace seed")
|
|
|
|
await db.commit()
|
|
print("Seed completed successfully.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(seed())
|