wldx-admin-cli 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +404 -2
- package/bin/wx-admin.mjs +5 -0
- package/package.json +36 -3
- package/src/cli.mjs +71 -0
- package/src/commands/acl.mjs +376 -0
- package/src/commands/api.mjs +66 -0
- package/src/commands/config.mjs +280 -0
- package/src/commands/device.mjs +275 -0
- package/src/commands/dialplan.mjs +488 -0
- package/src/commands/index.mjs +85 -0
- package/src/commands/pbx.mjs +197 -0
- package/src/commands/phonebook.mjs +558 -0
- package/src/commands/queue.mjs +456 -0
- package/src/commands/records.mjs +623 -0
- package/src/commands/routing.mjs +1059 -0
- package/src/commands/sound.mjs +232 -0
- package/src/commands/system.mjs +668 -0
- package/src/commands/trunk.mjs +808 -0
- package/src/commands/user.mjs +574 -0
- package/src/match.mjs +15 -0
- package/src/pbx.mjs +369 -0
- package/src/secret.mjs +42 -0
- package/src/version.mjs +3 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// wx-admin acl: the ACL groups of the PBX (what the users of a group may do, and to which groups), over the PBX REST
|
|
2
|
+
// API. Operations (OpenAPI tags PBX, ACL), scope aclgroups (the spec names none for PUT and DELETE: aclgroups:*):
|
|
3
|
+
// GET /PBX/AclGroups/ the groups with their rules (there is no GET of one group)
|
|
4
|
+
// POST /PBX/AclGroups/ create {name, wcgrp?, inherits?: {dn}, rules?: [{rule, ability, group}]}
|
|
5
|
+
// PUT /PBX/AclGroups/{groupId}/ change name, inherits, rules, adminRules (only the user "admin" may change
|
|
6
|
+
// more than rules)
|
|
7
|
+
// DELETE /PBX/AclGroups/{ids}/ delete (ids joined by commas)
|
|
8
|
+
// GET /PBX/AclGroups/Rules/ the rules there are: {list, default, adminList}
|
|
9
|
+
// GET /PBX/AclGroups/Permissions/ each permission and which groups have it; filters as permissions[]=KEY
|
|
10
|
+
// and groups[]=ID (the comma form of the spec is ignored by the PBX)
|
|
11
|
+
// A rule is {rule (a key such as CALL), ability (yes/no, set/noset, use/nouse, call/nocall), group (a group DN, "all",
|
|
12
|
+
// or "" for rules that are not about a group)}; an admin rule is {subject, operation (epbx, egroup, epage, eperm
|
|
13
|
+
// and their no… forms)}.
|
|
14
|
+
import {readJsonInput as readJsonFile} from 'wldx-core/files';
|
|
15
|
+
import {pickByName} from '../match.mjs';
|
|
16
|
+
import {UsageError} from 'wldx-core/errors';
|
|
17
|
+
import {paint, table} from 'wldx-core/format';
|
|
18
|
+
import {pbxClient} from '../pbx.mjs';
|
|
19
|
+
|
|
20
|
+
const SCOPE = {scope: 'aclgroups'};
|
|
21
|
+
// The abilities the PBX gives (the spec also names true and false).
|
|
22
|
+
export const ABILITIES = ['yes', 'no', 'set', 'noset', 'use', 'nouse', 'call', 'nocall', 'true', 'false'];
|
|
23
|
+
export const ADMIN_OPERATIONS = ['epbx', 'noepbx', 'egroup', 'noegroup', 'epage', 'noepage', 'eperm', 'noeperm'];
|
|
24
|
+
const NAME = /^[^\s/\\,][^/\\,]{0,63}$/;
|
|
25
|
+
const RULE_KEY = /^[A-Za-z0-9_]{1,64}$/;
|
|
26
|
+
|
|
27
|
+
async function readJsonInput(ctx, from) {
|
|
28
|
+
const data = await readJsonFile(ctx, from); // no credentials, nothing hidden in the home folder
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const listGroups = async (client) => (await client.call('GET', '/PBX/AclGroups/', undefined, SCOPE))?.records ?? [];
|
|
33
|
+
|
|
34
|
+
// A group of the list by id, name (any case) or DN.
|
|
35
|
+
function findGroup(list, ref) {
|
|
36
|
+
const r = String(ref);
|
|
37
|
+
const byIdOrDn = list.find((g) => String(g.id) === r || g.dn === r);
|
|
38
|
+
return byIdOrDn ?? pickByName(list, r, (g) => g.name, 'ACL group', 'wx-admin acl groups');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const groupPath = (...ids) => {
|
|
42
|
+
for (const id of ids) if (!/^\d+$/.test(String(id))) throw new UsageError(`Not an ACL group id: ${id}`);
|
|
43
|
+
return `/PBX/AclGroups/${ids.join(',')}/`;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// The DN a rule is about: "all", "" (none), a DN, or the id or name of a group.
|
|
47
|
+
function targetDn(list, ref) {
|
|
48
|
+
if (ref === undefined || ref === '' || ref === 'all') return ref ?? '';
|
|
49
|
+
if (/^gid=/.test(ref)) return ref;
|
|
50
|
+
return findGroup(list, ref).dn;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --rule "KEY ABILITY [GROUP]".
|
|
54
|
+
function parseRule(spec, list) {
|
|
55
|
+
const [rule, ability, ...rest] = String(spec).trim().split(/\s+/);
|
|
56
|
+
if (!RULE_KEY.test(rule ?? '') || !ability)
|
|
57
|
+
throw new UsageError(`--rule is "KEY ABILITY [GROUP]", e.g. "CALL yes all": ${spec}`);
|
|
58
|
+
if (!ABILITIES.includes(ability)) throw new UsageError(`An ability is one of ${ABILITIES.join(', ')}: ${ability}`);
|
|
59
|
+
return {rule, ability, group: targetDn(list, rest.join(' ') || '')};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --admin-rule "SUBJECT OPERATION".
|
|
63
|
+
function parseAdminRule(spec, list) {
|
|
64
|
+
const [subject, operation] = String(spec).trim().split(/\s+/);
|
|
65
|
+
if (!subject || !ADMIN_OPERATIONS.includes(operation))
|
|
66
|
+
throw new UsageError(
|
|
67
|
+
`--admin-rule is "SUBJECT OPERATION" (${ADMIN_OPERATIONS.join(', ')}), e.g. "users__users epage": ${spec}`,
|
|
68
|
+
);
|
|
69
|
+
const group = list.find((g) => g.name === subject || String(g.id) === subject);
|
|
70
|
+
return {subject: group && /group/.test(operation) ? group.dn : subject, operation};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const sameRule = (a, b) => a.rule === b.rule && (a.group ?? '') === (b.group ?? '');
|
|
74
|
+
|
|
75
|
+
// The rules with --rule (added, or replacing the one of the same key and group) and --remove-rule "KEY [GROUP]".
|
|
76
|
+
function changedRules(rules, values, list) {
|
|
77
|
+
let out = [...rules];
|
|
78
|
+
for (const spec of values['remove-rule'] ?? []) {
|
|
79
|
+
const [rule, ...rest] = String(spec).trim().split(/\s+/);
|
|
80
|
+
const group = rest.length ? targetDn(list, rest.join(' ')) : undefined;
|
|
81
|
+
const before = out.length;
|
|
82
|
+
out = out.filter((r) => !(r.rule === rule && (group === undefined || (r.group ?? '') === group)));
|
|
83
|
+
if (out.length === before) throw new UsageError(`The group has no rule ${spec}`);
|
|
84
|
+
}
|
|
85
|
+
for (const spec of values.rule ?? []) {
|
|
86
|
+
const r = parseRule(spec, list);
|
|
87
|
+
const at = out.findIndex((x) => sameRule(x, r));
|
|
88
|
+
if (at >= 0) out[at] = r;
|
|
89
|
+
else out.push(r);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function changedAdminRules(rules, values, list) {
|
|
95
|
+
let out = [...rules];
|
|
96
|
+
for (const subject of values['remove-admin-rule'] ?? []) {
|
|
97
|
+
const before = out.length;
|
|
98
|
+
const dn = list.find((g) => g.name === subject || String(g.id) === subject)?.dn;
|
|
99
|
+
out = out.filter((r) => r.subject !== subject && r.subject !== dn);
|
|
100
|
+
if (out.length === before) throw new UsageError(`The group has no admin rule for ${subject}`);
|
|
101
|
+
}
|
|
102
|
+
for (const spec of values['admin-rule'] ?? []) {
|
|
103
|
+
const r = parseAdminRule(spec, list);
|
|
104
|
+
const at = out.findIndex(
|
|
105
|
+
(x) => x.subject === r.subject && x.operation.replace(/^no/, '') === r.operation.replace(/^no/, ''),
|
|
106
|
+
);
|
|
107
|
+
if (at >= 0) out[at] = r;
|
|
108
|
+
else out.push(r);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const plainRules = (rules) => (rules ?? []).map(({rule, ability, group}) => ({rule, ability, group: group ?? ''}));
|
|
114
|
+
const plainAdminRules = (rules) => (rules ?? []).map(({subject, operation}) => ({subject, operation}));
|
|
115
|
+
|
|
116
|
+
// "gid=…" → the group name, when known.
|
|
117
|
+
const nameOf = (list, dn) => (dn === 'all' ? 'all groups' : (list.find((g) => g.dn === dn)?.name ?? dn));
|
|
118
|
+
|
|
119
|
+
function groupLines(g, list, ctx) {
|
|
120
|
+
const c = paint(ctx);
|
|
121
|
+
const lines = [
|
|
122
|
+
`${c.bold(g.name)} (id ${g.id})${g.inherits?.dn ? ` · inherits ${g.inherits.name ?? nameOf(list, g.inherits.dn)}` : ''}`,
|
|
123
|
+
];
|
|
124
|
+
const rules = g.rules ?? [];
|
|
125
|
+
lines.push(rules.length ? 'rules:' : c.dim('no rules'));
|
|
126
|
+
lines.push(...table(rules.map((r) => [` ${r.rule}`, r.ability, r.group ? nameOf(list, r.group) : ''])));
|
|
127
|
+
const admin = g.adminRules ?? [];
|
|
128
|
+
if (admin.length)
|
|
129
|
+
lines.push(
|
|
130
|
+
'admin rules:',
|
|
131
|
+
...table(admin.map((r) => [` ${r.operation}`, r.subject === 'all' ? 'all' : nameOf(list, r.subject)])),
|
|
132
|
+
);
|
|
133
|
+
return lines;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const ruleOptions = {
|
|
137
|
+
rule: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
multiple: true,
|
|
140
|
+
placeholder: '"KEY ABILITY [GROUP]"',
|
|
141
|
+
description: 'a rule, e.g. "CALL yes all" or "SEE_ANALYTICS yes Sales" (wx-admin acl rules)',
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const groups = {
|
|
146
|
+
acl: {
|
|
147
|
+
summary: 'ACL groups: what the users of a group may do; list, show, create, change, delete, permissions',
|
|
148
|
+
commands: {
|
|
149
|
+
groups: {
|
|
150
|
+
summary: 'the ACL groups of the PBX',
|
|
151
|
+
usage: 'wx-admin acl groups',
|
|
152
|
+
options: {},
|
|
153
|
+
args: [0, 0],
|
|
154
|
+
async run(ctx) {
|
|
155
|
+
return (await pbxClient(ctx)).call('GET', '/PBX/AclGroups/', undefined, SCOPE);
|
|
156
|
+
},
|
|
157
|
+
text: (r) => {
|
|
158
|
+
const list = r?.records ?? [];
|
|
159
|
+
if (!list.length) return 'No ACL groups';
|
|
160
|
+
return table(
|
|
161
|
+
list.map((g) => [
|
|
162
|
+
g.id,
|
|
163
|
+
g.name,
|
|
164
|
+
`${(g.rules ?? []).length} rules`,
|
|
165
|
+
`${(g.adminRules ?? []).length} admin rules`,
|
|
166
|
+
g.inherits?.dn ? `inherits ${nameOf(list, g.inherits.dn)}` : '',
|
|
167
|
+
]),
|
|
168
|
+
);
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
group: {
|
|
172
|
+
summary: 'one ACL group with its rules and admin rules',
|
|
173
|
+
usage: 'wx-admin acl group <group>',
|
|
174
|
+
details: 'A group is its id, its name or its DN.',
|
|
175
|
+
options: {},
|
|
176
|
+
args: [1, 1],
|
|
177
|
+
async run(ctx) {
|
|
178
|
+
const list = await listGroups(await pbxClient(ctx));
|
|
179
|
+
return {...findGroup(list, ctx.positionals[0]), groups: list.map(({id, dn, name}) => ({id, dn, name}))};
|
|
180
|
+
},
|
|
181
|
+
text: (g, ctx) => groupLines(g, g.groups ?? [], ctx),
|
|
182
|
+
},
|
|
183
|
+
create: {
|
|
184
|
+
summary: 'create an ACL group, empty, with --rule, or from a JSON file ({name?, inherits?, rules?})',
|
|
185
|
+
usage: 'wx-admin acl create <name> [--rule "KEY ABILITY [GROUP]"]... [--inherits GROUP] [--from FILE|-]',
|
|
186
|
+
details: 'Only the PBX user "admin" (or a Company API Key acting as admin) may create groups.',
|
|
187
|
+
options: {
|
|
188
|
+
...ruleOptions,
|
|
189
|
+
inherits: {type: 'string', placeholder: 'GROUP', description: 'the parent group (id, name or DN)'},
|
|
190
|
+
from: {
|
|
191
|
+
type: 'string',
|
|
192
|
+
placeholder: 'FILE',
|
|
193
|
+
description: 'the group as JSON, as acl group --output json gives it (a file, or - for stdin)',
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
args: [1, 1],
|
|
197
|
+
async run(ctx) {
|
|
198
|
+
const name = ctx.positionals[0];
|
|
199
|
+
if (!NAME.test(name))
|
|
200
|
+
throw new UsageError(`An ACL group name is 1 to 64 characters without / \\ or ,: ${name}`);
|
|
201
|
+
const src = ctx.values.from ? await readJsonInput(ctx, ctx.values.from) : {};
|
|
202
|
+
const client = await pbxClient(ctx);
|
|
203
|
+
const list = await listGroups(client);
|
|
204
|
+
if (list.some((g) => g.name === name)) throw new UsageError(`An ACL group ${name} exists already`);
|
|
205
|
+
const body = {name};
|
|
206
|
+
if (src.wcgrp !== undefined && src.wcgrp !== null && src.wcgrp !== '') body.wcgrp = String(src.wcgrp);
|
|
207
|
+
const parent = ctx.values.inherits ?? src.inherits?.dn;
|
|
208
|
+
if (parent) body.inherits = {dn: targetDn(list, parent)};
|
|
209
|
+
const rules = changedRules(plainRules(src.rules), ctx.values, list);
|
|
210
|
+
if (rules.length) body.rules = rules;
|
|
211
|
+
return client.call('POST', '/PBX/AclGroups/', body, SCOPE);
|
|
212
|
+
},
|
|
213
|
+
text: (g) => `Created the ACL group ${g.name} (id ${g.id})`,
|
|
214
|
+
},
|
|
215
|
+
update: {
|
|
216
|
+
summary: 'change an ACL group: name, parent, rules (added, replaced or removed) or admin rules',
|
|
217
|
+
usage:
|
|
218
|
+
'wx-admin acl update <group> [--name NAME] [--inherits GROUP | --no-inherits] [--rule "KEY ABILITY [GROUP]"]... [--remove-rule "KEY [GROUP]"]... [--admin-rule "SUBJECT OPERATION"]... [--remove-admin-rule SUBJECT]... [--from FILE|-] [--yes]',
|
|
219
|
+
details: [
|
|
220
|
+
'A --rule replaces the rule with the same key and group, or is added. --from replaces all rules (and admin rules)',
|
|
221
|
+
'with those of the file. Admins other than the user "admin" may change only the rules.',
|
|
222
|
+
].join('\n'),
|
|
223
|
+
options: {
|
|
224
|
+
name: {type: 'string', placeholder: 'NAME', description: 'the new name'},
|
|
225
|
+
inherits: {type: 'string', placeholder: 'GROUP', description: 'the new parent group (id, name or DN)'},
|
|
226
|
+
'no-inherits': {type: 'boolean', description: 'no parent group'},
|
|
227
|
+
...ruleOptions,
|
|
228
|
+
'remove-rule': {
|
|
229
|
+
type: 'string',
|
|
230
|
+
multiple: true,
|
|
231
|
+
placeholder: '"KEY [GROUP]"',
|
|
232
|
+
description: 'remove a rule (for every target group when no GROUP is given)',
|
|
233
|
+
},
|
|
234
|
+
'admin-rule': {
|
|
235
|
+
type: 'string',
|
|
236
|
+
multiple: true,
|
|
237
|
+
placeholder: '"SUBJECT OPERATION"',
|
|
238
|
+
description: 'an admin rule, e.g. "users__users epage" (wx-admin acl rules --admin)',
|
|
239
|
+
},
|
|
240
|
+
'remove-admin-rule': {
|
|
241
|
+
type: 'string',
|
|
242
|
+
multiple: true,
|
|
243
|
+
placeholder: 'SUBJECT',
|
|
244
|
+
description: 'remove the admin rules of a subject',
|
|
245
|
+
},
|
|
246
|
+
from: {
|
|
247
|
+
type: 'string',
|
|
248
|
+
placeholder: 'FILE',
|
|
249
|
+
description: 'rules and/or adminRules as JSON (a file, or - for stdin)',
|
|
250
|
+
},
|
|
251
|
+
yes: {type: 'boolean', description: 'yes, change the rights (rules, admin rules, --from)'},
|
|
252
|
+
},
|
|
253
|
+
args: [1, 1],
|
|
254
|
+
async run(ctx) {
|
|
255
|
+
const v = ctx.values;
|
|
256
|
+
const ruleChange = v.rule?.length || v['remove-rule']?.length;
|
|
257
|
+
const adminChange = v['admin-rule']?.length || v['remove-admin-rule']?.length;
|
|
258
|
+
if ((ruleChange || adminChange || v.from) && !v.yes)
|
|
259
|
+
throw new UsageError('This changes what every user of the group may do: confirm with --yes', ctx.usage());
|
|
260
|
+
if (
|
|
261
|
+
v.name === undefined &&
|
|
262
|
+
v.inherits === undefined &&
|
|
263
|
+
!v['no-inherits'] &&
|
|
264
|
+
!ruleChange &&
|
|
265
|
+
!adminChange &&
|
|
266
|
+
!v.from
|
|
267
|
+
)
|
|
268
|
+
throw new UsageError('Nothing to change', ctx.usage());
|
|
269
|
+
if (v.inherits !== undefined && v['no-inherits'])
|
|
270
|
+
throw new UsageError('Give --inherits or --no-inherits, not both');
|
|
271
|
+
if (v.name !== undefined && !NAME.test(v.name))
|
|
272
|
+
throw new UsageError(`An ACL group name is 1 to 64 characters without / \\ or ,: ${v.name}`);
|
|
273
|
+
const src = v.from ? await readJsonInput(ctx, v.from) : {};
|
|
274
|
+
const client = await pbxClient(ctx);
|
|
275
|
+
const list = await listGroups(client);
|
|
276
|
+
const g = findGroup(list, ctx.positionals[0]);
|
|
277
|
+
const body = {};
|
|
278
|
+
if (v.name !== undefined) body.name = v.name;
|
|
279
|
+
if (v.inherits !== undefined) body.inherits = {dn: targetDn(list, v.inherits)};
|
|
280
|
+
if (v['no-inherits']) body.inherits = '';
|
|
281
|
+
if (ruleChange || src.rules) body.rules = changedRules(plainRules(src.rules ?? g.rules), v, list);
|
|
282
|
+
if (adminChange || src.adminRules)
|
|
283
|
+
body.adminRules = changedAdminRules(plainAdminRules(src.adminRules ?? g.adminRules), v, list);
|
|
284
|
+
await client.call('PUT', groupPath(g.id), body, SCOPE);
|
|
285
|
+
const now = await listGroups(client);
|
|
286
|
+
return {...findGroup(now, g.id), groups: now.map(({id, dn, name}) => ({id, dn, name}))};
|
|
287
|
+
},
|
|
288
|
+
text: (g, ctx) => ['Updated:', ...groupLines(g, g.groups ?? [], ctx)],
|
|
289
|
+
},
|
|
290
|
+
delete: {
|
|
291
|
+
summary: 'delete ACL groups (needs --yes: their users lose the rights of the group)',
|
|
292
|
+
usage: 'wx-admin acl delete <group>... --yes',
|
|
293
|
+
options: {yes: {type: 'boolean', description: 'yes, delete them'}},
|
|
294
|
+
args: [1, 50],
|
|
295
|
+
async run(ctx) {
|
|
296
|
+
if (!ctx.values.yes)
|
|
297
|
+
throw new UsageError(
|
|
298
|
+
'Deleting an ACL group changes what its users may do: confirm with --yes',
|
|
299
|
+
ctx.usage(),
|
|
300
|
+
);
|
|
301
|
+
const client = await pbxClient(ctx);
|
|
302
|
+
const list = await listGroups(client);
|
|
303
|
+
const found = ctx.positionals.map((ref) => findGroup(list, ref));
|
|
304
|
+
await client.call('DELETE', groupPath(...found.map((g) => g.id)), undefined, SCOPE);
|
|
305
|
+
return {deleted: found.map(({id, name}) => ({id, name}))};
|
|
306
|
+
},
|
|
307
|
+
text: (r) =>
|
|
308
|
+
`Deleted the ACL ${r.deleted.length === 1 ? 'group' : 'groups'} ${r.deleted.map((g) => `${g.name} (${g.id})`).join(', ')}`,
|
|
309
|
+
},
|
|
310
|
+
permissions: {
|
|
311
|
+
summary: 'each permission and the groups that have it (filter by --permission and --group)',
|
|
312
|
+
usage: 'wx-admin acl permissions [--permission KEY]... [--group GROUP]...',
|
|
313
|
+
details:
|
|
314
|
+
'A permission about other groups (such as CALL) shows, for each group, the groups its users may do it to.',
|
|
315
|
+
options: {
|
|
316
|
+
permission: {
|
|
317
|
+
type: 'string',
|
|
318
|
+
multiple: true,
|
|
319
|
+
placeholder: 'KEY',
|
|
320
|
+
description: 'only this permission, e.g. SEE_CALL_RECORDINGS',
|
|
321
|
+
},
|
|
322
|
+
group: {type: 'string', multiple: true, placeholder: 'GROUP', description: 'only this group (id or name)'},
|
|
323
|
+
},
|
|
324
|
+
args: [0, 0],
|
|
325
|
+
async run(ctx) {
|
|
326
|
+
const client = await pbxClient(ctx);
|
|
327
|
+
const q = new URLSearchParams();
|
|
328
|
+
for (const p of ctx.values.permission ?? []) {
|
|
329
|
+
if (!RULE_KEY.test(p)) throw new UsageError(`Not a permission key: ${p}`);
|
|
330
|
+
q.append('permissions[]', p);
|
|
331
|
+
}
|
|
332
|
+
if (ctx.values.group?.length) {
|
|
333
|
+
const list = await listGroups(client);
|
|
334
|
+
for (const ref of ctx.values.group) q.append('groups[]', findGroup(list, ref).id);
|
|
335
|
+
}
|
|
336
|
+
return client.call('GET', `/PBX/AclGroups/Permissions/${q.size ? `?${q}` : ''}`, undefined, SCOPE);
|
|
337
|
+
},
|
|
338
|
+
text: (list, ctx) => {
|
|
339
|
+
const c = paint(ctx);
|
|
340
|
+
if (!list?.length) return 'No permissions';
|
|
341
|
+
const who = (p) =>
|
|
342
|
+
(p.groups ?? [])
|
|
343
|
+
.map((g) => {
|
|
344
|
+
if (!p.containSubGroups) return g.access ? g.groupName : c.dim(`-${g.groupName}`);
|
|
345
|
+
const to = (g.subGroups ?? []).filter((s) => s.access).map((s) => s.groupName);
|
|
346
|
+
return `${g.groupName}→${to.length === (g.subGroups ?? []).length ? 'all' : to.length ? to.join('+') : 'none'}`;
|
|
347
|
+
})
|
|
348
|
+
.join(' ');
|
|
349
|
+
return table(list.map((p) => [p.key, p.name ?? '', who(p)]));
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
rules: {
|
|
353
|
+
summary: 'the rules an ACL group can have: keys, abilities, whether they are about other groups',
|
|
354
|
+
usage: 'wx-admin acl rules [--admin]',
|
|
355
|
+
options: {
|
|
356
|
+
admin: {type: 'boolean', description: 'the admin rules (what an admin of the group may manage in WMS)'},
|
|
357
|
+
},
|
|
358
|
+
args: [0, 0],
|
|
359
|
+
async run(ctx) {
|
|
360
|
+
return (await pbxClient(ctx)).call('GET', '/PBX/AclGroups/Rules/', undefined, SCOPE);
|
|
361
|
+
},
|
|
362
|
+
text: (r, ctx) => {
|
|
363
|
+
if (ctx.values.admin)
|
|
364
|
+
return table(
|
|
365
|
+
(r.adminList ?? []).flatMap((x) => (x.access ?? []).map((a) => [a.subject, x.ability.join('/'), a.name])),
|
|
366
|
+
);
|
|
367
|
+
return table(
|
|
368
|
+
(r.list ?? []).flatMap((x) =>
|
|
369
|
+
(x.access ?? []).map((a) => [a.key, x.ability.join('/'), a.group ? 'to groups' : '', a.name]),
|
|
370
|
+
),
|
|
371
|
+
);
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// wx-admin api: any operation of the PBX REST API (https://<pbx>/api/v1, the OpenAPI at /api/v1/doc/), for what has no
|
|
2
|
+
// command yet. The access and the PBX are chosen as for every command (--pbx, the default PBX, the x-bees login).
|
|
3
|
+
import {UsageError} from 'wldx-core/errors';
|
|
4
|
+
import {readJsonInput} from 'wldx-core/files';
|
|
5
|
+
import {pbxClient} from '../pbx.mjs';
|
|
6
|
+
import {maskSecrets} from './trunk.mjs';
|
|
7
|
+
|
|
8
|
+
const METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
|
|
9
|
+
|
|
10
|
+
export const commands = {
|
|
11
|
+
api: {
|
|
12
|
+
summary: 'call any PBX API operation (GET reads; other methods change the PBX and need --yes)',
|
|
13
|
+
usage: 'wx-admin api <method> <path> [--data JSON | --data-file FILE|-] [--query KEY=VALUE]... [--yes]',
|
|
14
|
+
details: [
|
|
15
|
+
'path is relative to /api/v1, e.g. /PBX/Dialplans/ or /Colleagues/. The answer is printed as the PBX sends it,',
|
|
16
|
+
'with {"type":"result","result":X} unwrapped to X, passwords and tokens masked (--show-secrets shows them).',
|
|
17
|
+
'The operations: https://<pbx>/api/v1/doc/ (log in to WMS first).',
|
|
18
|
+
].join('\n'),
|
|
19
|
+
options: {
|
|
20
|
+
data: {type: 'string', placeholder: 'JSON', description: 'the JSON body'},
|
|
21
|
+
'data-file': {type: 'string', placeholder: 'FILE', description: 'the JSON body from a file, or - for stdin'},
|
|
22
|
+
query: {type: 'string', multiple: true, placeholder: 'KEY=VALUE', description: 'a query parameter'},
|
|
23
|
+
yes: {type: 'boolean', description: 'yes, run a method that changes the PBX'},
|
|
24
|
+
'show-secrets': {
|
|
25
|
+
type: 'boolean',
|
|
26
|
+
description: 'show passwords, secrets and tokens in the answer (masked otherwise)',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
args: [2, 2],
|
|
30
|
+
async run(ctx) {
|
|
31
|
+
const method = ctx.positionals[0].toUpperCase();
|
|
32
|
+
if (!METHODS.includes(method)) throw new UsageError(`The method is one of ${METHODS.join(', ')}`, ctx.usage());
|
|
33
|
+
if (method !== 'GET' && !ctx.values.yes)
|
|
34
|
+
throw new UsageError(`${method} changes the PBX: confirm with --yes`, ctx.usage());
|
|
35
|
+
const raw = ctx.positionals[1];
|
|
36
|
+
if (!/^\/[A-Za-z0-9_./,{}+-]*$/.test(raw) || raw.includes('..'))
|
|
37
|
+
throw new UsageError(`Not an API path (relative to /api/v1, e.g. /PBX/Dialplans/): ${raw}`);
|
|
38
|
+
if (ctx.values.data !== undefined && ctx.values['data-file'] !== undefined)
|
|
39
|
+
throw new UsageError('Give --data or --data-file, not both');
|
|
40
|
+
let body;
|
|
41
|
+
const text = ctx.values.data;
|
|
42
|
+
if (ctx.values['data-file'] !== undefined) {
|
|
43
|
+
if (method === 'GET') throw new UsageError('GET takes no body: use --query');
|
|
44
|
+
body = await readJsonInput(ctx, ctx.values['data-file']); // no credentials, nothing hidden in the home folder
|
|
45
|
+
} else if (text !== undefined) {
|
|
46
|
+
if (method === 'GET') throw new UsageError('GET takes no body: use --query');
|
|
47
|
+
try {
|
|
48
|
+
body = JSON.parse(text);
|
|
49
|
+
} catch {
|
|
50
|
+
throw new UsageError('--data is not JSON');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const q = new URLSearchParams();
|
|
54
|
+
for (const kv of ctx.values.query ?? []) {
|
|
55
|
+
const at = kv.indexOf('=');
|
|
56
|
+
if (at < 1) throw new UsageError(`--query is KEY=VALUE: ${kv}`);
|
|
57
|
+
q.append(kv.slice(0, at), kv.slice(at + 1));
|
|
58
|
+
}
|
|
59
|
+
const client = await pbxClient(ctx);
|
|
60
|
+
const answer = await client.call(method, raw + (q.size ? `?${q}` : ''), body);
|
|
61
|
+
// Passwords, SIP register strings, keys and tokens are masked unless --show-secrets.
|
|
62
|
+
return ctx.values['show-secrets'] ? answer : maskSecrets(answer);
|
|
63
|
+
},
|
|
64
|
+
text: (r) => JSON.stringify(r, null, 2),
|
|
65
|
+
},
|
|
66
|
+
};
|