From 7a866ec9c1500a0f8c6ee0f1e9effa23ac6acbe8 Mon Sep 17 00:00:00 2001 From: Tim-Oliver Husser Date: Sat, 22 Aug 2026 13:30:00 +0200 Subject: [PATCH 1/2] Dual login buttons: one-click IdP login via kc_idp_hint - PYOBS_AUTH['IDP_HINT']/['IDP_LABEL'] (static config, like the rest of the block); context processor exposes keycloak_idp_hint/keycloak_idp_label - Login template renders the hinted IdP button + a local-Keycloak-account button when a hint is configured, the plain single button otherwise (both gated on keycloak_login_enabled) - urlencode the existing unescaped ?next= on the Keycloak link (pre-existing) - Pin pyobs-auth>=2.0.0.dev8 --- modules/context_processors.py | 5 +++++ pyobs_web_admin/settings.py | 5 +++++ pyproject.toml | 2 +- templates/registration/login.html | 12 ++++++++++-- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/modules/context_processors.py b/modules/context_processors.py index 1336097..ce83bc7 100644 --- a/modules/context_processors.py +++ b/modules/context_processors.py @@ -62,6 +62,11 @@ def sidebar_modules(request): # which takes precedence over this processor when both provide it. "ejabberd_enabled": getattr(settings, "EJABBERD_ENABLED", False), "keycloak_login_enabled": bool(getattr(settings, "PYOBS_AUTH", {}).get("SERVER_URL")), + # IdP hint/label for the one-click IdP login button - see PYOBS_AUTH in settings.py. + # The template additionally gates on keycloak_login_enabled, so IDP_HINT without + # SERVER_URL (Keycloak disabled) degrades to no buttons rather than dead links. + "keycloak_idp_hint": getattr(settings, "PYOBS_AUTH", {}).get("IDP_HINT", ""), + "keycloak_idp_label": getattr(settings, "PYOBS_AUTH", {}).get("IDP_LABEL", ""), "git_enabled": getattr(settings, "PYOBS_CONFIG_GIT_ENABLED", False), "acl_matrix_enabled": getattr(settings, "PYOBS_CONFIG_ACL_MATRIX_ENABLED", False), "web_admin_version": _web_admin_version(), diff --git a/pyobs_web_admin/settings.py b/pyobs_web_admin/settings.py index 9eddd3b..3c8e561 100644 --- a/pyobs_web_admin/settings.py +++ b/pyobs_web_admin/settings.py @@ -93,6 +93,11 @@ "CLIENT_SECRET": "", "REDIRECT_URI": "", "POST_LOGOUT_REDIRECT_URI": "", + # Optional one-click IdP login: IDP_HINT is passed to Keycloak as kc_idp_hint (skips its + # login/IdP-selection page, going straight to that identity provider, e.g. GWDG SSO); + # IDP_LABEL is the button label on the login page. Both are deployment-specific. + "IDP_HINT": "", + "IDP_LABEL": "", "USER_RESOLVER": "pyobs_web_admin.authentication.keycloak.resolve_user", } diff --git a/pyproject.toml b/pyproject.toml index 70b542a..77b6a45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ dependencies = [ "gunicorn>=26.0.0", "packaging>=24.0", "psutil>=6.0", - "pyobs-auth>=2.0.0.dev7", + "pyobs-auth>=2.0.0.dev8", "pyyaml>=6.0", "requests>=2.32", "ruamel.yaml>=0.18", diff --git a/templates/registration/login.html b/templates/registration/login.html index 5766ec5..33b5c16 100644 --- a/templates/registration/login.html +++ b/templates/registration/login.html @@ -48,9 +48,17 @@
pyobs Web Admin
- {% if keycloak_login_enabled %} + {% if keycloak_idp_hint and keycloak_login_enabled %}
- + + Log in with {{ keycloak_idp_label|default:"Keycloak" }} + + + Log in with local Keycloak account + + {% elif keycloak_login_enabled %} +
+ Log in with Keycloak {% endif %} From 93ba9a738b2b35d93227a380a457d3c36e0a5520 Mon Sep 17 00:00:00 2001 From: Tim-Oliver Husser Date: Sun, 23 Aug 2026 13:14:36 +0200 Subject: [PATCH 2/2] Bump pyobs-auth pin to >=2.0.0.dev9; document IDP_HINT/IDP_LABEL in README and local_settings.py.example --- README.md | 12 ++++++++++++ pyobs_web_admin/local_settings.py.example | 6 ++++++ pyproject.toml | 2 +- uv.lock | 8 ++++---- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 15c55d0..be24e3f 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,12 @@ ADMIN_PASSWORD_HASH = "pbkdf2_sha256$..." # see generation command above # "CLIENT_SECRET": "", # "REDIRECT_URI": "https://your.domain.com/accounts/keycloak/callback/", # "POST_LOGOUT_REDIRECT_URI": "https://your.domain.com/", +# # Optional one-click IdP login: IDP_HINT is passed to Keycloak as kc_idp_hint (skips its +# # login/IdP-selection page, going straight to that identity provider, e.g. GWDG SSO); +# # IDP_LABEL is the button label on the login page. Leave both unset for the plain +# # single "Log in with Keycloak" button. +# "IDP_HINT": "gwdg", +# "IDP_LABEL": "GWDG", # "USER_RESOLVER": "pyobs_web_admin.authentication.keycloak.resolve_user", # } @@ -239,6 +245,12 @@ The login page then shows a "Log in with Keycloak" button. A first-time Keycloak local `User` (linked to an existing one by email, falling back to username, if either matches) with `is_active=False` — it can't do anything until activated. +Setting `IDP_HINT` (plus `IDP_LABEL` for the button text) switches the login page to two buttons: +"Log in with ``" goes straight to that identity provider (skipping Keycloak's own +login/IdP-selection page), and "Log in with local Keycloak account" keeps the local-account path +reachable for anyone without that IdP's identity. Leave `IDP_HINT` unset for the single-button +behavior above. + ### Activating / deactivating a user Django's built-in admin site is mounted at `/admin/` for exactly this — it isn't gated behind diff --git a/pyobs_web_admin/local_settings.py.example b/pyobs_web_admin/local_settings.py.example index 3bbc2a9..f975700 100644 --- a/pyobs_web_admin/local_settings.py.example +++ b/pyobs_web_admin/local_settings.py.example @@ -58,6 +58,12 @@ ADMIN_PASSWORD_HASH = "" # "CLIENT_SECRET": "", # "REDIRECT_URI": "https://your.domain.com/accounts/keycloak/callback/", # "POST_LOGOUT_REDIRECT_URI": "https://your.domain.com/", +# # Optional one-click IdP login: IDP_HINT is passed to Keycloak as kc_idp_hint (skips its +# # login/IdP-selection page, going straight to that identity provider, e.g. GWDG SSO); +# # IDP_LABEL is the button label on the login page. Leave both unset for the plain +# # single "Log in with Keycloak" button. +# "IDP_HINT": "gwdg", +# "IDP_LABEL": "GWDG", # "USER_RESOLVER": "pyobs_web_admin.authentication.keycloak.resolve_user", # } diff --git a/pyproject.toml b/pyproject.toml index 77b6a45..c3e7779 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ dependencies = [ "gunicorn>=26.0.0", "packaging>=24.0", "psutil>=6.0", - "pyobs-auth>=2.0.0.dev8", + "pyobs-auth>=2.0.0.dev9", "pyyaml>=6.0", "requests>=2.32", "ruamel.yaml>=0.18", diff --git a/uv.lock b/uv.lock index 13dec15..fd1b705 100644 --- a/uv.lock +++ b/uv.lock @@ -325,7 +325,7 @@ crypto = [ [[package]] name = "pyobs-auth" -version = "2.0.0.dev7" +version = "2.0.0.dev9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, @@ -333,9 +333,9 @@ dependencies = [ { name = "pyjwt", extra = ["crypto"] }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/6e/31ab5912ef3ba7435c62310f47828acf74ed1cc880dcb270857c4deaa25a/pyobs_auth-2.0.0.dev7.tar.gz", hash = "sha256:247265245e3a3a482c401c1b11546697b2b9806b00ac82f7a700542052196c25", size = 10521, upload-time = "2026-08-12T16:32:00.962Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ff/a64a7994969277066c095497a70751fbef8111ce732123744cf4fc7880bf/pyobs_auth-2.0.0.dev9.tar.gz", hash = "sha256:1c6b9cc5bb46598cf5b11f070aaaa8975b5133b52c2960588b3b5a5c42041288", size = 10731, upload-time = "2026-08-23T11:09:30.172Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/e9/b7804f56296c7f04f9a3ddcac9fb1a4114ea04b54ce0c08ee82be5982a8c/pyobs_auth-2.0.0.dev7-py3-none-any.whl", hash = "sha256:6f52c85d59a5c3f98a20a21faf7b094414c5c46325fedf2e175bcd9d500f2dc8", size = 12570, upload-time = "2026-08-12T16:32:00.115Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ea/c292cb6eb57247a64747b186cb0f0ff0b8190235d190d74bab9e9499d426/pyobs_auth-2.0.0.dev9-py3-none-any.whl", hash = "sha256:7122ac35d604d1d3f251c5c9f0d7180696ecdfefd7860a09585fd88e06dfdf72", size = 12827, upload-time = "2026-08-23T11:09:29.062Z" }, ] [[package]] @@ -360,7 +360,7 @@ requires-dist = [ { name = "gunicorn", specifier = ">=26.0.0" }, { name = "packaging", specifier = ">=24.0" }, { name = "psutil", specifier = ">=6.0" }, - { name = "pyobs-auth", specifier = ">=2.0.0.dev7" }, + { name = "pyobs-auth", specifier = ">=2.0.0.dev9" }, { name = "pyyaml", specifier = ">=6.0" }, { name = "requests", specifier = ">=2.32" }, { name = "ruamel-yaml", specifier = ">=0.18" },