mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
Implements docs/requirements/021-sso-flavor-migration.md atomically:
* Schema: every role's meta/services.yml replaces the parallel
`oauth2:` / `oidc:` top-level blocks with a single `sso:` block
carrying `enabled`, `shared`, and a `flavor` discriminator
(`oidc` | `oauth2` | `saml`; default `oidc`). Flavor-specific
sub-keys live under `sso.<flavor>.*` (`sso.oauth2.{origin,acl,
allowed_groups}`, `sso.oidc.plugin`).
* Provider role: roles/web-app-oauth2-proxy/ is deleted in its
entirety; its 5 sidecar templates move under
roles/web-app-keycloak/templates/sso_proxy/, and the per-consumer
rendering task lives at roles/web-app-keycloak/tasks/sso_proxy.yml.
web-app-keycloak now declares `provides: sso`.
* Runtime: new resolver utils/roles/applications/services/sso.py
centralises the SSO predicates (`is_enabled`, `is_proxy_gated`,
`is_oidc_native`) and the oauth2 sub-values
(`oauth2_origin_{host,port}`, `oauth2_acl`, `oauth2_allowed_groups`).
Three lookup plugins consume the resolver: `sso` (single-app
predicate access), `sso_proxy_consumers` (enumerates oauth2-flavored
consumers), `sso_oidc_plugin` (renamed from `oidc_flavor`, Nextcloud
plugin selector). sys-stk-backend, sys-svc-compose, sys-svc-proxy
and plugins/filter/compose_volumes.py all gate on
`lookup('sso', ..., 'is_proxy_gated')` instead of manually combining
`enabled` and `flavor`.
* Identifier renames repo-wide: `services.<entity>.ports.local.oauth2`
→ `ports.local.sso`; `PORT_BANDS.local.oauth2` → `PORT_BANDS.local.sso`;
`OAUTH2_PROXY_*` env/Jinja vars → `SSO_PROXY_*`;
`oauth2_proxy_cookie_secret` → `sso_proxy_cookie_secret`;
`OAUTH2_SERVICE_ENABLED` + `OIDC_SERVICE_ENABLED` collapse into
`SSO_SERVICE_ENABLED`; Playwright gating helpers
(isServiceEnabled/skipUnlessServiceEnabled/requireService/safeIsEnabled/
safeSkipUnlessEnabled) move from `oidc`/`oauth2` to `sso`.
* Dual-block resolution: per req's Background-Table, web-app-bookwyrm
is `flavor: oauth2`; web-app-gitea and web-app-friendica keep
`flavor: oauth2` with their existing `oauth2.acl.blacklist`
(/user/login) and `oauth2.acl.whitelist` (federation + discovery
endpoints) — preserving the operative oauth2-proxy gating.
* Tests: the two mutual-exclusion guards
(tests/integration/iam/oauth2_oidc/test_mutual_exclusive.py and
test_acl_mutual_exclusive.py) are deleted per Decision #10;
test_oauth2_contract.py is renamed and rewritten to
test_sso_contract.py, asserting that `flavor: oauth2` roles carry
`oauth2.origin.{host,port}` plus `ports.local.sso`; test_sso.py
(web-app integration) now requires the unified `sso:` block with an
explicit flavor or a `# nocheck: sso` opt-out; new
tests/lint/repository/no_legacy_sso_paths/ guards the source tree
against re-introducing the legacy strings.
* Docs: req 21 itself has every AC checked off, and reqs 006, 008,
009, 013, 015, 016, 019, 020 plus 10+ role READMEs and
docs/contributing/actions/testing/suppression.md are updated to the
unified SSO naming.
Empirically validated end-to-end against the validation app set
(web-app-keycloak, web-app-nextcloud, web-app-bookwyrm, web-app-gitea,
web-app-friendica, web-app-dashboard) in matrix variants 0 and 1; v2
also passes the 4 target roles' Playwright after a documented
pre-existing KC_HOSTNAME-drift workaround (separate concern, not
caused by this refactor).
BREAKING CHANGE: every consumer of `services.oauth2.*`,
`services.oidc.*`, `OAUTH2_PROXY_*`, `OAUTH2_SERVICE_ENABLED`,
`OIDC_SERVICE_ENABLED`, `oauth2_proxy_cookie_secret`,
`oauth2_proxy_application_id`, `ports.local.oauth2`, the
`web-app-oauth2-proxy` role, or the legacy gating helper service names
(`oidc`/`oauth2`) must migrate to the unified `sso` shape documented
in docs/requirements/021-sso-flavor-migration.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
80 lines
2.8 KiB
Python
80 lines
2.8 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from ansible.errors import AnsibleError
|
|
from ansible.plugins.lookup import LookupBase
|
|
|
|
from utils.cache.applications import get_merged_applications
|
|
from utils.roles.applications.config import get
|
|
from utils.roles.applications.services.sso import get_sso_config
|
|
|
|
_APPLICATION_ID = "web-app-nextcloud"
|
|
|
|
|
|
class LookupModule(LookupBase):
|
|
"""
|
|
lookup('sso_oidc_plugin')
|
|
|
|
Resolves the effective OIDC plugin flavor for the Nextcloud role.
|
|
|
|
Resolution order:
|
|
1. "" if services.sso.enabled is falsy (no OIDC plugin should be
|
|
active when the OIDC service itself is disabled — otherwise
|
|
Nextcloud would still hand off to Keycloak using a redirect_uri
|
|
that the client no longer whitelists).
|
|
2. An explicit string value at applications['web-app-nextcloud']
|
|
.services.sso.oidc.plugin (inventory override).
|
|
3. "oidc_login" if services.ldap.enabled is truthy
|
|
(pulsejet/nextcloud-oidc-login, proxy-LDAP capable).
|
|
4. "sociallogin" otherwise (nextcloud/sociallogin).
|
|
|
|
Mirrors the former `_applications_nextcloud_oidc_flavor` group_vars helper
|
|
that was removed in commit 77a0e16ea.
|
|
"""
|
|
|
|
def run(self, terms, variables: dict[str, Any] | None = None, **kwargs):
|
|
if terms:
|
|
raise AnsibleError("lookup('sso_oidc_plugin') takes no positional terms.")
|
|
|
|
templar = getattr(self, "_templar", None)
|
|
variables = variables or getattr(self._templar, "available_variables", {}) or {}
|
|
|
|
# Use the same merged+rendered applications payload that lookup('config')
|
|
# consumes. The raw `variables["applications"]` that Ansible hands the
|
|
# lookup is the pre-merge override slice, so nested defaults like
|
|
# services.ldap.enabled are not yet visible there and the flavor
|
|
# would silently fall back to 'sociallogin'.
|
|
applications = get_merged_applications(
|
|
variables=variables,
|
|
roles_dir=kwargs.get("roles_dir"),
|
|
templar=templar,
|
|
)
|
|
|
|
if not get_sso_config(applications, _APPLICATION_ID)["is_enabled"]:
|
|
return [""]
|
|
|
|
explicit = get(
|
|
applications=applications,
|
|
application_id=_APPLICATION_ID,
|
|
config_path="services.sso.oidc.plugin",
|
|
strict=False,
|
|
default=None,
|
|
skip_missing_app=True,
|
|
)
|
|
if isinstance(explicit, str) and explicit.strip():
|
|
return [explicit.strip()]
|
|
|
|
ldap_enabled = bool(
|
|
get(
|
|
applications=applications,
|
|
application_id=_APPLICATION_ID,
|
|
config_path="services.ldap.enabled",
|
|
strict=False,
|
|
default=False,
|
|
skip_missing_app=True,
|
|
)
|
|
)
|
|
|
|
return ["oidc_login" if ldap_enabled else "sociallogin"]
|