feat: add conditional bootstrap login + 23 admin login tests

- Add hash_password/verify_password to app/auth.py using bcrypt
- Fix /login to reject bootstrap creds (403) when admin users exist
- Add normal password-based login path for existing admin users
- Eagerly load User.roles to avoid lazy-load outside async session
- Fix test_user_models.py VARCHAR assertion (SQLAlchemy 2.0 uses 'string')
- Use shared-cache SQLite in conftest for endpoint-level tests
- Add 23 tests covering all 3 required scenarios plus edge cases:
  1. Bootstrap login with hardcoded creds when no admins → PASS (200)
  2. Bootstrap login with hardcoded creds when admins exist → FAIL (403)
  3. Password login for existing admin with hashed password → PASS (200)
- All 130 tests pass (107 existing + 23 new)
This commit is contained in:
Hermes Agent
2026-07-30 06:35:52 +00:00
parent 0eca079f28
commit f22802658e
5 changed files with 515 additions and 30 deletions
+52 -26
View File
@@ -2,13 +2,16 @@
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy import select, func
from sqlalchemy.orm import joinedload
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth import (
create_access_token,
get_current_user,
verify_google_id_token,
verify_password,
hash_password,
)
from app.config import settings
from app.database import get_session
@@ -48,18 +51,52 @@ class UserInfoResponse(BaseModel):
@router.post("/login", response_model=TokenResponse)
async def login(payload: LoginRequest):
"""Bootstrap admin login (plaintext credentials from env vars)."""
"""Conditional admin login.
- When NO admins exist: accepts hardcoded bootstrap credentials and
auto-provisions the first admin user.
- When admins DO exist: rejects hardcoded credentials (403) and requires
normal password-based login for existing admin users.
"""
if not settings.ADMIN_USERNAME or not settings.ADMIN_PASSWORD:
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Bootstrap admin not configured")
if payload.username != settings.ADMIN_USERNAME or payload.password != settings.ADMIN_PASSWORD:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
# This is handled in the auth router to keep it self-contained
from app.database import async_session
from app.user_models import Role
# Check if any admins already exist in the DB
async with async_session() as session:
admin_count_result = await session.execute(select(func.count(User.id)))
admin_count = admin_count_result.scalar()
if admin_count > 0:
# Admins exist — bootstrap creds are locked. Try normal password login.
if (payload.username == settings.ADMIN_USERNAME and
payload.password == settings.ADMIN_PASSWORD):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Bootstrap credentials disabled — admin users already exist",
)
# Normal password-based login for existing admin users
result = await session.execute(
select(User).options(joinedload(User.roles)).where(
(User.email == f"{payload.username}@local") |
(User.display_name == payload.username)
)
)
user = result.unique().scalar_one_or_none()
if user is None or not user.password_hash or not verify_password(payload.password, user.password_hash):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
if not user.is_admin_effective:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")
token = create_access_token(user.id, user.is_admin_effective)
return TokenResponse(access_token=token)
# Bootstrap path — no admins exist yet. Verify hardcoded creds.
if payload.username != settings.ADMIN_USERNAME or payload.password != settings.ADMIN_PASSWORD:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
# Ensure the 'admin' role exists
result = await session.execute(select(Role).where(Role.name == "admin"))
admin_role = result.scalar_one_or_none()
@@ -69,27 +106,16 @@ async def login(payload: LoginRequest):
await session.commit()
await session.refresh(admin_role)
# Find or create the bootstrap admin user
result = await session.execute(
select(User).where(
User.email == f"{settings.ADMIN_USERNAME}@local",
User.auth_provider == "local",
)
# Create the bootstrap admin user
user = User(
email=f"{settings.ADMIN_USERNAME}@local",
display_name=settings.ADMIN_USERNAME,
auth_provider="local",
password_hash=hash_password(settings.ADMIN_PASSWORD),
is_admin=True,
)
user = result.scalar_one_or_none()
if user is None:
user = User(
email=f"{settings.ADMIN_USERNAME}@local",
display_name=settings.ADMIN_USERNAME,
auth_provider="local",
is_admin=True,
)
user.roles.append(admin_role)
session.add(user)
else:
# Ensure existing bootstrap user also has the role
if admin_role not in user.roles:
user.roles.append(admin_role)
user.roles.append(admin_role)
session.add(user)
await session.commit()
await session.refresh(user)