release: v3.3.0 — platform hardening & admin unification (Track F.1–F.8)

Plugin visibility control, UI/UX fixes, unified admin panel,
database-backed settings, and security hardening.

Highlights:
- ENABLED_PLUGINS env var for plugin visibility (F.1)
- Admin designation via ADMIN_EMAILS (F.4a)
- Master key scope control (F.4b)
- Panel unification + settings from UI (F.4c)
- exec() removal, shell injection fix, bcrypt migration (F.8)
- 5 new test suites

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 09:55:49 +02:00
parent f6dbbeaab0
commit 9b70b259bb
47 changed files with 2122 additions and 366 deletions

View File

@@ -33,6 +33,7 @@ class SupabaseClient:
anon_key: str,
service_role_key: str,
meta_url: str | None = None,
meta_auth: str | None = None,
):
"""
Initialize Supabase API client.
@@ -45,12 +46,16 @@ class SupabaseClient:
meta_url: Optional direct postgres-meta URL (e.g. http://localhost:5555).
When provided, postgres-meta calls hit this URL directly instead of
the Kong /pg/ route. Useful when /pg/ is not exposed through Kong.
meta_auth: Optional Basic Auth credentials for postgres-meta (username:password).
When provided, requests to meta_base_url use Basic Auth instead of JWT.
Recommended when postgres-meta is exposed via a public URL.
"""
self.base_url = base_url.rstrip("/")
self.anon_key = anon_key
self.service_role_key = service_role_key
# postgres-meta base: custom URL or Kong /pg/ prefix
self.meta_base_url = (meta_url or f"{self.base_url}/pg").rstrip("/")
self.meta_auth = meta_auth
# Initialize logger
self.logger = logging.getLogger(f"SupabaseClient.{base_url}")
@@ -83,6 +88,23 @@ class SupabaseClient:
return headers
def _get_meta_headers(self, additional_headers: dict | None = None) -> dict[str, str]:
"""Get headers for postgres-meta requests (Basic Auth if configured, else JWT)."""
headers: dict[str, str] = {
"Content-Type": "application/json",
"Accept": "application/json",
}
if self.meta_auth:
encoded = base64.b64encode(self.meta_auth.encode()).decode()
headers["Authorization"] = f"Basic {encoded}"
else:
key = self.service_role_key
headers["apikey"] = key
headers["Authorization"] = f"Bearer {key}"
if additional_headers:
headers.update(additional_headers)
return headers
async def _head_request_headers(
self,
endpoint: str,
@@ -140,7 +162,11 @@ class SupabaseClient:
"""
url = f"{base_url_override or self.base_url}{endpoint}"
headers = self._get_headers(use_service_role, headers_override)
# Use Basic Auth headers for postgres-meta requests when meta_auth is configured
if base_url_override and base_url_override == self.meta_base_url:
headers = self._get_meta_headers(headers_override)
else:
headers = self._get_headers(use_service_role, headers_override)
# Remove Content-Type for binary data
if data is not None:
@@ -163,7 +189,7 @@ class SupabaseClient:
if params:
kwargs["params"] = params
if json_data:
if json_data is not None:
kwargs["json"] = json_data
if data:
kwargs["data"] = data
@@ -681,7 +707,10 @@ class SupabaseClient:
async def empty_bucket(self, bucket_id: str) -> dict:
"""Empty a bucket (delete all files)."""
return await self.request(
"POST", f"/storage/v1/bucket/{bucket_id}/empty", use_service_role=True
"POST",
f"/storage/v1/bucket/{bucket_id}/empty",
json_data={},
use_service_role=True,
)
async def list_files(

View File

@@ -241,13 +241,6 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
"scope": "admin",
},
{
"name": "get_auth_config",
"method_name": "get_auth_config",
"description": "Get current GoTrue authentication configuration.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
{
"name": "search_users",
"method_name": "search_users",
@@ -490,17 +483,6 @@ async def delete_user_factor(client: SupabaseClient, user_id: str, factor_id: st
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
async def get_auth_config(client: SupabaseClient) -> str:
"""Get auth configuration"""
try:
# Get health which includes some config info
result = await client.request("GET", "/auth/v1/health", use_service_role=False)
return json.dumps({"success": True, "config": result}, indent=2, ensure_ascii=False)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
async def search_users(
client: SupabaseClient, query: str, page: int = 1, per_page: int = 50
) -> str:

View File

@@ -59,6 +59,7 @@ class SupabasePlugin(BasePlugin):
- service_role_key: Admin API key (bypasses RLS). Required.
- anon_key: Public API key (RLS protected). Optional.
- meta_url: Direct postgres-meta URL. Optional.
- meta_auth: Basic Auth for postgres-meta (username:password). Optional.
project_id: Optional project ID (auto-generated if not provided)
"""
super().__init__(config, project_id=project_id)
@@ -69,6 +70,7 @@ class SupabasePlugin(BasePlugin):
anon_key=config.get("anon_key", ""),
service_role_key=config["service_role_key"],
meta_url=config.get("meta_url"),
meta_auth=config.get("meta_auth"),
)
@staticmethod