Gitea MCP OAuth 브리지 (DCR, Gitea 로그인)
Gitea에 동적 클라이언트 등록이 없어 MCP 클라이언트가 URL만으로 OAuth 연결을 못 하는 문제를 이 브리지가 인가 서버 역할을 대신해 해결한다. G3X의 plane-mcp-oauth-bridge(upstream_oauth 모드)에서 가져와 Gitea 전용으로 정리했다: Plane PAT 연결 페이지·워크스페이스 헤더·bridge_mode 분기를 없앴다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
916d149ab5
18 files changed
+1494
No files matched your search
@@ -0,0 +1,108 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import base64
|
||||
import time
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from app.crypto import encrypt_token
|
||||
from app.config import get_settings
|
||||
|
||||
|
||||
def _pkce_pair() -> tuple[str, str]:
|
||||
verifier = "test-verifier-string-long-enough-1234567890"
|
||||
digest = hashlib.sha256(verifier.encode("ascii")).digest()
|
||||
challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
|
||||
return verifier, challenge
|
||||
|
||||
|
||||
def test_health(client):
|
||||
resp = client.get("/health")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == {"ok": True}
|
||||
|
||||
|
||||
def test_registration_disabled_by_flag(client, monkeypatch):
|
||||
monkeypatch.setenv("DCR_ENABLED", "false")
|
||||
get_settings.cache_clear()
|
||||
resp = client.post("/register", json={"client_name": "x", "redirect_uris": ["http://localhost:1/cb"]})
|
||||
assert resp.status_code == 400
|
||||
assert resp.json()["error"] == "registration_not_supported"
|
||||
|
||||
|
||||
def test_authorize_rejects_unknown_client(client):
|
||||
_, challenge = _pkce_pair()
|
||||
resp = client.get(
|
||||
"/authorize",
|
||||
params={
|
||||
"response_type": "code",
|
||||
"client_id": "unknown",
|
||||
"redirect_uri": "http://localhost:8787/callback",
|
||||
"code_challenge": challenge,
|
||||
"code_challenge_method": "S256",
|
||||
},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_authorize_starts_for_allowed_client(client, database, monkeypatch):
|
||||
settings = get_settings()
|
||||
user = database.upsert_user("sub-1", "u@example.com")
|
||||
database.set_upstream_tokens(user.id, encrypt_token(settings.token_encryption_key, "gitea_tok"), None, time.time() + 3600)
|
||||
|
||||
import app.main as main
|
||||
|
||||
monkeypatch.setattr(main, "session_user_id", lambda request: user.id)
|
||||
|
||||
_, challenge = _pkce_pair()
|
||||
resp = client.get(
|
||||
"/authorize",
|
||||
params={
|
||||
"response_type": "code",
|
||||
"client_id": "cursor",
|
||||
"redirect_uri": "http://localhost:8787/callback",
|
||||
"code_challenge": challenge,
|
||||
"code_challenge_method": "S256",
|
||||
"state": "abc",
|
||||
},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert resp.status_code == 302
|
||||
assert resp.headers["location"].startswith("http://localhost:8787/callback?")
|
||||
assert "code=" in resp.headers["location"]
|
||||
assert "state=abc" in resp.headers["location"]
|
||||
|
||||
|
||||
def test_mcp_without_token_returns_401(client):
|
||||
resp = client.post("/mcp", json={"jsonrpc": "2.0", "method": "ping", "id": 1})
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
def test_mcp_proxies_with_gitea_token(client, database):
|
||||
settings = get_settings()
|
||||
user = database.upsert_user("sub-2", "u2@example.com")
|
||||
database.set_upstream_tokens(user.id, encrypt_token(settings.token_encryption_key, "gitea_secret"), None, time.time() + 3600)
|
||||
token = database.create_access_token("cursor", user.id)
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = b'{"ok":true}'
|
||||
mock_response.headers = {"content-type": "application/json"}
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.request = AsyncMock(return_value=mock_response)
|
||||
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
mock_client.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
with patch("app.main.httpx.AsyncClient", return_value=mock_client):
|
||||
resp = client.post(
|
||||
"/mcp",
|
||||
json={"jsonrpc": "2.0", "method": "tools/list", "id": 1},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
mock_client.request.assert_awaited_once()
|
||||
call_kwargs = mock_client.request.await_args.kwargs
|
||||
assert call_kwargs["headers"]["Authorization"] == "Bearer gitea_secret"
|
||||
Reference in new issue
Block a user