feat(init): bootstrap OIDC from wizard config
init_wizard applies config['oidc'] on first init: creates the oidc_settings row and, when admin_subject is given, binds the bootstrap admin so OIDC-only deployments are reachable. Idempotent — an existing row / admin binding is preserved (later admin edits via the API survive restarts). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.models import models
|
||||
from app.models.role_permission import Role, Permission, RolePermission
|
||||
from app.models.oidc_settings import OidcSettings
|
||||
from app.api.deps import get_password_hash
|
||||
|
||||
logger = logging.getLogger("harborforge.init")
|
||||
@@ -328,6 +329,49 @@ def init_deleted_user(db: Session) -> models.User | None:
|
||||
return user
|
||||
|
||||
|
||||
def init_oidc_settings(db: Session, oidc_cfg: dict, admin_user: models.User | None) -> None:
|
||||
"""Bootstrap OIDC from the wizard config (first init only).
|
||||
|
||||
Creates the single oidc_settings row if absent so the deployment comes
|
||||
up with OIDC configured. If admin_subject is given, binds the bootstrap
|
||||
admin so it can sign in (critical in OIDC-only mode). Idempotent: an
|
||||
existing row / existing admin binding is left untouched so later admin
|
||||
edits via the API are not clobbered on restart."""
|
||||
if not oidc_cfg:
|
||||
return
|
||||
|
||||
existing = db.query(OidcSettings).filter(OidcSettings.id == 1).first()
|
||||
if existing is None:
|
||||
db.add(OidcSettings(
|
||||
id=1,
|
||||
enabled=bool(oidc_cfg.get("enabled", True)),
|
||||
issuer=(oidc_cfg.get("issuer") or "").strip() or None,
|
||||
client_id=(oidc_cfg.get("client_id") or "").strip() or None,
|
||||
client_secret=oidc_cfg.get("client_secret") or None,
|
||||
redirect_uri=(oidc_cfg.get("redirect_uri") or "").strip() or None,
|
||||
scopes=(oidc_cfg.get("scopes") or "").strip() or None,
|
||||
post_login_redirect=(oidc_cfg.get("post_login_redirect") or "").strip() or None,
|
||||
))
|
||||
db.commit()
|
||||
logger.info("OIDC settings bootstrapped from wizard config")
|
||||
|
||||
admin_subject = (oidc_cfg.get("admin_subject") or "").strip()
|
||||
issuer = (oidc_cfg.get("issuer") or "").strip()
|
||||
if admin_user and admin_subject and issuer and not admin_user.oidc_subject:
|
||||
clash = db.query(models.User).filter(
|
||||
models.User.oidc_issuer == issuer,
|
||||
models.User.oidc_subject == admin_subject,
|
||||
models.User.id != admin_user.id,
|
||||
).first()
|
||||
if clash:
|
||||
logger.warning("Admin OIDC subject already bound to '%s'; skipping admin bind", clash.username)
|
||||
else:
|
||||
admin_user.oidc_issuer = issuer
|
||||
admin_user.oidc_subject = admin_subject
|
||||
db.commit()
|
||||
logger.info("Bootstrap admin '%s' bound to OIDC subject", admin_user.username)
|
||||
|
||||
|
||||
def run_init(db: Session) -> None:
|
||||
"""Main initialization entry point. Reads config from shared volume."""
|
||||
config = load_config()
|
||||
@@ -360,4 +404,7 @@ def run_init(db: Session) -> None:
|
||||
if project_cfg and admin_user:
|
||||
init_default_project(db, project_cfg, admin_user.id, admin_user.username)
|
||||
|
||||
# OIDC bootstrap (provider config + optional bootstrap-admin binding)
|
||||
init_oidc_settings(db, config.get("oidc") or {}, admin_user)
|
||||
|
||||
logger.info("Initialization complete")
|
||||
|
||||
Reference in New Issue
Block a user