Files
computer-playbook/plugins/lookup/bounded_name.py
kevinveenbirkenbachandClaude Opus 5 641f7a40dd fix(nextcloud): keep the sociallogin uid within 64 chars on onion deploys
Sociallogin creates a Nextcloud user as "<provider name>-<keycloak sub>",
and the provider name was the Keycloak domain. On an onion variant that
domain alone is 67 chars, so every OIDC callback died with
InvalidArgumentException "Username is too long" and HTTP 500 at
/apps/sociallogin/custom_oidc/auth.<onion> (run 34615516009,
Nextcloud#1, 204 occurrences). The whiteboard and admin OIDC specs failed
on the same login.

This is the second derived identifier an onion domain overflows; the
kernel hostname is the first, clamped to 63 bytes by container_hostname
since the pretix and bookwyrm fixes. The domain-or-entity rule now lives in
one lookup, bounded_name(application_id, max_length): the primary domain
when it fits, otherwise the entity name. container_hostname delegates to it
at 63 and keeps its eight callers and its behaviour.

The provider name is bounded_name('web-app-keycloak', 64 - 1 - 36), the uid
limit minus the separator and the UUID sub. A short clearnet domain stays
the name, so existing users keep their uids; an onion deploy gets
"keycloak", which, unlike a hash of the domain, survives a rotated onion
address.

Tests cover the lookup's fit, boundary and fallback cases and its argument
validation, container_hostname's delegation at 63, and the provider name's
room for the sub.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 21:14:55 +02:00

71 lines
2.4 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 utils.domains.primary_domain import get_domain
from utils.roles.entity.name import get_entity_name
class LookupModule(LookupBase):
"""
Usage:
{{ lookup('bounded_name', application_id, max_length) }}
An application's canonical primary domain when it has at most max_length
characters, otherwise its short entity name (e.g. ``keycloak``).
Args:
application_id: the application whose domain names the value.
max_length: the longest value the consumer accepts; a positive integer.
"""
def run(self, terms, variables: dict[str, Any] | None = None, **kwargs):
if not terms or len(terms) != 2:
raise AnsibleError(
"lookup('bounded_name', application_id, max_length) expects exactly 2 terms"
)
application_id, max_length = terms
if not isinstance(application_id, str) or not application_id.strip():
raise AnsibleError(
"lookup('bounded_name'): application_id must be a "
f"non-empty string, got {application_id!r}"
)
application_id = application_id.strip()
try:
limit = int(max_length)
except (TypeError, ValueError) as e:
raise AnsibleError(
f"lookup('bounded_name'): max_length must be an integer, got {max_length!r}"
) from e
if limit < 1:
raise AnsibleError(
f"lookup('bounded_name'): max_length must be positive, got {limit}"
)
variables = variables or getattr(self._templar, "available_variables", {}) or {}
domains = lookup_loader.get(
"domains",
loader=getattr(self, "_loader", None),
templar=getattr(self, "_templar", None),
).run([], variables=variables, roles_dir=kwargs.get("roles_dir"))[0]
try:
domain = get_domain(domains, application_id)
except Exception as e:
raise AnsibleError(
f"lookup('bounded_name'): failed to resolve domain for "
f"'{application_id}': {e}"
) from e
name = str(domain or "")
if 0 < len(name) <= limit:
return [name]
return [get_entity_name(application_id)]