From 7851e2befd1611d54231da143826e6dd7aabd554 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 05:38:36 +0000 Subject: [PATCH] Decouple internal SIP messaging management from PSTN trunk installation Messaging has no dependency on a PSTN trunk existing (no cost, no carrier, no DID), but the whole PSTN Trunk tab - including the messaging checkbox added last commit - was hidden behind pstn_installed(), which only becomes true once services/pstn-trunk.sh's dialplan is actually wired in. That meant enabling messaging required going through full SIP trunk/provider setup first for no real reason. Added a standalone "Internal SIP messaging" card that's always visible in the tab regardless of trunk status, backed by a new write_messaging() that only touches the messaging key (leaving tier/allowed_numbers/ personal_did untouched) and creates pstn-permissions.conf from scratch if it doesn't exist yet. Confirmed the systemd unit's ReadWritePaths and the group/chmod grants already covered this - both are set up whenever a base Asterisk install is detected, independent of pstn-trunk - so no permission-layer changes were needed, only the dashboard's own artificial UI gate. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf --- services/pstn-trunk.sh | 7 ++- services/security-dashboard.sh | 106 ++++++++++++++++++++++++++++----- 2 files changed, 96 insertions(+), 17 deletions(-) diff --git a/services/pstn-trunk.sh b/services/pstn-trunk.sh index 10dec8c..cb1c23b 100644 --- a/services/pstn-trunk.sh +++ b/services/pstn-trunk.sh @@ -1817,9 +1817,10 @@ no carrier SMS, no PSTN, no cost) is gated by a \`messaging=yes\` flag per extension in \`pstn-permissions.conf\`, independent of the PSTN calling tiers above — off by default, same "opt in" posture. Currently enabled for: ${MESSAGING_EXTS:-none}. Live-editable any time via the Security -Dashboard's "PSTN Trunk" tab (a checkbox per extension, right in the same -table as the calling tiers) — no need to re-run this installer just to -change who can message. +Dashboard's "PSTN Trunk" tab, in its own always-available "Internal SIP +messaging" card — no need to re-run this installer, and no dependency on +this trunk (or any PSTN trunk at all) being installed, unlike the +calling-permissions table below it in that same tab. **Won't show up in Easy Asterisk's own web admin, by design** — same as the PSTN calling tiers, this is a permission this repo layers on top, diff --git a/services/security-dashboard.sh b/services/security-dashboard.sh index c58328b..6daa0db 100644 --- a/services/security-dashboard.sh +++ b/services/security-dashboard.sh @@ -208,18 +208,22 @@ not in Docker — it needs to call \`cscli\` and read Asterisk's log directly. - **Unwhitelist + Ban** does that *and* immediately bans (24h) every IP CrowdSec has ever recorded for that ASN, for accidental-whitelist cases where you don't want to wait for it to misbehave again. -- **PSTN Trunk** — detects whether \`services/pstn-trunk.sh\`'s dialplan is - actually installed (\`pstn-trunk-dialplan.conf\` present) and shows a clear - "not installed" message instead of the caps/tiers editor if not, so it - never shows real-looking-but-unenforced defaults. When installed: the - outbound/inbound concurrent-call caps, and every known extension (parsed - from \`pjsip.conf\`) with its current permission tier (internal / - restricted / full), for restricted its approved numbers, and its - internal-SIP-messaging flag (independent of the calling tier — see - \`services/pstn-trunk.sh\`'s "Known gap" note on messaging for what this - flag does and doesn't do yet) — all editable live, no Asterisk restart, - no reinstall. Also manages personal-number assignments (DID -> owner - extension), additive to the shared trunk DID. Writes directly to +- **PSTN Trunk** — an "Internal SIP messaging" card at the top is always + available, whether or not a PSTN trunk has ever been installed: a + checkbox per known extension (parsed from \`pjsip.conf\`) for + Asterisk's native SIP texting, independent of PSTN calling entirely (no + cost, no carrier, no DID, no dependency on \`services/pstn-trunk.sh\` + having been run — see its "Known gap" note on messaging for what this + flag does and doesn't do yet at the Asterisk level). Below that, the + rest of the tab detects whether \`services/pstn-trunk.sh\`'s dialplan is + actually installed (\`pstn-trunk-dialplan.conf\` present) and shows a + clear "not installed" message instead of the calling-permissions editor + if not, so it never shows real-looking-but-unenforced defaults. When + installed: the outbound/inbound concurrent-call caps, and every known + extension's permission tier (internal / restricted / full) and, for + restricted, its approved numbers — all editable live, no Asterisk + restart, no reinstall. Also manages personal-number assignments (DID -> + owner extension), additive to the shared trunk DID. Writes directly to \`pstn-limits.conf\` / \`pstn-permissions.conf\` / \`pstn-personal-dids.conf\`, which the dialplan reads fresh on every call. The spend-cap kill-switch and international-calling allow-list are deliberately **not** managed @@ -1077,6 +1081,37 @@ def write_permission(ext, tier, numbers_raw, messaging_enabled=False): return True, "Saved" +def write_messaging(ext, enabled): + """Sets/clears just the messaging flag for one extension, leaving any + tier/allowed_numbers/personal_did untouched. This is the write path for + the standalone "Internal SIP messaging" card, which works whether or + not a PSTN trunk has ever been installed — messaging has no dependency + on one (no cost, no carrier, no DID), unlike the calling-permissions + table this dashboard otherwise gates behind pstn_installed(). Creates + pstn-permissions.conf from scratch if it doesn't exist yet.""" + if not ASTERISK_CONFIG_DIR: + return False, "No Asterisk install detected on this box" + ext = str(ext).strip() + if not EXTEN_RE.match(ext): + return False, "Invalid extension" + + cp = _read_permissions_cp() + if enabled: + if not cp.has_section(ext): + cp.add_section(ext) + cp.set(ext, "messaging", "yes") + elif cp.has_section(ext) and cp.has_option(ext, "messaging"): + cp.remove_option(ext, "messaging") + + if cp.has_section(ext) and not cp.options(ext): + cp.remove_section(ext) + + ok, err = _write_ini_cp(_permissions_path(), PERMISSIONS_HEADER, cp) + if not ok: + return False, err + return True, "Saved" + + LIMIT_RE = re.compile(r"^\d+$") @@ -1327,9 +1362,17 @@ INDEX_HTML = """