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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
This commit is contained in:
@@ -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
|
||||
|
||||
+22
-10
@@ -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
|
||||
|
||||
|
||||
@@ -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 = """<!doctype html>
|
||||
<b>full</b> — internal, plus any US number.
|
||||
Changes apply live, on the next call — no Asterisk restart needed.
|
||||
</p>
|
||||
<table id="pstn-table"><thead><tr><th>Ext</th><th>Name</th><th>Tier</th><th>Approved numbers (restricted only)</th><th></th></tr></thead><tbody></tbody></table>
|
||||
<p class="muted">
|
||||
<b>Messaging</b> — 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.
|
||||
</p>
|
||||
<table id="pstn-table"><thead><tr><th>Ext</th><th>Name</th><th>Tier</th><th>Approved numbers (restricted only)</th><th>Messaging</th><th></th></tr></thead><tbody></tbody></table>
|
||||
<div id="pstn-msg" class="muted" style="margin-top:0.5rem"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
@@ -1544,7 +1567,7 @@ async function loadPstnPermissions() {
|
||||
|
||||
const tbody = document.querySelector("#pstn-table tbody");
|
||||
if (!exts.length) {
|
||||
tbody.innerHTML = '<tr><td colspan=5 class=muted>No extensions found (no Asterisk install detected, or pjsip.conf has no devices yet).</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan=6 class=muted>No extensions found (no Asterisk install detected, or pjsip.conf has no devices yet).</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = exts.map(e => `<tr data-ext="${esc(e.ext)}">
|
||||
@@ -1558,6 +1581,7 @@ async function loadPstnPermissions() {
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" class="pstn-numbers" value="${esc(e.allowed_numbers)}" placeholder="15551234567,15559876543" ${e.tier === "restricted" ? "" : "disabled"}></td>
|
||||
<td style="text-align:center"><input type="checkbox" class="pstn-messaging" ${e.messaging ? "checked" : ""}></td>
|
||||
<td><button class="action" onclick="savePstnPermission('${esc(e.ext)}')">Save</button></td>
|
||||
</tr>`).join("");
|
||||
|
||||
@@ -1572,9 +1596,10 @@ async function savePstnPermission(ext) {
|
||||
const row = document.querySelector(`#pstn-table tr[data-ext="${ext}"]`);
|
||||
const tier = row.querySelector(".pstn-tier").value;
|
||||
const numbers = row.querySelector(".pstn-numbers").value;
|
||||
const messaging = row.querySelector(".pstn-messaging").checked;
|
||||
const res = await fetch("/api/pstn-permissions", {
|
||||
method: "POST", headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({ext: ext, tier: tier, allowed_numbers: numbers}),
|
||||
body: JSON.stringify({ext: ext, tier: tier, allowed_numbers: numbers, messaging: messaging}),
|
||||
});
|
||||
const data = await res.json();
|
||||
document.getElementById("pstn-msg").textContent = (data.message || (data.ok ? "Saved" : "Failed")) + " (extension " + ext + ")";
|
||||
@@ -1669,9 +1694,9 @@ class Handler(BaseHTTPRequestHandler):
|
||||
perms = get_all_permissions()
|
||||
extensions = []
|
||||
for e in list_extensions():
|
||||
p = perms.get(e["ext"], {"tier": "internal", "allowed_numbers": ""})
|
||||
extensions.append({"ext": e["ext"], "name": e["name"],
|
||||
"tier": p["tier"], "allowed_numbers": p["allowed_numbers"]})
|
||||
p = perms.get(e["ext"], {"tier": "internal", "allowed_numbers": "", "messaging": False})
|
||||
extensions.append({"ext": e["ext"], "name": e["name"], "tier": p["tier"],
|
||||
"allowed_numbers": p["allowed_numbers"], "messaging": p["messaging"]})
|
||||
self._json({"extensions": extensions})
|
||||
elif self.path == "/api/pstn-limits":
|
||||
self._json(get_limits())
|
||||
@@ -1702,7 +1727,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self._json(ban_asn(payload.get("asn", "")))
|
||||
elif self.path == "/api/pstn-permissions":
|
||||
ok, message = write_permission(
|
||||
payload.get("ext", ""), payload.get("tier", ""), payload.get("allowed_numbers", "")
|
||||
payload.get("ext", ""), payload.get("tier", ""), payload.get("allowed_numbers", ""),
|
||||
bool(payload.get("messaging", False))
|
||||
)
|
||||
self._json({"ok": ok, "message": message})
|
||||
elif self.path == "/api/pstn-limits":
|
||||
|
||||
Reference in New Issue
Block a user