@i4e/invest4edu-access-core 0.41.0 → 0.42.1
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@i4e/invest4edu-access-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.1",
|
|
4
4
|
"description": "Shared access-control primitives for NeoFindesk: tenant keystone, role capabilities, reportee tree, entity/role vocabulary, mobile app identity, feature flags, and the unified access engine (registry schema, snapshot resolver, visibleWhen).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -128,7 +128,10 @@ export function createClientProfileService({ getDb, getSettings, getSideContext
|
|
|
128
128
|
async function roleNamesOf(userId, session) {
|
|
129
129
|
const maps = await db().collection("userentitymappings").find({ user_id: userId, status: { $ne: 0 }, role_id: { $ne: null } }, { session, projection: { role_id: 1 } }).toArray();
|
|
130
130
|
if (!maps.length) return [];
|
|
131
|
-
|
|
131
|
+
// role_id is an ARRAY on real mappings; `$in` must get the role ids themselves, not the arrays
|
|
132
|
+
// (an array inside `$in` only matches an equal array, so every caller read as role-less).
|
|
133
|
+
const roleIds = maps.flatMap((m) => (Array.isArray(m.role_id) ? m.role_id : [m.role_id])).filter(Boolean);
|
|
134
|
+
const roles = await db().collection("roles").find({ _id: { $in: roleIds } }, { session, projection: { role_name: 1 } }).toArray();
|
|
132
135
|
return roles.map((r) => r.role_name).filter(Boolean);
|
|
133
136
|
}
|
|
134
137
|
|
|
@@ -363,6 +366,40 @@ export function createClientProfileService({ getDb, getSettings, getSideContext
|
|
|
363
366
|
}
|
|
364
367
|
}
|
|
365
368
|
|
|
369
|
+
/**
|
|
370
|
+
* May this caller write to this client? A client-app session only to their own records — the
|
|
371
|
+
* same person's profiles under one master; portal callers are scoped by their own rules and
|
|
372
|
+
* always pass here. Gated by modes.own_records_only (audit logs, enforce refuses).
|
|
373
|
+
* @returns {Promise<{ allowed: boolean, reason?: string }>}
|
|
374
|
+
*/
|
|
375
|
+
async function callerMayWriteClient({ userId, clientId } = {}) {
|
|
376
|
+
const settings = getSettings();
|
|
377
|
+
if (!isEvaluated(settings, "own_records_only") || !userId || !clientId) return { allowed: true };
|
|
378
|
+
let actor;
|
|
379
|
+
try {
|
|
380
|
+
actor = await resolveActor(userId);
|
|
381
|
+
} catch (err) {
|
|
382
|
+
onAudit({ kind: "own_records_error", client_id: clientId, message: err?.message });
|
|
383
|
+
return { allowed: true };
|
|
384
|
+
}
|
|
385
|
+
if (actor?.callerClass !== CALLER_CLASSES.CLIENT) return { allowed: true };
|
|
386
|
+
let own = [];
|
|
387
|
+
try {
|
|
388
|
+
const user = await db().collection("users").findOne({ _id: toId(userId) }, { projection: { client_id: 1 } });
|
|
389
|
+
if (user?.client_id) {
|
|
390
|
+
const master = await db().collection("masterchildclients").findOne({ child_client_ids: user.client_id }, { projection: { child_client_ids: 1 } });
|
|
391
|
+
own = (master?.child_client_ids?.length ? master.child_client_ids : [user.client_id]).map(String);
|
|
392
|
+
}
|
|
393
|
+
} catch (err) {
|
|
394
|
+
// A client session whose records cannot be read must not write anywhere.
|
|
395
|
+
onAudit({ kind: "own_records_error", client_id: clientId, message: err?.message });
|
|
396
|
+
return { allowed: !isEnforced(settings, "own_records_only"), reason: "unresolved" };
|
|
397
|
+
}
|
|
398
|
+
if (own.includes(String(clientId))) return { allowed: true };
|
|
399
|
+
onAudit({ kind: "own_records_only", client_id: clientId, user_id: String(userId) });
|
|
400
|
+
return { allowed: !isEnforced(settings, "own_records_only"), reason: "not_own_record" };
|
|
401
|
+
}
|
|
402
|
+
|
|
366
403
|
/** The UCC was updated: nothing is pending any more. Never throws. */
|
|
367
404
|
async function clearUccPending(clientId) {
|
|
368
405
|
try {
|
|
@@ -374,5 +411,5 @@ export function createClientProfileService({ getDb, getSettings, getSideContext
|
|
|
374
411
|
}
|
|
375
412
|
}
|
|
376
413
|
|
|
377
|
-
return Object.freeze({ findActiveProfilesByPan, findExisting, attach, attachUser, resolveActor, sideFilterFor, evaluatePersonalEdit, goalContextFor, noteClientUccEdit, clearUccPending });
|
|
414
|
+
return Object.freeze({ findActiveProfilesByPan, findExisting, attach, attachUser, resolveActor, sideFilterFor, evaluatePersonalEdit, goalContextFor, noteClientUccEdit, clearUccPending, callerMayWriteClient });
|
|
378
415
|
}
|
|
@@ -73,6 +73,7 @@ export const DEFAULT_CLIENT_PROFILE_SETTINGS = deepFreeze({
|
|
|
73
73
|
mf_one_investor: MODES.OFF,
|
|
74
74
|
detail_scope: MODES.OFF,
|
|
75
75
|
ucc_followup: MODES.OFF,
|
|
76
|
+
own_records_only: MODES.OFF,
|
|
76
77
|
},
|
|
77
78
|
corporate_distributor_types: [],
|
|
78
79
|
house_distributor_codes: [],
|
|
@@ -186,6 +187,7 @@ export const CLIENT_PROFILE_SETTING_DESCRIPTIONS = Object.freeze({
|
|
|
186
187
|
"modes.goal_approval": "Hold partner-added goals until the client approves them.",
|
|
187
188
|
"modes.resolve_by_pan": "Show an existing client instead of a duplicate error when a PAN is found.",
|
|
188
189
|
"modes.mf_one_investor": "Refuse a second MF investor for a person who already has one (D9).",
|
|
190
|
+
"modes.own_records_only": "A client-app session may write only its own records (one person under one master); anything else is refused.",
|
|
189
191
|
"modes.ucc_followup": "Flag a profile for a UCC update when its client changes UCC details in the app (never pushed by the client).",
|
|
190
192
|
"modes.detail_scope": "A client's full record by id only for callers whose client list includes them; everyone else gets the limited card (R15).",
|
|
191
193
|
"modes.linked_visibility": "Clients reached only through a secondary mapping or a linked user appear in lists, as the limited card.",
|