feat(F.7+F.17): v3.11.0 — Coolify plugin (67 tools) + tool access overhaul
Catch-up sync spanning v3.7.0 → v3.11.0 of the internal repo. Platform - Total tools: 565 → 633 (+68) across 10 plugins (Coolify added) - Tests: 481 → 828 passing New plugin: Coolify (67 tools, Track F.17) - Applications (17): CRUD, lifecycle, logs, env vars - Deployments (5): list/get/cancel/deploy, app history - Servers (8): CRUD, resources, domains, validation - Projects (8), Databases (16, 6 DB types + backups), Services (13) Tool access system (Track F.7 → F.7d) - Scope → category mapping with per-tool `category` + `sensitivity` - Schema v7: `site_tool_toggles(site_id)` + `sites.tool_scope` column - Schema v8: per-site API keys (`api_keys.site_id`) - Plugin-specific access-level presets (WP / WC / Gitea / OpenPanel / Coolify 5-tier) - Credential-requirement notice tailored per plugin and tier - Admin Tools count card on service page - Dropped redundant `write` tier on WP / WP Advanced / WooCommerce (admin-scope tool count = 0 → identical to admin tier) Dashboard - Unified site manage page (Connection / Tool Access / Connect) - /dashboard/keys unified (was /api-keys and /connect) - CSRF interceptor via meta-tag; removed conflicting cookie reader - Tailwind: pre-built CSS (scripts/build-css.sh) replaces CDN Docs - README / DOCKER_README / CLAUDE updated to 633 tools / 10 plugins - CHANGELOG entries for v3.7.0 → v3.11.0 - FastMCP compatibility note updated to 3.x (post-v3.5 upgrade) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -112,6 +112,8 @@ def mock_key_mgr():
|
||||
def mock_db():
|
||||
"""Patch get_database to return a mock."""
|
||||
db = AsyncMock()
|
||||
db.get_site_tool_scope = AsyncMock(return_value="admin")
|
||||
db.get_site_tool_toggles = AsyncMock(return_value={})
|
||||
db.get_site_by_alias = AsyncMock(
|
||||
return_value={
|
||||
"id": "site-uuid-001",
|
||||
@@ -148,6 +150,10 @@ def mock_tool_registry():
|
||||
tool_def = MagicMock()
|
||||
tool_def.name = "wordpress_list_posts"
|
||||
tool_def.description = "List WordPress posts"
|
||||
tool_def.plugin_type = "wordpress"
|
||||
tool_def.required_scope = "read"
|
||||
tool_def.category = "read"
|
||||
tool_def.sensitivity = "normal"
|
||||
tool_def.input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -221,6 +227,62 @@ class TestAuthentication:
|
||||
body = json.loads(response.body)
|
||||
assert "does not match" in body["error"]["message"]
|
||||
|
||||
@pytest.mark.unit
|
||||
async def test_site_scoped_key_wrong_site(self, mock_key_mgr, mock_db):
|
||||
"""Site-scoped key (site_id=A) used for site B should return 403."""
|
||||
# Key is scoped to site-A
|
||||
mock_key_mgr.validate_key.return_value = {
|
||||
"key_id": "key-uuid-001",
|
||||
"user_id": "user-uuid-001",
|
||||
"scopes": "read write",
|
||||
"site_id": "site-uuid-A",
|
||||
}
|
||||
# The site looked up by site_id (A) has alias "blog-a", but the request
|
||||
# is for alias "myblog" (which is site-B in get_site_by_alias).
|
||||
mock_db.get_site = AsyncMock(
|
||||
return_value={
|
||||
"id": "site-uuid-A",
|
||||
"alias": "blog-a",
|
||||
"user_id": "user-uuid-001",
|
||||
"plugin_type": "wordpress",
|
||||
"url": "https://blog-a.example.com",
|
||||
"credentials": b"x",
|
||||
"status": "active",
|
||||
}
|
||||
)
|
||||
request = _make_request(alias="myblog")
|
||||
response = await user_mcp_handler(request)
|
||||
assert response.status_code == 403
|
||||
body = json.loads(response.body)
|
||||
assert "scoped to a different site" in body["error"]["message"]
|
||||
|
||||
@pytest.mark.unit
|
||||
async def test_site_scoped_key_matching_site(self, mock_key_mgr, mock_db, mock_tool_registry):
|
||||
"""Site-scoped key used for the matching alias should pass auth."""
|
||||
mock_key_mgr.validate_key.return_value = {
|
||||
"key_id": "key-uuid-001",
|
||||
"user_id": "user-uuid-001",
|
||||
"scopes": "read write",
|
||||
"site_id": "site-uuid-001",
|
||||
}
|
||||
mock_db.get_site = AsyncMock(
|
||||
return_value={
|
||||
"id": "site-uuid-001",
|
||||
"alias": "myblog",
|
||||
"user_id": "user-uuid-001",
|
||||
"plugin_type": "wordpress",
|
||||
"url": "https://myblog.example.com",
|
||||
"credentials": b"x",
|
||||
"status": "active",
|
||||
}
|
||||
)
|
||||
request = _make_request(alias="myblog", method_name="tools/list")
|
||||
response = await user_mcp_handler(request)
|
||||
assert response.status_code == 200
|
||||
body = json.loads(response.body)
|
||||
assert "result" in body
|
||||
assert "tools" in body["result"]
|
||||
|
||||
|
||||
# ── Site Lookup Tests ────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user