diff --git a/api/auth.py b/api/auth.py index fef5dc95..b6a30940 100644 --- a/api/auth.py +++ b/api/auth.py @@ -722,7 +722,16 @@ def _trusted_groups_header_value(handler) -> list[str]: if not raw: return [] values = [] - for part in str(raw).replace('\n', ',').split(','): + # Authentik's outpost typically joins multiple group names with a comma, + # but this deployment's proxy provider/property mapping emits a + # pipe-separated list instead (e.g. "admins|developpeur"). A bare comma + # split silently treats a multi-group pipe-joined value as one unmatched + # group name, dropping the session to the unbound "default" profile even + # though the identity is a legitimate member of a mapped group. Accept + # comma, pipe, and newline as equivalent separators so either format + # works regardless of which the outpost happens to send. + normalized = str(raw).replace('\n', ',').replace('|', ',') + for part in normalized.split(','): part = part.strip() if part: values.append(part) diff --git a/tests/test_trusted_header_auth.py b/tests/test_trusted_header_auth.py index 8b07fbc1..5ebe65c0 100644 --- a/tests/test_trusted_header_auth.py +++ b/tests/test_trusted_header_auth.py @@ -212,6 +212,65 @@ def test_group_map_prefers_mapping_order_over_header_order(monkeypatch): assert auth.ensure_trusted_auth_session(second)["bound_profile"] == "ops" +@pytest.mark.parametrize( + "raw_header, expected", + [ + # Single group, no separator to parse. + ("admins", ["admins"]), + # Comma-separated (the format this parser originally supported). + ("admins,developpeur", ["admins", "developpeur"]), + # Pipe-separated (this deployment's Authentik outpost format). + ("admins|developpeur", ["admins", "developpeur"]), + # Newline-separated (already supported before this fix). + ("admins\ndeveloppeur", ["admins", "developpeur"]), + # Mixed separators in the same value. + ("admins,developpeur|it\nops", ["admins", "developpeur", "it", "ops"]), + # Repeated/adjacent separators collapse instead of producing + # empty group names. + ("admins||developpeur", ["admins", "developpeur"]), + ("admins,,developpeur", ["admins", "developpeur"]), + # Surrounding and interior whitespace is trimmed per group. + (" admins | developpeur ", ["admins", "developpeur"]), + ("admins, developpeur", ["admins", "developpeur"]), + ], +) +def test_trusted_groups_header_value_accepts_separator_variants( + monkeypatch, raw_header, expected +): + _trusted_env(monkeypatch, groups_header="Remote-Groups") + handler = _Handler(headers={"Remote-User": "alice", "Remote-Groups": raw_header}) + + assert auth._trusted_groups_header_value(handler) == expected + + +def test_group_map_accepts_pipe_separated_header_value(monkeypatch): + # Some Authentik proxy provider / property mapping configs join multiple + # group names with "|" instead of ",". Regression case for the identity + # falling back to the unbound "default" profile despite being a member + # of a mapped group: a lone-membership identity works fine (no + # separator to parse), but a second group membership starts producing a + # pipe-joined header value that a comma-only split can't see through. + # + # NOTE: this only covers the concrete-mapped-group case. The + # wildcard-mapped-group case (a "*" mapping value dominating a concrete + # one) is intentionally not exercised here — that precedence rule isn't + # implemented on this branch yet; it ships separately in #6798. See the + # PR description for the full scope note. + _trusted_env( + monkeypatch, + groups_header="Remote-Groups", + group_map={"hermes_devops": "devops"}, + ) + handler = _Handler( + headers={"Remote-User": "alice", "Remote-Groups": "hermes_devops|other_group"} + ) + + info = auth.ensure_trusted_auth_session(handler) + + assert info["bound_profile"] == "devops" + assert auth.trusted_session_allows_active_profile(info) is True + + @pytest.mark.parametrize( "group_map", [