Files
computer-playbook/plugins/lookup/users_non_reserved.py
kevinveenbirkenbachandClaude Opus 5 155c903b6b fix(keycloak): read directory users from one lookup so seeding cannot diverge
08_users/realm.yml seeded the realm from `lookup('users') | non_reserved_users`
while 08_rbac_groups_realm.yml built group memberships from the raw
`lookup('users')`. The membership builder has its own guards, but both are dead
against real data: across all 269 default users, none carries a `reserved` key
and none has an empty password. So membership was effectively unfiltered while
seeding filtered on `accounts`.

helpdesk and no-reply (accounts: [mailbox], roles: [bot]) were therefore
demanded as members of roles/web-app-mailu/bot but never created, and
ensure_group_path_members.sh exits 1 on the first miss. Four jobs -- Mattermost 1,
Friendica 1, Matrix 1, OpenProject 1 -- each burned five cross-node recovery
attempts on a data failure no reschedule could fix.

Rather than adding the filter at the site that forgot it, remove the ability to
forget: lookup('users_non_reserved') resolves users and applies the predicate in
one place, and all four consumers now read it -- the two Keycloak user tasks, the
RBAC group builder, and OPENLDAP_USERS, which had it right all along. The filter
stays as the pure predicate the lookup calls.

Reproduced offline against the real user model, byte-identical to the CI member
list: before, bot demanded helpdesk and no-reply; after, members are exactly the
four seeded accounts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 10:56:47 +02:00

40 lines
1.2 KiB
Python

from __future__ import annotations
from typing import Any
from ansible.errors import AnsibleError
from ansible.plugins.loader import lookup_loader
from ansible.plugins.lookup import LookupBase
from plugins.filter.reserved_users import non_reserved_users
class LookupModule(LookupBase):
"""
Usage:
{{ lookup('users_non_reserved') }}
Returns the users an identity directory provisions -- those carrying
``identity`` in ``accounts`` -- as the same dict shape ``lookup('users')``
yields. Every consumer that provisions or references directory users must
read this, so no two of them can disagree about who exists.
"""
def run(
self,
terms: list[Any] | None,
variables: dict[str, Any] | None = None,
**kwargs: Any,
) -> list[dict[str, Any]]:
if terms:
raise AnsibleError("lookup('users_non_reserved') expects no terms.")
variables = variables or getattr(self._templar, "available_variables", {}) or {}
users = lookup_loader.get(
"users",
loader=getattr(self, "_loader", None),
templar=getattr(self, "_templar", None),
).run([], variables=variables, **kwargs)[0]
return [non_reserved_users(users)]