Fix all integrations
This commit is contained in:
111
core/security/capabilities.py
Normal file
111
core/security/capabilities.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""Canonical capability registry for command/task/gateway scope policy."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CapabilityScope:
|
||||
key: str
|
||||
label: str
|
||||
description: str
|
||||
group: str
|
||||
configurable: bool = True
|
||||
owner_path: str = "/settings/security/permissions/"
|
||||
|
||||
|
||||
GLOBAL_SCOPE_KEY = "global.override"
|
||||
|
||||
CAPABILITY_SCOPES: tuple[CapabilityScope, ...] = (
|
||||
CapabilityScope(
|
||||
key="gateway.contacts",
|
||||
label="Gateway contacts command",
|
||||
description="Handles .contacts over gateway channels.",
|
||||
group="gateway",
|
||||
),
|
||||
CapabilityScope(
|
||||
key="gateway.help",
|
||||
label="Gateway help command",
|
||||
description="Handles .help over gateway channels.",
|
||||
group="gateway",
|
||||
),
|
||||
CapabilityScope(
|
||||
key="gateway.whoami",
|
||||
label="Gateway whoami command",
|
||||
description="Handles .whoami over gateway channels.",
|
||||
group="gateway",
|
||||
),
|
||||
CapabilityScope(
|
||||
key="gateway.tasks",
|
||||
label="Gateway .tasks commands",
|
||||
description="Handles .tasks list/show/complete/undo over gateway channels.",
|
||||
group="tasks",
|
||||
),
|
||||
CapabilityScope(
|
||||
key="gateway.approval",
|
||||
label="Gateway approval commands",
|
||||
description="Handles .approval/.codex/.claude approve/deny over gateway channels.",
|
||||
group="command",
|
||||
),
|
||||
CapabilityScope(
|
||||
key="tasks.submit",
|
||||
label="Task submissions from chat",
|
||||
description="Controls automatic task creation from inbound messages.",
|
||||
group="tasks",
|
||||
owner_path="/settings/tasks/",
|
||||
),
|
||||
CapabilityScope(
|
||||
key="tasks.commands",
|
||||
label="Task command verbs (.task/.undo/.epic)",
|
||||
description="Controls explicit task command verbs.",
|
||||
group="tasks",
|
||||
owner_path="/settings/tasks/",
|
||||
),
|
||||
CapabilityScope(
|
||||
key="command.bp",
|
||||
label="Business plan command",
|
||||
description="Controls Business Plan command execution.",
|
||||
group="command",
|
||||
owner_path="/settings/command-routing/",
|
||||
),
|
||||
CapabilityScope(
|
||||
key="command.codex",
|
||||
label="Codex command",
|
||||
description="Controls Codex command execution.",
|
||||
group="agentic",
|
||||
owner_path="/settings/command-routing/",
|
||||
),
|
||||
CapabilityScope(
|
||||
key="command.claude",
|
||||
label="Claude command",
|
||||
description="Controls Claude command execution.",
|
||||
group="agentic",
|
||||
owner_path="/settings/command-routing/",
|
||||
),
|
||||
)
|
||||
|
||||
SCOPE_BY_KEY = {row.key: row for row in CAPABILITY_SCOPES}
|
||||
|
||||
GROUP_LABELS: dict[str, str] = {
|
||||
"gateway": "Gateway",
|
||||
"tasks": "Tasks",
|
||||
"command": "Commands",
|
||||
"agentic": "Agentic",
|
||||
"other": "Other",
|
||||
}
|
||||
|
||||
|
||||
def all_scope_keys(*, configurable_only: bool = False) -> list[str]:
|
||||
rows = [
|
||||
row.key
|
||||
for row in CAPABILITY_SCOPES
|
||||
if (not configurable_only or bool(row.configurable))
|
||||
]
|
||||
return rows
|
||||
|
||||
|
||||
def scope_record(scope_key: str) -> CapabilityScope | None:
|
||||
key = str(scope_key or "").strip().lower()
|
||||
return SCOPE_BY_KEY.get(key)
|
||||
|
||||
@@ -2,7 +2,10 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from core.models import CommandSecurityPolicy, UserXmppOmemoState
|
||||
from core.models import (
|
||||
CommandSecurityPolicy,
|
||||
UserXmppOmemoTrustedKey,
|
||||
)
|
||||
|
||||
GLOBAL_SCOPE_KEY = "global.override"
|
||||
OVERRIDE_OPTIONS = {"per_scope", "on", "off"}
|
||||
@@ -64,7 +67,7 @@ def _match_channel(rule: str, channel: str) -> bool:
|
||||
return current == value
|
||||
|
||||
|
||||
def _omemo_facts(ctx: CommandSecurityContext) -> tuple[str, str]:
|
||||
def _omemo_facts(ctx: CommandSecurityContext) -> tuple[str, str, str]:
|
||||
message_meta = dict(ctx.message_meta or {})
|
||||
payload = dict(ctx.payload or {})
|
||||
xmpp_meta = dict(message_meta.get("xmpp") or {})
|
||||
@@ -76,7 +79,10 @@ def _omemo_facts(ctx: CommandSecurityContext) -> tuple[str, str]:
|
||||
client_key = str(
|
||||
xmpp_meta.get("omemo_client_key") or payload.get("omemo_client_key") or ""
|
||||
).strip()
|
||||
return status, client_key
|
||||
sender_jid = str(
|
||||
xmpp_meta.get("sender_jid") or payload.get("sender_jid") or ""
|
||||
).strip()
|
||||
return status, client_key, sender_jid
|
||||
|
||||
|
||||
def _channel_allowed_for_rules(rules: dict, service: str, channel: str) -> bool:
|
||||
@@ -192,7 +198,7 @@ def evaluate_command_policy(
|
||||
reason=f"channel={channel or '-'} not allowed by global override",
|
||||
)
|
||||
|
||||
omemo_status, omemo_client_key = _omemo_facts(context)
|
||||
omemo_status, omemo_client_key, sender_jid = _omemo_facts(context)
|
||||
if require_omemo and omemo_status != "detected":
|
||||
return CommandPolicyDecision(
|
||||
allowed=False,
|
||||
@@ -205,15 +211,25 @@ def evaluate_command_policy(
|
||||
return CommandPolicyDecision(
|
||||
allowed=False,
|
||||
code="trusted_fingerprint_required",
|
||||
reason=f"scope={scope} requires trusted OMEMO fingerprint",
|
||||
reason=f"scope={scope} requires a trusted OMEMO key",
|
||||
)
|
||||
state = UserXmppOmemoState.objects.filter(user=user).first()
|
||||
expected_key = str(getattr(state, "latest_client_key", "") or "").strip()
|
||||
if not expected_key or expected_key != omemo_client_key:
|
||||
jid_bare = (
|
||||
str(sender_jid.split("/", 1)[0] if sender_jid else "").strip().lower()
|
||||
)
|
||||
trusted_query = UserXmppOmemoTrustedKey.objects.filter(
|
||||
user=user,
|
||||
key_type="client_key",
|
||||
key_id=omemo_client_key,
|
||||
trusted=True,
|
||||
)
|
||||
if jid_bare:
|
||||
trusted_query = trusted_query.filter(jid__iexact=jid_bare)
|
||||
trusted_match = trusted_query.order_by("-updated_at").first()
|
||||
if trusted_match is None:
|
||||
return CommandPolicyDecision(
|
||||
allowed=False,
|
||||
code="fingerprint_mismatch",
|
||||
reason=f"scope={scope} OMEMO fingerprint does not match enrolled key",
|
||||
code="trusted_key_missing",
|
||||
reason=f"scope={scope} requires a trusted OMEMO key for this sender",
|
||||
)
|
||||
|
||||
return CommandPolicyDecision(allowed=True)
|
||||
|
||||
Reference in New Issue
Block a user