feat(security): resource-type-aware authorization with fail-closed defaults

Add aegis_gitea_mcp.authz: classify every dispatched call (typed tools and
gitea_request) by resource type (repository/org/user_self/user_owned/
misc_global/admin/unknown) and enforce a type-specific rule in service-PAT
mode, on top of policy + WRITE_MODE. Every decision fails closed:

- org: signed-in user must be a verified org member (Gitea-checked).
- user_owned: owner must be the caller or a member org of the caller.
- user_self: token-owner-scoped endpoints denied (token is the bot's).
- admin: default-deny; allowed only with RAW_API_ALLOW_SENSITIVE opt-in AND a
  verified site admin.
- misc_global: reads allowed, writes denied.
- unknown / unverifiable: denied and audited.

Wire it into the server's service-PAT dispatch: repository calls keep the
existing per-user collaborator check; non-repo calls (previously blanket-denied)
now go through the resource-type gate, opening the org/user/admin surface
safely. Verification results are cached briefly (fail-closed: positives only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
LatteandClaude Opus 4.8 committed 2026-06-27 11:09:50 +02:00
1 parent 2d7f12d0d0
commit 3392d8f69b
4 files changed
+629 -21

No files matched your search

+23 -21
View File
@@ -19,6 +19,7 @@ from fastapi.responses import JSONResponse, PlainTextResponse, RedirectResponse,
from pydantic import BaseModel, Field, ValidationError
from aegis_gitea_mcp.audit import get_audit_logger
from aegis_gitea_mcp.authz import authorize_non_repository_access, classify_tool
from aegis_gitea_mcp.automation import AutomationError, AutomationManager
from aegis_gitea_mcp.cache import BoundedTTLCache
from aegis_gitea_mcp.config import get_settings
@@ -1143,33 +1144,34 @@ async def _execute_tool_call(
raise HTTPException(status_code=401, detail="Missing authenticated user token context")
if settings.gitea_token.strip():
if not repository:
# list_repositories is not repo-scoped; the handler scopes it to
# the authenticated user's own repositories instead. Every other
# tool requires a repository target so per-user permission can be
# verified before the privileged service PAT is used.
if tool_name != "list_repositories":
audit.log_access_denied(
tool_name=tool_name,
reason="service_pat_requires_repository_target",
correlation_id=correlation_id,
)
raise HTTPException(
status_code=403,
detail=(
"Service PAT mode requires a repository target so per-user "
"permission can be verified."
),
)
else:
user_login = get_gitea_user_login()
user_login = get_gitea_user_login() or ""
if repository:
# Repository-scoped: verify the signed-in user's collaborator
# permission before the privileged service PAT is used.
await _verify_user_repository_access(
repository=repository,
required_scope=required_scope,
user_login=user_login or "",
user_login=user_login,
correlation_id=correlation_id,
tool_name=tool_name,
)
elif tool_name == "list_repositories":
# Not repo-scoped; the handler scopes it to the authenticated
# user's own repositories.
pass
else:
# Non-repository call (org/user/admin/misc, incl. gitea_request):
# classify by resource type and enforce the fail-closed rule.
classification = classify_tool(tool_name, arguments)
try:
await authorize_non_repository_access(
classification=classification,
user_login=user_login,
tool_name=tool_name,
correlation_id=correlation_id,
)
except ToolError as exc:
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
# In OAuth mode, Gitea OIDC access_tokens can't call the Gitea REST API
# (they only carry OIDC scopes). If a service PAT is configured via