fix(web): name the scope problem behind a Gitea 403
Both listing endpoints answering 403 means Gitea authenticated the token and then rejected its scope -- exactly what an OIDC login token with only `openid profile` gets. The card showed a bare "Gitea API returned HTTP 403", which sends the operator hunting the wrong thing (admin rights, allowlist, URL). Say what it is and name both fixes: add an API read scope to `[web.oidc] scopes`, or configure the keyring PAT fallback. A 403 on the admin listing alone still falls through to the user listing as before, and a 404 there keeps its plain message.
This commit is contained in:
@@ -54,6 +54,15 @@ _RUNNING = "running"
|
|||||||
_QUEUED = "queued"
|
_QUEUED = "queued"
|
||||||
_UNKNOWN = "unknown"
|
_UNKNOWN = "unknown"
|
||||||
|
|
||||||
|
# Shown when Gitea accepts the token but refuses both listing endpoints: the
|
||||||
|
# token carries no API read scope. Names both fixes, since which one applies
|
||||||
|
# depends on whether the operator can add scopes to the OAuth2 app.
|
||||||
|
_SCOPE_HINT = (
|
||||||
|
"Gitea accepted the token but refused it (HTTP 403): it carries no API "
|
||||||
|
"read scope. Add read:repository (or read:admin) to [web.oidc] scopes and "
|
||||||
|
"log in again, or configure a keyring PAT via [web] gitea_token_cred."
|
||||||
|
)
|
||||||
|
|
||||||
# Gitea's ``status`` field, for runs that have not reached a conclusion yet.
|
# Gitea's ``status`` field, for runs that have not reached a conclusion yet.
|
||||||
_STATUS_MAP = {
|
_STATUS_MAP = {
|
||||||
"in_progress": _RUNNING,
|
"in_progress": _RUNNING,
|
||||||
@@ -250,6 +259,11 @@ def _fetch_runs(
|
|||||||
if exc.code in (403, 404) and source == "admin":
|
if exc.code in (403, 404) and source == "admin":
|
||||||
# Token is not admin-scoped: try the per-user listing.
|
# Token is not admin-scoped: try the per-user listing.
|
||||||
continue
|
continue
|
||||||
|
if exc.code == 403:
|
||||||
|
# Both endpoints refused an otherwise-valid token: Gitea
|
||||||
|
# authenticated it and then rejected its scope. Say so --
|
||||||
|
# "HTTP 403" alone sends the operator hunting the wrong thing.
|
||||||
|
last_error = _SCOPE_HINT
|
||||||
break
|
break
|
||||||
except (urllib.error.URLError, TimeoutError, OSError) as exc:
|
except (urllib.error.URLError, TimeoutError, OSError) as exc:
|
||||||
last_error = f"Gitea unreachable: {exc}."
|
last_error = f"Gitea unreachable: {exc}."
|
||||||
|
|||||||
@@ -330,9 +330,10 @@ def test_admin_denied_falls_back_to_user_endpoint(
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_both_endpoints_denied_degrades(
|
def test_both_endpoints_denied_names_the_scope_problem(
|
||||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""403 on both listings means "valid token, no API scope" -- say that."""
|
||||||
_install_urlopen(monkeypatch, lambda _url: _http_error(403))
|
_install_urlopen(monkeypatch, lambda _url: _http_error(403))
|
||||||
|
|
||||||
view = list_executions(_make_config(tmp_path), [], token="tok", now=_NOW)
|
view = list_executions(_make_config(tmp_path), [], token="tok", now=_NOW)
|
||||||
@@ -341,6 +342,24 @@ def test_both_endpoints_denied_degrades(
|
|||||||
assert view.source == "none"
|
assert view.source == "none"
|
||||||
assert view.error is not None
|
assert view.error is not None
|
||||||
assert "403" in view.error
|
assert "403" in view.error
|
||||||
|
assert "read:repository" in view.error
|
||||||
|
assert "gitea_token_cred" in view.error
|
||||||
|
assert view.auth_failed is False # a refresh would not fix a scope problem
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_endpoint_404_keeps_the_plain_http_error(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
"""Only a 403 on the *user* listing implies a scope problem, not a 404."""
|
||||||
|
|
||||||
|
def handler(url: str) -> Any:
|
||||||
|
return _http_error(403 if "/admin/" in url else 404)
|
||||||
|
|
||||||
|
_install_urlopen(monkeypatch, handler)
|
||||||
|
|
||||||
|
view = list_executions(_make_config(tmp_path), [], token="tok", now=_NOW)
|
||||||
|
|
||||||
|
assert view.error == "Gitea API returned HTTP 404."
|
||||||
|
|
||||||
|
|
||||||
# ── degraded paths ──────────────────────────────────────────────────────────
|
# ── degraded paths ──────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user