@i4e/invest4edu-access-core 0.3.0 → 0.4.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 +4 -3
- package/src/access-config.js +68 -0
- package/src/index.js +7 -0
- package/src/tenant-plugin.js +8 -4
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@i4e/invest4edu-access-core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Shared
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Shared access-control primitives (Track 3: tenant keystone, role capabilities, reportee tree, feature flags) for NeoFindesk backends.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.js",
|
|
8
8
|
"./tenant-context": "./src/tenant-context.js",
|
|
9
9
|
"./tenant-plugin": "./src/tenant-plugin.js",
|
|
10
10
|
"./role-capabilities": "./src/role-capabilities.js",
|
|
11
|
-
"./reportee-tree": "./src/reportee-tree.js"
|
|
11
|
+
"./reportee-tree": "./src/reportee-tree.js",
|
|
12
|
+
"./access-config": "./src/access-config.js"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"src",
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access feature flags — @i4e/invest4edu-access-core (Track 3 master kill-switch).
|
|
3
|
+
*
|
|
4
|
+
* A single place to toggle the newer, riskier access behaviours on/off WITHOUT a code deploy,
|
|
5
|
+
* so a rollout can ship "off" (legacy behaviour) and be flipped on — or killed instantly if it
|
|
6
|
+
* misbehaves. Precedence (lowest → highest):
|
|
7
|
+
*
|
|
8
|
+
* DEFAULT_ACCESS_FLAGS < DB (`accessconfig` collection) < env var
|
|
9
|
+
*
|
|
10
|
+
* DB toggles propagate in ~60s via the backend helper's background refresh (edit in the admin
|
|
11
|
+
* UI, no restart). The env var is the emergency kill: set `ACCESS_FLAG_<UPPER_SNAKE>=off` on the
|
|
12
|
+
* App Service and restart to force a flag regardless of DB.
|
|
13
|
+
*
|
|
14
|
+
* ALL defaults are the SAFE/legacy value — turning a flag on is an explicit, reversible act.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_ACCESS_FLAGS = Object.freeze({
|
|
18
|
+
// #1 — v2 client-lead reads use the canonical 5-field visibility engine (else legacy lead-scope).
|
|
19
|
+
v2LeadFiveFieldVisibility: false,
|
|
20
|
+
// M1 — send grant/revoke emails via the comms service.
|
|
21
|
+
delegationEmailNotifications: false,
|
|
22
|
+
// H3 — write an access-event when giver-widening actually broadened a delegate's result.
|
|
23
|
+
delegationReadAudit: false,
|
|
24
|
+
// H1 — enable the "acting-as" session path (x-acting-as-giver).
|
|
25
|
+
delegationActingAs: false,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const ENV_PREFIX = "ACCESS_FLAG_";
|
|
29
|
+
|
|
30
|
+
/** camelCase flag key → env var name, e.g. v2LeadFiveFieldVisibility → ACCESS_FLAG_V2_LEAD_FIVE_FIELD_VISIBILITY */
|
|
31
|
+
export function flagEnvName(key) {
|
|
32
|
+
return ENV_PREFIX + key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const ACCESS_FLAG_ENV_NAMES = Object.freeze(
|
|
36
|
+
Object.fromEntries(Object.keys(DEFAULT_ACCESS_FLAGS).map((k) => [k, flagEnvName(k)])),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
function toBool(v) {
|
|
40
|
+
if (typeof v === "boolean") return v;
|
|
41
|
+
if (v == null || v === "") return undefined;
|
|
42
|
+
return /^(1|true|on|yes|enabled)$/i.test(String(v).trim());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Read any recognised flags from a process.env-shaped object. Unset vars are omitted. */
|
|
46
|
+
export function readEnvFlags(env = {}) {
|
|
47
|
+
const out = {};
|
|
48
|
+
for (const key of Object.keys(DEFAULT_ACCESS_FLAGS)) {
|
|
49
|
+
const b = toBool(env[flagEnvName(key)]);
|
|
50
|
+
if (b !== undefined) out[key] = b;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Merge sources into the effective flag set. Only known keys are honoured.
|
|
57
|
+
* @param {{ dbFlags?: object, envFlags?: object }} sources
|
|
58
|
+
*/
|
|
59
|
+
export function resolveFlags({ dbFlags = {}, envFlags = {} } = {}) {
|
|
60
|
+
const out = { ...DEFAULT_ACCESS_FLAGS };
|
|
61
|
+
for (const key of Object.keys(DEFAULT_ACCESS_FLAGS)) {
|
|
62
|
+
const d = toBool(dbFlags ? dbFlags[key] : undefined);
|
|
63
|
+
if (d !== undefined) out[key] = d;
|
|
64
|
+
const e = toBool(envFlags ? envFlags[key] : undefined);
|
|
65
|
+
if (e !== undefined) out[key] = e; // env wins — emergency kill
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
package/src/index.js
CHANGED
|
@@ -11,3 +11,10 @@ export {
|
|
|
11
11
|
resolveCapabilities,
|
|
12
12
|
} from "./role-capabilities.js";
|
|
13
13
|
export { resolveReporteeUserIds } from "./reportee-tree.js";
|
|
14
|
+
export {
|
|
15
|
+
DEFAULT_ACCESS_FLAGS,
|
|
16
|
+
ACCESS_FLAG_ENV_NAMES,
|
|
17
|
+
flagEnvName,
|
|
18
|
+
readEnvFlags,
|
|
19
|
+
resolveFlags,
|
|
20
|
+
} from "./access-config.js";
|
package/src/tenant-plugin.js
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* injects `account_id` from the request's ALS store (tenant-context.js), so a query cannot
|
|
6
6
|
* reach Mongo without the caller's tenant boundary — even if the controller forgot it.
|
|
7
7
|
*
|
|
8
|
-
* Modes (env `TENANT_ENFORCEMENT`, default `
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
8
|
+
* Modes (env `TENANT_ENFORCEMENT`, default `off`):
|
|
9
|
+
* - `off` (DEFAULT) → literal no-op; behaviour identical to pre-Track-3. Safe merge baseline.
|
|
10
|
+
* - `warn` → inject `account_id` when identity present; log identity-less reads.
|
|
11
|
+
* - `enforce` → inject when present; THROW on identity-less reads (fail-closed).
|
|
12
|
+
* Rollout ladder: off → warn (burn-in, watch `[tenant] identity-less`) → enforce.
|
|
11
13
|
*
|
|
12
14
|
* Bypass: `runAsSystem(fn)` or `.setOptions({ skipTenant: true })` for legitimately
|
|
13
15
|
* cross-tenant reads. NOT covered: aggregation pipelines and write ops (reads are C0 scope).
|
|
@@ -18,9 +20,11 @@
|
|
|
18
20
|
import als from "./tenant-context.js";
|
|
19
21
|
|
|
20
22
|
const READ_OPS = ["find", "findOne", "countDocuments"];
|
|
21
|
-
const mode = () => (process.env.TENANT_ENFORCEMENT || "
|
|
23
|
+
const mode = () => (process.env.TENANT_ENFORCEMENT || "off").toLowerCase();
|
|
22
24
|
|
|
23
25
|
function tenantHook() {
|
|
26
|
+
if (mode() === "off") return; // literal no-op — identical to pre-Track-3
|
|
27
|
+
|
|
24
28
|
const opts = typeof this.getOptions === "function" ? this.getOptions() : this.options || {};
|
|
25
29
|
if (opts && opts.skipTenant) return;
|
|
26
30
|
|