fix(web): fall back to the PAT when Gitea refuses the login token
The login token took precedence unconditionally, so a configured keyring PAT could never help while an operator was logged in — and on this instance the OIDC token (scopes `openid profile`) is exactly what Gitea refuses with a 403 for insufficient API scope. `ExecutionsView` now carries `refused` (401 or a final 403) and the web layer retries once with the PAT when the token it used was not already the PAT. A refused PAT is never sent twice, and a 401 still gets its single refresh attempt first.
This commit is contained in:
@@ -565,6 +565,94 @@ def test_login_token_is_used_for_the_card(
|
||||
assert tokens == ["login-token"]
|
||||
|
||||
|
||||
def test_scope_refused_login_token_falls_back_to_the_pat(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""A logged-in operator whose token lacks API scope must still get runs."""
|
||||
_install_fake_keyring(monkeypatch)
|
||||
app = create_app(_make_config(tmp_path, oidc=True, token_cred=_PAT_CRED_NAME))
|
||||
app.state.oauth.gitea = _StubOAuthClient({"access_token": "scope-less"})
|
||||
app.dependency_overrides[get_backend] = lambda: _FakeBackend([])
|
||||
client = TestClient(app)
|
||||
_login(app, client)
|
||||
|
||||
seen: list[str | None] = []
|
||||
|
||||
def fake_list_executions(
|
||||
_config: Config, _vms: list[LiveVm], *, token: str | None, **_kw: Any
|
||||
) -> ExecutionsView:
|
||||
seen.append(token)
|
||||
if token == "scope-less":
|
||||
return ExecutionsView(
|
||||
runs=[],
|
||||
unmatched_vms=[],
|
||||
source="none",
|
||||
error="no API read scope",
|
||||
refused=True,
|
||||
)
|
||||
return ExecutionsView(
|
||||
runs=[_execution()], unmatched_vms=[], source="admin", error=None
|
||||
)
|
||||
|
||||
monkeypatch.setattr(app_module, "list_executions", fake_list_executions)
|
||||
|
||||
resp = client.get("/executions")
|
||||
|
||||
assert seen == ["scope-less", _FAKE_PAT]
|
||||
assert "#12" in resp.text
|
||||
|
||||
|
||||
def test_no_pat_fallback_when_none_configured(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Without a PAT there is nothing to retry with -- one call, then degrade."""
|
||||
_install_fake_keyring(monkeypatch)
|
||||
app = create_app(_make_config(tmp_path, oidc=True))
|
||||
app.state.oauth.gitea = _StubOAuthClient({"access_token": "scope-less"})
|
||||
app.dependency_overrides[get_backend] = lambda: _FakeBackend([])
|
||||
client = TestClient(app)
|
||||
_login(app, client)
|
||||
tokens = _stub_view(
|
||||
monkeypatch,
|
||||
ExecutionsView(
|
||||
runs=[],
|
||||
unmatched_vms=[],
|
||||
source="none",
|
||||
error="no API read scope",
|
||||
refused=True,
|
||||
),
|
||||
)
|
||||
|
||||
resp = client.get("/executions")
|
||||
|
||||
assert tokens == ["scope-less"]
|
||||
assert "no API read scope" in resp.text
|
||||
|
||||
|
||||
def test_pat_is_not_retried_against_itself(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""A refused PAT (no login token in play) must not be sent twice."""
|
||||
_install_fake_keyring(monkeypatch)
|
||||
tokens = _stub_view(
|
||||
monkeypatch,
|
||||
ExecutionsView(
|
||||
runs=[],
|
||||
unmatched_vms=[],
|
||||
source="none",
|
||||
error="no API read scope",
|
||||
refused=True,
|
||||
),
|
||||
)
|
||||
app = create_app(_make_config(tmp_path, token_cred=_PAT_CRED_NAME))
|
||||
app.dependency_overrides[get_backend] = lambda: _FakeBackend([])
|
||||
client = TestClient(app)
|
||||
|
||||
client.get("/executions")
|
||||
|
||||
assert tokens == [_FAKE_PAT]
|
||||
|
||||
|
||||
def test_401_triggers_one_refresh_then_retries(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user