Phase 7: Plugin-Gate, Event-Envelope, Pro-Handler Outbox-Verarbeitung
7.1 Plugin-Gate korrigiert: - require_active_plugin nutzt current_user fuer tenant_id statt current_setting() - Keine neue DB-Session mehr — nutzt bestehende get_db Dependency - Fail-closed bei Fehlern 7.4 Einheitlicher Event-Envelope: - Sauberes Envelope mit event_id, event_name, tenant_id, aggregate_type, aggregate_id, occurred_at, correlation_id, schema_version, data - Keine _-Praefixe mehr im payload - Handler empfangen envelope statt rohes payload 7.6 Verarbeitung pro Handler: - Globaler consumer_inbox Check entfernt - Pro-Handler Idempotency: outbox_deliveries pruefen ob Handler bereits erfolgreich - Bereits erfolgreiche Handler werden uebersprungen - consumer_inbox pro Handler geschrieben 7.7 no_handlers: Bereits implementiert (terminaler Status) 7.8 Cron-Jobs: Bereits mit Redis SET NX Locking implementiert Tests: 23/23 Outbox-Tests bestanden
This commit is contained in:
+41
-54
@@ -363,6 +363,9 @@ def require_active_plugin(plugin_name: str):
|
||||
Checks both global activation (permission registry) and per-tenant
|
||||
activation (tenant_plugin_activation table).
|
||||
|
||||
Uses the current_user dependency to get tenant_id — does NOT guess
|
||||
the tenant from a new DB session via current_setting().
|
||||
|
||||
Uses Redis cache for per-tenant check to avoid DB query on every request.
|
||||
Cache key: plugin-activation:{tenant_id}:{plugin_name}
|
||||
TTL: 60 seconds. Invalidated on activate/deactivate.
|
||||
@@ -370,7 +373,10 @@ def require_active_plugin(plugin_name: str):
|
||||
Returns 403 if the plugin is not active.
|
||||
Fails closed (503) on errors.
|
||||
"""
|
||||
async def _check() -> None:
|
||||
async def _check(
|
||||
current_user: dict[str, Any] = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> None:
|
||||
from app.core.permission_registry import get_permission_registry
|
||||
try:
|
||||
registry = get_permission_registry()
|
||||
@@ -382,21 +388,22 @@ def require_active_plugin(plugin_name: str):
|
||||
"code": "plugin_inactive",
|
||||
},
|
||||
)
|
||||
# Get tenant_id from current_user — NOT from current_setting()
|
||||
tenant_id_str = current_user.get("tenant_id")
|
||||
if not tenant_id_str:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail={"detail": "No tenant context", "code": "no_tenant"},
|
||||
)
|
||||
tenant_id = uuid.UUID(tenant_id_str)
|
||||
|
||||
# Per-tenant activation check with Redis cache
|
||||
from app.core.redis import get_redis
|
||||
from app.core.db import async_session_maker
|
||||
from sqlalchemy import text
|
||||
import json
|
||||
|
||||
redis = get_redis()
|
||||
# Get tenant_id from current session context
|
||||
async with async_session_maker() as db:
|
||||
result = await db.execute(
|
||||
text("SELECT current_setting('app.current_tenant_id', true)::uuid")
|
||||
)
|
||||
tenant_id = result.scalar()
|
||||
|
||||
if tenant_id is not None and redis is not None:
|
||||
if redis is not None:
|
||||
cache_key = f"plugin-activation:{tenant_id}:{plugin_name}"
|
||||
cached = await redis.get(cache_key)
|
||||
if cached is not None:
|
||||
@@ -411,52 +418,32 @@ def require_active_plugin(plugin_name: str):
|
||||
)
|
||||
return # Cache hit — plugin is active for this tenant
|
||||
|
||||
# Cache miss — query DB
|
||||
async with async_session_maker() as db:
|
||||
result = await db.execute(
|
||||
text("""
|
||||
SELECT is_active FROM tenant_plugin_activation
|
||||
WHERE plugin_name = :name
|
||||
AND tenant_id = :tid
|
||||
"""),
|
||||
{"name": plugin_name, "tid": tenant_id},
|
||||
# Cache miss — query DB using the existing db session (tenant context already set)
|
||||
result = await db.execute(
|
||||
text("""
|
||||
SELECT is_active FROM tenant_plugin_activation
|
||||
WHERE plugin_name = :name
|
||||
AND tenant_id = :tid
|
||||
"""),
|
||||
{"name": plugin_name, "tid": tenant_id},
|
||||
)
|
||||
row = result.first()
|
||||
if row is not None:
|
||||
is_active = row[0]
|
||||
if redis is not None:
|
||||
await redis.setex(cache_key, 60, json.dumps(is_active))
|
||||
if not is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail={
|
||||
"detail": f"Plugin '{plugin_name}' is not active for this tenant",
|
||||
"code": "plugin_inactive_tenant",
|
||||
},
|
||||
)
|
||||
row = result.first()
|
||||
if row is not None:
|
||||
is_active = row[0]
|
||||
# Cache the result (60s TTL)
|
||||
await redis.setex(cache_key, 60, json.dumps(is_active))
|
||||
if not is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail={
|
||||
"detail": f"Plugin '{plugin_name}' is not active for this tenant",
|
||||
"code": "plugin_inactive_tenant",
|
||||
},
|
||||
)
|
||||
else:
|
||||
# No entry = default active (backward compatible)
|
||||
await redis.setex(cache_key, 60, json.dumps(True))
|
||||
else:
|
||||
# No Redis or no tenant_id — fallback to DB query without cache
|
||||
async with async_session_maker() as db:
|
||||
result = await db.execute(
|
||||
text("""
|
||||
SELECT is_active FROM tenant_plugin_activation
|
||||
WHERE plugin_name = :name
|
||||
AND tenant_id = current_setting('app.current_tenant_id', true)::uuid
|
||||
"""),
|
||||
{"name": plugin_name},
|
||||
)
|
||||
row = result.first()
|
||||
if row is not None and not row[0]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail={
|
||||
"detail": f"Plugin '{plugin_name}' is not active for this tenant",
|
||||
"code": "plugin_inactive_tenant",
|
||||
},
|
||||
)
|
||||
# No entry = default active (backward compatible)
|
||||
if redis is not None:
|
||||
await redis.setex(cache_key, 60, json.dumps(True))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
|
||||
Reference in New Issue
Block a user