Compare commits

...
Author SHA1 Message Date
hermes-bot 5e814c525a fix(authentik-gate-sync): stop sweeping every gate as an OIDC client
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
Outage since 10:11Z: the first pass of image 2026-11-01-0959 (#1535)
removed all 14 gates ("removed OIDC client from Authentik", "14 objects
removed"), and every pass since recreates and removes them again.

ProxyProvider subclasses OAuth2Provider (authentik 2026.8.4
providers/proxy/models.py: class ProxyProvider(OutpostModel,
OAuth2Provider)), so GET providers/oauth2/ also lists every proxy
provider. owned_applications() yielded each gate a second time as an
OAuth2 application; it was not in the OIDC keep set, so the sweep
deleted its application and provider.

Both the sweep and ensure_oidc's provider lookup now take from
providers/oauth2/ only providers whose meta_model_name is
authentik_providers_oauth2.oauth2provider. The test fake now lists proxy
providers under providers/oauth2/ like the real API; the new tests
(gate-only and gate + OIDC client stable over two passes) fail against
the previous controller.
2026-11-01 10:12:58 +00:00
security-bot 1a99c02996 chore(image-automation): bump gitea.renner.uno/gitea_admin/authentik-gate-sync:2026-11-01-0959
Updates managed by image-automation-controller.
2026-11-01 10:07:04 +00:00
k.renner 09a03dfd76 Merge pull request 'feat(authentik-gate-sync): OIDC clients from OIDCClient resources' (#1535) from feat/gate-sync-oidc into main
ci/woodpecker/push/build-ci-images Pipeline was successful
Reviewed-on: #1535
Reviewed-by: Review Bot <review-bot@renner.uno>
2026-11-01 09:59:01 +00:00
3 changed files with 41 additions and 7 deletions
+10 -1
View File
@@ -566,7 +566,8 @@ def ensure_oidc(ak, spec, flows, refs):
raise GateError(f"application {slug} exists and is not managed by {MANAGER}")
want = oauth2_body(spec, flows, refs)
provider = next((p for p in ak.list("providers/oauth2/") if p.get("name", "").lower() == name), None)
provider = next((p for p in ak.list("providers/oauth2/")
if is_plain_oauth2(p) and p.get("name", "").lower() == name), None)
if app is not None and (provider is None or app.get("provider") != provider["pk"]):
# E.g. a gate's application: never re-point it at an OAuth2 provider.
raise GateError(f"application {slug} exists without OAuth2 provider {name}")
@@ -597,6 +598,12 @@ def ensure_oidc(ak, spec, flows, refs):
PROVIDER_PATHS = ("providers/proxy/", "providers/oauth2/")
OAUTH2_MODEL = "authentik_providers_oauth2.oauth2provider"
def is_plain_oauth2(p):
# ProxyProvider subclasses OAuth2Provider, so providers/oauth2/ lists proxy providers too.
return p.get("meta_model_name") == OAUTH2_MODEL
def policy_name(app):
@@ -628,6 +635,8 @@ def owned_applications(ak):
and OAuth2 provider lists and confirmed one by one."""
for path in PROVIDER_PATHS:
for p in ak.list(path):
if path == "providers/oauth2/" and not is_plain_oauth2(p):
continue
slug = p.get("assigned_application_slug")
if not slug:
continue
@@ -206,7 +206,13 @@ class FakeAuthentik:
def list(self, path, **q):
out = []
for o in self.db[path].values():
objs = list(self.db[path].values())
if path == "providers/oauth2/":
# ProxyProvider subclasses OAuth2Provider: the real list endpoint returns both.
objs = [dict(o, meta_model_name=c.OAUTH2_MODEL) for o in objs] + \
[dict(o, meta_model_name="authentik_providers_proxy.proxyprovider")
for o in self.db["providers/proxy/"].values()]
for o in objs:
ok = True
for k, v in q.items():
field = k.replace("__iexact", "")
@@ -482,7 +488,7 @@ class ReconcileOidc(unittest.TestCase):
self.ak.db["core/groups/"].clear()
with self.assertRaises(c.GateError):
self.run_once()
self.assertEqual(self.ak.list("providers/oauth2/"), [])
self.assertEqual([p for p in self.ak.list("providers/oauth2/") if c.is_plain_oauth2(p)], [])
def test_refuses_unowned_application(self):
self.ak.add("core/applications/", {"slug": "youtrack", "meta_publisher": "", "provider": None})
@@ -520,7 +526,7 @@ class ReconcileOidc(unittest.TestCase):
"provider": gate_provider["pk"], "pbm_uuid": "pbm-x"})
with self.assertRaises(c.GateError):
self.run_once()
self.assertEqual(self.ak.list("providers/oauth2/"), [])
self.assertEqual([p for p in self.ak.list("providers/oauth2/") if c.is_plain_oauth2(p)], [])
self.assertEqual(self.ak.get("core/applications/youtrack/")["provider"], gate_provider["pk"])
@@ -602,7 +608,26 @@ class ReconcileLoop(unittest.TestCase):
k8s.clients = []
c.reconcile(k8s, self.ak)
self.assertIsNone(self.ak.get("core/applications/youtrack/"))
self.assertEqual(self.ak.list("providers/oauth2/"), [])
self.assertEqual([p for p in self.ak.list("providers/oauth2/") if c.is_plain_oauth2(p)], [])
def test_gates_survive_alongside_oidc(self):
k8s = FakeK8s(ingresses=[LLAMA], services={"llama-server": SERVICE}, clients=[self.client()])
c.reconcile(k8s, self.ak)
gate = c.gate_name(LLAMA)
app = self.ak.get(f"core/applications/{gate}/")
before = len(self.ak.calls)
c.reconcile(k8s, self.ak)
self.assertEqual(self.ak.calls[before:], [])
self.assertEqual(self.ak.get(f"core/applications/{gate}/")["provider"], app["provider"])
self.assertIsNotNone(self.ak.get("core/applications/youtrack/"))
def test_gate_only_is_stable(self):
k8s = FakeK8s(ingresses=[LLAMA], services={"llama-server": SERVICE})
c.reconcile(k8s, self.ak)
before = len(self.ak.calls)
c.reconcile(k8s, self.ak)
self.assertEqual([m for m, _ in self.ak.calls[before:] if m != "GET"], [])
self.assertIsNotNone(self.ak.get(f"core/applications/{c.gate_name(LLAMA)}/"))
def test_slug_equal_to_gate_name_leaves_gate_alone(self):
k8s = FakeK8s(ingresses=[LLAMA], services={"llama-server": SERVICE})
@@ -612,7 +637,7 @@ class ReconcileLoop(unittest.TestCase):
k8s.clients = [self.client("evil", slug=gate)]
c.reconcile(k8s, self.ak)
self.assertEqual(self.ak.get(f"core/applications/{gate}/")["provider"], app["provider"])
self.assertEqual(self.ak.list("providers/oauth2/"), [])
self.assertEqual([p for p in self.ak.list("providers/oauth2/") if c.is_plain_oauth2(p)], [])
self.assertEqual(k8s.status["evil"]["conditions"][0]["status"], "False")
+1 -1
View File
@@ -111,7 +111,7 @@ spec:
type: RuntimeDefault
containers:
- name: controller
image: gitea.renner.uno/gitea_admin/authentik-gate-sync:2026-10-29-1349 # {"$imagepolicy": "flux-system:authentik-gate-sync"}
image: gitea.renner.uno/gitea_admin/authentik-gate-sync:2026-11-01-0959 # {"$imagepolicy": "flux-system:authentik-gate-sync"}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true