mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 03:38:23 +00:00
fix(web-app-jenkins): pin the OIDC scopes to openid profile email
Main run 42235551402 (5d78f3ae7d), job 128223280276 Jenkins#0: the OIDC redirect ended at /securityRealm/finishLogin?error=invalid_scope. With a wellKnown server configuration and no scopesOverride, oic-auth requests every entry of the discovery document's scopes_supported (OicServerWellKnownConfiguration). Keycloak rejects the whole request when one scope is not a default or optional scope of the client. The requested list minus the scopes of the infinito.nexus client leaves exactly kc-admin-api, a Keycloak built-in assigned only to admin-cli and security-admin-console.
casc.yaml.j2 now pins scopesOverride to 'openid profile email'; groups keeps arriving as a default client scope. The new unit test renders casc.yaml.j2 and the shared client template default.json.j2 and asserts every requested scope is one the client carries; without the override both tests fail.
🎯 Fix: 91 %
💥 Break: 7 %
💸 Cost: 100 %
⏱️ Time: 100 %
🛡️ Security: ~100 %
🏆 Score: 85
🚦 Gate: ⛔ break 7 % > 5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5d78f3ae7d
commit
68c9aa5b3a
4 files changed
+108
No files matched your search
@@ -17,6 +17,7 @@ jenkins:
|
||||
serverConfiguration:
|
||||
wellKnown:
|
||||
wellKnownOpenIDConfigurationUrl: "{{ OIDC.CLIENT.DISCOVERY_DOCUMENT }}"
|
||||
scopesOverride: "openid profile email"
|
||||
userNameField: "preferred_username"
|
||||
groupsFieldName: "groups"
|
||||
logoutFromOpenidProvider: true
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,5 @@
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT: Path = Path(__file__).resolve().parents[6]
|
||||
|
||||
__all__ = ["PROJECT_ROOT"]
|
||||
@@ -0,0 +1,102 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
import yaml
|
||||
from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape
|
||||
|
||||
from . import PROJECT_ROOT
|
||||
|
||||
CASC = "roles/web-app-jenkins/templates/casc.yaml.j2"
|
||||
KEYCLOAK_CLIENT = "roles/web-app-keycloak/templates/import/clients/default.json.j2"
|
||||
OIDC = {
|
||||
"CLIENT": {
|
||||
"ID": "infinito.nexus",
|
||||
"SECRET": "secret",
|
||||
"DISCOVERY_DOCUMENT": "https://auth.example.org/realms/infinito.nexus/.well-known/openid-configuration",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def environment() -> Environment:
|
||||
env = Environment(
|
||||
loader=FileSystemLoader(str(PROJECT_ROOT)),
|
||||
undefined=StrictUndefined,
|
||||
autoescape=select_autoescape(),
|
||||
)
|
||||
env.filters["bool"] = bool
|
||||
env.filters["to_json"] = json.dumps
|
||||
env.filters["regex_replace"] = lambda value, pattern, repl: re.sub(
|
||||
pattern, repl, value
|
||||
)
|
||||
return env
|
||||
|
||||
|
||||
def requested_scopes() -> list[str]:
|
||||
administrator = SimpleNamespace(username="administrator", password="secret")
|
||||
|
||||
def lookup(kind, *_args, **_kw):
|
||||
if kind == "users":
|
||||
return administrator
|
||||
if kind == "tls":
|
||||
return "https://jenkins.example.org/"
|
||||
raise AssertionError(f"unexpected lookup {kind!r}")
|
||||
|
||||
rendered = (
|
||||
environment()
|
||||
.get_template(CASC)
|
||||
.render(
|
||||
SOFTWARE_NAME="Infinito.Nexus",
|
||||
JENKINS_OIDC_ENABLED=True,
|
||||
JENKINS_LDAP_ENABLED=False,
|
||||
OIDC=OIDC,
|
||||
application_id="web-app-jenkins",
|
||||
lookup=lookup,
|
||||
)
|
||||
)
|
||||
well_known = yaml.safe_load(rendered)["jenkins"]["securityRealm"]["oic"][
|
||||
"serverConfiguration"
|
||||
]["wellKnown"]
|
||||
return well_known["scopesOverride"].split()
|
||||
|
||||
|
||||
def client_scopes() -> set[str]:
|
||||
def lookup(kind, _app, key):
|
||||
if kind != "config":
|
||||
raise AssertionError(f"unexpected lookup {kind!r}")
|
||||
return key.rsplit(".", 1)[-1]
|
||||
|
||||
rendered = (
|
||||
environment()
|
||||
.get_template(KEYCLOAK_CLIENT)
|
||||
.render(
|
||||
KEYCLOAK_CLIENT_ID="infinito.nexus",
|
||||
KEYCLOAK_REALM_URL="https://auth.example.org/realms/infinito.nexus",
|
||||
OIDC=OIDC,
|
||||
KEYCLOAK_REDIRECT_URIS=[],
|
||||
KEYCLOAK_WEB_ORIGINS=[],
|
||||
KEYCLOAK_FRONTCHANNEL_LOGOUT_URL="",
|
||||
KEYCLOAK_POST_LOGOUT_URIS="",
|
||||
KEYCLOAK_RBAC_GROUP_CLAIM="groups",
|
||||
KEYCLOAK_MOODLE_ENABLED=False,
|
||||
application_id="web-app-keycloak",
|
||||
lookup=lookup,
|
||||
)
|
||||
)
|
||||
client = json.loads(rendered)
|
||||
return set(client["defaultClientScopes"]) | set(client["optionalClientScopes"])
|
||||
|
||||
|
||||
class TestOidcScopes(unittest.TestCase):
|
||||
def test_jenkins_asks_for_identity_scopes_only(self) -> None:
|
||||
self.assertEqual(requested_scopes(), ["openid", "profile", "email"])
|
||||
|
||||
def test_every_requested_scope_is_one_the_client_carries(self) -> None:
|
||||
self.assertLessEqual(set(requested_scopes()), client_scopes())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user