Dev #72

Merged
Latte merged 71 commits from dev into main 2026-10-14 17:52:23 +00:00
2 changed files with 35 additions and 5 deletions
Showing only changes of commit c73a7cc3ce - Show all commits

No files matched your search

+8 -3
View File
@@ -9,6 +9,7 @@ token to the :class:`~aegis_gitea_mcp.identity.Identity` a tool call runs as.
from __future__ import annotations from __future__ import annotations
import asyncio
import hashlib import hashlib
import hmac import hmac
import time import time
@@ -103,6 +104,7 @@ class AdminIdentityResolver:
self._executor = executor self._executor = executor
self._login: str | None = None self._login: str | None = None
self._retry_at = 0.0 self._retry_at = 0.0
self._lookup_lock = asyncio.Lock()
async def resolve(self, access: AccessToken | None) -> Identity: async def resolve(self, access: AccessToken | None) -> Identity:
"""Return the admin identity, or raise for an access token of another kind.""" """Return the admin identity, or raise for an access token of another kind."""
@@ -113,9 +115,12 @@ class AdminIdentityResolver:
raise ToolError("admin-token mode needs GITEA_TOKEN, which is not configured") raise ToolError("admin-token mode needs GITEA_TOKEN, which is not configured")
token = secret.get_secret_value() token = secret.get_secret_value()
if self._login is None and time.monotonic() >= self._retry_at: if self._login is None and time.monotonic() >= self._retry_at:
self._login = await self._lookup_login(token) # One lookup for concurrent first calls: later callers wait, then see the result.
if self._login is None: async with self._lookup_lock:
self._retry_at = time.monotonic() + _LOGIN_RETRY_SECONDS if self._login is None and time.monotonic() >= self._retry_at:
self._login = await self._lookup_login(token)
if self._login is None:
self._retry_at = time.monotonic() + _LOGIN_RETRY_SECONDS
return Identity(login=self._login, mode="admin-token", token=token) return Identity(login=self._login, mode="admin-token", token=token)
async def _lookup_login(self, token: str) -> str | None: async def _lookup_login(self, token: str) -> str | None:
+27 -2
View File
@@ -17,8 +17,13 @@ from aegis_gitea_mcp.auth import (
StaticBearerVerifier, StaticBearerVerifier,
) )
from aegis_gitea_mcp.errors import ToolError from aegis_gitea_mcp.errors import ToolError
from aegis_gitea_mcp.gitea import GiteaExecutor from aegis_gitea_mcp.gitea import GiteaExecutor, ToolOutcome
from tests.test_server_helpers import API, BEARER_TOKEN, GITEA_TOKEN, server_settings from tests.test_server_helpers import (
API,
BEARER_TOKEN,
GITEA_TOKEN,
server_settings,
)
# Static bearer verifier # Static bearer verifier
@@ -171,3 +176,23 @@ async def test_missing_gitea_token_is_refused() -> None:
with pytest.raises(ToolError, match="GITEA_TOKEN"): with pytest.raises(ToolError, match="GITEA_TOKEN"):
await resolver.resolve(None) await resolver.resolve(None)
async def test_concurrent_first_calls_send_one_login_lookup() -> None:
import asyncio
lookups: list[str] = []
class SlowExecutor(GiteaExecutor):
async def request(self, method: str, path: str, *args: Any, **kwargs: Any) -> ToolOutcome:
lookups.append(path)
await asyncio.sleep(0.01) # a real network round trip yields to other calls
return ToolOutcome(False, {"data": {"login": "admin"}}, 200)
settings = server_settings()
resolver = AdminIdentityResolver(settings, SlowExecutor(settings))
identities = await asyncio.gather(*(resolver.resolve(None) for _ in range(10)))
assert {identity.login for identity in identities} == {"admin"}
assert lookups == ["/user"]