Increase security and reformat

This commit is contained in:
2026-03-07 20:52:13 +00:00
parent 10588a18b9
commit bca4d6898f
144 changed files with 6735 additions and 3960 deletions

View File

@@ -7,6 +7,8 @@ from django.urls import reverse
from django.views import View
from core.clients import transport
from core.events.projection import shadow_compare_session
from core.memory.search_backend import backend_status, get_memory_search_backend
from core.models import (
AdapterHealthEvent,
AIRequest,
@@ -14,6 +16,7 @@ from core.models import (
AIResultSignal,
Chat,
ChatSession,
CommandSecurityPolicy,
ConversationEvent,
Group,
MemoryItem,
@@ -30,15 +33,13 @@ from core.models import (
Persona,
PersonIdentifier,
QueuedMessage,
CommandSecurityPolicy,
UserAccessibilitySettings,
UserXmppOmemoState,
UserXmppOmemoTrustedKey,
UserXmppSecuritySettings,
WorkspaceConversation,
WorkspaceMetricSnapshot,
)
from core.events.projection import shadow_compare_session
from core.memory.search_backend import backend_status, get_memory_search_backend
from core.transports.capabilities import capability_snapshot
from core.views.manage.permissions import SuperUserRequiredMixin
@@ -53,7 +54,9 @@ class SystemSettings(SuperUserRequiredMixin, View):
"queued_messages": QueuedMessage.objects.filter(user=user).count(),
"message_events": MessageEvent.objects.filter(user=user).count(),
"conversation_events": ConversationEvent.objects.filter(user=user).count(),
"adapter_health_events": AdapterHealthEvent.objects.filter(user=user).count(),
"adapter_health_events": AdapterHealthEvent.objects.filter(
user=user
).count(),
"workspace_conversations": WorkspaceConversation.objects.filter(
user=user
).count(),
@@ -472,7 +475,41 @@ def _parse_xmpp_jid(jid_str: str) -> dict:
raw = str(jid_str or "").strip()
bare, _, resource = raw.partition("/")
localpart, _, domain = bare.partition("@")
return {"full": raw, "bare": bare, "localpart": localpart, "domain": domain, "resource": resource}
return {
"full": raw,
"bare": bare,
"localpart": localpart,
"domain": domain,
"resource": resource,
}
def _parse_omemo_client_key(client_key: str) -> dict:
raw = str(client_key or "").strip()
parts = {}
for row in raw.split(","):
key, _, value = str(row or "").partition(":")
k = key.strip().lower()
v = value.strip()
if k and v:
parts[k] = v
sid = str(parts.get("sid") or "").strip()
rid = str(parts.get("rid") or "").strip()
return {
"raw": raw,
"sid": sid,
"rid": rid,
"has_ids": bool(sid or rid),
}
def _latest_client_omemo_fingerprint(omemo_row) -> str:
if omemo_row is None:
return ""
details = getattr(omemo_row, "details", {}) or {}
if not isinstance(details, dict):
return ""
return str(details.get("latest_client_fingerprint") or "").strip()
def _to_bool(value, default=False):
@@ -505,12 +542,36 @@ class SecurityPage(LoginRequiredMixin, View):
"require_trusted_fingerprint",
)
POLICY_SCOPES = [
("gateway.tasks", "Gateway .tasks commands", "Handles .tasks list/show/complete/undo over gateway channels."),
("gateway.approval", "Gateway approval commands", "Handles .approval/.codex/.claude approve/deny over gateway channels."),
("gateway.totp", "Gateway TOTP enrollment", "Controls TOTP enrollment/status commands over gateway channels."),
("tasks.submit", "Task submissions from chat", "Controls automatic task creation from inbound messages."),
("tasks.commands", "Task command verbs (.task/.undo/.epic)", "Controls explicit task command verbs."),
("command.bp", "Business plan command", "Controls Business Plan command execution."),
(
"gateway.tasks",
"Gateway .tasks commands",
"Handles .tasks list/show/complete/undo over gateway channels.",
),
(
"gateway.approval",
"Gateway approval commands",
"Handles .approval/.codex/.claude approve/deny over gateway channels.",
),
(
"gateway.totp",
"Gateway TOTP enrollment",
"Controls TOTP enrollment/status commands over gateway channels.",
),
(
"tasks.submit",
"Task submissions from chat",
"Controls automatic task creation from inbound messages.",
),
(
"tasks.commands",
"Task command verbs (.task/.undo/.epic)",
"Controls explicit task command verbs.",
),
(
"command.bp",
"Business plan command",
"Controls Business Plan command execution.",
),
("command.codex", "Codex command", "Controls Codex command execution."),
("command.claude", "Claude command", "Controls Claude command execution."),
]
@@ -538,6 +599,77 @@ class SecurityPage(LoginRequiredMixin, View):
row, _ = UserXmppSecuritySettings.objects.get_or_create(user=request.user)
return row
def _omemo_discovered_keys(self, request, xmpp_state, omemo_row):
discovered = []
sender = _parse_xmpp_jid(getattr(omemo_row, "last_sender_jid", "") or "")
client_jid = str(sender.get("bare") or "").strip()
client_fingerprint = _latest_client_omemo_fingerprint(omemo_row)
client_key = str(getattr(omemo_row, "latest_client_key", "") or "").strip()
if client_jid and client_fingerprint:
discovered.append(
{
"jid": client_jid,
"key_type": "fingerprint",
"key_id": client_fingerprint,
"source": "client_observed",
"label": "Observed client fingerprint",
}
)
elif client_jid and client_key:
discovered.append(
{
"jid": client_jid,
"key_type": "client_key",
"key_id": client_key,
"source": "client_observed",
"label": "Observed client key",
}
)
# De-duplicate while preserving order.
deduped = []
seen = set()
for row in discovered:
key = (
str(row.get("jid") or "").strip().lower(),
str(row.get("key_type") or "").strip().lower(),
str(row.get("key_id") or "").strip(),
)
if key in seen:
continue
seen.add(key)
deduped.append(row)
discovered = deduped
trusted_map = {}
rows = UserXmppOmemoTrustedKey.objects.filter(user=request.user)
for item in rows:
trusted_map[
(
str(item.jid or "").strip().lower(),
str(item.key_type or "").strip().lower(),
str(item.key_id or "").strip(),
)
] = item
payload = []
for row in discovered:
map_key = (
str(row.get("jid") or "").strip().lower(),
str(row.get("key_type") or "").strip().lower(),
str(row.get("key_id") or "").strip(),
)
existing = trusted_map.get(map_key)
payload.append(
{
**row,
"trusted": bool(getattr(existing, "trusted", False)),
"updated_at": getattr(existing, "updated_at", None),
}
)
return payload
def _parse_override_value(self, value):
option = str(value or "").strip().lower()
if option == "inherit":
@@ -604,10 +736,12 @@ class SecurityPage(LoginRequiredMixin, View):
for pattern in patterns:
pattern_text = str(pattern or "").strip()
if pattern_text:
rows.append({
"service": service_name,
"pattern": pattern_text,
})
rows.append(
{
"service": service_name,
"pattern": pattern_text,
}
)
return rows
def _channels_map_from_post(self, request):
@@ -618,9 +752,11 @@ class SecurityPage(LoginRequiredMixin, View):
pattern = str(raw_pattern or "").strip()
if not pattern:
continue
service_name = str(
channel_services[idx] if idx < len(channel_services) else ""
).strip().lower()
service_name = (
str(channel_services[idx] if idx < len(channel_services) else "")
.strip()
.lower()
)
if not service_name:
service_name = "*"
allowed_channels.setdefault(service_name, [])
@@ -652,41 +788,42 @@ class SecurityPage(LoginRequiredMixin, View):
if not channel_rules:
channel_rules = [{"service": "xmpp", "pattern": ""}]
enabled_locked = global_overrides["scope_enabled"] != "per_scope"
require_omemo_locked = (
global_overrides["require_omemo"] != "per_scope"
or bool(security_settings.require_omemo)
)
require_omemo_locked = global_overrides[
"require_omemo"
] != "per_scope" or bool(security_settings.require_omemo)
require_trusted_locked = (
global_overrides["require_trusted_fingerprint"] != "per_scope"
)
payload.append({
"scope_key": key,
"label": label,
"description": description,
"enabled": self._apply_global_override(
bool(getattr(item, "enabled", True)),
global_overrides["scope_enabled"],
),
"require_omemo": self._apply_global_override(
bool(getattr(item, "require_omemo", False)),
global_overrides["require_omemo"],
),
"require_trusted_fingerprint": self._apply_global_override(
bool(getattr(item, "require_trusted_omemo_fingerprint", False)),
global_overrides["require_trusted_fingerprint"],
),
"enabled_locked": enabled_locked,
"require_omemo_locked": require_omemo_locked,
"require_trusted_fingerprint_locked": require_trusted_locked,
"lock_help": "Set this field to 'Per Scope' in Global Scope Override to edit it here.",
"require_omemo_lock_help": (
"Disable 'Require OMEMO encryption' in Encryption settings to edit this field."
if bool(security_settings.require_omemo)
else "Set this field to 'Per Scope' in Global Scope Override to edit it here."
),
"allowed_services": raw_allowed_services,
"channel_rules": channel_rules,
})
payload.append(
{
"scope_key": key,
"label": label,
"description": description,
"enabled": self._apply_global_override(
bool(getattr(item, "enabled", True)),
global_overrides["scope_enabled"],
),
"require_omemo": self._apply_global_override(
bool(getattr(item, "require_omemo", False)),
global_overrides["require_omemo"],
),
"require_trusted_fingerprint": self._apply_global_override(
bool(getattr(item, "require_trusted_omemo_fingerprint", False)),
global_overrides["require_trusted_fingerprint"],
),
"enabled_locked": enabled_locked,
"require_omemo_locked": require_omemo_locked,
"require_trusted_fingerprint_locked": require_trusted_locked,
"lock_help": "Set this field to 'Per Scope' in Global Scope Override to edit it here.",
"require_omemo_lock_help": (
"Disable 'Require OMEMO encryption' in Encryption settings to edit this field."
if bool(security_settings.require_omemo)
else "Set this field to 'Per Scope' in Global Scope Override to edit it here."
),
"allowed_services": raw_allowed_services,
"channel_rules": channel_rules,
}
)
return payload
def _scope_group_key(self, scope_key: str) -> str:
@@ -725,18 +862,52 @@ class SecurityPage(LoginRequiredMixin, View):
items = grouped.get(group_key) or []
if not items:
continue
payload.append({
"key": group_key,
"label": self.POLICY_GROUP_LABELS.get(group_key, group_key.title()),
"rows": items,
})
payload.append(
{
"key": group_key,
"label": self.POLICY_GROUP_LABELS.get(group_key, group_key.title()),
"rows": items,
}
)
return payload
def post(self, request):
row = self._security_settings(request)
if "require_omemo" in request.POST:
if str(request.POST.get("encryption_settings_submit") or "").strip() == "1":
row.require_omemo = _to_bool(request.POST.get("require_omemo"), False)
row.save(update_fields=["require_omemo", "updated_at"])
row.encrypt_contact_messages_with_omemo = _to_bool(
request.POST.get("encrypt_contact_messages_with_omemo"),
False,
)
row.save(
update_fields=[
"require_omemo",
"encrypt_contact_messages_with_omemo",
"updated_at",
]
)
if "omemo_trust_update" in request.POST:
key_type = (
str(request.POST.get("key_type") or "fingerprint").strip().lower()
)
if key_type not in {"fingerprint", "client_key"}:
key_type = "fingerprint"
jid = str(request.POST.get("jid") or "").strip()
key_id = str(request.POST.get("key_id") or "").strip()
source = str(request.POST.get("source") or "").strip().lower()
trusted = _to_bool(request.POST.get("trusted"), False)
if jid and key_id:
trust_row, _ = UserXmppOmemoTrustedKey.objects.get_or_create(
user=request.user,
jid=jid,
key_type=key_type,
key_id=key_id,
defaults={"source": source},
)
trust_row.trusted = trusted
if source:
trust_row.source = source
trust_row.save(update_fields=["trusted", "source", "updated_at"])
redirect_to = HttpResponseRedirect(request.path)
scope_key = str(request.POST.get("scope_key") or "").strip().lower()
if not self._show_permission():
@@ -787,9 +958,8 @@ class SecurityPage(LoginRequiredMixin, View):
policy.allowed_channels = allowed_channels
if global_overrides["scope_enabled"] == "per_scope":
policy.enabled = _to_bool(request.POST.get("policy_enabled"), True)
if (
global_overrides["require_omemo"] == "per_scope"
and not bool(security_settings.require_omemo)
if global_overrides["require_omemo"] == "per_scope" and not bool(
security_settings.require_omemo
):
policy.require_omemo = _to_bool(
request.POST.get("policy_require_omemo"), False
@@ -821,8 +991,15 @@ class SecurityPage(LoginRequiredMixin, View):
omemo_row = UserXmppOmemoState.objects.get(user=request.user)
except UserXmppOmemoState.DoesNotExist:
omemo_row = None
discovered_omemo_keys = self._omemo_discovered_keys(
request, xmpp_state, omemo_row
)
security_settings = self._security_settings(request)
sender_jid = _parse_xmpp_jid(getattr(omemo_row, "last_sender_jid", "") or "")
omemo_client_fingerprint = _latest_client_omemo_fingerprint(omemo_row)
omemo_client_key_info = _parse_omemo_client_key(
getattr(omemo_row, "latest_client_key", "") if omemo_row else ""
)
omemo_plan = []
if show_encryption:
omemo_plan = [
@@ -847,19 +1024,26 @@ class SecurityPage(LoginRequiredMixin, View):
"hint": "Enable 'Require OMEMO encryption' in Security Policy above to enforce this policy.",
},
]
return render(request, self.template_name, {
"xmpp_state": xmpp_state,
"omemo_row": omemo_row,
"security_settings": security_settings,
"global_override": self._global_override_payload(request),
"policy_services": self.POLICY_SERVICES,
"policy_rows": self._scope_rows(request),
"policy_groups": self._grouped_scope_rows(request),
"sender_jid": sender_jid,
"omemo_plan": omemo_plan,
"show_encryption": show_encryption,
"show_permission": show_permission,
})
return render(
request,
self.template_name,
{
"xmpp_state": xmpp_state,
"omemo_row": omemo_row,
"discovered_omemo_keys": discovered_omemo_keys,
"security_settings": security_settings,
"global_override": self._global_override_payload(request),
"policy_services": self.POLICY_SERVICES,
"policy_rows": self._scope_rows(request),
"policy_groups": self._grouped_scope_rows(request),
"sender_jid": sender_jid,
"omemo_client_fingerprint": omemo_client_fingerprint,
"omemo_client_key_info": omemo_client_key_info,
"omemo_plan": omemo_plan,
"show_encryption": show_encryption,
"show_permission": show_permission,
},
)
class AccessibilitySettings(LoginRequiredMixin, View):
@@ -870,9 +1054,13 @@ class AccessibilitySettings(LoginRequiredMixin, View):
return row
def get(self, request):
return render(request, self.template_name, {
"accessibility_settings": self._row(request),
})
return render(
request,
self.template_name,
{
"accessibility_settings": self._row(request),
},
)
def post(self, request):
row = self._row(request)
@@ -892,20 +1080,26 @@ class _SettingsCategoryPage(LoginRequiredMixin, View):
current_path = str(getattr(self.request, "path", "") or "")
rows = []
for label, href in self.tabs:
rows.append({
"label": label,
"href": href,
"active": current_path == href,
})
rows.append(
{
"label": label,
"href": href,
"active": current_path == href,
}
)
return rows
def get(self, request):
return render(request, self.template_name, {
"category_key": self.category_key,
"category_title": self.category_title,
"category_description": self.category_description,
"category_tabs": self._tab_rows(),
})
return render(
request,
self.template_name,
{
"category_key": self.category_key,
"category_title": self.category_title,
"category_description": self.category_description,
"category_tabs": self._tab_rows(),
},
)
class AISettingsPage(LoginRequiredMixin, View):