feat(mcp): migrations OAuth/usage + câblage app.py (#114)
Ajoute les migrations 0008-0011 (oauth_clients, oauth_codes, oauth_tokens, mcp_usage) et câble le serveur d'autorisation OAuth 2.1 dans le bloc DASH_MCP_ENABLED de app.py : init des schémas oauth/usage, purge périodique, enregistrement des routes /oauth et /.well-known, et exemption CSRF de ces routes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+23
@@ -137,6 +137,29 @@ if _mcp_enabled:
|
|||||||
|
|
||||||
init_mcp_auth(app.server)
|
init_mcp_auth(app.server)
|
||||||
|
|
||||||
|
# Serveur d'autorisation OAuth 2.1 (scope B2, #114) : découverte, DCR,
|
||||||
|
# authorize/token, révocation. Réutilise la session flask_login.
|
||||||
|
from src.mcp import usage # noqa: E402
|
||||||
|
from src.mcp.oauth import routes as oauth_routes # noqa: E402
|
||||||
|
from src.mcp.oauth import store as oauth_store # noqa: E402
|
||||||
|
|
||||||
|
_users_db = os.environ["USERS_DB_PATH"]
|
||||||
|
oauth_store.init_schema(_users_db)
|
||||||
|
usage.init_schema(_users_db)
|
||||||
|
usage.purge_older_than(_users_db)
|
||||||
|
|
||||||
|
oauth_routes.init_oauth(app.server)
|
||||||
|
|
||||||
|
# Exempter les routes OAuth du CSRF (POST externes JSON-RPC/form sans jeton).
|
||||||
|
if _auth_csrf is not None:
|
||||||
|
for _rule in app.server.url_map.iter_rules():
|
||||||
|
if _rule.rule.startswith("/oauth") or _rule.rule.startswith(
|
||||||
|
"/.well-known/oauth"
|
||||||
|
):
|
||||||
|
_vf = app.server.view_functions.get(_rule.endpoint)
|
||||||
|
if _vf is not None:
|
||||||
|
_auth_csrf.exempt(_vf)
|
||||||
|
|
||||||
from src.subscriptions.setup import init_subscriptions # noqa: E402
|
from src.subscriptions.setup import init_subscriptions # noqa: E402
|
||||||
|
|
||||||
init_subscriptions(app.server)
|
init_subscriptions(app.server)
|
||||||
|
|||||||
@@ -46,6 +46,34 @@ _MIGRATIONS: list[tuple[str, str]] = [
|
|||||||
"0007_add_kind_to_api_tokens",
|
"0007_add_kind_to_api_tokens",
|
||||||
"ALTER TABLE api_tokens ADD COLUMN kind TEXT NOT NULL DEFAULT 'api'",
|
"ALTER TABLE api_tokens ADD COLUMN kind TEXT NOT NULL DEFAULT 'api'",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"0008_create_oauth_clients",
|
||||||
|
"CREATE TABLE IF NOT EXISTS oauth_clients ("
|
||||||
|
"client_id TEXT PRIMARY KEY, client_metadata TEXT NOT NULL, "
|
||||||
|
"created_at TEXT NOT NULL)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"0009_create_oauth_codes",
|
||||||
|
"CREATE TABLE IF NOT EXISTS oauth_codes ("
|
||||||
|
"code_hash TEXT PRIMARY KEY, client_id TEXT NOT NULL, user_id INTEGER NOT NULL, "
|
||||||
|
"redirect_uri TEXT, code_challenge TEXT, code_challenge_method TEXT, "
|
||||||
|
"scope TEXT, resource TEXT, expires_at TEXT NOT NULL, used INTEGER NOT NULL DEFAULT 0)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"0010_create_oauth_tokens",
|
||||||
|
"CREATE TABLE IF NOT EXISTS oauth_tokens ("
|
||||||
|
"id INTEGER PRIMARY KEY, access_token_hash TEXT NOT NULL UNIQUE, "
|
||||||
|
"refresh_token_hash TEXT UNIQUE, client_id TEXT NOT NULL, user_id INTEGER NOT NULL, "
|
||||||
|
"scope TEXT, resource TEXT NOT NULL, issued_at TEXT NOT NULL, "
|
||||||
|
"access_expires_at TEXT NOT NULL, refresh_expires_at TEXT, revoked_at TEXT, "
|
||||||
|
"count_total INTEGER NOT NULL DEFAULT 0, last_used_at TEXT)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"0011_create_mcp_usage",
|
||||||
|
"CREATE TABLE IF NOT EXISTS mcp_usage ("
|
||||||
|
"id INTEGER PRIMARY KEY, user_id INTEGER, token_id INTEGER, "
|
||||||
|
"kind TEXT NOT NULL, created_at TEXT NOT NULL)",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -49,3 +49,41 @@ def test_mcp_endpoint_guarded_and_csrf_exempt(monkeypatch, tmp_path):
|
|||||||
else:
|
else:
|
||||||
del sys.modules[name]
|
del sys.modules[name]
|
||||||
auth_db.reset_conn_for_tests()
|
auth_db.reset_conn_for_tests()
|
||||||
|
|
||||||
|
|
||||||
|
def test_oauth_wellknown_served_when_mcp_enabled(monkeypatch, tmp_path):
|
||||||
|
# DB éphémère + secrets requis par init_auth/init_subscriptions.
|
||||||
|
monkeypatch.setenv("USERS_DB_PATH", str(tmp_path / "users.test.sqlite"))
|
||||||
|
monkeypatch.setenv("SECRET_KEY", "test-secret-key")
|
||||||
|
monkeypatch.setenv("APP_BASE_URL", "https://colibre.fr")
|
||||||
|
monkeypatch.setenv("DASH_MCP_ENABLED", "true")
|
||||||
|
|
||||||
|
from src.auth import db as auth_db
|
||||||
|
|
||||||
|
auth_db.reset_conn_for_tests()
|
||||||
|
|
||||||
|
# Voir commentaire ci-dessus : reload de src.app pollue sys.modules pour
|
||||||
|
# src.pages.* ; on snapshot/restaure de la même façon.
|
||||||
|
snapshot = {
|
||||||
|
name: mod
|
||||||
|
for name, mod in sys.modules.items()
|
||||||
|
if name == "src.app" or name.startswith("src.pages")
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
import src.app as app_module
|
||||||
|
|
||||||
|
app_module = importlib.reload(app_module)
|
||||||
|
client = app_module.app.server.test_client()
|
||||||
|
|
||||||
|
resp = client.get("/.well-known/oauth-protected-resource")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.get_json()["resource"] == "https://colibre.fr/_mcp"
|
||||||
|
finally:
|
||||||
|
for name in [
|
||||||
|
n for n in sys.modules if n == "src.app" or n.startswith("src.pages")
|
||||||
|
]:
|
||||||
|
if name in snapshot:
|
||||||
|
sys.modules[name] = snapshot[name]
|
||||||
|
else:
|
||||||
|
del sys.modules[name]
|
||||||
|
auth_db.reset_conn_for_tests()
|
||||||
|
|||||||
Reference in New Issue
Block a user