Skip to content

feat(auth): SSO + Bypass trusted für alle Scopes (ADR 0014)#133

Merged
strausmann merged 1 commit into
mainfrom
feat/sso-bypass-all-scopes
Jun 23, 2026
Merged

feat(auth): SSO + Bypass trusted für alle Scopes (ADR 0014)#133
strausmann merged 1 commit into
mainfrom
feat/sso-bypass-all-scopes

Conversation

@strausmann

Copy link
Copy Markdown
Owner

Summary

SSO und Bypass-Pfade waren auf read scope only beschränkt — Konsequenz: Browser-User sahen 503 auf allen Admin-UI-Routen. API-Key-Scopes bleiben unverändert.

ADR 0014 dokumentiert die Entscheidung und die zwei verworfenen Alternativen.

Auth-Modell danach

Pfad Scope-Verhalten
X-Label-Hub-Key (API-Key) Key's stored scope muss required erfüllen — unverändert
Pangolin-SSO (Standard + Legacy) Trusted für jeden Scope. Pangolin Resource Policy gated den Zugang
Pangolin Bypass (claude-automation) Trusted für jeden Scope. Secret in Vault, Rotation bei Verdacht

Was sich für Integrationen ändert

  • Nichts für Hangar, Snipe-IT, Grocy — die nutzen weiter scoped API-Keys
  • Nichts für PRINTER_HUB_WEBHOOK_API_KEY (separater Pfad)
  • Browser-User mit aktiver SSO-Session können das Admin-UI nutzen
  • claude-automation Bypass kann auch schreiben (nicht nur lesen)

Tests

75 passed in 20.53s

Inkl. neuer parametrisierter Test test_pangolin_sso_allows_print_and_admin_scopes.

Test Plan

  • CI grün (lint, mypy, pytest, privacy)
  • Backend-Image-Build durch
  • Stack-Recreate auf hhdocker03
  • Browser-Smoke: /admin/printers und /admin/api-keys zeigen UI (200) statt 503

ADR

docs/decisions/0014-pangolin-sso-and-bypass-trusted-for-all-scopes.md

Refs

In Phase 7c hatten wir SSO und Bypass auf "read scope only" beschraenkt,
um Leak-Defense fuer den claude-automation Bypass zu haben. Konsequenz:
das HTML-Admin-UI ist via SSO nicht nutzbar, weil Admin-Routen "admin"
Scope verlangen und SSO nur "read" liefert.

Diese Aenderung trusted SSO und Bypass fuer den jeweils verlangten Scope.
Die Defense-in-Depth ist:
- SSO: Pangolin Resource Policy gated, wer ueberhaupt auf labels.* kommt.
- Bypass: Secret liegt in Vault, rotiert via Vault bei Verdacht auf Leak.
  Production-Integrationen (Hangar, Snipe-IT, Grocy) sollen API-Keys mit
  spezifischem Scope nutzen, nicht den Bypass.

Multi-Scope-System auf X-Label-Hub-Key API-Keys bleibt unveraendert:
jeder Key hat weiterhin read/print/admin Scope-Liste und per-printer ACL.

Tests:
- test_pangolin_sso_blocked_on_print_scope ersetzt durch
  test_pangolin_sso_allows_print_and_admin_scopes (parametrisiert)
- 75/75 auth + admin tests gruen, inkl. Race-Detector

ADR 0014 dokumentiert die Entscheidung inkl. der zwei verworfenen Optionen
(SSO-Admin-User-Liste in ENV; Scope-System komplett entfernen).

Refs: Issue #78 (Phase 7c original), PR #130/#132 (Header-Forwarding),
ADR 0014
@strausmann strausmann merged commit 4fe1a91 into main Jun 23, 2026
12 of 14 checks passed
@strausmann strausmann deleted the feat/sso-bypass-all-scopes branch June 23, 2026 11:46
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

Dieser Pull Request ändert das Auth-Modell für Pangolin-SSO und den Bypass-Pfad, um Browser-Usern den Zugriff auf Admin-UI-Routen zu ermöglichen. Zuvor waren diese Pfade auf 'read-only' beschränkt, was zu 503-Fehlern in der Admin-Oberfläche führte. Die Entscheidung basiert auf ADR 0014, wobei die feingranulare Scoping-Logik für API-Keys unverändert bleibt, um die Sicherheit für Integrationen zu gewährleisten.

Highlights

  • SSO and Bypass Trust Model: Updated the authentication dependency to treat Pangolin-SSO and Pangolin-Bypass as fully trusted for all scopes, resolving issues where browser users were unable to access admin-UI routes.
  • ADR 0014 Documentation: Added ADR 0014 to document the decision to move away from read-only restrictions for SSO and bypass paths, while maintaining granular scoping for API keys.
  • Test Suite Updates: Replaced the restrictive SSO test with a parameterized test verifying that SSO sessions correctly grant 'print' and 'admin' scopes.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements ADR 0014, granting full trust for all scopes to Pangolin-SSO and Pangolin-Bypass authentication paths to resolve UI and scripting limitations, while maintaining scoped restrictions on API keys. The review feedback recommends returning a hardcoded "admin" scope in the AuthContext for these trusted paths rather than dynamically echoing the requested scope. This avoids potential downstream authorization issues where a fully trusted session might be incorrectly restricted. Unit tests should be updated to reflect this change.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +327 to 333
if _has_pangolin_sso_session(request, effective_settings):
return AuthContext(
source="pangolin-sso",
scope="read",
scope=required,
api_key_id=None,
ip=client_ip,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Da Pangolin-SSO-Sitzungen für alle Scopes voll vertrauenswürdig sind (äquivalent zu admin), führt die dynamische Rückgabe von scope=required dazu, dass das Berechtigungsniveau im zurückgegebenen AuthContext künstlich auf das für den aktuellen Endpunkt erforderliche Minimum herabgestuft wird. Wenn ein Handler oder eine nachgelagerte Abhängigkeit auth_context.scope prüft (z. B. um Admin-Aktionen innerhalb eines mit read geschützten Endpunkts bedingt zu erlauben), wird ein voll vertrauenswürdiger SSO-Benutzer fälschlicherweise eingeschränkt. Das Hardcoden von scope="admin" für diese vertrauenswürdigen Pfade stellt sicher, dass sie immer als voll berechtigt erkannt werden.

Suggested change
if _has_pangolin_sso_session(request, effective_settings):
return AuthContext(
source="pangolin-sso",
scope="read",
scope=required,
api_key_id=None,
ip=client_ip,
)
if _has_pangolin_sso_session(request, effective_settings):
return AuthContext(
source="pangolin-sso",
scope="admin",
api_key_id=None,
ip=client_ip,
)

Comment on lines 338 to 344
if _is_pangolin_bypass(request):
# After Phase 7c, bypass is downgraded to read-only.
# The feature flag controls when the downgrade is enforced.
if required == "read" or not effective_settings.pangolin_bypass_scope_downgrade:
return AuthContext(
source="pangolin-bypass",
scope="read",
api_key_id=None,
ip=client_ip,
)
# Downgrade enforced: bypass cannot satisfy print/admin
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={
"error_code": "bypass_scope_downgraded",
"error_message": (
"Pangolin bypass is read-only. Use X-Label-Hub-Key for write operations."
),
},
return AuthContext(
source="pangolin-bypass",
scope=required,
api_key_id=None,
ip=client_ip,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Analog zum SSO-Pfad sollte auch der Bypass-Pfad (der als Admin-äquivalente Anmeldeinformation behandelt wird) immer den Scope "admin" zurückgeben, anstatt ihn dynamisch auf required herabzustufen. Dies verhindert unerwartete Einschränkungen in nachgelagerten Handlern, die den Scope im AuthContext überprüfen.

Suggested change
if _is_pangolin_bypass(request):
# After Phase 7c, bypass is downgraded to read-only.
# The feature flag controls when the downgrade is enforced.
if required == "read" or not effective_settings.pangolin_bypass_scope_downgrade:
return AuthContext(
source="pangolin-bypass",
scope="read",
api_key_id=None,
ip=client_ip,
)
# Downgrade enforced: bypass cannot satisfy print/admin
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={
"error_code": "bypass_scope_downgraded",
"error_message": (
"Pangolin bypass is read-only. Use X-Label-Hub-Key for write operations."
),
},
return AuthContext(
source="pangolin-bypass",
scope=required,
api_key_id=None,
ip=client_ip,
)
if _is_pangolin_bypass(request):
return AuthContext(
source="pangolin-bypass",
scope="admin",
api_key_id=None,
ip=client_ip,
)

Comment on lines +359 to +362
assert resp.status_code == 200
body = resp.json()
assert body["source"] == "pangolin-sso"
assert body["scope"] == required_scope

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Da voll vertrauenswürdige SSO- und Bypass-Sitzungen nun immer den Scope "admin" zurückgeben, sollte die Test-Assertion angepasst werden, um zu überprüfen, ob der zurückgegebene Scope tatsächlich "admin" ist, unabhängig vom angeforderten Scope.

Suggested change
assert resp.status_code == 200
body = resp.json()
assert body["source"] == "pangolin-sso"
assert body["scope"] == required_scope
assert resp.status_code == 200
body = resp.json()
assert body["source"] == "pangolin-sso"
assert body["scope"] == "admin"

strausmann added a commit that referenced this pull request Jun 23, 2026
…min-Routes) (#134)

PR #130/#132 hatten WithAuthFrom im oapi-Client um X-Pangolin-Token und
Remote-User ergänzt. forwardAuth in admin_api_keys.go ist eine zweite,
parallele Implementierung für die Admin-Routes (/admin/printers,
/admin/api-keys) die raw http.DefaultClient verwendet — die Header-Liste
dort wurde nicht mitgezogen.

Konsequenz: SSO-User konnten Read-Routes nutzen, alle Admin-Routes gaben
weiterhin 503 weil das Backend mit "missing_credentials" 401 zurück lieferte.

Fix: dieselbe Header-Liste in forwardAuth. Plus Regression-Test
TestListPrintersPage_ForwardetPangolinSSOHeaders der httptest-Server-seitig
beide Header verifiziert.

Race-Detector grün, alle 4 Frontend-Packages grün.

Refs PR #130 (X-Pangolin-Token), PR #132 (Remote-User), PR #133 (ADR 0014
Backend-Seite)
github-actions Bot pushed a commit that referenced this pull request Jun 24, 2026
## 0.11.0 (2026-06-24)

* feat(auth): Pangolin-SSO + Bypass für alle Scopes trusted (ADR 0014) (#133) ([4fe1a91](4fe1a91)), closes [#133](#133) [#78](#78) [130/#132](#132)
* feat(nav): "Drucker" Link für Admin-Drucker-Verwaltung + getrennte ActiveNav-Werte (#135) ([1e982b0](1e982b0)), closes [#135](#135) [#104](#104) [#104](#104) [#104](#104)
* fix(frontend): forward Remote-User zum Backend (Pangolin SSO-Standard-Header) (#132) ([38c0cc3](38c0cc3)), closes [#132](#132) [#130](#130) [#130](#130) [#131](#131) [#130](#130)
* fix(frontend): forward X-Pangolin-Token zum Backend (Browser-User 503-Fix) (#130) ([5fb2038](5fb2038)), closes [#130](#130)
* fix(frontend): forwardAuth ergänzt X-Pangolin-Token + Remote-User (Admin-Routes) (#134) ([af9ee28](af9ee28)), closes [#134](#134) [130/#132](#132) [#130](#130) [#132](#132) [#133](#133)
* chore(deps): bump the go-minor-and-patch group across 1 directory with 2 updates (#128) ([a72dd90](a72dd90)), closes [#128](#128)
* ci(deps): bump lewagon/wait-on-check-action in the actions-all group (#127) ([e4139ab](e4139ab)), closes [#127](#127)
* docs(api): printers.yaml weg, Drucker in DB + /admin/printers Admin-UI (#124) [DRAFT] (#125) ([41bef28](41bef28)), closes [#124](#124) [#125](#125) [#124](#124) [#124](#124) [#124](#124) [#124](#124) [#124](#124) [#3099](https://github.com/strausmann/label-printer-hub/issues/3099) [#124](#124) [#124](#124) [#124](#124) [#124](#124) [#124](#124) [#124](#124) [#3099](https://github.com/strausmann/label-printer-hub/issues/3099) [#124](#124) [#124](#124) [#124](#124) [#124](#124) [#124](#124) [#124](#124) [#124](#124) [#124](#124) [#124](#124) [#124](#124) [#124](#124) [#124](#124) [#124](#124) [#124](#124) [#124](#124) [#124](#124) [compose-passthrou#Pflicht](https://github.com/compose-passthrou/issues/Pflicht)

[skip ci]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant