-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix(auth): offer the API-key login on server-mode admin 403s #1569
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
paoloantinori
wants to merge
2
commits into
debpalash:main
Choose a base branch
from
paoloantinori:fix/remote-admin-403-auth-gate
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| """Cross-layer contract lock for the admin-gate 403 detail string. | ||
|
|
||
| The backend's ``require_admin``/``require_admin_action`` answer 403 with a | ||
| mode-distinct ``detail`` (``_admin_gate_403`` in backend/api/dependencies.py): | ||
| "loopback origin or admin API key required" in server mode, plain | ||
| "loopback origin required" on the desktop build. The SPA's ``apiFetch`` routes | ||
| a 403 to the API-key login gate exactly when the detail contains the substring | ||
| "admin api key" (frontend/src/api/client.ts) — i.e. when presenting the key | ||
| could actually satisfy the gate. The per-mode behaviour is pinned by | ||
| tests/test_loopback_server_mode.py; this file pins the LITERAL contract across | ||
| layers: a backend reword keeps backend tests green while the frontend matcher | ||
| silently stops firing, and a LAN user is back to raw 403 spam instead of the | ||
| login form. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import re | ||
| from pathlib import Path | ||
|
|
||
| ROOT = Path(__file__).resolve().parents[1] | ||
| DEPS = ROOT / "backend" / "api" / "dependencies.py" | ||
| CLIENT = ROOT / "frontend" / "src" / "api" / "client.ts" | ||
|
|
||
|
|
||
| def _frontend_sniff() -> str: | ||
| """The substring apiFetch matches on a 403 to admit it to the auth gate.""" | ||
| text = CLIENT.read_text(encoding="utf-8") | ||
| # adminGate403 = ... detail.toLowerCase().includes('<sniff>') | ||
| m = re.search(r"adminGate403 =.*?includes\('([^']+)'\)", text, re.DOTALL) | ||
| assert m, "adminGate403 matcher not found in frontend/src/api/client.ts" | ||
| return m.group(1) | ||
|
|
||
|
|
||
| def _key_named_details() -> set[str]: | ||
| """Every quoted string in dependencies.py that names the admin API key.""" | ||
| return set(re.findall(r'"([^"]*admin API key[^"]*)"', DEPS.read_text(encoding="utf-8"))) | ||
|
|
||
|
|
||
| def test_key_named_details_match_frontend_sniff(): | ||
| """Every backend literal naming the admin key must contain the SPA matcher.""" | ||
| details = _key_named_details() | ||
| assert details, ( | ||
| "no 'admin API key' detail literal left in dependencies.py — moved or " | ||
| "reworded? Update frontend/src/api/client.ts in the same change." | ||
| ) | ||
| sniff = _frontend_sniff() | ||
| for detail in details: | ||
| # Case-insensitive substring, mirroring apiFetch's toLowerCase match. | ||
| assert sniff in detail.lower(), ( | ||
| f"backend detail {detail!r} no longer contains the frontend matcher " | ||
| f"{sniff!r} — the SPA would stop routing it to the API-key gate. " | ||
| "Update frontend/src/api/client.ts in the same change." | ||
| ) | ||
|
|
||
|
|
||
| def test_frontend_sniff_rejects_details_a_key_cannot_fix(): | ||
| """The sniff must not swallow 403s an API key cannot satisfy. | ||
|
|
||
| The desktop admin-gate arm (loopback-only regardless of credentials), the | ||
| legacy require_loopback desktop 403, the CSRF rejection, and the | ||
| desktop-only filesystem gate: routing any of these to the login form would | ||
| trap the user in a form that can never succeed. | ||
| """ | ||
| sniff = _frontend_sniff() | ||
| unfixable = ( | ||
| "loopback origin required", # desktop admin arm + require_loopback | ||
| "browser origin rejected", # BearerKeyMiddleware CSRF (main.py) | ||
| "desktop origin required", # require_desktop — loopback-only forever | ||
| "native filesystem access requires loopback origin", # require_native | ||
| ) | ||
| for detail in unfixable: | ||
| assert sniff not in detail.lower(), ( | ||
| f"frontend matcher {sniff!r} now also matches {detail!r}, which " | ||
| "an API key cannot satisfy — the login gate would loop." | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.