ec-web 0.0.19 → 0.0.21
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/dist/index.js +5 -0
- package/dist/ipc-client.js +17 -4
- package/dist/server.js +117 -23
- package/dist/sources/aid.js +125 -35
- package/dist/sources/baseagent-detector.js +60 -6
- package/dist/sources/basic-settings.js +818 -0
- package/dist/sources/gateway.js +5 -4
- package/dist/sources/msg.js +4 -96
- package/dist/sources/relation-roles.js +150 -191
- package/dist/sources/role-definitions.js +138 -331
- package/dist/sources/session-codex.js +189 -78
- package/dist/sources/system.js +10 -9
- package/dist/sources/triggers.js +24 -13
- package/dist/sources/types.js +20 -1
- package/dist/static/app.js +85 -11
- package/dist/static/basic-settings.js +1576 -0
- package/dist/static/index.html +43 -1
- package/dist/static/style.css +586 -2
- package/ecweb-wrapper.cjs +68 -0
- package/package.json +2 -2
|
@@ -3,25 +3,6 @@ import path from 'path';
|
|
|
3
3
|
import { resolvePaths } from '../paths.js';
|
|
4
4
|
import { getSessionsAunDir, listPeers, readJsonFile, readMessages, } from '../fs-utils.js';
|
|
5
5
|
import { resolveParentDistModule, toFileUrl } from './parent-package.js';
|
|
6
|
-
import { ipcQuery } from '../ipc-client.js';
|
|
7
|
-
async function execRoleMenu(aid, payload, auth) {
|
|
8
|
-
const response = await ipcQuery(resolvePaths().socket, {
|
|
9
|
-
type: 'menu.exec',
|
|
10
|
-
payload: { ...payload, name: 'role', agent: aid, args: { self: aid, ...(payload.args ?? {}) } },
|
|
11
|
-
auth: { source: 'ecweb', actorAid: auth.actorAid ?? null, localDirect: !!auth.localDirect },
|
|
12
|
-
}, 10_000);
|
|
13
|
-
if (!response?.ok || !response.response)
|
|
14
|
-
throw new Error(response?.error || 'Role Menu is unavailable');
|
|
15
|
-
return response.response;
|
|
16
|
-
}
|
|
17
|
-
function sendRoleMenuError(res, response) {
|
|
18
|
-
const error = response?.error ?? { code: 'INTERNAL_ERROR', message: 'Role Menu request failed' };
|
|
19
|
-
const status = error.code === 'NOT_FOUND' ? 404
|
|
20
|
-
: error.code === 'CONFLICT' ? 409
|
|
21
|
-
: error.code === 'PERMISSION_DENIED' || error.code === 'NOT_ALLOWED' ? 403
|
|
22
|
-
: error.code === 'TEMPORARILY_UNAVAILABLE' ? 503 : 400;
|
|
23
|
-
sendJson(res, status, { error: error.message, code: error.code, data: error.data });
|
|
24
|
-
}
|
|
25
6
|
async function getParentModules() {
|
|
26
7
|
const configPath = resolveParentDistModule('config', 'config-manager.js');
|
|
27
8
|
const rolesPath = resolveParentDistModule('config', 'roles.js');
|
|
@@ -32,8 +13,8 @@ async function getParentModules() {
|
|
|
32
13
|
return { config, roles, resolver };
|
|
33
14
|
}
|
|
34
15
|
async function getCommandModules() {
|
|
35
|
-
const permissionPath = resolveParentDistModule('core', '
|
|
36
|
-
const auditPath = resolveParentDistModule('core', '
|
|
16
|
+
const permissionPath = resolveParentDistModule('core', 'auth', 'operation-authorizer.js');
|
|
17
|
+
const auditPath = resolveParentDistModule('core', 'auth', 'authorization-audit.js');
|
|
37
18
|
const permission = await import(toFileUrl(permissionPath));
|
|
38
19
|
const audit = await import(toFileUrl(auditPath));
|
|
39
20
|
return { permission, audit };
|
|
@@ -77,9 +58,15 @@ async function getAgentsFromIpc() {
|
|
|
77
58
|
return [];
|
|
78
59
|
}
|
|
79
60
|
}
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
61
|
+
function resolveEcwebActor(modules, aid, auth) {
|
|
62
|
+
const processConfig = modules.config?.read && modules.config?.ConfigTarget
|
|
63
|
+
? modules.config.read(modules.config.ConfigTarget.Process)
|
|
64
|
+
: undefined;
|
|
65
|
+
const processOwners = Array.isArray(processConfig?.owners)
|
|
66
|
+
? processConfig.owners.map((value) => String(value || '').trim()).filter(Boolean)
|
|
67
|
+
: [];
|
|
68
|
+
const daemonOwner = !!auth.localDirect || (!!auth.actorAid && processOwners.includes(auth.actorAid));
|
|
69
|
+
const actorRole = daemonOwner
|
|
83
70
|
? 'owner'
|
|
84
71
|
: auth.actorAid
|
|
85
72
|
? (modules.resolver.resolvePeerRoleDetail({
|
|
@@ -90,6 +77,61 @@ async function authorizeRoleAssignmentWrite(modules, aid, auth, operation, targe
|
|
|
90
77
|
conversationId: auth.actorAid,
|
|
91
78
|
}).effectiveRole || 'none')
|
|
92
79
|
: 'none';
|
|
80
|
+
return {
|
|
81
|
+
daemonOwner,
|
|
82
|
+
actorRole,
|
|
83
|
+
actorId: auth.localDirect ? 'local-direct' : auth.actorAid || undefined,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function authorizeRoleAssignmentRead(modules, aid, auth) {
|
|
87
|
+
const { permission, audit } = await getCommandModules();
|
|
88
|
+
const actor = resolveEcwebActor(modules, aid, auth);
|
|
89
|
+
const decision = permission.authorizeCommand({
|
|
90
|
+
intent: {
|
|
91
|
+
operation: 'role.relation.read',
|
|
92
|
+
scope: 'agent',
|
|
93
|
+
source: 'ecweb',
|
|
94
|
+
args: { self: aid },
|
|
95
|
+
},
|
|
96
|
+
actorId: actor.actorId,
|
|
97
|
+
selfAid: aid,
|
|
98
|
+
role: actor.actorRole,
|
|
99
|
+
processRole: actor.daemonOwner ? 'daemon-owner' : 'none',
|
|
100
|
+
isDaemonOwner: actor.daemonOwner,
|
|
101
|
+
source: 'ecweb',
|
|
102
|
+
});
|
|
103
|
+
await audit.auditCommandAuthorization({
|
|
104
|
+
ts: Date.now(),
|
|
105
|
+
source: 'ecweb',
|
|
106
|
+
operation: 'role.relation.read',
|
|
107
|
+
scope: 'agent',
|
|
108
|
+
dangerous: false,
|
|
109
|
+
actorId: actor.actorId,
|
|
110
|
+
selfAid: aid,
|
|
111
|
+
role: actor.actorRole,
|
|
112
|
+
processRole: actor.daemonOwner ? 'daemon-owner' : 'none',
|
|
113
|
+
isDaemonOwner: actor.daemonOwner,
|
|
114
|
+
decision: decision.allow ? 'allow' : 'deny',
|
|
115
|
+
code: decision.allow ? undefined : decision.code,
|
|
116
|
+
reason: decision.allow ? undefined : decision.reason,
|
|
117
|
+
matchedRule: decision.matchedRule,
|
|
118
|
+
argsSummary: { self: aid },
|
|
119
|
+
});
|
|
120
|
+
if (decision.allow)
|
|
121
|
+
return { allow: true, actorRole: actor.actorRole };
|
|
122
|
+
return {
|
|
123
|
+
allow: false,
|
|
124
|
+
status: 403,
|
|
125
|
+
error: 'forbidden: role assignment read not allowed',
|
|
126
|
+
code: decision.code,
|
|
127
|
+
reason: decision.reason,
|
|
128
|
+
actorRole: actor.actorRole,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
async function authorizeRoleAssignmentWrite(modules, aid, auth, operation, targetRole, args) {
|
|
132
|
+
const { permission, audit } = await getCommandModules();
|
|
133
|
+
const actor = resolveEcwebActor(modules, aid, auth);
|
|
134
|
+
const { daemonOwner, actorRole } = actor;
|
|
93
135
|
const targetPeerId = typeof args.peerId === 'string' ? args.peerId : undefined;
|
|
94
136
|
if (targetPeerId && modules.resolver.isStaticAgentOwner?.(aid, targetPeerId)) {
|
|
95
137
|
const reason = `Static owner ${targetPeerId} is defined in agents/${aid}/config.json and cannot be changed by relation role assignments`;
|
|
@@ -99,9 +141,11 @@ async function authorizeRoleAssignmentWrite(modules, aid, auth, operation, targe
|
|
|
99
141
|
operation,
|
|
100
142
|
scope: 'agent',
|
|
101
143
|
dangerous: false,
|
|
102
|
-
actorId:
|
|
144
|
+
actorId: actor.actorId,
|
|
103
145
|
selfAid: aid,
|
|
104
146
|
role: actorRole,
|
|
147
|
+
processRole: daemonOwner ? 'daemon-owner' : 'none',
|
|
148
|
+
isDaemonOwner: daemonOwner,
|
|
105
149
|
decision: 'deny',
|
|
106
150
|
code: 'IMMUTABLE_STATIC_OWNER',
|
|
107
151
|
reason,
|
|
@@ -126,9 +170,11 @@ async function authorizeRoleAssignmentWrite(modules, aid, auth, operation, targe
|
|
|
126
170
|
targetRole,
|
|
127
171
|
},
|
|
128
172
|
},
|
|
129
|
-
actorId:
|
|
173
|
+
actorId: actor.actorId,
|
|
130
174
|
selfAid: aid,
|
|
131
175
|
role: actorRole,
|
|
176
|
+
processRole: daemonOwner ? 'daemon-owner' : 'none',
|
|
177
|
+
isDaemonOwner: daemonOwner,
|
|
132
178
|
source: 'ecweb',
|
|
133
179
|
});
|
|
134
180
|
if (!decision.allow) {
|
|
@@ -138,9 +184,11 @@ async function authorizeRoleAssignmentWrite(modules, aid, auth, operation, targe
|
|
|
138
184
|
operation,
|
|
139
185
|
scope: 'agent',
|
|
140
186
|
dangerous: false,
|
|
141
|
-
actorId:
|
|
187
|
+
actorId: actor.actorId,
|
|
142
188
|
selfAid: aid,
|
|
143
189
|
role: actorRole,
|
|
190
|
+
processRole: daemonOwner ? 'daemon-owner' : 'none',
|
|
191
|
+
isDaemonOwner: daemonOwner,
|
|
144
192
|
decision: 'deny',
|
|
145
193
|
code: decision.code,
|
|
146
194
|
reason: decision.reason,
|
|
@@ -156,9 +204,9 @@ async function authorizeRoleAssignmentWrite(modules, aid, auth, operation, targe
|
|
|
156
204
|
actorRole,
|
|
157
205
|
};
|
|
158
206
|
}
|
|
159
|
-
const policyAllowed = operation === 'role.assign'
|
|
207
|
+
const policyAllowed = daemonOwner || (operation === 'role.assign'
|
|
160
208
|
? !!targetRole && canAssignRole(actorRole, targetRole)
|
|
161
|
-
: canRevokeRole(actorRole, targetRole);
|
|
209
|
+
: canRevokeRole(actorRole, targetRole));
|
|
162
210
|
if (!policyAllowed) {
|
|
163
211
|
const reason = operation === 'role.assign'
|
|
164
212
|
? `Role ${actorRole} cannot assign target role ${targetRole || '(missing)'}`
|
|
@@ -169,9 +217,11 @@ async function authorizeRoleAssignmentWrite(modules, aid, auth, operation, targe
|
|
|
169
217
|
operation,
|
|
170
218
|
scope: 'agent',
|
|
171
219
|
dangerous: false,
|
|
172
|
-
actorId:
|
|
220
|
+
actorId: actor.actorId,
|
|
173
221
|
selfAid: aid,
|
|
174
222
|
role: actorRole,
|
|
223
|
+
processRole: daemonOwner ? 'daemon-owner' : 'none',
|
|
224
|
+
isDaemonOwner: daemonOwner,
|
|
175
225
|
decision: 'deny',
|
|
176
226
|
code: 'NOT_ALLOWED',
|
|
177
227
|
reason,
|
|
@@ -193,9 +243,11 @@ async function authorizeRoleAssignmentWrite(modules, aid, auth, operation, targe
|
|
|
193
243
|
operation,
|
|
194
244
|
scope: 'agent',
|
|
195
245
|
dangerous: false,
|
|
196
|
-
actorId:
|
|
246
|
+
actorId: actor.actorId,
|
|
197
247
|
selfAid: aid,
|
|
198
248
|
role: actorRole,
|
|
249
|
+
processRole: daemonOwner ? 'daemon-owner' : 'none',
|
|
250
|
+
isDaemonOwner: daemonOwner,
|
|
199
251
|
decision: 'allow',
|
|
200
252
|
matchedRule: decision.matchedRule,
|
|
201
253
|
argsSummary: { ...args, targetRole },
|
|
@@ -492,8 +544,8 @@ function explicitAssignment(scope, peerKey, role, ids) {
|
|
|
492
544
|
function resolveGroupRoleDetail(modules, aid, groupId) {
|
|
493
545
|
const target = relationTargetFromId(groupId);
|
|
494
546
|
const relation = readRelationConfig(modules.config, aid, target.peerKey);
|
|
495
|
-
const assigned = typeof relation.
|
|
496
|
-
? relation.
|
|
547
|
+
const assigned = typeof relation.role === 'string' && roleExists(modules.roles, aid, relation.role)
|
|
548
|
+
? relation.role
|
|
497
549
|
: null;
|
|
498
550
|
if (assigned) {
|
|
499
551
|
return {
|
|
@@ -519,7 +571,7 @@ function assignmentForDetail(detail, modules, seed, member) {
|
|
|
519
571
|
if (member && detail.source === 'group-member') {
|
|
520
572
|
return explicitAssignment('group-member', groupTarget.peerKey, role, { groupId: groupTarget.channelId, peerId: member.peerId });
|
|
521
573
|
}
|
|
522
|
-
if (member && detail.source === 'private-inherited') {
|
|
574
|
+
if (member && (detail.source === 'private-inherited' || detail.source === 'relation-actor')) {
|
|
523
575
|
const privateTarget = relationTargetFromId(member.peerId);
|
|
524
576
|
return explicitAssignment('private', privateTarget.peerKey, role, { peerId: member.peerId });
|
|
525
577
|
}
|
|
@@ -584,34 +636,14 @@ function listRelationRoleAssignments(config, aid) {
|
|
|
584
636
|
continue;
|
|
585
637
|
const peerKey = entry.name;
|
|
586
638
|
const relation = readRelationConfig(config, aid, peerKey);
|
|
587
|
-
const roles = relation.roles;
|
|
588
|
-
if (!roles || typeof roles !== 'object')
|
|
589
|
-
continue;
|
|
590
639
|
const parsed = parsePeerKey(peerKey);
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
source: 'relation-config',
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
if (roles.members && typeof roles.members === 'object') {
|
|
602
|
-
for (const [peerId, role] of Object.entries(roles.members)) {
|
|
603
|
-
if (typeof role !== 'string')
|
|
604
|
-
continue;
|
|
605
|
-
out.push({
|
|
606
|
-
scope: 'group-member',
|
|
607
|
-
peerKey,
|
|
608
|
-
groupId: parsed.channelId,
|
|
609
|
-
peerId,
|
|
610
|
-
role,
|
|
611
|
-
source: 'relation-config',
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
}
|
|
640
|
+
if (typeof relation.role !== 'string')
|
|
641
|
+
continue;
|
|
642
|
+
const isGroup = relation.targetKind === 'group'
|
|
643
|
+
|| /^group\.agentid\.pub\//i.test(parsed.channelId);
|
|
644
|
+
out.push(isGroup
|
|
645
|
+
? { scope: 'group', groupId: parsed.channelId, peerKey, role: relation.role, source: 'relation-config' }
|
|
646
|
+
: { scope: 'private', peerId: parsed.channelId, peerKey, role: relation.role, source: 'relation-config' });
|
|
615
647
|
}
|
|
616
648
|
return out;
|
|
617
649
|
}
|
|
@@ -624,26 +656,17 @@ async function buildSnapshot(auth = {}) {
|
|
|
624
656
|
const aid = agent.aid;
|
|
625
657
|
if (!aid)
|
|
626
658
|
continue;
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
type: 'menu.options', id: `ecweb-relation-role-options-${Date.now()}`,
|
|
630
|
-
args: { option: 'roles' },
|
|
631
|
-
}, auth),
|
|
632
|
-
execRoleMenu(aid, {
|
|
633
|
-
type: 'menu.query', id: `ecweb-relation-role-defaults-${Date.now()}`,
|
|
634
|
-
args: { view: 'defaults' },
|
|
635
|
-
}, auth),
|
|
636
|
-
listAssignmentsThroughMenu(aid, auth),
|
|
637
|
-
]);
|
|
638
|
-
if (roleOptions.error || defaults.error || assignmentResult.error)
|
|
659
|
+
const readDecision = await authorizeRoleAssignmentRead(modules, aid, auth);
|
|
660
|
+
if (!readDecision.allow)
|
|
639
661
|
continue;
|
|
662
|
+
const roleConfig = modules.roles.readRolesConfig(aid);
|
|
640
663
|
snapshotAgents.push({
|
|
641
664
|
aid,
|
|
642
665
|
displayName: agent.displayName ?? agent.personalName,
|
|
643
666
|
name: agent.name,
|
|
644
|
-
roles:
|
|
645
|
-
defaultRoles:
|
|
646
|
-
assignments:
|
|
667
|
+
roles: Object.keys(roleConfig.roles || {}).filter(role => isAssignableUserRole(role)),
|
|
668
|
+
defaultRoles: roleConfig.defaultRoles ?? { private: null, group: null },
|
|
669
|
+
assignments: listRelationRoleAssignments(modules.config, aid),
|
|
647
670
|
});
|
|
648
671
|
for (const seed of loadConversationSeeds(aid)) {
|
|
649
672
|
conversations.push(withResolvedRoles(seed, modules));
|
|
@@ -691,80 +714,12 @@ function scopedAssignmentArgs(body, fallbackTargetId) {
|
|
|
691
714
|
const peerId = body.peerId || fallbackTargetId;
|
|
692
715
|
if (!groupRaw || !peerId)
|
|
693
716
|
throw new Error('groupId and peerId are required');
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
if (target.scope === 'group')
|
|
701
|
-
return { kind: 'group', groupId: target.groupId };
|
|
702
|
-
return { kind: 'group-member', groupId: target.groupId, memberAid: target.peerId };
|
|
703
|
-
}
|
|
704
|
-
function toLegacyAssignment(target, role) {
|
|
705
|
-
if (target.scope === 'private')
|
|
706
|
-
return { scope: 'private', peerId: target.peerId, peerKey: target.peerKey, role, source: 'relation-config' };
|
|
707
|
-
if (target.scope === 'group')
|
|
708
|
-
return { scope: 'group', groupId: target.groupId, peerKey: target.peerKey, role, source: 'relation-config' };
|
|
709
|
-
return { scope: 'group-member', groupId: target.groupId, peerId: target.peerId, peerKey: target.peerKey, role, source: 'relation-config' };
|
|
710
|
-
}
|
|
711
|
-
async function updateAssignmentThroughMenu(aid, body, fallbackTargetId, role, auth) {
|
|
712
|
-
const target = scopedAssignmentArgs(body, fallbackTargetId);
|
|
713
|
-
const menuTarget = toMenuTarget(target);
|
|
714
|
-
const snapshot = await execRoleMenu(aid, {
|
|
715
|
-
type: 'menu.query', id: `ecweb-assignment-target-${Date.now()}`,
|
|
716
|
-
args: { view: 'target', target: menuTarget },
|
|
717
|
-
}, auth);
|
|
718
|
-
if (snapshot.error)
|
|
719
|
-
return { response: snapshot, target };
|
|
720
|
-
const response = await execRoleMenu(aid, {
|
|
721
|
-
type: 'menu.update', id: `ecweb-assignment-update-${Date.now()}`,
|
|
722
|
-
args: {
|
|
723
|
-
resource: 'assignment',
|
|
724
|
-
target: menuTarget,
|
|
725
|
-
expectedPolicyRevision: snapshot.data.policyRevision,
|
|
726
|
-
expectedRelationRevision: snapshot.data.relationRevision,
|
|
727
|
-
},
|
|
728
|
-
value: JSON.stringify(role),
|
|
729
|
-
}, auth);
|
|
730
|
-
return { response, target };
|
|
731
|
-
}
|
|
732
|
-
async function listAssignmentsThroughMenu(aid, auth) {
|
|
733
|
-
const items = [];
|
|
734
|
-
let cursor = null;
|
|
735
|
-
do {
|
|
736
|
-
const response = await execRoleMenu(aid, {
|
|
737
|
-
type: 'menu.query', id: `ecweb-assignment-list-${Date.now()}-${items.length}`,
|
|
738
|
-
args: { view: 'assignments', limit: 200, cursor },
|
|
739
|
-
}, auth);
|
|
740
|
-
if (response.error)
|
|
741
|
-
return response;
|
|
742
|
-
items.push(...response.data.items);
|
|
743
|
-
cursor = response.data.nextCursor;
|
|
744
|
-
} while (cursor);
|
|
745
|
-
const assignments = items
|
|
746
|
-
.filter((item) => item.target?.kind !== 'default' && item.target?.kind !== 'unknown')
|
|
747
|
-
.map((item) => {
|
|
748
|
-
const target = item.target;
|
|
749
|
-
if (target.kind === 'private')
|
|
750
|
-
return { scope: 'private', peerId: target.peerAid, peerKey: target.peerKey, role: item.roleId, source: 'relation-config' };
|
|
751
|
-
if (target.kind === 'group')
|
|
752
|
-
return { scope: 'group', groupId: target.groupId, peerKey: target.peerKey, role: item.roleId, source: 'relation-config' };
|
|
753
|
-
return { scope: 'group-member', groupId: target.groupId, peerId: target.memberAid, peerKey: target.peerKey, role: item.roleId, source: 'relation-config' };
|
|
754
|
-
});
|
|
755
|
-
return { data: { $schema_version: 1, assignments } };
|
|
756
|
-
}
|
|
757
|
-
function pruneEmptyRoles(config) {
|
|
758
|
-
const roles = config.roles;
|
|
759
|
-
if (!roles || typeof roles !== 'object')
|
|
760
|
-
return config;
|
|
761
|
-
if (roles.members && typeof roles.members === 'object' && Object.keys(roles.members).length === 0) {
|
|
762
|
-
delete roles.members;
|
|
763
|
-
}
|
|
764
|
-
if (!Object.prototype.hasOwnProperty.call(roles, 'assigned') && !roles.members) {
|
|
765
|
-
delete config.roles;
|
|
766
|
-
}
|
|
767
|
-
return config;
|
|
717
|
+
const groupTarget = relationTargetFromId(groupRaw);
|
|
718
|
+
// The canonical relation schema has one role per relation. A member override
|
|
719
|
+
// is therefore stored on that member's private relation; groupId is retained
|
|
720
|
+
// only for the ECWeb presentation layer.
|
|
721
|
+
const memberTarget = relationTargetFromId(peerId);
|
|
722
|
+
return { scope, groupId: groupTarget.channelId, peerId, peerKey: memberTarget.peerKey };
|
|
768
723
|
}
|
|
769
724
|
function setScopedAssignment(configMod, aid, body, fallbackTargetId) {
|
|
770
725
|
const target = scopedAssignmentArgs(body, fallbackTargetId);
|
|
@@ -772,15 +727,12 @@ function setScopedAssignment(configMod, aid, body, fallbackTargetId) {
|
|
|
772
727
|
if (!role)
|
|
773
728
|
throw new Error('role is required');
|
|
774
729
|
const config = readRelationConfig(configMod, aid, target.peerKey);
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
roles.assigned = role;
|
|
782
|
-
}
|
|
783
|
-
writeRelationConfig(configMod, aid, target.peerKey, { ...config, roles });
|
|
730
|
+
const next = { ...config };
|
|
731
|
+
delete next.roles;
|
|
732
|
+
next.$schema_version = 8;
|
|
733
|
+
next.targetKind = target.scope === 'group' ? 'group' : 'private';
|
|
734
|
+
next.role = role;
|
|
735
|
+
writeRelationConfig(configMod, aid, target.peerKey, next);
|
|
784
736
|
if (target.scope === 'private')
|
|
785
737
|
return { scope: 'private', peerId: target.peerId, peerKey: target.peerKey, role, source: 'relation-config' };
|
|
786
738
|
if (target.scope === 'group')
|
|
@@ -790,11 +742,10 @@ function setScopedAssignment(configMod, aid, body, fallbackTargetId) {
|
|
|
790
742
|
function getScopedAssignment(configMod, aid, body, fallbackTargetId) {
|
|
791
743
|
const target = scopedAssignmentArgs(body, fallbackTargetId);
|
|
792
744
|
const config = readRelationConfig(configMod, aid, target.peerKey);
|
|
745
|
+
const role = config.role;
|
|
793
746
|
if (target.scope === 'group-member') {
|
|
794
|
-
const role = config.roles?.members?.[target.peerId];
|
|
795
747
|
return explicitAssignment('group-member', target.peerKey, role, { groupId: target.groupId, peerId: target.peerId });
|
|
796
748
|
}
|
|
797
|
-
const role = config.roles?.assigned;
|
|
798
749
|
if (target.scope === 'private')
|
|
799
750
|
return explicitAssignment('private', target.peerKey, role, { peerId: target.peerId });
|
|
800
751
|
return explicitAssignment('group', target.peerKey, role, { groupId: target.groupId });
|
|
@@ -802,34 +753,27 @@ function getScopedAssignment(configMod, aid, body, fallbackTargetId) {
|
|
|
802
753
|
function deleteScopedAssignment(configMod, aid, body, fallbackTargetId) {
|
|
803
754
|
const target = scopedAssignmentArgs(body, fallbackTargetId);
|
|
804
755
|
const config = readRelationConfig(configMod, aid, target.peerKey);
|
|
805
|
-
|
|
756
|
+
const deleted = typeof config.role === 'string';
|
|
757
|
+
if (!deleted)
|
|
806
758
|
return false;
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
deleted = Object.prototype.hasOwnProperty.call(members, target.peerId);
|
|
812
|
-
delete members[target.peerId];
|
|
813
|
-
roles.members = members;
|
|
814
|
-
}
|
|
815
|
-
else {
|
|
816
|
-
deleted = Object.prototype.hasOwnProperty.call(roles, 'assigned');
|
|
817
|
-
delete roles.assigned;
|
|
818
|
-
}
|
|
819
|
-
writeRelationConfig(configMod, aid, target.peerKey, pruneEmptyRoles({ ...config, roles }));
|
|
759
|
+
const next = { ...config };
|
|
760
|
+
delete next.role;
|
|
761
|
+
delete next.roles;
|
|
762
|
+
writeRelationConfig(configMod, aid, target.peerKey, next);
|
|
820
763
|
return deleted;
|
|
821
764
|
}
|
|
822
765
|
export async function handleRelationRolesApi(req, res, auth = {}) {
|
|
823
766
|
try {
|
|
824
767
|
const urlPath = (req.url || '').split('?')[0];
|
|
768
|
+
const modules = await getParentModules();
|
|
825
769
|
if (req.method === 'GET' && urlPath.startsWith('/api/roles/agent/')) {
|
|
826
770
|
const aid = decodeURIComponent(urlPath.split('/').filter(Boolean).pop() || '');
|
|
827
771
|
if (!aid)
|
|
828
772
|
return sendJson(res, 400, { error: 'missing aid' });
|
|
829
|
-
const
|
|
830
|
-
if (
|
|
831
|
-
return
|
|
832
|
-
return sendJson(res, 200,
|
|
773
|
+
const decision = await authorizeRoleAssignmentRead(modules, aid, auth);
|
|
774
|
+
if (!decision.allow)
|
|
775
|
+
return sendAuthorizationDenied(res, decision);
|
|
776
|
+
return sendJson(res, 200, { $schema_version: 1, assignments: listRelationRoleAssignments(modules.config, aid) });
|
|
833
777
|
}
|
|
834
778
|
if ((req.method === 'POST' || req.method === 'PUT') && urlPath.startsWith('/api/roles/agent/')) {
|
|
835
779
|
const aid = decodeURIComponent(urlPath.split('/').filter(Boolean).pop() || '');
|
|
@@ -839,10 +783,15 @@ export async function handleRelationRolesApi(req, res, auth = {}) {
|
|
|
839
783
|
if (typeof body.role !== 'string' || !body.role) {
|
|
840
784
|
return sendJson(res, 400, { error: 'role is required' });
|
|
841
785
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
786
|
+
if (!roleExists(modules.roles, aid, body.role))
|
|
787
|
+
return sendJson(res, 404, { error: 'role not found' });
|
|
788
|
+
const target = scopedAssignmentArgs(body);
|
|
789
|
+
const decision = await authorizeRoleAssignmentWrite(modules, aid, auth, 'role.assign', body.role, {
|
|
790
|
+
scope: body.scope, peerId: target.peerId, groupId: target.groupId, peerKey: target.peerKey,
|
|
791
|
+
});
|
|
792
|
+
if (!decision.allow)
|
|
793
|
+
return sendAuthorizationDenied(res, decision);
|
|
794
|
+
return sendJson(res, 200, { ok: true, assignment: setScopedAssignment(modules.config, aid, body) });
|
|
846
795
|
}
|
|
847
796
|
return sendJson(res, 404, { error: 'not found' });
|
|
848
797
|
}
|
|
@@ -856,22 +805,32 @@ export async function handlePeerRoleApi(req, res, auth = {}) {
|
|
|
856
805
|
const target = assignmentFromPath(urlPath);
|
|
857
806
|
if (!target)
|
|
858
807
|
return sendJson(res, 400, { error: 'missing aid or target id' });
|
|
808
|
+
const modules = await getParentModules();
|
|
859
809
|
if (req.method === 'DELETE') {
|
|
860
810
|
const body = await parseBody(req);
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
811
|
+
const scoped = scopedAssignmentArgs(body, target.targetId);
|
|
812
|
+
const current = getScopedAssignment(modules.config, target.aid, body, target.targetId);
|
|
813
|
+
const decision = await authorizeRoleAssignmentWrite(modules, target.aid, auth, 'role.revoke', current?.role, {
|
|
814
|
+
scope: body.scope, peerId: scoped.peerId, groupId: scoped.groupId, peerKey: scoped.peerKey,
|
|
815
|
+
});
|
|
816
|
+
if (!decision.allow)
|
|
817
|
+
return sendAuthorizationDenied(res, decision);
|
|
818
|
+
return sendJson(res, 200, { ok: true, deleted: deleteScopedAssignment(modules.config, target.aid, body, target.targetId) });
|
|
865
819
|
}
|
|
866
820
|
if (req.method === 'PUT') {
|
|
867
821
|
const body = await parseBody(req);
|
|
868
822
|
if (typeof body.role !== 'string' || !body.role) {
|
|
869
823
|
return sendJson(res, 400, { error: 'role is required' });
|
|
870
824
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
825
|
+
if (!roleExists(modules.roles, target.aid, body.role))
|
|
826
|
+
return sendJson(res, 404, { error: 'role not found' });
|
|
827
|
+
const scoped = scopedAssignmentArgs(body, target.targetId);
|
|
828
|
+
const decision = await authorizeRoleAssignmentWrite(modules, target.aid, auth, 'role.assign', body.role, {
|
|
829
|
+
scope: body.scope, peerId: scoped.peerId, groupId: scoped.groupId, peerKey: scoped.peerKey,
|
|
830
|
+
});
|
|
831
|
+
if (!decision.allow)
|
|
832
|
+
return sendAuthorizationDenied(res, decision);
|
|
833
|
+
return sendJson(res, 200, { ok: true, assignment: setScopedAssignment(modules.config, target.aid, body, target.targetId) });
|
|
875
834
|
}
|
|
876
835
|
return sendJson(res, 404, { error: 'not found' });
|
|
877
836
|
}
|