fix: redact identity secrets through a public hook and mask issued agm_ tokens
ci / gitleaks (push) Successful in 8s
ci / docs-check (push) Failing after 30s
ci / lint (push) Successful in 42s
ci / test-integration (push) Successful in 46s
ci / security (push) Failing after 27s
ci / test-mcp (push) Successful in 1m16s
ci / test-unit (push) Successful in 2m17s
ci / build-and-push (push) Successful in 17s

Identity gains a redact field that the executor honours, so the OAuth
resolver no longer needs an executor subclass overriding private
internals. Opaque agm_ tokens issued by our authorization server are now
masked by the secret patterns wherever they appear in content.
This commit is contained in:
Latte committed 2026-10-14 16:45:36 +02:00
1 parent e02cede9f2
commit ce5ae6caa5
8 files changed
+37 -38

No files matched your search

+3 -2
View File
@@ -15,10 +15,11 @@ from starlette.routing import Route
from aegis_gitea_mcp.audit import AuditLogger
from aegis_gitea_mcp.auth import MCP_SCOPE, AdminIdentityResolver
from aegis_gitea_mcp.config import Settings
from aegis_gitea_mcp.gitea import GiteaExecutor
from aegis_gitea_mcp.oauth import OAuthSetupError
from aegis_gitea_mcp.oauth.callback import build_browser_routes
from aegis_gitea_mcp.oauth.provider import CONSENT_PATH, GiteaOAuthProvider, OAuthTokenVerifier
from aegis_gitea_mcp.oauth.resolver import OAuthGiteaExecutor, OAuthIdentityResolver
from aegis_gitea_mcp.oauth.resolver import OAuthIdentityResolver
from aegis_gitea_mcp.oauth.store import OAuthStore, OAuthStoreError
from aegis_gitea_mcp.oauth.upstream import CALLBACK_PATH, GiteaUpstream
from aegis_gitea_mcp.transports import HttpAuthExtras
@@ -88,7 +89,7 @@ def build_oauth_components(
upstream = GiteaUpstream(settings, client_factory=client_factory, clock=clock)
provider = GiteaOAuthProvider(settings, store, upstream, clock=clock, audit=audit)
executor = OAuthGiteaExecutor(
executor = GiteaExecutor(
settings, client=executor_client, extra_secrets=[key.get_secret_value()]
)
admin = AdminIdentityResolver(settings, executor) if settings.gitea_token is not None else None
+3 -25
View File
@@ -18,7 +18,6 @@ import dataclasses
import logging
import time
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from mcp.server.auth.provider import AccessToken
@@ -26,14 +25,12 @@ from mcp.server.auth.provider import AccessToken
from aegis_gitea_mcp.audit import AuditLogger, get_audit_logger
from aegis_gitea_mcp.auth import STATIC_CLIENT_ID, IdentityResolver
from aegis_gitea_mcp.errors import ToolError
from aegis_gitea_mcp.gitea import GiteaExecutor
from aegis_gitea_mcp.gitea.redaction import normalize_secrets
from aegis_gitea_mcp.identity import Identity
from aegis_gitea_mcp.oauth.store import OAuthStore, OAuthStoreError, SessionRecord
from aegis_gitea_mcp.oauth.upstream import GiteaUpstream, UpstreamError, UpstreamInvalidGrant
if TYPE_CHECKING:
from aegis_gitea_mcp.gitea.executor import _Call
pass
logger = logging.getLogger(__name__)
@@ -47,26 +44,6 @@ OAUTH_ONLY = (
)
@dataclass(frozen=True)
class SessionIdentity(Identity):
"""An OAuth identity that also names session secrets to redact from results.
Attributes:
redact: Further secrets of the session (the Gitea refresh token) that must
never appear in a result. Excluded from ``repr`` and comparisons.
"""
redact: tuple[str, ...] = field(default=(), repr=False, compare=False)
class OAuthGiteaExecutor(GiteaExecutor):
"""Executor that also redacts the session secrets carried by a :class:`SessionIdentity`."""
def _secrets(self, identity: Identity, call: _Call) -> tuple[str, ...]:
extra = identity.redact if isinstance(identity, SessionIdentity) else ()
return normalize_secrets([*super()._secrets(identity, call), *extra])
class OAuthIdentityResolver:
"""Maps a verified access token to the identity a tool call runs as.
@@ -120,7 +97,8 @@ class OAuthIdentityResolver:
if self._needs_refresh(session):
session = await self._refresh(session_id)
redact = (session.gitea_refresh_token,) if session.gitea_refresh_token else ()
return SessionIdentity(
# The executor redacts ``Identity.redact`` (the Gitea refresh token) from results.
return Identity(
login=session.login, mode="oauth", token=session.gitea_access_token, redact=redact
)