feat(lint): enforce auth-isolation in variants.yml

Auth-isolated matrix variants (exactly one of LDAP / OIDC-OAuth2
enabled, and no non-auth `enabled: true` alongside) must explicitly
pin every non-auth service-key in the role's variant union to literal
`enabled: false, shared: false`. Without this, the variant's
auth-only test scope balloons into a full integration deploy via the
services.yml-default propagation, wasting CI minutes and obscuring
the regression signal.

Adds `tests/integration/roles/meta/variants/test_auth_isolation.py`
(rule key `variants-auth-isolation`) plus the matching catalog entry.
The trigger is narrow on purpose: a variant that enables auth
alongside other services is a deliberate integration test, not an
auth-isolated one, and the rule does not fire there.

Mass-fixes the 24 legacy auth-isolated variants the rule turned up:
each gets the missing `enabled: false, shared: false` declarations
appended to its `services:` block. Where the false-override clashes
with a static-literal base in meta/services.yml, the override line
also picks up a `# nocheck: variants-static-override` marker. The
svc-bkp-container-2-local variant 2 additionally carries
`# nocheck: variants-services-match` on the new keys because they
are resolver-only matrix hooks with no consumer contract in the base
meta.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 4.7 committed 2026-05-23 04:23:46 +02:00
1 parent 515b735522
commit f63118f34d
27 files changed
+671

No files matched your search

@@ -66,6 +66,7 @@ The placement rule is per check. The catalog "Position" column uses these labels
| `native-metrics` | comment block above | [test_native_metrics_contract.py](../../../../tests/integration/roles/meta/services/prometheus/test_native_metrics_contract.py) | Marks a role whose `services.prometheus.native_metrics.enabled` and `templates/prometheus.yml.j2` presence are intentionally inconsistent. Reserved for prepared-but-default-off scrape fragments (operator opt-in). |
| `variants-static-override` | same line | [test_variants_no_static_override.py](../../../../tests/integration/roles/meta/services/test_variants_no_static_override.py) | Allows a `roles/*/meta/variants.yml` leaf override of an attribute whose base value in `meta/services.yml` is a static literal. Place on the override line in `variants.yml`. Use only when the consumer chain is provably immune to the variant-merge propagation gap; the default is to make the base value dynamic via `"{{ '<role>' in group_names }}"`. |
| `variants-services-match` | same or above | [test_services_match.py](../../../../tests/integration/roles/meta/variants/test_services_match.py) | Allows a `roles/*/meta/variants.yml` `services.<key>` entry to reference a service key that is NOT declared as a top-level entry in `meta/services.yml`. Reserved for resolver-only matrix hooks (typically on invokable infrastructure roles) whose variant entries pin the round's companion topology via service-key flags the resolver maps to provider roles, with no real consumer contract in the base meta. |
| `variants-auth-isolation` | same or above | [test_auth_isolation.py](../../../../tests/integration/roles/meta/variants/test_auth_isolation.py) | Exempts an auth-isolated variant entry in `roles/*/meta/variants.yml` (one that enables exactly one of LDAP / OIDC / OAuth2) from the rule that every non-auth service-key in the role's variant union must be pinned to literal `false`. Place on the leading `- services:` line of the variant or on the line above. Use only when the broader closure is intentional. |
| `lookup-config-path` | same or above | [config/](../../../../tests/integration/ansible/lookups/config/) | Skips a single `lookup('config', …)` call from path-validation passes. Use when the call resolves against runtime-only state, typically self-referential roles. |
| `unused-var` | same or above | [test_role_and_group_vars_used.py](../../../../tests/lint/ansible/variables/test_role_and_group_vars_used.py) | Exempts a top-level key in `roles/<role>/{vars,defaults}/main.yml` or `group_vars/**/*.yml` from the "must be referenced" check. Use when the consumer is non-YAML/Jinja (Python plugins, external tools). |
| `project-root-import` | same or above | [test_project_root_import.py](../../../../tests/lint/filesystem/python/test_project_root_import.py) | Permits a local `PROJECT_ROOT` / `parents[N]` / `os.pardir` chain. Reserved for `__main__.py` `sys.path` bootstrap and standalone scripts under `roles/<role>/files/`. The marker MUST carry an inline explanation. |
@@ -42,3 +42,15 @@
ldap: # nocheck: variants-services-match
enabled: true
shared: true
gitea: # nocheck: variants-services-match
enabled: false
shared: false
keycloak: # nocheck: variants-services-match
enabled: false
shared: false
mariadb: # nocheck: variants-services-match
enabled: false
shared: false
postgres: # nocheck: variants-services-match
enabled: false
shared: false
+18
View File
@@ -69,3 +69,21 @@
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
mariadb:
enabled: false # nocheck: variants-static-override
shared: false
matomo:
enabled: false
shared: false
prometheus:
enabled: false
shared: false
+18
View File
@@ -68,3 +68,21 @@
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
matomo:
enabled: false
shared: false
postgres:
enabled: false # nocheck: variants-static-override
shared: false
prometheus:
enabled: false
shared: false
+15
View File
@@ -69,3 +69,18 @@
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
matomo:
enabled: false
shared: false
prometheus:
enabled: false
shared: false
+18
View File
@@ -78,3 +78,21 @@
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
matomo:
enabled: false
shared: false
postgres:
enabled: false # nocheck: variants-static-override
shared: false
prometheus:
enabled: false
shared: false
+21
View File
@@ -65,3 +65,24 @@
oidc:
enabled: false
shared: false
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
mariadb:
enabled: false # nocheck: variants-static-override
shared: false
matomo:
enabled: false
shared: false
prometheus:
enabled: false
shared: false
+18
View File
@@ -74,3 +74,21 @@
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
matomo:
enabled: false
shared: false
ollama:
enabled: false
shared: false
prometheus:
enabled: false
shared: false
+21
View File
@@ -74,3 +74,24 @@
oauth2:
enabled: false
shared: false
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
mariadb:
enabled: false # nocheck: variants-static-override
shared: false
matomo:
enabled: false
shared: false
prometheus:
enabled: false
shared: false
+21
View File
@@ -65,3 +65,24 @@
oauth2:
enabled: false
shared: false
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
matomo:
enabled: false
shared: false
postgres:
enabled: false # nocheck: variants-static-override
shared: false
prometheus:
enabled: false
shared: false
+18
View File
@@ -77,3 +77,21 @@
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
mariadb:
enabled: false # nocheck: variants-static-override
shared: false
matomo:
enabled: false
shared: false
prometheus:
enabled: false
shared: false
+12
View File
@@ -60,3 +60,15 @@
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
logout:
enabled: false
shared: false
matomo:
enabled: false
shared: false
prometheus:
enabled: false
shared: false
+18
View File
@@ -61,3 +61,21 @@
oauth2:
enabled: false
shared: false
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
matomo:
enabled: false
shared: false
prometheus:
enabled: false
shared: false
+15
View File
@@ -66,3 +66,18 @@
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
logout:
enabled: false
shared: false
matomo:
enabled: false
shared: false
ollama:
enabled: false
shared: false
prometheus:
enabled: false
shared: false
+21
View File
@@ -40,6 +40,27 @@
oidc:
enabled: false
shared: false
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
mariadb:
enabled: false # nocheck: variants-static-override
shared: false
matomo:
enabled: false
shared: false
prometheus:
enabled: false
shared: false
- services:
css:
enabled: false
+33
View File
@@ -87,3 +87,36 @@
oidc:
enabled: false
shared: false
collabora:
enabled: false
shared: false
coturn:
enabled: false
shared: false
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
mariadb:
enabled: false # nocheck: variants-static-override
shared: false
matomo:
enabled: false
shared: false
onlyoffice:
enabled: false
shared: false
prometheus:
enabled: false
shared: false
talk:
enabled: false
shared: false
+21
View File
@@ -65,3 +65,24 @@
oidc:
enabled: false
shared: false
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
matomo:
enabled: false
shared: false
postgres:
enabled: false # nocheck: variants-static-override
shared: false
prometheus:
enabled: false
shared: false
+18
View File
@@ -69,3 +69,21 @@
oidc:
enabled: false
shared: false
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
matomo:
enabled: false
shared: false
prometheus:
enabled: false
shared: false
@@ -65,3 +65,24 @@
oauth2:
enabled: false
shared: false
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
matomo:
enabled: false
shared: false
postgres:
enabled: false # nocheck: variants-static-override
shared: false
prometheus:
enabled: false
shared: false
+24
View File
@@ -71,3 +71,27 @@
oidc:
enabled: false
shared: false
coturn:
enabled: false
shared: false
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
matomo:
enabled: false
shared: false
postgres:
enabled: false # nocheck: variants-static-override
shared: false
prometheus:
enabled: false
shared: false
+21
View File
@@ -67,3 +67,24 @@
oidc:
enabled: false
shared: false
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
matomo:
enabled: false
shared: false
ollama:
enabled: false
shared: false
prometheus:
enabled: false
shared: false
+18
View File
@@ -71,3 +71,21 @@
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
mariadb:
enabled: false # nocheck: variants-static-override
shared: false
matomo:
enabled: false
shared: false
prometheus:
enabled: false
shared: false
+21
View File
@@ -65,3 +65,24 @@
oauth2:
enabled: false
shared: false
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
mariadb:
enabled: false # nocheck: variants-static-override
shared: false
matomo:
enabled: false
shared: false
prometheus:
enabled: false
shared: false
+21
View File
@@ -65,3 +65,24 @@
oauth2:
enabled: false
shared: false
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
mariadb:
enabled: false # nocheck: variants-static-override
shared: false
matomo:
enabled: false
shared: false
prometheus:
enabled: false
shared: false
+18
View File
@@ -68,3 +68,21 @@
css:
enabled: false
shared: false
dashboard:
enabled: false
shared: false
email:
enabled: false
shared: false
logout:
enabled: false
shared: false
matomo:
enabled: false
shared: false
postgres:
enabled: false # nocheck: variants-static-override
shared: false
prometheus:
enabled: false
shared: false
+3
View File
@@ -41,3 +41,6 @@
oidc:
enabled: false
shared: false
prometheus:
enabled: false
shared: false
@@ -0,0 +1,205 @@
"""Integration guard: an **auth-isolated** variant in
``roles/<role>/meta/variants.yml`` MUST explicitly disable every
non-auth service-key that any other variant of the same role declares.
Why
---
When a variant pins exactly one auth flavor (LDAP vs. OIDC/OAuth2)
the round under test exists to exercise that one auth path. Leaving
unrelated services (matomo, prometheus, dashboard, …) unset means
their effective value is decided by the deep-merge against
``meta/services.yml`` at deploy time — usually a dynamic
``"{{ '<role>' in group_names }}"`` expression — which makes the
resolver pull every matching provider role into the round's include
closure. The auth-only test scope balloons into a full integration
deploy, wasting CI minutes and obscuring the regression signal.
Pinning every non-auth flag to ``enabled: false, shared: false`` in
the auth-isolated variant keeps the closure tight: the resolver gets
a deterministic, minimal include set and the variant clearly states
"only this auth path, nothing else." The cost is a few explicit
``false`` lines per variant; the benefit is faster, more focused
matrix rounds.
Trigger
-------
A variant V is **auth-isolated** when:
1. EXACTLY ONE of the two auth flavors is set to ``enabled: true`` in
V's ``services:`` block:
* LDAP flavor: ``services.ldap.enabled: true``
* OIDC/OAuth2 flavor: ``services.oidc.enabled: true`` OR
``services.oauth2.enabled: true``
2. AND no non-auth service-key in V's ``services:`` block has
``enabled: true``.
Variants that enable both auth flavors (a full integration test), or
neither (a no-auth baseline / teardown variant), or that pin auth
alongside other ``enabled: true`` services (a mixed integration test)
are NOT auth-isolated and this rule does not fire for them.
Obligation
----------
When V is auth-isolated, every service-key K that appears in any
variant of this role's ``meta/variants.yml`` AND is not in
``{ldap, oidc, oauth2}`` MUST be declared in V as
``enabled: false`` AND ``shared: false`` (literal).
Exemption
---------
Place ``# nocheck: variants-auth-isolation`` on the same line as the
variant's leading ``- services:`` (or on the line immediately above)
to skip the check for that single variant entry.
"""
from __future__ import annotations
import unittest
from typing import TYPE_CHECKING
from utils.annotations.suppress import is_suppressed_at
from utils.cache.files import read_text
from utils.cache.yaml import load_yaml_any
from utils.roles.mapping import ROLE_FILE_META_VARIANTS
from . import PROJECT_ROOT
if TYPE_CHECKING:
from pathlib import Path
ROLES_DIR = PROJECT_ROOT / "roles"
_RULE = "variants-auth-isolation"
_AUTH = frozenset({"ldap", "oidc", "oauth2"})
_OIDC_GROUP = frozenset({"oidc", "oauth2"})
_LDAP_GROUP = frozenset({"ldap"})
def _is_enabled_true(entry: object) -> bool:
return isinstance(entry, dict) and entry.get("enabled") is True
def _auth_isolated(services: dict) -> bool:
"""A variant is auth-isolated when exactly one auth flavor is
enabled=true in its services map AND no non-auth service-key is
enabled=true. A variant that enables auth alongside non-auth
services is a mixed integration test and falls outside this rule.
"""
oidc_on = any(_is_enabled_true(services.get(k)) for k in _OIDC_GROUP)
ldap_on = any(_is_enabled_true(services.get(k)) for k in _LDAP_GROUP)
if oidc_on == ldap_on:
return False
for key, entry in services.items():
if key in _AUTH:
continue
if _is_enabled_true(entry):
return False
return True
def _variant_header_line_numbers(variants_file: Path) -> dict[int, int]:
"""Map variant index (0-based) to the 1-based line number of its
leading ``- services:`` (or ``- {}`` / ``- key:`` header).
"""
lines = read_text(str(variants_file)).splitlines()
out: dict[int, int] = {}
variant_index = -1
for idx, raw in enumerate(lines):
if raw.startswith("- "):
variant_index += 1
out[variant_index] = idx + 1
return out
def _is_literal_false(value: object) -> bool:
return value is False
def _entry_pinned_false(entry: object) -> bool:
return (
isinstance(entry, dict)
and _is_literal_false(entry.get("enabled"))
and _is_literal_false(entry.get("shared"))
)
class TestVariantsAuthIsolation(unittest.TestCase):
def test_auth_isolated_variants_explicitly_disable_non_auth_services(self):
offenders: list[str] = []
for role_dir in sorted(p for p in ROLES_DIR.iterdir() if p.is_dir()):
role_name = role_dir.name
variants_file = role_dir / ROLE_FILE_META_VARIANTS
if not variants_file.is_file():
continue
try:
variants_raw = load_yaml_any(str(variants_file), default_if_missing=[])
except Exception as exc:
offenders.append(f"{role_name}: meta/variants.yml parse error: {exc}")
continue
if not isinstance(variants_raw, list):
continue
non_auth_union: set[str] = set()
for variant in variants_raw:
if not isinstance(variant, dict):
continue
services = variant.get("services") or {}
if not isinstance(services, dict):
continue
for key in services:
if isinstance(key, str) and key not in _AUTH:
non_auth_union.add(key)
header_lines = _variant_header_line_numbers(variants_file)
text_lines = read_text(str(variants_file)).splitlines()
for index, variant in enumerate(variants_raw):
if not isinstance(variant, dict):
continue
services = variant.get("services") or {}
if not isinstance(services, dict):
continue
if not _auth_isolated(services):
continue
header_line = header_lines.get(index)
if header_line is not None and is_suppressed_at(
text_lines, header_line, _RULE
):
continue
missing = [
key
for key in sorted(non_auth_union)
if not _entry_pinned_false(services.get(key))
]
if missing:
offenders.append(
f"{role_name}: variant[{index}] is auth-isolated "
f"but does not pin "
f"{', '.join(missing)} to "
f"`enabled: false, shared: false`. Either add the "
f"explicit false declarations or mark the variant "
f"with ``# nocheck: {_RULE}``."
)
if offenders:
self.fail(
f"meta/variants.yml auth-isolated variants must pin every "
f"non-auth service-key to literal false "
f"({_RULE}, {len(offenders)} offender(s)):\n"
+ "\n".join(f" - {o}" for o in offenders)
)
if __name__ == "__main__": # pragma: no cover
unittest.main()