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
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:
1 parent
e02cede9f2
commit
ce5ae6caa5
8 files changed
+37
-38
No files matched your search
@@ -11,13 +11,12 @@ from mcp.server.auth.provider import AccessToken
|
||||
|
||||
from aegis_gitea_mcp.auth import STATIC_CLIENT_ID
|
||||
from aegis_gitea_mcp.errors import ToolError
|
||||
from aegis_gitea_mcp.gitea import GiteaExecutor
|
||||
from aegis_gitea_mcp.identity import Identity
|
||||
from aegis_gitea_mcp.oauth.resolver import (
|
||||
OAUTH_ONLY,
|
||||
SIGN_IN_AGAIN,
|
||||
OAuthGiteaExecutor,
|
||||
OAuthIdentityResolver,
|
||||
SessionIdentity,
|
||||
)
|
||||
from aegis_gitea_mcp.oauth.store import OAuthStore, SessionRecord, TokenRecord
|
||||
from aegis_gitea_mcp.oauth.upstream import UpstreamError, UpstreamInvalidGrant, UpstreamTokens
|
||||
@@ -108,7 +107,6 @@ async def test_oauth_token_resolves_to_the_session_user(tmp_path: Path) -> None:
|
||||
clock = FakeClock()
|
||||
identity = await resolver(make_store(tmp_path, clock), clock=clock).resolve(oauth_access())
|
||||
|
||||
assert isinstance(identity, SessionIdentity)
|
||||
assert (identity.login, identity.mode, identity.token) == ("alice", "oauth", USER_TOKEN)
|
||||
assert identity.redact == (USER_REFRESH,)
|
||||
assert USER_TOKEN not in repr(identity) and USER_REFRESH not in repr(identity)
|
||||
@@ -341,14 +339,12 @@ async def test_audit_failure_does_not_block_ending_a_session(
|
||||
|
||||
|
||||
async def test_executor_redacts_session_secrets(httpx_mock: Any) -> None:
|
||||
executor = OAuthGiteaExecutor(oauth_settings(), extra_secrets=[ENCRYPTION_KEY])
|
||||
executor = GiteaExecutor(oauth_settings(), extra_secrets=[ENCRYPTION_KEY])
|
||||
httpx_mock.add_response(
|
||||
url="https://gitea.example.com/api/v1/version",
|
||||
json={"a": USER_REFRESH, "b": ENCRYPTION_KEY, "c": USER_TOKEN},
|
||||
)
|
||||
identity = SessionIdentity(
|
||||
login="alice", mode="oauth", token=USER_TOKEN, redact=(USER_REFRESH,)
|
||||
)
|
||||
identity = Identity(login="alice", mode="oauth", token=USER_TOKEN, redact=(USER_REFRESH,))
|
||||
|
||||
outcome = await executor.request("GET", "/version", identity)
|
||||
await executor.aclose()
|
||||
|
||||
@@ -10,10 +10,10 @@ import pytest
|
||||
from pytest_httpx import HTTPXMock
|
||||
|
||||
from aegis_gitea_mcp import transports
|
||||
from aegis_gitea_mcp.gitea import GiteaExecutor
|
||||
from aegis_gitea_mcp.oauth import OAuthSetupError
|
||||
from aegis_gitea_mcp.oauth import components as components_module
|
||||
from aegis_gitea_mcp.oauth.components import RATE_LIMITED_PATHS, build_oauth_components
|
||||
from aegis_gitea_mcp.oauth.resolver import OAuthGiteaExecutor
|
||||
from aegis_gitea_mcp.oauth.store import STORE_FILENAME
|
||||
from aegis_gitea_mcp.transports import AccessLogQueryFilter, http_startup_errors, run_http
|
||||
from tests.test_oauth_helpers import (
|
||||
@@ -129,7 +129,7 @@ def test_components_wiring() -> None:
|
||||
settings = oauth_settings()
|
||||
extras = build_oauth_components(settings)
|
||||
|
||||
assert isinstance(extras.executor, OAuthGiteaExecutor)
|
||||
assert isinstance(extras.executor, GiteaExecutor)
|
||||
assert extras.rate_limited_paths == RATE_LIMITED_PATHS
|
||||
paths = {route.path for route in extras.extra_routes} # type: ignore[attr-defined]
|
||||
assert paths == {"/oauth/consent", "/oauth/callback", "/.well-known/oauth-protected-resource"}
|
||||
|
||||
@@ -9,6 +9,19 @@ def test_detect_secrets_api_key_pattern() -> None:
|
||||
assert findings
|
||||
|
||||
|
||||
def test_issued_oauth_tokens_are_masked() -> None:
|
||||
"""Our own ``agm_`` OAuth tokens are masked wherever they show up in content."""
|
||||
issued = "agm_" + "Ab3_-x" * 7 + "Z" # the shape of agm_ + token_urlsafe(32)
|
||||
text = f"pasted by mistake: {issued}."
|
||||
|
||||
findings = detect_secrets(text)
|
||||
masked = sanitize_data({"body": text})
|
||||
|
||||
assert [finding.secret_type for finding in findings] == ["aegis_oauth_token"]
|
||||
assert issued not in masked["body"] and masked["body"].endswith(".")
|
||||
assert detect_secrets("agm_short") == []
|
||||
|
||||
|
||||
def test_sanitize_data_mask_mode() -> None:
|
||||
"""Mask mode should preserve structure while redacting values."""
|
||||
payload = {"content": "api_key=AKIA1234567890ABCDEF"}
|
||||
|
||||
Reference in new issue
Block a user