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:
4 files changed
+629
-21
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user