Name the PSTN modes as extensions of the original tiers
The behaviour was right but the vocabulary wasn't: the previous commit replaced full/restricted/internal with a new none/open/out/in/both naming, when the ask was to extend the existing tier dropdown rather than supplant it. The dropdown now reads: full, restricted (both ways), restricted incoming, restricted outgoing, internal. The stored values match — internal, restricted, full, restricted-in, restricted-out — so the original three keep their own names in the config file and migrating a legacy install is now near-identity for them (tier=full becomes restrict=full, tier=restricted becomes restrict=restricted, which is what that tier already meant). restricted incoming = dials anywhere, only whitelisted numbers get through. restricted outgoing = anyone can call in, may only dial the whitelist. Named in parallel rather than as "full incoming", so the two sit next to each other in the list without needing the parenthetical to tell them apart. No behavioural change: the derived tier_out/allowed_out/tier_in/allowed_in the dialplan reads are unchanged, so the dialplan itself is untouched. Verified: legacy migration mapping the original tiers to their same-named modes; both new modes round-tripping through the dashboard and compiling to the right derived keys (restricted-in sets allowed_in only, restricted-out sets allowed_out only); whitelist disabled on full and internal; and the group-ring helper still ringing restricted-incoming only for a whitelisted caller while restricted-outgoing rings for everyone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAddJGE1G6eGaPzmScG5Vh
This commit is contained in:
+25
-22
@@ -3,8 +3,10 @@
|
||||
# US-only outbound (NANP dialplan
|
||||
# restriction), independent outbound/inbound concurrent-call caps, a
|
||||
# per-extension permission model built on ONE whitelist plus a mode saying
|
||||
# which direction(s) it applies to (none / unrestricted / restrict outbound /
|
||||
# restrict inbound / restrict both), a configurable inbound ring-group,
|
||||
# which direction(s) it applies to — the original full / restricted /
|
||||
# internal tiers, plus restricted-in (whitelist gates incoming, dials
|
||||
# anywhere) and restricted-out (whitelist gates outgoing, anyone can call
|
||||
# in) — a configurable inbound ring-group,
|
||||
# IP-authenticated trunk (no SIP password stored), ntfy alerts on
|
||||
# denied/rejected calls, and a periodic spend/volume check.
|
||||
#
|
||||
@@ -784,32 +786,32 @@ _pstn_migrate_permissions_split() {
|
||||
function flush_section() {
|
||||
if (!have) return
|
||||
if (header != "") print header
|
||||
mode = "none"
|
||||
if (tier == "full") mode = "open"
|
||||
else if (tier == "restricted") mode = "both"
|
||||
mode = "internal"
|
||||
if (tier == "full") mode = "full"
|
||||
else if (tier == "restricted") mode = "restricted"
|
||||
# An install that predates the split has no tier_out; one made
|
||||
# between the split and this change may, so honour it if present.
|
||||
if (tier_out != "" || tier_in != "") {
|
||||
o = (tier_out != "" ? tier_out : tier)
|
||||
i = (tier_in != "" ? tier_in : tier)
|
||||
if (o == "full" && i == "full") mode = "open"
|
||||
else if (o == "restricted" && i == "restricted") mode = "both"
|
||||
else if (o == "restricted") mode = "out"
|
||||
else if (i == "restricted") mode = "in"
|
||||
else mode = "none"
|
||||
if (o == "full" && i == "full") mode = "full"
|
||||
else if (o == "restricted" && i == "restricted") mode = "restricted"
|
||||
else if (o == "restricted") mode = "restricted-out"
|
||||
else if (i == "restricted") mode = "restricted-in"
|
||||
else mode = "internal"
|
||||
}
|
||||
print "restrict=" mode
|
||||
if (nums != "") print "allowed_numbers=" nums
|
||||
if (mode == "open") { print "tier_out=full"; print "tier_in=full"; print "tier=full" }
|
||||
else if (mode == "out") {
|
||||
if (mode == "full") { print "tier_out=full"; print "tier_in=full"; print "tier=full" }
|
||||
else if (mode == "restricted-out") {
|
||||
print "tier_out=restricted"; print "allowed_out=" nums
|
||||
print "tier_in=full"; print "tier=restricted"
|
||||
}
|
||||
else if (mode == "in") {
|
||||
else if (mode == "restricted-in") {
|
||||
print "tier_out=full"; print "tier_in=restricted"
|
||||
print "allowed_in=" nums; print "tier=full"
|
||||
}
|
||||
else if (mode == "both") {
|
||||
else if (mode == "restricted") {
|
||||
print "tier_out=restricted"; print "allowed_out=" nums
|
||||
print "tier_in=restricted"; print "allowed_in=" nums
|
||||
print "tier=restricted"
|
||||
@@ -887,12 +889,13 @@ _pstn_write_permissions_file() {
|
||||
local _written_exts=""
|
||||
{
|
||||
echo "; PSTN permissions. Each extension has ONE whitelist and a 'restrict' mode"
|
||||
echo "; saying which direction(s) that whitelist applies to:"
|
||||
echo "; none - no PSTN at all (internal extension calling still works)"
|
||||
echo "; open - unrestricted both ways"
|
||||
echo "; out - may only DIAL numbers on the whitelist; anyone may call in"
|
||||
echo "; in - may only be CALLED BY numbers on the whitelist; may dial anywhere"
|
||||
echo "; both - the whitelist applies in both directions"
|
||||
echo "; saying which direction(s) that whitelist applies to. The first three are"
|
||||
echo "; the original tiers, unchanged; the last two are the new half-restrictions:"
|
||||
echo "; full - no whitelist, calls both ways"
|
||||
echo "; restricted - the whitelist applies BOTH ways"
|
||||
echo "; internal - no PSTN at all (internal extension calling still works)"
|
||||
echo "; restricted-in - whitelist gates INCOMING only; may dial anywhere"
|
||||
echo "; restricted-out - whitelist gates OUTGOING only; anyone may call in"
|
||||
echo "; 'allowed_numbers' is that whitelist: pipe-separated 11-digit numbers, used"
|
||||
echo "; directly as a REGEX() alternation (see this file's own comments on why"
|
||||
echo "; untrusted call data is always the string being tested, never the pattern)."
|
||||
@@ -927,7 +930,7 @@ _pstn_write_permissions_file() {
|
||||
local _ext
|
||||
for _ext in $FULL_EXTS; do
|
||||
echo "[$_ext]"
|
||||
echo "restrict=open"
|
||||
echo "restrict=full"
|
||||
echo "tier_out=full"
|
||||
echo "tier_in=full"
|
||||
echo "tier=full"
|
||||
@@ -940,7 +943,7 @@ _pstn_write_permissions_file() {
|
||||
_ext="$1"; local _nums="$2"
|
||||
shift 2
|
||||
echo "[$_ext]"
|
||||
echo "restrict=both"
|
||||
echo "restrict=restricted"
|
||||
echo "allowed_numbers=${_nums}"
|
||||
echo "tier_out=restricted"
|
||||
echo "allowed_out=${_nums}"
|
||||
|
||||
@@ -1144,8 +1144,9 @@ def _write_ini_cp(path, header, cp):
|
||||
PERMISSIONS_HEADER = (
|
||||
"; PSTN permissions. Each extension has ONE whitelist (allowed_numbers)\n"
|
||||
"; and a 'restrict' mode saying which direction(s) it applies to:\n"
|
||||
"; none / open / out (may only dial the list) / in (may only be called\n"
|
||||
"; by the list) / both.\n"
|
||||
"; full / restricted (both ways) / internal - the original tiers - plus\n"
|
||||
"; restricted-in (whitelist gates incoming only) and restricted-out\n"
|
||||
"; (whitelist gates outgoing only).\n"
|
||||
"; restrict + allowed_numbers are the authored pair; tier_out/allowed_out\n"
|
||||
"; and tier_in/allowed_in are DERIVED from them and are what the dialplan\n"
|
||||
"; reads; 'tier' is a rollback mirror for a pre-split pstn-trunk.sh.\n"
|
||||
@@ -1187,7 +1188,7 @@ def _read_permissions_cp():
|
||||
return cp
|
||||
|
||||
|
||||
RESTRICT_RE = re.compile(r"^(none|open|out|in|both)$")
|
||||
RESTRICT_RE = re.compile(r"^(internal|restricted|full|restricted-in|restricted-out)$")
|
||||
|
||||
|
||||
def _derive_restrict(tier_out, tier_in):
|
||||
@@ -1197,14 +1198,14 @@ def _derive_restrict(tier_out, tier_in):
|
||||
the more restrictive reading: anything that isn't clearly open in a
|
||||
direction is treated as restricted or none, never widened."""
|
||||
if tier_out == "full" and tier_in == "full":
|
||||
return "open"
|
||||
return "full"
|
||||
if tier_out == "restricted" and tier_in == "restricted":
|
||||
return "both"
|
||||
return "restricted"
|
||||
if tier_out == "restricted":
|
||||
return "out"
|
||||
return "restricted-out"
|
||||
if tier_in == "restricted":
|
||||
return "in"
|
||||
return "none"
|
||||
return "restricted-in"
|
||||
return "internal"
|
||||
|
||||
|
||||
def get_all_permissions():
|
||||
@@ -1212,8 +1213,9 @@ def get_all_permissions():
|
||||
with a non-default record.
|
||||
|
||||
Each extension has ONE whitelist and a mode saying which direction(s) it
|
||||
applies to: none (no PSTN), open (unrestricted), out (may only dial the
|
||||
list), in (may only be called by the list), both. That authored pair is
|
||||
applies to: the original internal / restricted / full, plus restricted-in
|
||||
(whitelist gates incoming, dials anywhere) and restricted-out (whitelist
|
||||
gates outgoing, anyone may call in). That authored pair is
|
||||
what this returns and what the UI edits; the dialplan reads the derived
|
||||
tier_out/allowed_out/tier_in/allowed_in that write_permission keeps in
|
||||
step with it.
|
||||
@@ -1254,12 +1256,15 @@ def get_all_permissions():
|
||||
# Which per-direction tiers each authored mode compiles down to. The dialplan
|
||||
# only ever reads the compiled keys; this table is the single place the
|
||||
# mapping is defined.
|
||||
# The first three are the original tiers, unchanged. restricted-in and
|
||||
# restricted-out are the halves: the whitelist gates one direction while the
|
||||
# other stays wide open.
|
||||
_RESTRICT_TIERS = {
|
||||
"none": ("internal", "internal"),
|
||||
"open": ("full", "full"),
|
||||
"out": ("restricted", "full"),
|
||||
"in": ("full", "restricted"),
|
||||
"both": ("restricted", "restricted"),
|
||||
"internal": ("internal", "internal"),
|
||||
"full": ("full", "full"),
|
||||
"restricted": ("restricted", "restricted"),
|
||||
"restricted-in": ("full", "restricted"),
|
||||
"restricted-out": ("restricted", "full"),
|
||||
}
|
||||
|
||||
|
||||
@@ -1276,7 +1281,8 @@ def write_permission(ext, restrict, numbers_raw, messaging_enabled=False):
|
||||
|
||||
One list, not two: the whitelist is "the numbers this extension deals
|
||||
with", and the mode says whether that constrains dialling out, being
|
||||
called, or both. Modes are none / open / out / in / both.
|
||||
called, or both. Modes are internal / restricted / full (the original
|
||||
tiers) plus restricted-in and restricted-out.
|
||||
|
||||
Writes three layers, all derived from those two authored values:
|
||||
restrict, allowed_numbers what a human edits (and what this reads back)
|
||||
@@ -1308,7 +1314,7 @@ def write_permission(ext, restrict, numbers_raw, messaging_enabled=False):
|
||||
tier_out, tier_in = _RESTRICT_TIERS[restrict]
|
||||
|
||||
cp = _read_permissions_cp()
|
||||
if restrict == "none":
|
||||
if restrict == "internal":
|
||||
# Clear the PSTN keys only, never the whole section — messaging and
|
||||
# personal_did are independent and must survive. (Removing the
|
||||
# section here was a real, confirmed bug in an earlier version.)
|
||||
@@ -1321,7 +1327,7 @@ def write_permission(ext, restrict, numbers_raw, messaging_enabled=False):
|
||||
if not cp.has_section(ext):
|
||||
cp.add_section(ext)
|
||||
cp.set(ext, "restrict", restrict)
|
||||
_set_or_clear(cp, ext, "allowed_numbers", numbers if restrict != "open" else "")
|
||||
_set_or_clear(cp, ext, "allowed_numbers", numbers if restrict != "full" else "")
|
||||
cp.set(ext, "tier_out", tier_out)
|
||||
cp.set(ext, "tier_in", tier_in)
|
||||
_set_or_clear(cp, ext, "allowed_out", numbers if tier_out == "restricted" else "")
|
||||
@@ -1336,8 +1342,8 @@ def write_permission(ext, restrict, numbers_raw, messaging_enabled=False):
|
||||
cp.remove_option(ext, "messaging")
|
||||
|
||||
# Drop the section entirely once nothing is left in it — only reachable
|
||||
# when the mode is none, messaging is off, and no personal_did was ever
|
||||
# assigned.
|
||||
# when the mode 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)
|
||||
|
||||
@@ -1345,7 +1351,7 @@ def write_permission(ext, restrict, numbers_raw, messaging_enabled=False):
|
||||
if not ok:
|
||||
return False, err
|
||||
|
||||
if restrict != "none" and restrict != "open" and not clean:
|
||||
if restrict not in ("internal", "full") and not clean:
|
||||
return True, ("Saved, but the whitelist is EMPTY — with this mode that means no PSTN "
|
||||
"number is permitted in the restricted direction yet.")
|
||||
return True, "Saved"
|
||||
@@ -2693,11 +2699,11 @@ INDEX_HTML = """<!doctype html>
|
||||
<p class="muted"><b>Name</b> and <b>Category</b> are editable in place — click, type, then Save. Adding an extension generates a random password and reloads PJSIP + rebuilds the dialplan automatically; the password is shown once, at the top of this card.</p>
|
||||
<p class="muted pstn-only"><b>PSTN</b> sets how the outside phone network reaches this extension, and the <b>Whitelist</b> beside it is the one list of numbers that mode applies to:</p>
|
||||
<ul class="muted pstn-only" style="margin:0 0 var(--sp-2); padding-left:1.2rem">
|
||||
<li><b>No PSTN</b> — outside calls neither in nor out.</li>
|
||||
<li><b>Unrestricted</b> — dial anyone, anyone can call.</li>
|
||||
<li><b>Restrict outbound</b> — may only dial the whitelist; anyone can call in.</li>
|
||||
<li><b>Restrict inbound</b> — may dial anywhere; only the whitelist can call in.</li>
|
||||
<li><b>Restrict both</b> — the whitelist applies in both directions.</li>
|
||||
<li><b>full</b> — dial anyone, anyone can call. No whitelist.</li>
|
||||
<li><b>restricted (both ways)</b> — the whitelist applies to outgoing <i>and</i> incoming.</li>
|
||||
<li><b>restricted incoming</b> — dials anywhere; only whitelisted numbers can call in.</li>
|
||||
<li><b>restricted outgoing</b> — anyone can call in; may only dial the whitelist.</li>
|
||||
<li><b>internal</b> — no PSTN at all, either direction.</li>
|
||||
</ul>
|
||||
<p class="muted pstn-only">Internal extension-to-extension calling and ring groups are never gated by any of this. Changes are usually live on the next call; if one doesn't seem to take effect, use "Commit changes" above.</p>
|
||||
<p class="muted"><b>Messaging</b> — Asterisk's native SIP texting between extensions: no carrier SMS, no PSTN, no cost, and no dependency on a PSTN trunk at all (which is why this column is here even with no trunk installed). Independent of the calling tier. Enforced live by a dedicated dialplan context — see <code>services/asterisk.sh</code>'s README, including its caveat that the sender-extraction logic still needs real-traffic confirmation. If this box predates that wiring, rerun <code>sudo ./setup.sh asterisk</code>.</p>
|
||||
@@ -3224,15 +3230,19 @@ async function loadExtensions() {
|
||||
let extRows = [];
|
||||
let extSort = { key: null, dir: 1 };
|
||||
|
||||
// Sort the PSTN column by how much reach the mode grants, not alphabetically
|
||||
// — "open" would otherwise land between "both" and "in".
|
||||
const RESTRICT_ORDER = { none: 0, both: 1, in: 2, out: 3, open: 4 };
|
||||
// Sort by how much reach the mode grants, not alphabetically — "full" would
|
||||
// otherwise land between "internal" and "restricted".
|
||||
const RESTRICT_ORDER = {
|
||||
"internal": 0, "restricted": 1, "restricted-in": 2, "restricted-out": 3, "full": 4,
|
||||
};
|
||||
// Order as offered in the dropdown: the original three tiers still read the
|
||||
// same, with the two half-restrictions slotted in between.
|
||||
const RESTRICT_LABELS = [
|
||||
["none", "No PSTN"],
|
||||
["open", "Unrestricted"],
|
||||
["out", "Restrict outbound"],
|
||||
["in", "Restrict inbound"],
|
||||
["both", "Restrict both"],
|
||||
["full", "full"],
|
||||
["restricted", "restricted (both ways)"],
|
||||
["restricted-in", "restricted incoming"],
|
||||
["restricted-out", "restricted outgoing"],
|
||||
["internal", "internal"],
|
||||
];
|
||||
|
||||
function extSortValue(e, key) {
|
||||
@@ -3255,7 +3265,7 @@ function restrictSelect(value) {
|
||||
}
|
||||
|
||||
// The whitelist is meaningless for "no PSTN" and "unrestricted".
|
||||
function restrictUsesList(mode) { return mode === "out" || mode === "in" || mode === "both"; }
|
||||
function restrictUsesList(mode) { return mode.indexOf("restricted") === 0; }
|
||||
|
||||
function renderExtensions() {
|
||||
const tbody = document.querySelector("#ext-table tbody");
|
||||
@@ -3935,7 +3945,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
perms = get_all_permissions()
|
||||
extensions = []
|
||||
for e in list_extensions():
|
||||
p = perms.get(e["ext"], {"restrict": "none", "allowed_numbers": "", "messaging": False})
|
||||
p = perms.get(e["ext"], {"restrict": "internal", "allowed_numbers": "", "messaging": False})
|
||||
extensions.append({"ext": e["ext"], "name": e["name"],
|
||||
"restrict": p["restrict"],
|
||||
"allowed_numbers": p["allowed_numbers"],
|
||||
|
||||
Reference in New Issue
Block a user