@cldmv/slothlet 3.2.3 → 3.3.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/README.md +20 -5
- package/dist/lib/builders/api_builder.mjs +233 -3
- package/dist/lib/handlers/api-manager.mjs +23 -0
- package/dist/lib/handlers/context-async.mjs +4 -0
- package/dist/lib/handlers/context-live.mjs +5 -0
- package/dist/lib/handlers/hook-manager.mjs +5 -111
- package/dist/lib/handlers/permission-manager.mjs +408 -0
- package/dist/lib/handlers/unified-wrapper.mjs +90 -22
- package/dist/lib/helpers/config.mjs +91 -7
- package/dist/lib/helpers/pattern-matcher.mjs +141 -0
- package/dist/lib/i18n/languages/de-de.json +21 -1
- package/dist/lib/i18n/languages/en-gb.json +21 -1
- package/dist/lib/i18n/languages/en-us.json +21 -1
- package/dist/lib/i18n/languages/es-mx.json +21 -1
- package/dist/lib/i18n/languages/fr-fr.json +21 -1
- package/dist/lib/i18n/languages/hi-in.json +21 -1
- package/dist/lib/i18n/languages/ja-jp.json +21 -1
- package/dist/lib/i18n/languages/ko-kr.json +21 -1
- package/dist/lib/i18n/languages/pt-br.json +21 -1
- package/dist/lib/i18n/languages/ru-ru.json +21 -1
- package/dist/lib/i18n/languages/zh-cn.json +21 -1
- package/dist/slothlet.mjs +11 -2
- package/package.json +4 -1
- package/types/dist/lib/builders/api_builder.d.mts.map +1 -1
- package/types/dist/lib/handlers/api-manager.d.mts.map +1 -1
- package/types/dist/lib/handlers/context-async.d.mts.map +1 -1
- package/types/dist/lib/handlers/context-live.d.mts.map +1 -1
- package/types/dist/lib/handlers/hook-manager.d.mts.map +1 -1
- package/types/dist/lib/handlers/permission-manager.d.mts +151 -0
- package/types/dist/lib/handlers/permission-manager.d.mts.map +1 -0
- package/types/dist/lib/handlers/unified-wrapper.d.mts.map +1 -1
- package/types/dist/lib/helpers/config.d.mts +16 -0
- package/types/dist/lib/helpers/config.d.mts.map +1 -1
- package/types/dist/lib/helpers/pattern-matcher.d.mts +44 -0
- package/types/dist/lib/helpers/pattern-matcher.d.mts.map +1 -0
- package/types/dist/slothlet.d.mts.map +1 -1
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2026 CLDMV/Shinrai
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
import { ComponentBase } from "@cldmv/slothlet/factories/component-base";
|
|
22
|
+
import { compilePattern } from "@cldmv/slothlet/helpers/pattern-matcher";
|
|
23
|
+
import { translate } from "@cldmv/slothlet/i18n";
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
let ruleIdCounter = 0;
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
export class PermissionManager extends ComponentBase {
|
|
30
|
+
|
|
31
|
+
static slothletProperty = "permissionManager";
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
#rules = new Map();
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
#defaultPolicy = "allow";
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
#enabled = false;
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
#audit = "default";
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
#resolvedCache = new Map();
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
#compiledCache = new Map();
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
constructor(slothlet) {
|
|
53
|
+
super(slothlet);
|
|
54
|
+
|
|
55
|
+
const permConfig = slothlet.config?.permissions;
|
|
56
|
+
if (permConfig) {
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
this.#defaultPolicy = permConfig.defaultPolicy || "allow";
|
|
60
|
+
this.#enabled = permConfig.enabled !== false;
|
|
61
|
+
this.#audit = permConfig.audit || "default";
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if (Array.isArray(permConfig.rules)) {
|
|
65
|
+
for (const rule of permConfig.rules) {
|
|
66
|
+
this.addRule(rule, null);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
this.addRule({ caller: "**", target: "slothlet.permissions.control.**", effect: "deny" }, "__builtin__");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
addRule(rule, ownerModuleID = null, ruleId = null) {
|
|
77
|
+
this.#validateRule(rule);
|
|
78
|
+
|
|
79
|
+
const id = ruleId || `perm-${++ruleIdCounter}`;
|
|
80
|
+
const entry = {
|
|
81
|
+
id,
|
|
82
|
+
caller: rule.caller,
|
|
83
|
+
target: rule.target,
|
|
84
|
+
effect: rule.effect,
|
|
85
|
+
ownerModuleID: ownerModuleID,
|
|
86
|
+
registeredAt: Date.now()
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
this.#rules.set(id, entry);
|
|
90
|
+
this.#clearCache();
|
|
91
|
+
|
|
92
|
+
this.debug("permissions", {
|
|
93
|
+
key: "DEBUG_PERMISSION_RULE_ADDED",
|
|
94
|
+
ruleId: id,
|
|
95
|
+
caller: rule.caller,
|
|
96
|
+
target: rule.target,
|
|
97
|
+
effect: rule.effect,
|
|
98
|
+
ownerModuleID
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return id;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
removeRule(ruleId, callerModuleID = null) {
|
|
106
|
+
const entry = this.#rules.get(ruleId);
|
|
107
|
+
if (!entry) return false;
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if (callerModuleID && entry.ownerModuleID && callerModuleID === entry.ownerModuleID) {
|
|
115
|
+
throw new this.SlothletError("PERMISSION_SELF_MODIFY", {
|
|
116
|
+
ruleId,
|
|
117
|
+
moduleID: callerModuleID
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.#rules.delete(ruleId);
|
|
122
|
+
this.#clearCache();
|
|
123
|
+
|
|
124
|
+
this.debug("permissions", {
|
|
125
|
+
key: "DEBUG_PERMISSION_RULE_REMOVED",
|
|
126
|
+
ruleId,
|
|
127
|
+
caller: entry.caller,
|
|
128
|
+
target: entry.target,
|
|
129
|
+
effect: entry.effect
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
checkAccess(callerPath, targetPath, callerFilePath = null, targetFilePath = null) {
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
const isControlTarget = targetPath?.startsWith("slothlet.permissions.control.");
|
|
144
|
+
if (!this.#enabled && !isControlTarget) return true;
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if (callerFilePath && targetFilePath && callerFilePath === targetFilePath) {
|
|
148
|
+
this.#emitAuditEvent("permission:self-bypass", {
|
|
149
|
+
caller: callerPath,
|
|
150
|
+
target: targetPath,
|
|
151
|
+
filePath: callerFilePath
|
|
152
|
+
});
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
const cacheKey = `${callerPath}::${targetPath}`;
|
|
158
|
+
if (this.#resolvedCache.has(cacheKey)) {
|
|
159
|
+
const cached = this.#resolvedCache.get(cacheKey);
|
|
160
|
+
this.#emitAuditEvent(cached.event, cached.payload);
|
|
161
|
+
return cached.allowed;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
const entry = this.#evaluate(callerPath, targetPath);
|
|
166
|
+
this.#resolvedCache.set(cacheKey, entry);
|
|
167
|
+
this.#emitAuditEvent(entry.event, entry.payload);
|
|
168
|
+
return entry.allowed;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
getRulesForPath(targetPath) {
|
|
173
|
+
const matching = [];
|
|
174
|
+
for (const entry of this.#rules.values()) {
|
|
175
|
+
const targetMatcher = this.#getCompiledPattern(entry.target);
|
|
176
|
+
if (targetMatcher(targetPath)) {
|
|
177
|
+
matching.push(this.#serializeRule(entry));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return matching;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
getRulesByModule(moduleID) {
|
|
185
|
+
const matching = [];
|
|
186
|
+
for (const entry of this.#rules.values()) {
|
|
187
|
+
if (entry.ownerModuleID === moduleID) {
|
|
188
|
+
matching.push(this.#serializeRule(entry));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return matching;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
getRulesForCaller(callerPath) {
|
|
196
|
+
const matching = [];
|
|
197
|
+
for (const entry of this.#rules.values()) {
|
|
198
|
+
const callerMatcher = this.#getCompiledPattern(entry.caller);
|
|
199
|
+
if (callerMatcher(callerPath)) {
|
|
200
|
+
matching.push(this.#serializeRule(entry));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return matching;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
enable() {
|
|
208
|
+
this.#enabled = true;
|
|
209
|
+
this.#clearCache();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
disable() {
|
|
214
|
+
this.#enabled = false;
|
|
215
|
+
this.#clearCache();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
isEnabled() {
|
|
220
|
+
return this.#enabled;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
exportRules() {
|
|
228
|
+
const rules = [];
|
|
229
|
+
for (const entry of this.#rules.values()) {
|
|
230
|
+
rules.push({
|
|
231
|
+
rule: { caller: entry.caller, target: entry.target, effect: entry.effect },
|
|
232
|
+
ownerModuleID: entry.ownerModuleID
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return rules;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
importRules(registrations) {
|
|
240
|
+
if (!Array.isArray(registrations)) return;
|
|
241
|
+
for (const reg of registrations) {
|
|
242
|
+
this.addRule(reg.rule, reg.ownerModuleID);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
async shutdown() {
|
|
249
|
+
this.#rules.clear();
|
|
250
|
+
this.#resolvedCache.clear();
|
|
251
|
+
this.#compiledCache.clear();
|
|
252
|
+
this.#enabled = false;
|
|
253
|
+
this.#defaultPolicy = "allow";
|
|
254
|
+
this.#audit = "default";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
#validateRule(rule) {
|
|
261
|
+
if (!rule || typeof rule !== "object") {
|
|
262
|
+
throw new this.SlothletError("INVALID_PERMISSION_RULE", {
|
|
263
|
+
reason: translate("PERM_RULE_NOT_OBJECT"),
|
|
264
|
+
received: typeof rule
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (typeof rule.caller !== "string" || !rule.caller) {
|
|
268
|
+
throw new this.SlothletError("INVALID_PERMISSION_RULE", {
|
|
269
|
+
reason: translate("PERM_RULE_CALLER_REQUIRED"),
|
|
270
|
+
received: typeof rule.caller
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
if (typeof rule.target !== "string" || !rule.target) {
|
|
274
|
+
throw new this.SlothletError("INVALID_PERMISSION_RULE", {
|
|
275
|
+
reason: translate("PERM_RULE_TARGET_REQUIRED"),
|
|
276
|
+
received: typeof rule.target
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (rule.effect !== "allow" && rule.effect !== "deny") {
|
|
280
|
+
throw new this.SlothletError("INVALID_PERMISSION_RULE", {
|
|
281
|
+
reason: translate("PERM_RULE_EFFECT_INVALID"),
|
|
282
|
+
received: rule.effect
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
#evaluate(callerPath, targetPath) {
|
|
289
|
+
|
|
290
|
+
const matches = [];
|
|
291
|
+
|
|
292
|
+
for (const entry of this.#rules.values()) {
|
|
293
|
+
const callerMatcher = this.#getCompiledPattern(entry.caller);
|
|
294
|
+
const targetMatcher = this.#getCompiledPattern(entry.target);
|
|
295
|
+
|
|
296
|
+
if (callerMatcher(callerPath) && targetMatcher(targetPath)) {
|
|
297
|
+
matches.push(entry);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
if (matches.length === 0) {
|
|
303
|
+
const allowed = this.#defaultPolicy === "allow";
|
|
304
|
+
return {
|
|
305
|
+
allowed,
|
|
306
|
+
event: "permission:default",
|
|
307
|
+
payload: { caller: callerPath, target: targetPath, policy: this.#defaultPolicy }
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
matches.sort((a, b) => {
|
|
313
|
+
const specA = this.#computeSpecificity(a, callerPath, targetPath);
|
|
314
|
+
const specB = this.#computeSpecificity(b, callerPath, targetPath);
|
|
315
|
+
if (specA !== specB) return specB - specA;
|
|
316
|
+
return a.registeredAt - b.registeredAt;
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
const highestSpec = this.#computeSpecificity(matches[0], callerPath, targetPath);
|
|
321
|
+
const topTier = matches.filter((m) => this.#computeSpecificity(m, callerPath, targetPath) === highestSpec);
|
|
322
|
+
|
|
323
|
+
const winner = topTier[topTier.length - 1];
|
|
324
|
+
const allowed = winner.effect === "allow";
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
allowed,
|
|
328
|
+
event: allowed ? "permission:allowed" : "permission:denied",
|
|
329
|
+
payload: { caller: callerPath, target: targetPath, rule: this.#serializeRule(winner) }
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
#computeSpecificity(entry, callerPath, targetPath) {
|
|
335
|
+
return this.#patternSpecificity(entry.caller, callerPath) + this.#patternSpecificity(entry.target, targetPath);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
#patternSpecificity(pattern, _path) {
|
|
340
|
+
|
|
341
|
+
if (!pattern.includes("*") && !pattern.includes("?") && !pattern.includes("{")) {
|
|
342
|
+
return 3;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (pattern.includes("**")) {
|
|
346
|
+
return 1;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return 2;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
#getCompiledPattern(pattern) {
|
|
354
|
+
let matcher = this.#compiledCache.get(pattern);
|
|
355
|
+
if (!matcher) {
|
|
356
|
+
matcher = compilePattern(pattern);
|
|
357
|
+
this.#compiledCache.set(pattern, matcher);
|
|
358
|
+
}
|
|
359
|
+
return matcher;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
#clearCache() {
|
|
364
|
+
this.#resolvedCache.clear();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
#emitAuditEvent(event, payload) {
|
|
369
|
+
|
|
370
|
+
this.debug("permissions", {
|
|
371
|
+
key:
|
|
372
|
+
event === "permission:denied"
|
|
373
|
+
? "DEBUG_PERMISSION_DENIED"
|
|
374
|
+
: event === "permission:allowed"
|
|
375
|
+
? "DEBUG_PERMISSION_ALLOWED"
|
|
376
|
+
: event === "permission:self-bypass"
|
|
377
|
+
? "DEBUG_PERMISSION_SELF_BYPASS"
|
|
378
|
+
: "DEBUG_PERMISSION_DEFAULT",
|
|
379
|
+
...payload
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
const alwaysEmit = event === "permission:denied" || event === "permission:self-bypass";
|
|
384
|
+
if (!alwaysEmit && this.#audit !== "verbose") return;
|
|
385
|
+
|
|
386
|
+
const lifecycle = this.slothlet.handlers?.lifecycle;
|
|
387
|
+
if (lifecycle) {
|
|
388
|
+
lifecycle.emit(event, { ...payload, timestamp: Date.now() });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
#serializeRule(entry) {
|
|
394
|
+
return {
|
|
395
|
+
id: entry.id,
|
|
396
|
+
caller: entry.caller,
|
|
397
|
+
target: entry.target,
|
|
398
|
+
effect: entry.effect,
|
|
399
|
+
ownerModuleID: entry.ownerModuleID,
|
|
400
|
+
registeredAt: entry.registeredAt
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
debug(category, data) {
|
|
406
|
+
this.slothlet.debug(category, data);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
@@ -1654,6 +1654,13 @@ export class UnifiedWrapper extends ComponentBase {
|
|
|
1654
1654
|
},
|
|
1655
1655
|
|
|
1656
1656
|
async apply(___target, ___thisArg, args) {
|
|
1657
|
+
|
|
1658
|
+
|
|
1659
|
+
|
|
1660
|
+
|
|
1661
|
+
|
|
1662
|
+
const ___capturedCallerWrapper = wrapper.slothlet.contextManager?.tryGetContext?.()?.currentWrapper ?? null;
|
|
1663
|
+
|
|
1657
1664
|
wrapper.slothlet.debug("wrapper", {
|
|
1658
1665
|
key: "DEBUG_MODE_WAITING_APPLY_ENTRY",
|
|
1659
1666
|
apiPath: wrapper.____slothletInternal.apiPath,
|
|
@@ -1834,6 +1841,27 @@ export class UnifiedWrapper extends ComponentBase {
|
|
|
1834
1841
|
|
|
1835
1842
|
|
|
1836
1843
|
|
|
1844
|
+
|
|
1845
|
+
|
|
1846
|
+
|
|
1847
|
+
|
|
1848
|
+
|
|
1849
|
+
|
|
1850
|
+
if (___capturedCallerWrapper && wrapper.slothlet.contextManager) {
|
|
1851
|
+
const ___instanceStore = wrapper.slothlet.contextManager.instances?.get?.(wrapper.instanceID);
|
|
1852
|
+
|
|
1853
|
+
|
|
1854
|
+
|
|
1855
|
+
if (___instanceStore && !___instanceStore.currentWrapper) {
|
|
1856
|
+
return wrapper.slothlet.contextManager.runInContext(
|
|
1857
|
+
wrapper.instanceID,
|
|
1858
|
+
() => Reflect.apply(current, lastObject, args),
|
|
1859
|
+
null,
|
|
1860
|
+
[],
|
|
1861
|
+
___capturedCallerWrapper
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1837
1865
|
return Reflect.apply(current, lastObject, args);
|
|
1838
1866
|
}
|
|
1839
1867
|
|
|
@@ -2540,6 +2568,35 @@ export class UnifiedWrapper extends ComponentBase {
|
|
|
2540
2568
|
}
|
|
2541
2569
|
|
|
2542
2570
|
|
|
2571
|
+
|
|
2572
|
+
|
|
2573
|
+
const permissionManager = wrapper.slothlet.handlers?.permissionManager;
|
|
2574
|
+
if (permissionManager && permissionManager.isEnabled()) {
|
|
2575
|
+
const ctx = wrapper.slothlet.contextManager?.tryGetContext?.();
|
|
2576
|
+
|
|
2577
|
+
|
|
2578
|
+
const callerWrapper = ctx?.currentWrapper;
|
|
2579
|
+
|
|
2580
|
+
|
|
2581
|
+
if (callerWrapper) {
|
|
2582
|
+
|
|
2583
|
+
|
|
2584
|
+
const callerPath = callerWrapper.____slothletInternal?.apiPath ?? "";
|
|
2585
|
+
const callerFilePath = callerWrapper.____slothletInternal?.filePath ?? null;
|
|
2586
|
+
const targetPath = wrapper.____slothletInternal.apiPath;
|
|
2587
|
+
const targetFilePath = wrapper.____slothletInternal.filePath ?? null;
|
|
2588
|
+
|
|
2589
|
+
|
|
2590
|
+
if (!permissionManager.checkAccess(callerPath, targetPath, callerFilePath, targetFilePath)) {
|
|
2591
|
+
throw new wrapper.SlothletError("PERMISSION_DENIED", {
|
|
2592
|
+
caller: callerPath,
|
|
2593
|
+
target: targetPath
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
|
|
2543
2600
|
const hookManager = wrapper.slothlet.handlers?.hookManager;
|
|
2544
2601
|
|
|
2545
2602
|
const hasHooks = hookManager && hookManager.enabled && !wrapper.____slothletInternal.apiPath.startsWith("slothlet.hook");
|
|
@@ -2584,33 +2641,43 @@ export class UnifiedWrapper extends ComponentBase {
|
|
|
2584
2641
|
const checkMaterialized = () => {
|
|
2585
2642
|
if (wrapper.____slothletInternal.state.materialized) {
|
|
2586
2643
|
const impl = wrapper.____slothletInternal.impl;
|
|
2587
|
-
|
|
2588
|
-
if (
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
if (
|
|
2595
|
-
|
|
2644
|
+
try {
|
|
2645
|
+
if (typeof impl === "function") {
|
|
2646
|
+
if (wrapper.slothlet.contextManager) {
|
|
2647
|
+
resolve(wrapper.slothlet.contextManager.runInContext(wrapper.instanceID, impl, thisArg, args, wrapper));
|
|
2648
|
+
} else {
|
|
2649
|
+
resolve(impl.apply(thisArg, args));
|
|
2650
|
+
}
|
|
2651
|
+
} else if (impl && typeof impl === "object" && typeof impl.default === "function") {
|
|
2652
|
+
if (wrapper.contextManager) {
|
|
2653
|
+
resolve(wrapper.contextManager.runInContext(wrapper.instanceID, impl.default, impl, args, wrapper));
|
|
2654
|
+
} else {
|
|
2655
|
+
resolve(impl.default.apply(impl, args));
|
|
2656
|
+
}
|
|
2596
2657
|
} else {
|
|
2597
|
-
|
|
2658
|
+
reject(
|
|
2659
|
+
new wrapper.slothlet.SlothletError(
|
|
2660
|
+
"INVALID_CONFIG_NOT_A_FUNCTION",
|
|
2661
|
+
{
|
|
2662
|
+
apiPath: wrapper.____slothletInternal.apiPath,
|
|
2663
|
+
actualType: typeof impl
|
|
2664
|
+
},
|
|
2665
|
+
null,
|
|
2666
|
+
{ validationError: true }
|
|
2667
|
+
)
|
|
2668
|
+
);
|
|
2598
2669
|
}
|
|
2599
|
-
}
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
apiPath: wrapper.____slothletInternal.apiPath,
|
|
2605
|
-
actualType: typeof impl
|
|
2606
|
-
},
|
|
2607
|
-
null,
|
|
2608
|
-
{ validationError: true }
|
|
2609
|
-
)
|
|
2610
|
-
);
|
|
2670
|
+
} catch (err) {
|
|
2671
|
+
|
|
2672
|
+
|
|
2673
|
+
|
|
2674
|
+
reject(err);
|
|
2611
2675
|
}
|
|
2612
2676
|
return;
|
|
2613
2677
|
}
|
|
2678
|
+
|
|
2679
|
+
|
|
2680
|
+
|
|
2614
2681
|
if (!wrapper.____slothletInternal.state.inFlight) {
|
|
2615
2682
|
reject(
|
|
2616
2683
|
new wrapper.slothlet.SlothletError(
|
|
@@ -2623,6 +2690,7 @@ export class UnifiedWrapper extends ComponentBase {
|
|
|
2623
2690
|
)
|
|
2624
2691
|
);
|
|
2625
2692
|
return;
|
|
2693
|
+
|
|
2626
2694
|
}
|
|
2627
2695
|
setImmediate(checkMaterialized);
|
|
2628
2696
|
};
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
import { ComponentBase } from "@cldmv/slothlet/factories/component-base";
|
|
21
|
+
import { SlothletError } from "@cldmv/slothlet/errors";
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
export class Config extends ComponentBase {
|
|
@@ -98,7 +99,7 @@ export class Config extends ComponentBase {
|
|
|
98
99
|
|
|
99
100
|
|
|
100
101
|
normalizeMutations(mutations) {
|
|
101
|
-
const defaults = { add: true, remove: true, reload: true };
|
|
102
|
+
const defaults = { add: true, remove: true, reload: true, permissions: true };
|
|
102
103
|
|
|
103
104
|
|
|
104
105
|
if (!mutations || typeof mutations !== "object") {
|
|
@@ -109,7 +110,8 @@ export class Config extends ComponentBase {
|
|
|
109
110
|
return {
|
|
110
111
|
add: mutations.add === false ? false : true,
|
|
111
112
|
remove: mutations.remove === false ? false : true,
|
|
112
|
-
reload: mutations.reload === false ? false : true
|
|
113
|
+
reload: mutations.reload === false ? false : true,
|
|
114
|
+
permissions: mutations.permissions === false ? false : true
|
|
113
115
|
};
|
|
114
116
|
}
|
|
115
117
|
|
|
@@ -126,7 +128,8 @@ export class Config extends ComponentBase {
|
|
|
126
128
|
context: false,
|
|
127
129
|
initialization: false,
|
|
128
130
|
materialize: false,
|
|
129
|
-
versioning: false
|
|
131
|
+
versioning: false,
|
|
132
|
+
permissions: false
|
|
130
133
|
};
|
|
131
134
|
}
|
|
132
135
|
|
|
@@ -142,7 +145,8 @@ export class Config extends ComponentBase {
|
|
|
142
145
|
context: true,
|
|
143
146
|
initialization: true,
|
|
144
147
|
materialize: true,
|
|
145
|
-
versioning: true
|
|
148
|
+
versioning: true,
|
|
149
|
+
permissions: true
|
|
146
150
|
};
|
|
147
151
|
}
|
|
148
152
|
|
|
@@ -158,7 +162,8 @@ export class Config extends ComponentBase {
|
|
|
158
162
|
context: debug.context || false,
|
|
159
163
|
initialization: debug.initialization || false,
|
|
160
164
|
materialize: debug.materialize || false,
|
|
161
|
-
versioning: debug.versioning || false
|
|
165
|
+
versioning: debug.versioning || false,
|
|
166
|
+
permissions: debug.permissions || false
|
|
162
167
|
};
|
|
163
168
|
}
|
|
164
169
|
|
|
@@ -173,7 +178,8 @@ export class Config extends ComponentBase {
|
|
|
173
178
|
context: false,
|
|
174
179
|
initialization: false,
|
|
175
180
|
materialize: false,
|
|
176
|
-
versioning: false
|
|
181
|
+
versioning: false,
|
|
182
|
+
permissions: false
|
|
177
183
|
};
|
|
178
184
|
}
|
|
179
185
|
|
|
@@ -284,6 +290,9 @@ export class Config extends ComponentBase {
|
|
|
284
290
|
}
|
|
285
291
|
|
|
286
292
|
|
|
293
|
+
const permissionsConfig = this.normalizePermissions(config.permissions);
|
|
294
|
+
|
|
295
|
+
|
|
287
296
|
let i18nConfig = null;
|
|
288
297
|
if (config.i18n && typeof config.i18n === "object") {
|
|
289
298
|
i18nConfig = {
|
|
@@ -315,7 +324,8 @@ export class Config extends ComponentBase {
|
|
|
315
324
|
silent: config.silent === true,
|
|
316
325
|
typescript: this.normalizeTypeScript(config.typescript),
|
|
317
326
|
env: this.normalizeEnv(config.env),
|
|
318
|
-
versionDispatcher: config.versionDispatcher ?? null
|
|
327
|
+
versionDispatcher: config.versionDispatcher ?? null,
|
|
328
|
+
permissions: permissionsConfig
|
|
319
329
|
};
|
|
320
330
|
}
|
|
321
331
|
|
|
@@ -368,4 +378,78 @@ export class Config extends ComponentBase {
|
|
|
368
378
|
}
|
|
369
379
|
return null;
|
|
370
380
|
}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
normalizePermissions(permissions) {
|
|
384
|
+
if (!permissions || typeof permissions !== "object") {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
let defaultPolicy;
|
|
390
|
+
if (permissions.defaultPolicy === "deny") {
|
|
391
|
+
defaultPolicy = "deny";
|
|
392
|
+
} else if (permissions.defaultPolicy === "allow" || permissions.defaultPolicy === undefined) {
|
|
393
|
+
defaultPolicy = "allow";
|
|
394
|
+
} else {
|
|
395
|
+
throw new SlothletError(
|
|
396
|
+
"INVALID_CONFIG",
|
|
397
|
+
{
|
|
398
|
+
option: "permissions.defaultPolicy",
|
|
399
|
+
value: permissions.defaultPolicy,
|
|
400
|
+
expected: '"allow" or "deny"',
|
|
401
|
+
hint: "HINT_INVALID_CONFIG"
|
|
402
|
+
},
|
|
403
|
+
null,
|
|
404
|
+
{ validationError: true }
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const enabled = permissions.enabled !== false;
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
let audit;
|
|
412
|
+
if (permissions.audit === "verbose") {
|
|
413
|
+
audit = "verbose";
|
|
414
|
+
} else if (permissions.audit === "default" || permissions.audit === undefined) {
|
|
415
|
+
audit = "default";
|
|
416
|
+
} else if (permissions.audit === true) {
|
|
417
|
+
|
|
418
|
+
audit = "default";
|
|
419
|
+
} else if (permissions.audit === false) {
|
|
420
|
+
|
|
421
|
+
audit = "default";
|
|
422
|
+
} else {
|
|
423
|
+
throw new SlothletError(
|
|
424
|
+
"INVALID_CONFIG",
|
|
425
|
+
{
|
|
426
|
+
option: "permissions.audit",
|
|
427
|
+
value: permissions.audit,
|
|
428
|
+
expected: '"default" or "verbose"',
|
|
429
|
+
hint: "HINT_INVALID_CONFIG"
|
|
430
|
+
},
|
|
431
|
+
null,
|
|
432
|
+
{ validationError: true }
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
if (permissions.rules !== undefined && !Array.isArray(permissions.rules)) {
|
|
438
|
+
throw new SlothletError(
|
|
439
|
+
"INVALID_CONFIG",
|
|
440
|
+
{
|
|
441
|
+
option: "permissions.rules",
|
|
442
|
+
value: permissions.rules,
|
|
443
|
+
expected: "array",
|
|
444
|
+
hint: "HINT_INVALID_CONFIG"
|
|
445
|
+
},
|
|
446
|
+
null,
|
|
447
|
+
{ validationError: true }
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const rules = Array.isArray(permissions.rules) ? permissions.rules : [];
|
|
452
|
+
|
|
453
|
+
return { defaultPolicy, enabled, audit, rules };
|
|
454
|
+
}
|
|
371
455
|
}
|