Allow assigning a personal number to a group, not just a single extension
The dashboard's Personal Numbers card now accepts a group (stored as "@GroupName", unambiguous against a same-named numeric extension) as well as a plain extension. A group-owned DID rings every CURRENT member whose own tier/approved-numbers authorize the caller, computed fresh on every call by a generated pstn-personal-group-ring.sh (invoked via the dialplan's SHELL() function) rather than unrolled at install time, since group membership can change any time via the dashboard with no reinstall - unlike the shared ring-group, which is fixed at install/update time. Applies the identical per-member permission check the shared ring-group already bakes into the dialplan, just computed in a plain shell loop against the same two config files - group ownership doesn't bypass the tier/approved-numbers model. Tested standalone against mock config data (full/restricted/internal mix, matching/non-matching caller, empty and nonexistent groups) - all four cases behaved correctly. The dialplan's own SHELL() invocation is still unverified against a real call. Group ownership never touches pstn-permissions.conf's personal_did (outbound Caller-ID override) field, since there's no single extension to hang that on for a group.
This commit is contained in:
@@ -1395,24 +1395,42 @@ def write_personal_did(did, owner):
|
||||
it, and giving an extension a new personal DID drops whichever one it
|
||||
had before — this always leaves a clean 1:1 mapping in both files,
|
||||
rather than requiring the caller to clean up the old assignment
|
||||
itself."""
|
||||
itself.
|
||||
|
||||
owner may also be a group reference, written as "@GroupName" (the '@'
|
||||
makes it unambiguous against a same-named numeric extension - group
|
||||
names are free text and could otherwise collide, e.g. a group literally
|
||||
named "201"). A group-owned DID rings every CURRENT member whose own
|
||||
tier/approved-numbers authorize the caller, computed fresh on every
|
||||
call (see pstn-personal-group-ring.sh) rather than baked in at
|
||||
assignment time - membership changes take effect immediately, unlike
|
||||
the Groups card's other bulk actions. Group ownership has no single
|
||||
extension to hang an outbound Caller-ID override on, so it never
|
||||
touches pstn-permissions.conf the way a single-extension owner does."""
|
||||
if not ASTERISK_CONFIG_DIR:
|
||||
return False, "No Asterisk install detected on this box"
|
||||
did = str(did).strip()
|
||||
owner = str(owner).strip()
|
||||
if not PERSONAL_DID_RE.match(did):
|
||||
return False, "DID must be a 10-digit US number"
|
||||
if not EXTEN_RE.match(owner):
|
||||
|
||||
is_group = owner.startswith("@")
|
||||
group_name = owner[1:] if is_group else ""
|
||||
if is_group:
|
||||
if not group_name or not _read_groups_cp().has_section(group_name):
|
||||
return False, "Group '%s' not found" % group_name
|
||||
elif not EXTEN_RE.match(owner):
|
||||
return False, "Invalid owner extension"
|
||||
|
||||
dids_cp = _read_personal_dids_cp()
|
||||
perms_cp = _read_permissions_cp()
|
||||
|
||||
for section in perms_cp.sections():
|
||||
if section != owner and perms_cp.get(section, "personal_did", fallback="") == did:
|
||||
perms_cp.remove_option(section, "personal_did")
|
||||
if not perms_cp.options(section):
|
||||
perms_cp.remove_section(section)
|
||||
if not is_group:
|
||||
for section in perms_cp.sections():
|
||||
if section != owner and perms_cp.get(section, "personal_did", fallback="") == did:
|
||||
perms_cp.remove_option(section, "personal_did")
|
||||
if not perms_cp.options(section):
|
||||
perms_cp.remove_section(section)
|
||||
|
||||
for section in list(dids_cp.sections()):
|
||||
if section != did and dids_cp.get(section, "owner", fallback="") == owner:
|
||||
@@ -1422,13 +1440,17 @@ def write_personal_did(did, owner):
|
||||
dids_cp.add_section(did)
|
||||
dids_cp.set(did, "owner", owner)
|
||||
|
||||
ok, err = _write_ini_cp(_personal_dids_path(), PERSONAL_DIDS_HEADER, dids_cp)
|
||||
if not ok:
|
||||
return False, err
|
||||
|
||||
if is_group:
|
||||
return True, "Assigned %s to group %s" % (did, group_name)
|
||||
|
||||
if not perms_cp.has_section(owner):
|
||||
perms_cp.add_section(owner)
|
||||
perms_cp.set(owner, "personal_did", did)
|
||||
|
||||
ok, err = _write_ini_cp(_personal_dids_path(), PERSONAL_DIDS_HEADER, dids_cp)
|
||||
if not ok:
|
||||
return False, err
|
||||
ok, err = _write_ini_cp(_permissions_path(), PERMISSIONS_HEADER, perms_cp)
|
||||
if not ok:
|
||||
return False, err
|
||||
@@ -1941,9 +1963,14 @@ async function loadPstnPermissions() {
|
||||
const data = await res.json();
|
||||
const exts = data.extensions || [];
|
||||
|
||||
const grpRes = await fetch("/api/pstn-groups");
|
||||
const grpData = await grpRes.json();
|
||||
const groups = grpData.groups || [];
|
||||
|
||||
const ownerSel = document.getElementById("pd-owner");
|
||||
ownerSel.innerHTML = exts.map(e => `<option value="${esc(e.ext)}">${esc(e.ext)} — ${esc(e.name)}</option>`).join("")
|
||||
|| '<option value="">No extensions found</option>';
|
||||
const extOptions = exts.map(e => `<option value="${esc(e.ext)}">${esc(e.ext)} — ${esc(e.name)}</option>`).join("");
|
||||
const groupOptions = groups.map(g => `<option value="@${esc(g.name)}">Group: ${esc(g.name)}</option>`).join("");
|
||||
ownerSel.innerHTML = (extOptions + groupOptions) || '<option value="">No extensions found</option>';
|
||||
|
||||
const tbody = document.querySelector("#pstn-table tbody");
|
||||
if (!exts.length) {
|
||||
@@ -1991,11 +2018,16 @@ async function loadPersonalDids() {
|
||||
const data = await res.json();
|
||||
const dids = data.dids || [];
|
||||
const tbody = document.querySelector("#pd-table tbody");
|
||||
tbody.innerHTML = dids.map(d => `<tr>
|
||||
tbody.innerHTML = dids.map(d => {
|
||||
const ownerDisplay = d.owner.startsWith("@")
|
||||
? "Group: " + esc(d.owner.slice(1))
|
||||
: esc(d.owner) + (d.owner_name ? " — " + esc(d.owner_name) : "");
|
||||
return `<tr>
|
||||
<td>${esc(d.did)}</td>
|
||||
<td>${esc(d.owner)}${d.owner_name ? " — " + esc(d.owner_name) : ""}</td>
|
||||
<td>${ownerDisplay}</td>
|
||||
<td><button class="action" onclick="removePersonalDid('${esc(d.did)}')">Remove</button></td>
|
||||
</tr>`).join("") || "<tr><td colspan=3 class=muted>No personal numbers assigned — every extension shares the main trunk DID.</td></tr>";
|
||||
</tr>`;
|
||||
}).join("") || "<tr><td colspan=3 class=muted>No personal numbers assigned — every extension shares the main trunk DID.</td></tr>";
|
||||
}
|
||||
|
||||
document.getElementById("pd-save").addEventListener("click", async () => {
|
||||
|
||||
Reference in New Issue
Block a user