Improve security
This commit is contained in:
1
core/gateway/__init__.py
Normal file
1
core/gateway/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Gateway command routing utilities."""
|
||||
133
core/gateway/commands.py
Normal file
133
core/gateway/commands.py
Normal file
@@ -0,0 +1,133 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Awaitable, Callable
|
||||
|
||||
from asgiref.sync import sync_to_async
|
||||
|
||||
from core.models import GatewayCommandEvent
|
||||
from core.security.command_policy import CommandSecurityContext, evaluate_command_policy
|
||||
|
||||
|
||||
GatewayEmit = Callable[[str], None]
|
||||
GatewayHandler = Callable[["GatewayCommandContext", GatewayEmit], Awaitable[bool]]
|
||||
GatewayMatcher = Callable[[str], bool]
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class GatewayCommandContext:
|
||||
user: object
|
||||
source_message: object
|
||||
service: str
|
||||
channel_identifier: str
|
||||
sender_identifier: str
|
||||
message_text: str
|
||||
message_meta: dict
|
||||
payload: dict
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class GatewayCommandRoute:
|
||||
name: str
|
||||
scope_key: str
|
||||
matcher: GatewayMatcher
|
||||
handler: GatewayHandler
|
||||
|
||||
|
||||
def _first_token(text: str) -> str:
|
||||
body = str(text or "").strip()
|
||||
if not body:
|
||||
return ""
|
||||
return str(body.split()[0] or "").strip().lower()
|
||||
|
||||
|
||||
def _derive_unknown_scope(text: str) -> str:
|
||||
token = _first_token(text).lstrip(".")
|
||||
if not token:
|
||||
token = "message"
|
||||
return f"gateway.{token}"
|
||||
|
||||
|
||||
async def dispatch_gateway_command(
|
||||
*,
|
||||
context: GatewayCommandContext,
|
||||
routes: list[GatewayCommandRoute],
|
||||
emit: GatewayEmit,
|
||||
) -> bool:
|
||||
text = str(context.message_text or "").strip()
|
||||
if not text:
|
||||
return False
|
||||
|
||||
route = next((row for row in routes if row.matcher(text)), None)
|
||||
scope_key = route.scope_key if route is not None else _derive_unknown_scope(text)
|
||||
command_name = route.name if route is not None else _first_token(text).lstrip(".")
|
||||
|
||||
event = await sync_to_async(GatewayCommandEvent.objects.create)(
|
||||
user=context.user,
|
||||
source_message=context.source_message,
|
||||
service=str(context.service or "").strip().lower() or "xmpp",
|
||||
channel_identifier=str(context.channel_identifier or "").strip(),
|
||||
sender_identifier=str(context.sender_identifier or "").strip(),
|
||||
scope_key=scope_key,
|
||||
command_name=command_name,
|
||||
command_text=text,
|
||||
status="pending",
|
||||
request_meta={
|
||||
"payload": dict(context.payload or {}),
|
||||
"message_meta": dict(context.message_meta or {}),
|
||||
},
|
||||
)
|
||||
|
||||
if route is None:
|
||||
event.status = "ignored"
|
||||
event.error = "unmatched_gateway_command"
|
||||
await sync_to_async(event.save)(update_fields=["status", "error", "updated_at"])
|
||||
return False
|
||||
|
||||
decision = await sync_to_async(evaluate_command_policy)(
|
||||
user=context.user,
|
||||
scope_key=scope_key,
|
||||
context=CommandSecurityContext(
|
||||
service=context.service,
|
||||
channel_identifier=context.channel_identifier,
|
||||
message_meta=dict(context.message_meta or {}),
|
||||
payload=dict(context.payload or {}),
|
||||
),
|
||||
)
|
||||
if not decision.allowed:
|
||||
message = (
|
||||
f"blocked by policy: {decision.code}"
|
||||
if not decision.reason
|
||||
else f"blocked by policy: {decision.reason}"
|
||||
)
|
||||
emit(message)
|
||||
event.status = "blocked"
|
||||
event.error = f"{decision.code}:{decision.reason}"
|
||||
event.response_meta = {"policy_code": decision.code, "policy_reason": decision.reason}
|
||||
await sync_to_async(event.save)(
|
||||
update_fields=["status", "error", "response_meta", "updated_at"]
|
||||
)
|
||||
return True
|
||||
|
||||
responses: list[str] = []
|
||||
|
||||
def _captured_emit(value: str) -> None:
|
||||
row = str(value or "")
|
||||
responses.append(row)
|
||||
emit(row)
|
||||
|
||||
try:
|
||||
handled = await route.handler(context, _captured_emit)
|
||||
except Exception as exc:
|
||||
event.status = "failed"
|
||||
event.error = f"handler_exception:{exc}"
|
||||
event.response_meta = {"responses": responses}
|
||||
await sync_to_async(event.save)(
|
||||
update_fields=["status", "error", "response_meta", "updated_at"]
|
||||
)
|
||||
return True
|
||||
|
||||
event.status = "ok" if handled else "ignored"
|
||||
event.response_meta = {"responses": responses}
|
||||
await sync_to_async(event.save)(update_fields=["status", "response_meta", "updated_at"])
|
||||
return bool(handled)
|
||||
Reference in New Issue
Block a user