From 8291eba55e2bef2784fb9f4fe191646e7252b49f Mon Sep 17 00:00:00 2001
From: Claude
Date: Thu, 23 Jul 2026 05:27:58 +0000
Subject: [PATCH] Make the messaging permission flag live-editable via the
dashboard
get_all_permissions()/write_permission() now handle messaging alongside
tier/allowed_numbers as one save action, and the Security Dashboard's PSTN
Trunk table gets a Messaging checkbox column - no more needing to re-run
pstn-trunk.sh's CLI just to change who can use internal SIP texting,
matching how tier/allowed_numbers/personal_did already worked.
Tested that messaging correctly survives tier changes and personal-DID
assignment/removal on the same extension (independent axes, as intended).
Still explicitly not done, and said so in both READMEs rather than
implying otherwise now that there's a nice UI for it: the actual SIP
MESSAGE dialplan wiring that would make Asterisk enforce this flag. That
gap hasn't changed - only the permission storage/UI layer around it has.
Co-Authored-By: Claude Sonnet 5
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
---
docs/pstn-calling-voipms-plan.md | 11 ++++-
services/pstn-trunk.sh | 32 +++++++++----
services/security-dashboard.sh | 78 +++++++++++++++++++++-----------
3 files changed, 83 insertions(+), 38 deletions(-)
diff --git a/docs/pstn-calling-voipms-plan.md b/docs/pstn-calling-voipms-plan.md
index c6b54d4..ec6b488 100644
--- a/docs/pstn-calling-voipms-plan.md
+++ b/docs/pstn-calling-voipms-plan.md
@@ -416,8 +416,15 @@ generator output. Fixed by quoting every value in that heredoc.
`messaging=yes` flag per extension in `pstn-permissions.conf`,
independent of the PSTN calling tiers (an extension can be
internal-tier for calling and still messaging-enabled, or vice versa),
- prompted at install time. **Not done**: the actual dialplan wiring that
- would make Asterisk *enforce* this flag on inbound `MESSAGE` requests.
+ prompted at install time AND now a checkbox right in the Security
+ Dashboard's PSTN Trunk permissions table (alongside tier/approved-
+ numbers) — no need to re-run the CLI installer just to change who can
+ message. Confirmed it correctly survives tier changes and personal-DID
+ assignment/removal on the same extension (this is what surfaced the
+ tier=internal section-wipe bug fixed above). **Not done**: the actual
+ dialplan wiring that would make Asterisk *enforce* this flag on
+ inbound `MESSAGE` requests — this flag currently does nothing at the
+ Asterisk level yet, it's groundwork.
Reasoned through but deliberately not shipped: Easy Asterisk dispatches
messages through the same `[intercom]` context calls use (no
`message_context` override), and whether a hand-written pattern there
diff --git a/services/pstn-trunk.sh b/services/pstn-trunk.sh
index a2f1978..10dec8c 100644
--- a/services/pstn-trunk.sh
+++ b/services/pstn-trunk.sh
@@ -1816,17 +1816,29 @@ Asterisk's native SIP \`MESSAGE\` support (extension-to-extension texting —
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}.
+${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.
-**Known gap:** this installer writes the permission flag (live-editable,
-same mechanism as the calling tiers), but the actual SIP \`MESSAGE\` routing
-dialplan wiring depends on how Easy Asterisk's own generated
-\`extensions.conf\`/\`pjsip.conf\` route inbound messages, which needs to be
-verified against a live install before it's safely automated here — shipping
-a guessed pattern risked either silently not working or interfering with
-call-routing precedence in the same \`[intercom]\` context. Treat the
-permission flag as ready for a dashboard/CLI-managed allow-list once that
-routing is confirmed, not as fully wired yet.
+**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,
+not an Easy Asterisk feature, so it's only manageable here or via the
+Security Dashboard.
+
+**Known gap:** the flag above is real and live-editable, but the actual
+SIP \`MESSAGE\` routing dialplan wiring — does Asterisk actually deliver/
+gate a message using this flag — depends on how Easy Asterisk's own
+generated \`extensions.conf\`/\`pjsip.conf\` route inbound messages, which
+needs to be verified against a live install before it's safely automated
+here. Shipping a guessed pattern risked either silently not working or
+interfering with call-routing precedence in the same \`[intercom]\`
+context, so it hasn't been guessed at. If you want this working end to
+end, the fastest path is checking a few things on a live box (e.g.
+whether an endpoint has \`message_context\` set, and what happens when you
+send a test SIP MESSAGE to one) so the dialplan gate can be built against
+real behavior instead of assumption — ask if you want to walk through
+that.
## Personal numbers
diff --git a/services/security-dashboard.sh b/services/security-dashboard.sh
index 6bb851a..c58328b 100644
--- a/services/security-dashboard.sh
+++ b/services/security-dashboard.sh
@@ -214,13 +214,17 @@ not in Docker — it needs to call \`cscli\` and read Asterisk's log directly.
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) and, for restricted, its approved numbers — all
- editable live, no Asterisk restart, no reinstall. Writes directly to
- \`pstn-limits.conf\` / \`pstn-permissions.conf\`, which the dialplan reads
- fresh on every call. The spend-cap kill-switch and international-calling
- allow-list are deliberately **not** managed here — CLI-only, via
- \`sudo ./setup.sh pstn-trunk\` — since both are more security-sensitive
- than what this tab already exposes.
+ 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-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
+ here — CLI-only, via \`sudo ./setup.sh pstn-trunk\` — since both are more
+ security-sensitive than what this tab already exposes.
- Link to the Asterisk web admin itself (doesn't embed it, just links out).
## Manage
@@ -986,12 +990,13 @@ def _read_permissions_cp():
def get_all_permissions():
- """{ext: {"tier": ..., "allowed_numbers": "num|num|..."}} for every
- extension with a non-internal tier on record. Extensions with no section
- are implicitly "internal" — the dialplan's AST_CONFIG() lookup treats a
- missing section as empty/denied the same way, so there's nothing to
- return for them here; the UI fills in "internal" as the default for any
- known extension (from list_extensions()) not present in this dict."""
+ """{ext: {"tier": ..., "allowed_numbers": "num|num|...", "messaging":
+ bool}} for every extension with a non-default record. Extensions with
+ no section are implicitly "internal"/messaging-disabled — the
+ dialplan's AST_CONFIG() lookup treats a missing section/key as empty/
+ denied the same way, so there's nothing to return for them here; the
+ UI fills in the defaults for any known extension (from
+ list_extensions()) not present in this dict."""
cp = _read_permissions_cp()
result = {}
for section in cp.sections():
@@ -1000,12 +1005,17 @@ def get_all_permissions():
result[section] = {
"tier": cp.get(section, "tier", fallback="internal"),
"allowed_numbers": cp.get(section, "allowed_numbers", fallback=""),
+ "messaging": cp.getboolean(section, "messaging", fallback=False),
}
return result
-def write_permission(ext, tier, numbers_raw):
- """Saves one extension's tier + (for restricted) approved-number list.
+def write_permission(ext, tier, numbers_raw, messaging_enabled=False):
+ """Saves one extension's tier + (for restricted) approved-number list +
+ messaging flag in one action — messaging is an independent axis from
+ the calling tier (see pstn-trunk.sh's file-level comment: an extension
+ can be internal-tier for calling and still messaging-enabled, or vice
+ versa), so it's set/cleared regardless of which tier branch runs below.
Numbers are normalized to a pipe-separated list of 11-digit US numbers —
pipe, not comma, because the dialplan uses this value directly as a
REGEX() alternation pattern (see services/pstn-trunk.sh's file-level
@@ -1030,15 +1040,12 @@ def write_permission(ext, tier, numbers_raw):
# personal_did assigned, and those must survive a tier change back
# to internal. Confirmed live as a real bug: cp.remove_section(ext)
# here used to silently discard both whenever tier was set to
- # internal. Only remove the section itself once nothing else is
- # left in it.
+ # internal.
if cp.has_section(ext):
if cp.has_option(ext, "tier"):
cp.remove_option(ext, "tier")
if cp.has_option(ext, "allowed_numbers"):
cp.remove_option(ext, "allowed_numbers")
- if not cp.options(ext):
- cp.remove_section(ext)
else:
if not cp.has_section(ext):
cp.add_section(ext)
@@ -1048,6 +1055,19 @@ def write_permission(ext, tier, numbers_raw):
elif cp.has_option(ext, "allowed_numbers"):
cp.remove_option(ext, "allowed_numbers")
+ if messaging_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")
+
+ # Drop the section entirely once nothing (tier, numbers, messaging,
+ # personal_did) is left in it — only reached this way when tier is
+ # internal, messaging is off, and no personal_did was ever assigned.
+ 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
@@ -1330,7 +1350,10 @@ INDEX_HTML = """
full — internal, plus any US number.
Changes apply live, on the next call — no Asterisk restart needed.
-
Ext
Name
Tier
Approved numbers (restricted only)
+
+ Messaging — Asterisk's native internal SIP texting (no carrier SMS, no PSTN, no cost), independent of the calling tier. Note: this flag is live-editable here, but whether Asterisk actually delivers/gates messages using it depends on dialplan wiring not yet verified against a live install — see this service's README.
+
+
Ext
Name
Tier
Approved numbers (restricted only)
Messaging
@@ -1544,7 +1567,7 @@ async function loadPstnPermissions() {
const tbody = document.querySelector("#pstn-table tbody");
if (!exts.length) {
- tbody.innerHTML = '
No extensions found (no Asterisk install detected, or pjsip.conf has no devices yet).
';
+ tbody.innerHTML = '
No extensions found (no Asterisk install detected, or pjsip.conf has no devices yet).
';
return;
}
tbody.innerHTML = exts.map(e => `
@@ -1558,6 +1581,7 @@ async function loadPstnPermissions() {