Increase security and reformat
This commit is contained in:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user