fix: create system account for service user (prod account_id NOT NULL) #91
@@ -13,7 +13,6 @@ from typing import Sequence, Union
|
|||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
@@ -25,18 +24,40 @@ depends_on: Union[str, Sequence[str], None] = None
|
|||||||
|
|
||||||
SERVICE_ACCOUNT_EMAIL = "noreply@resolutionflow.com"
|
SERVICE_ACCOUNT_EMAIL = "noreply@resolutionflow.com"
|
||||||
SERVICE_ACCOUNT_NAME = "ResolutionFlow"
|
SERVICE_ACCOUNT_NAME = "ResolutionFlow"
|
||||||
|
SYSTEM_ACCOUNT_NAME = "ResolutionFlow System"
|
||||||
|
SYSTEM_ACCOUNT_DISPLAY_CODE = "RF-SYS-1"
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
conn = op.get_bind()
|
conn = op.get_bind()
|
||||||
|
|
||||||
# Ensure service account exists
|
# Check if service user already exists
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
sa.text("SELECT id FROM users WHERE email = :email"),
|
sa.text("SELECT id, account_id FROM users WHERE email = :email"),
|
||||||
{"email": SERVICE_ACCOUNT_EMAIL},
|
{"email": SERVICE_ACCOUNT_EMAIL},
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|
||||||
if row is None:
|
if row is not None:
|
||||||
|
service_id = str(row[0])
|
||||||
|
else:
|
||||||
|
# Ensure a system account exists for the service user
|
||||||
|
acct_row = conn.execute(
|
||||||
|
sa.text("SELECT id FROM accounts WHERE display_code = :code"),
|
||||||
|
{"code": SYSTEM_ACCOUNT_DISPLAY_CODE},
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if acct_row is None:
|
||||||
|
account_id = str(uuid.uuid4())
|
||||||
|
conn.execute(
|
||||||
|
sa.text("""
|
||||||
|
INSERT INTO accounts (id, name, display_code, created_at, updated_at)
|
||||||
|
VALUES (:id, :name, :code, NOW(), NOW())
|
||||||
|
"""),
|
||||||
|
{"id": account_id, "name": SYSTEM_ACCOUNT_NAME, "code": SYSTEM_ACCOUNT_DISPLAY_CODE},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
account_id = str(acct_row[0])
|
||||||
|
|
||||||
service_id = str(uuid.uuid4())
|
service_id = str(uuid.uuid4())
|
||||||
conn.execute(
|
conn.execute(
|
||||||
sa.text("""
|
sa.text("""
|
||||||
@@ -44,12 +65,12 @@ def upgrade() -> None:
|
|||||||
id, email, name, password_hash, role,
|
id, email, name, password_hash, role,
|
||||||
is_super_admin, is_team_admin, is_active,
|
is_super_admin, is_team_admin, is_active,
|
||||||
is_service_account, must_change_password,
|
is_service_account, must_change_password,
|
||||||
account_role, created_at
|
account_id, account_role, created_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:id, :email, :name, :password_hash, 'engineer',
|
:id, :email, :name, :password_hash, 'engineer',
|
||||||
false, false, true,
|
false, false, true,
|
||||||
true, false,
|
true, false,
|
||||||
'engineer', NOW()
|
:account_id, 'engineer', NOW()
|
||||||
)
|
)
|
||||||
"""),
|
"""),
|
||||||
{
|
{
|
||||||
@@ -57,10 +78,9 @@ def upgrade() -> None:
|
|||||||
"email": SERVICE_ACCOUNT_EMAIL,
|
"email": SERVICE_ACCOUNT_EMAIL,
|
||||||
"name": SERVICE_ACCOUNT_NAME,
|
"name": SERVICE_ACCOUNT_NAME,
|
||||||
"password_hash": "!service-account-no-login",
|
"password_hash": "!service-account-no-login",
|
||||||
|
"account_id": account_id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
service_id = str(row[0])
|
|
||||||
|
|
||||||
# Backfill is_default trees that have no author
|
# Backfill is_default trees that have no author
|
||||||
result = conn.execute(
|
result = conn.execute(
|
||||||
@@ -75,7 +95,6 @@ def upgrade() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# Restore NULL on trees that were authored by the service account and are default
|
|
||||||
conn = op.get_bind()
|
conn = op.get_bind()
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
sa.text("SELECT id FROM users WHERE email = :email"),
|
sa.text("SELECT id FROM users WHERE email = :email"),
|
||||||
|
|||||||
@@ -18,6 +18,31 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
SERVICE_ACCOUNT_EMAIL = "noreply@resolutionflow.com"
|
SERVICE_ACCOUNT_EMAIL = "noreply@resolutionflow.com"
|
||||||
SERVICE_ACCOUNT_NAME = "ResolutionFlow"
|
SERVICE_ACCOUNT_NAME = "ResolutionFlow"
|
||||||
|
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:
|
async def ensure_service_account(db: AsyncSession) -> uuid.UUID:
|
||||||
@@ -40,7 +65,8 @@ async def ensure_service_account(db: AsyncSession) -> uuid.UUID:
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
return user.id
|
return user.id
|
||||||
|
|
||||||
# Create the service account with a random, unusable password hash
|
account_id = await _ensure_system_account(db)
|
||||||
|
|
||||||
new_user = User(
|
new_user = User(
|
||||||
id=uuid.uuid4(),
|
id=uuid.uuid4(),
|
||||||
email=SERVICE_ACCOUNT_EMAIL,
|
email=SERVICE_ACCOUNT_EMAIL,
|
||||||
@@ -52,6 +78,7 @@ async def ensure_service_account(db: AsyncSession) -> uuid.UUID:
|
|||||||
is_active=True,
|
is_active=True,
|
||||||
is_service_account=True,
|
is_service_account=True,
|
||||||
must_change_password=False,
|
must_change_password=False,
|
||||||
|
account_id=account_id,
|
||||||
account_role="engineer",
|
account_role="engineer",
|
||||||
)
|
)
|
||||||
db.add(new_user)
|
db.add(new_user)
|
||||||
|
|||||||
Reference in New Issue
Block a user