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 import models
|
||||||
from app.models.role_permission import Role, Permission, RolePermission
|
from app.models.role_permission import Role, Permission, RolePermission
|
||||||
|
from app.models.oidc_settings import OidcSettings
|
||||||
from app.api.deps import get_password_hash
|
from app.api.deps import get_password_hash
|
||||||
|
|
||||||
logger = logging.getLogger("harborforge.init")
|
logger = logging.getLogger("harborforge.init")
|
||||||
@@ -328,6 +329,49 @@ def init_deleted_user(db: Session) -> models.User | None:
|
|||||||
return user
|
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:
|
def run_init(db: Session) -> None:
|
||||||
"""Main initialization entry point. Reads config from shared volume."""
|
"""Main initialization entry point. Reads config from shared volume."""
|
||||||
config = load_config()
|
config = load_config()
|
||||||
@@ -360,4 +404,7 @@ def run_init(db: Session) -> None:
|
|||||||
if project_cfg and admin_user:
|
if project_cfg and admin_user:
|
||||||
init_default_project(db, project_cfg, admin_user.id, admin_user.username)
|
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")
|
logger.info("Initialization complete")
|
||||||
|
|||||||
Reference in New Issue
Block a user