@objectstack/core 0.8.2 → 0.9.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/API_REGISTRY.md +392 -0
- package/CHANGELOG.md +8 -0
- package/README.md +36 -0
- package/dist/api-registry-plugin.d.ts +54 -0
- package/dist/api-registry-plugin.d.ts.map +1 -0
- package/dist/api-registry-plugin.js +53 -0
- package/dist/api-registry-plugin.test.d.ts +2 -0
- package/dist/api-registry-plugin.test.d.ts.map +1 -0
- package/dist/api-registry-plugin.test.js +332 -0
- package/dist/api-registry.d.ts +259 -0
- package/dist/api-registry.d.ts.map +1 -0
- package/dist/api-registry.js +599 -0
- package/dist/api-registry.test.d.ts +2 -0
- package/dist/api-registry.test.d.ts.map +1 -0
- package/dist/api-registry.test.js +957 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +35 -11
- package/dist/plugin-loader.d.ts +3 -2
- package/dist/plugin-loader.d.ts.map +1 -1
- package/dist/plugin-loader.js +13 -11
- package/dist/qa/adapter.d.ts +14 -0
- package/dist/qa/adapter.d.ts.map +1 -0
- package/dist/qa/adapter.js +1 -0
- package/dist/qa/http-adapter.d.ts +16 -0
- package/dist/qa/http-adapter.d.ts.map +1 -0
- package/dist/qa/http-adapter.js +107 -0
- package/dist/qa/index.d.ts +4 -0
- package/dist/qa/index.d.ts.map +1 -0
- package/dist/qa/index.js +3 -0
- package/dist/qa/runner.d.ts +27 -0
- package/dist/qa/runner.d.ts.map +1 -0
- package/dist/qa/runner.js +157 -0
- package/dist/security/index.d.ts +14 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +13 -0
- package/dist/security/plugin-config-validator.d.ts +79 -0
- package/dist/security/plugin-config-validator.d.ts.map +1 -0
- package/dist/security/plugin-config-validator.js +166 -0
- package/dist/security/plugin-config-validator.test.d.ts +2 -0
- package/dist/security/plugin-config-validator.test.d.ts.map +1 -0
- package/dist/security/plugin-config-validator.test.js +223 -0
- package/dist/security/plugin-permission-enforcer.d.ts +154 -0
- package/dist/security/plugin-permission-enforcer.d.ts.map +1 -0
- package/dist/security/plugin-permission-enforcer.js +323 -0
- package/dist/security/plugin-permission-enforcer.test.d.ts +2 -0
- package/dist/security/plugin-permission-enforcer.test.d.ts.map +1 -0
- package/dist/security/plugin-permission-enforcer.test.js +205 -0
- package/dist/security/plugin-signature-verifier.d.ts +96 -0
- package/dist/security/plugin-signature-verifier.d.ts.map +1 -0
- package/dist/security/plugin-signature-verifier.js +250 -0
- package/examples/api-registry-example.ts +557 -0
- package/package.json +2 -2
- package/src/api-registry-plugin.test.ts +391 -0
- package/src/api-registry-plugin.ts +86 -0
- package/src/api-registry.test.ts +1089 -0
- package/src/api-registry.ts +736 -0
- package/src/index.ts +6 -0
- package/src/logger.ts +36 -11
- package/src/plugin-loader.ts +17 -13
- package/src/qa/adapter.ts +14 -0
- package/src/qa/http-adapter.ts +114 -0
- package/src/qa/index.ts +3 -0
- package/src/qa/runner.ts +179 -0
- package/src/security/index.ts +29 -0
- package/src/security/plugin-config-validator.test.ts +276 -0
- package/src/security/plugin-config-validator.ts +191 -0
- package/src/security/plugin-permission-enforcer.test.ts +251 -0
- package/src/security/plugin-permission-enforcer.ts +408 -0
- package/src/security/plugin-signature-verifier.ts +359 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Permission Enforcer
|
|
3
|
+
*
|
|
4
|
+
* Implements capability-based security model to enforce:
|
|
5
|
+
* 1. Service access control - which services a plugin can use
|
|
6
|
+
* 2. Hook restrictions - which hooks a plugin can trigger
|
|
7
|
+
* 3. File system permissions - what files a plugin can read/write
|
|
8
|
+
* 4. Network permissions - what URLs a plugin can access
|
|
9
|
+
*
|
|
10
|
+
* Architecture:
|
|
11
|
+
* - Uses capability declarations from plugin manifest
|
|
12
|
+
* - Checks permissions before allowing operations
|
|
13
|
+
* - Logs all permission denials for security audit
|
|
14
|
+
* - Supports allowlist and denylist patterns
|
|
15
|
+
*
|
|
16
|
+
* Security Model:
|
|
17
|
+
* - Principle of least privilege - plugins get minimal permissions
|
|
18
|
+
* - Explicit declaration - all capabilities must be declared
|
|
19
|
+
* - Runtime enforcement - checks happen at operation time
|
|
20
|
+
* - Audit trail - all denials are logged
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* ```typescript
|
|
24
|
+
* const enforcer = new PluginPermissionEnforcer(logger);
|
|
25
|
+
* enforcer.registerPluginPermissions(pluginName, capabilities);
|
|
26
|
+
* enforcer.enforceServiceAccess(pluginName, 'database');
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export class PluginPermissionEnforcer {
|
|
30
|
+
constructor(logger) {
|
|
31
|
+
this.permissionRegistry = new Map();
|
|
32
|
+
this.capabilityRegistry = new Map();
|
|
33
|
+
this.logger = logger;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Register plugin capabilities and build permission set
|
|
37
|
+
*
|
|
38
|
+
* @param pluginName - Plugin identifier
|
|
39
|
+
* @param capabilities - Array of capability declarations
|
|
40
|
+
*/
|
|
41
|
+
registerPluginPermissions(pluginName, capabilities) {
|
|
42
|
+
this.capabilityRegistry.set(pluginName, capabilities);
|
|
43
|
+
const permissions = {
|
|
44
|
+
canAccessService: (service) => this.checkServiceAccess(capabilities, service),
|
|
45
|
+
canTriggerHook: (hook) => this.checkHookAccess(capabilities, hook),
|
|
46
|
+
canReadFile: (path) => this.checkFileRead(capabilities, path),
|
|
47
|
+
canWriteFile: (path) => this.checkFileWrite(capabilities, path),
|
|
48
|
+
canNetworkRequest: (url) => this.checkNetworkAccess(capabilities, url),
|
|
49
|
+
};
|
|
50
|
+
this.permissionRegistry.set(pluginName, permissions);
|
|
51
|
+
this.logger.info(`Permissions registered for plugin: ${pluginName}`, {
|
|
52
|
+
plugin: pluginName,
|
|
53
|
+
capabilityCount: capabilities.length,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Enforce service access permission
|
|
58
|
+
*
|
|
59
|
+
* @param pluginName - Plugin requesting access
|
|
60
|
+
* @param serviceName - Service to access
|
|
61
|
+
* @throws Error if permission denied
|
|
62
|
+
*/
|
|
63
|
+
enforceServiceAccess(pluginName, serviceName) {
|
|
64
|
+
const result = this.checkPermission(pluginName, (perms) => perms.canAccessService(serviceName));
|
|
65
|
+
if (!result.allowed) {
|
|
66
|
+
const error = `Permission denied: Plugin ${pluginName} cannot access service ${serviceName}`;
|
|
67
|
+
this.logger.warn(error, {
|
|
68
|
+
plugin: pluginName,
|
|
69
|
+
service: serviceName,
|
|
70
|
+
reason: result.reason,
|
|
71
|
+
});
|
|
72
|
+
throw new Error(error);
|
|
73
|
+
}
|
|
74
|
+
this.logger.debug(`Service access granted: ${pluginName} -> ${serviceName}`);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Enforce hook trigger permission
|
|
78
|
+
*
|
|
79
|
+
* @param pluginName - Plugin requesting access
|
|
80
|
+
* @param hookName - Hook to trigger
|
|
81
|
+
* @throws Error if permission denied
|
|
82
|
+
*/
|
|
83
|
+
enforceHookTrigger(pluginName, hookName) {
|
|
84
|
+
const result = this.checkPermission(pluginName, (perms) => perms.canTriggerHook(hookName));
|
|
85
|
+
if (!result.allowed) {
|
|
86
|
+
const error = `Permission denied: Plugin ${pluginName} cannot trigger hook ${hookName}`;
|
|
87
|
+
this.logger.warn(error, {
|
|
88
|
+
plugin: pluginName,
|
|
89
|
+
hook: hookName,
|
|
90
|
+
reason: result.reason,
|
|
91
|
+
});
|
|
92
|
+
throw new Error(error);
|
|
93
|
+
}
|
|
94
|
+
this.logger.debug(`Hook trigger granted: ${pluginName} -> ${hookName}`);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Enforce file read permission
|
|
98
|
+
*
|
|
99
|
+
* @param pluginName - Plugin requesting access
|
|
100
|
+
* @param path - File path to read
|
|
101
|
+
* @throws Error if permission denied
|
|
102
|
+
*/
|
|
103
|
+
enforceFileRead(pluginName, path) {
|
|
104
|
+
const result = this.checkPermission(pluginName, (perms) => perms.canReadFile(path));
|
|
105
|
+
if (!result.allowed) {
|
|
106
|
+
const error = `Permission denied: Plugin ${pluginName} cannot read file ${path}`;
|
|
107
|
+
this.logger.warn(error, {
|
|
108
|
+
plugin: pluginName,
|
|
109
|
+
path,
|
|
110
|
+
reason: result.reason,
|
|
111
|
+
});
|
|
112
|
+
throw new Error(error);
|
|
113
|
+
}
|
|
114
|
+
this.logger.debug(`File read granted: ${pluginName} -> ${path}`);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Enforce file write permission
|
|
118
|
+
*
|
|
119
|
+
* @param pluginName - Plugin requesting access
|
|
120
|
+
* @param path - File path to write
|
|
121
|
+
* @throws Error if permission denied
|
|
122
|
+
*/
|
|
123
|
+
enforceFileWrite(pluginName, path) {
|
|
124
|
+
const result = this.checkPermission(pluginName, (perms) => perms.canWriteFile(path));
|
|
125
|
+
if (!result.allowed) {
|
|
126
|
+
const error = `Permission denied: Plugin ${pluginName} cannot write file ${path}`;
|
|
127
|
+
this.logger.warn(error, {
|
|
128
|
+
plugin: pluginName,
|
|
129
|
+
path,
|
|
130
|
+
reason: result.reason,
|
|
131
|
+
});
|
|
132
|
+
throw new Error(error);
|
|
133
|
+
}
|
|
134
|
+
this.logger.debug(`File write granted: ${pluginName} -> ${path}`);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Enforce network request permission
|
|
138
|
+
*
|
|
139
|
+
* @param pluginName - Plugin requesting access
|
|
140
|
+
* @param url - URL to access
|
|
141
|
+
* @throws Error if permission denied
|
|
142
|
+
*/
|
|
143
|
+
enforceNetworkRequest(pluginName, url) {
|
|
144
|
+
const result = this.checkPermission(pluginName, (perms) => perms.canNetworkRequest(url));
|
|
145
|
+
if (!result.allowed) {
|
|
146
|
+
const error = `Permission denied: Plugin ${pluginName} cannot access URL ${url}`;
|
|
147
|
+
this.logger.warn(error, {
|
|
148
|
+
plugin: pluginName,
|
|
149
|
+
url,
|
|
150
|
+
reason: result.reason,
|
|
151
|
+
});
|
|
152
|
+
throw new Error(error);
|
|
153
|
+
}
|
|
154
|
+
this.logger.debug(`Network request granted: ${pluginName} -> ${url}`);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Get plugin capabilities
|
|
158
|
+
*
|
|
159
|
+
* @param pluginName - Plugin identifier
|
|
160
|
+
* @returns Array of capabilities or undefined
|
|
161
|
+
*/
|
|
162
|
+
getPluginCapabilities(pluginName) {
|
|
163
|
+
return this.capabilityRegistry.get(pluginName);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Get plugin permissions
|
|
167
|
+
*
|
|
168
|
+
* @param pluginName - Plugin identifier
|
|
169
|
+
* @returns Permissions object or undefined
|
|
170
|
+
*/
|
|
171
|
+
getPluginPermissions(pluginName) {
|
|
172
|
+
return this.permissionRegistry.get(pluginName);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Revoke all permissions for a plugin
|
|
176
|
+
*
|
|
177
|
+
* @param pluginName - Plugin identifier
|
|
178
|
+
*/
|
|
179
|
+
revokePermissions(pluginName) {
|
|
180
|
+
this.permissionRegistry.delete(pluginName);
|
|
181
|
+
this.capabilityRegistry.delete(pluginName);
|
|
182
|
+
this.logger.warn(`Permissions revoked for plugin: ${pluginName}`);
|
|
183
|
+
}
|
|
184
|
+
// Private methods
|
|
185
|
+
checkPermission(pluginName, check) {
|
|
186
|
+
const permissions = this.permissionRegistry.get(pluginName);
|
|
187
|
+
if (!permissions) {
|
|
188
|
+
return {
|
|
189
|
+
allowed: false,
|
|
190
|
+
reason: 'Plugin permissions not registered',
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const allowed = check(permissions);
|
|
194
|
+
return {
|
|
195
|
+
allowed,
|
|
196
|
+
reason: allowed ? undefined : 'No matching capability found',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
checkServiceAccess(capabilities, serviceName) {
|
|
200
|
+
// Check if plugin has capability to access this service
|
|
201
|
+
return capabilities.some(cap => {
|
|
202
|
+
const protocolId = cap.protocol.id;
|
|
203
|
+
// Check for wildcard service access
|
|
204
|
+
if (protocolId.includes('protocol.service.all')) {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
// Check for specific service protocol
|
|
208
|
+
if (protocolId.includes(`protocol.service.${serviceName}`)) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
// Check for service category match
|
|
212
|
+
const serviceCategory = serviceName.split('.')[0];
|
|
213
|
+
if (protocolId.includes(`protocol.service.${serviceCategory}`)) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
checkHookAccess(capabilities, hookName) {
|
|
220
|
+
// Check if plugin has capability to trigger this hook
|
|
221
|
+
return capabilities.some(cap => {
|
|
222
|
+
const protocolId = cap.protocol.id;
|
|
223
|
+
// Check for wildcard hook access
|
|
224
|
+
if (protocolId.includes('protocol.hook.all')) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
// Check for specific hook protocol
|
|
228
|
+
if (protocolId.includes(`protocol.hook.${hookName}`)) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
// Check for hook category match
|
|
232
|
+
const hookCategory = hookName.split(':')[0];
|
|
233
|
+
if (protocolId.includes(`protocol.hook.${hookCategory}`)) {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
checkFileRead(capabilities, _path) {
|
|
240
|
+
// Check if plugin has capability to read this file
|
|
241
|
+
return capabilities.some(cap => {
|
|
242
|
+
const protocolId = cap.protocol.id;
|
|
243
|
+
// Check for file read capability
|
|
244
|
+
if (protocolId.includes('protocol.filesystem.read')) {
|
|
245
|
+
// TODO: Add path pattern matching
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
return false;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
checkFileWrite(capabilities, _path) {
|
|
252
|
+
// Check if plugin has capability to write this file
|
|
253
|
+
return capabilities.some(cap => {
|
|
254
|
+
const protocolId = cap.protocol.id;
|
|
255
|
+
// Check for file write capability
|
|
256
|
+
if (protocolId.includes('protocol.filesystem.write')) {
|
|
257
|
+
// TODO: Add path pattern matching
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
checkNetworkAccess(capabilities, _url) {
|
|
264
|
+
// Check if plugin has capability to access this URL
|
|
265
|
+
return capabilities.some(cap => {
|
|
266
|
+
const protocolId = cap.protocol.id;
|
|
267
|
+
// Check for network capability
|
|
268
|
+
if (protocolId.includes('protocol.network')) {
|
|
269
|
+
// TODO: Add URL pattern matching
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Secure Plugin Context
|
|
278
|
+
* Wraps PluginContext with permission checks
|
|
279
|
+
*/
|
|
280
|
+
export class SecurePluginContext {
|
|
281
|
+
constructor(pluginName, permissionEnforcer, baseContext) {
|
|
282
|
+
this.pluginName = pluginName;
|
|
283
|
+
this.permissionEnforcer = permissionEnforcer;
|
|
284
|
+
this.baseContext = baseContext;
|
|
285
|
+
}
|
|
286
|
+
registerService(name, service) {
|
|
287
|
+
// No permission check for service registration (handled during init)
|
|
288
|
+
this.baseContext.registerService(name, service);
|
|
289
|
+
}
|
|
290
|
+
getService(name) {
|
|
291
|
+
// Check permission before accessing service
|
|
292
|
+
this.permissionEnforcer.enforceServiceAccess(this.pluginName, name);
|
|
293
|
+
return this.baseContext.getService(name);
|
|
294
|
+
}
|
|
295
|
+
getServices() {
|
|
296
|
+
// Return all services (no permission check for listing)
|
|
297
|
+
return this.baseContext.getServices();
|
|
298
|
+
}
|
|
299
|
+
hook(name, handler) {
|
|
300
|
+
// No permission check for registering hooks (handled during init)
|
|
301
|
+
this.baseContext.hook(name, handler);
|
|
302
|
+
}
|
|
303
|
+
async trigger(name, ...args) {
|
|
304
|
+
// Check permission before triggering hook
|
|
305
|
+
this.permissionEnforcer.enforceHookTrigger(this.pluginName, name);
|
|
306
|
+
await this.baseContext.trigger(name, ...args);
|
|
307
|
+
}
|
|
308
|
+
get logger() {
|
|
309
|
+
return this.baseContext.logger;
|
|
310
|
+
}
|
|
311
|
+
getKernel() {
|
|
312
|
+
return this.baseContext.getKernel();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Create a plugin permission enforcer
|
|
317
|
+
*
|
|
318
|
+
* @param logger - Logger instance
|
|
319
|
+
* @returns Plugin permission enforcer
|
|
320
|
+
*/
|
|
321
|
+
export function createPluginPermissionEnforcer(logger) {
|
|
322
|
+
return new PluginPermissionEnforcer(logger);
|
|
323
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-permission-enforcer.test.d.ts","sourceRoot":"","sources":["../../src/security/plugin-permission-enforcer.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { PluginPermissionEnforcer, SecurePluginContext } from './plugin-permission-enforcer.js';
|
|
3
|
+
import { createLogger } from '../logger.js';
|
|
4
|
+
describe('PluginPermissionEnforcer', () => {
|
|
5
|
+
let enforcer;
|
|
6
|
+
let logger;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
logger = createLogger({ level: 'error' });
|
|
9
|
+
enforcer = new PluginPermissionEnforcer(logger);
|
|
10
|
+
});
|
|
11
|
+
describe('registerPluginPermissions', () => {
|
|
12
|
+
it('should register plugin capabilities', () => {
|
|
13
|
+
const capabilities = [
|
|
14
|
+
{
|
|
15
|
+
protocol: {
|
|
16
|
+
id: 'com.objectstack.protocol.service.database.v1',
|
|
17
|
+
label: 'Database Service',
|
|
18
|
+
version: { major: 1, minor: 0, patch: 0 },
|
|
19
|
+
},
|
|
20
|
+
conformance: 'full',
|
|
21
|
+
certified: false,
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
enforcer.registerPluginPermissions('com.test.plugin', capabilities);
|
|
25
|
+
const registeredCapabilities = enforcer.getPluginCapabilities('com.test.plugin');
|
|
26
|
+
expect(registeredCapabilities).toEqual(capabilities);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe('enforceServiceAccess', () => {
|
|
30
|
+
it('should allow access to declared services', () => {
|
|
31
|
+
const capabilities = [
|
|
32
|
+
{
|
|
33
|
+
protocol: {
|
|
34
|
+
id: 'com.objectstack.protocol.service.database.v1',
|
|
35
|
+
label: 'Database Service',
|
|
36
|
+
version: { major: 1, minor: 0, patch: 0 },
|
|
37
|
+
},
|
|
38
|
+
conformance: 'full',
|
|
39
|
+
certified: false,
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
enforcer.registerPluginPermissions('com.test.plugin', capabilities);
|
|
43
|
+
// Should not throw
|
|
44
|
+
expect(() => {
|
|
45
|
+
enforcer.enforceServiceAccess('com.test.plugin', 'database');
|
|
46
|
+
}).not.toThrow();
|
|
47
|
+
});
|
|
48
|
+
it('should deny access to undeclared services', () => {
|
|
49
|
+
const capabilities = [
|
|
50
|
+
{
|
|
51
|
+
protocol: {
|
|
52
|
+
id: 'com.objectstack.protocol.service.database.v1',
|
|
53
|
+
label: 'Database Service',
|
|
54
|
+
version: { major: 1, minor: 0, patch: 0 },
|
|
55
|
+
},
|
|
56
|
+
conformance: 'full',
|
|
57
|
+
certified: false,
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
enforcer.registerPluginPermissions('com.test.plugin', capabilities);
|
|
61
|
+
expect(() => {
|
|
62
|
+
enforcer.enforceServiceAccess('com.test.plugin', 'network');
|
|
63
|
+
}).toThrow(/Permission denied/);
|
|
64
|
+
});
|
|
65
|
+
it('should allow wildcard service access', () => {
|
|
66
|
+
const capabilities = [
|
|
67
|
+
{
|
|
68
|
+
protocol: {
|
|
69
|
+
id: 'com.objectstack.protocol.service.all.v1',
|
|
70
|
+
label: 'All Services',
|
|
71
|
+
version: { major: 1, minor: 0, patch: 0 },
|
|
72
|
+
},
|
|
73
|
+
conformance: 'full',
|
|
74
|
+
certified: false,
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
enforcer.registerPluginPermissions('com.test.plugin', capabilities);
|
|
78
|
+
// Should allow any service
|
|
79
|
+
expect(() => {
|
|
80
|
+
enforcer.enforceServiceAccess('com.test.plugin', 'database');
|
|
81
|
+
enforcer.enforceServiceAccess('com.test.plugin', 'network');
|
|
82
|
+
enforcer.enforceServiceAccess('com.test.plugin', 'filesystem');
|
|
83
|
+
}).not.toThrow();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('enforceHookTrigger', () => {
|
|
87
|
+
it('should allow triggering declared hooks', () => {
|
|
88
|
+
const capabilities = [
|
|
89
|
+
{
|
|
90
|
+
protocol: {
|
|
91
|
+
id: 'com.objectstack.protocol.hook.data.v1',
|
|
92
|
+
label: 'Data Hooks',
|
|
93
|
+
version: { major: 1, minor: 0, patch: 0 },
|
|
94
|
+
},
|
|
95
|
+
conformance: 'full',
|
|
96
|
+
certified: false,
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
enforcer.registerPluginPermissions('com.test.plugin', capabilities);
|
|
100
|
+
expect(() => {
|
|
101
|
+
enforcer.enforceHookTrigger('com.test.plugin', 'data:beforeCreate');
|
|
102
|
+
}).not.toThrow();
|
|
103
|
+
});
|
|
104
|
+
it('should deny triggering undeclared hooks', () => {
|
|
105
|
+
const capabilities = [
|
|
106
|
+
{
|
|
107
|
+
protocol: {
|
|
108
|
+
id: 'com.objectstack.protocol.hook.data.v1',
|
|
109
|
+
label: 'Data Hooks',
|
|
110
|
+
version: { major: 1, minor: 0, patch: 0 },
|
|
111
|
+
},
|
|
112
|
+
conformance: 'full',
|
|
113
|
+
certified: false,
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
enforcer.registerPluginPermissions('com.test.plugin', capabilities);
|
|
117
|
+
expect(() => {
|
|
118
|
+
enforcer.enforceHookTrigger('com.test.plugin', 'kernel:shutdown');
|
|
119
|
+
}).toThrow(/Permission denied/);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe('revokePermissions', () => {
|
|
123
|
+
it('should revoke plugin permissions', () => {
|
|
124
|
+
const capabilities = [
|
|
125
|
+
{
|
|
126
|
+
protocol: {
|
|
127
|
+
id: 'com.objectstack.protocol.service.database.v1',
|
|
128
|
+
label: 'Database Service',
|
|
129
|
+
version: { major: 1, minor: 0, patch: 0 },
|
|
130
|
+
},
|
|
131
|
+
conformance: 'full',
|
|
132
|
+
certified: false,
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
enforcer.registerPluginPermissions('com.test.plugin', capabilities);
|
|
136
|
+
enforcer.revokePermissions('com.test.plugin');
|
|
137
|
+
expect(() => {
|
|
138
|
+
enforcer.enforceServiceAccess('com.test.plugin', 'database');
|
|
139
|
+
}).toThrow(/Permission denied/);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe('SecurePluginContext', () => {
|
|
144
|
+
let enforcer;
|
|
145
|
+
let logger;
|
|
146
|
+
let mockBaseContext;
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
logger = createLogger({ level: 'error' });
|
|
149
|
+
enforcer = new PluginPermissionEnforcer(logger);
|
|
150
|
+
mockBaseContext = {
|
|
151
|
+
registerService: () => { },
|
|
152
|
+
getService: (name) => ({ name }),
|
|
153
|
+
getServices: () => new Map(),
|
|
154
|
+
hook: () => { },
|
|
155
|
+
trigger: async () => { },
|
|
156
|
+
logger,
|
|
157
|
+
getKernel: () => ({}),
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
describe('getService', () => {
|
|
161
|
+
it('should check permission before accessing service', () => {
|
|
162
|
+
const capabilities = [
|
|
163
|
+
{
|
|
164
|
+
protocol: {
|
|
165
|
+
id: 'com.objectstack.protocol.service.database.v1',
|
|
166
|
+
label: 'Database Service',
|
|
167
|
+
version: { major: 1, minor: 0, patch: 0 },
|
|
168
|
+
},
|
|
169
|
+
conformance: 'full',
|
|
170
|
+
certified: false,
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
enforcer.registerPluginPermissions('com.test.plugin', capabilities);
|
|
174
|
+
const secureContext = new SecurePluginContext('com.test.plugin', enforcer, mockBaseContext);
|
|
175
|
+
// Should succeed with permission
|
|
176
|
+
const service = secureContext.getService('database');
|
|
177
|
+
expect(service).toBeDefined();
|
|
178
|
+
// Should fail without permission
|
|
179
|
+
expect(() => {
|
|
180
|
+
secureContext.getService('network');
|
|
181
|
+
}).toThrow(/Permission denied/);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
describe('trigger', () => {
|
|
185
|
+
it('should check permission before triggering hook', async () => {
|
|
186
|
+
const capabilities = [
|
|
187
|
+
{
|
|
188
|
+
protocol: {
|
|
189
|
+
id: 'com.objectstack.protocol.hook.data.v1',
|
|
190
|
+
label: 'Data Hooks',
|
|
191
|
+
version: { major: 1, minor: 0, patch: 0 },
|
|
192
|
+
},
|
|
193
|
+
conformance: 'full',
|
|
194
|
+
certified: false,
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
enforcer.registerPluginPermissions('com.test.plugin', capabilities);
|
|
198
|
+
const secureContext = new SecurePluginContext('com.test.plugin', enforcer, mockBaseContext);
|
|
199
|
+
// Should succeed with permission
|
|
200
|
+
await expect(secureContext.trigger('data:beforeCreate')).resolves.not.toThrow();
|
|
201
|
+
// Should fail without permission
|
|
202
|
+
await expect(secureContext.trigger('kernel:shutdown')).rejects.toThrow(/Permission denied/);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { Logger } from '@objectstack/spec/contracts';
|
|
2
|
+
import type { PluginMetadata } from '../plugin-loader.js';
|
|
3
|
+
/**
|
|
4
|
+
* Plugin Signature Configuration
|
|
5
|
+
* Controls how plugin signatures are verified
|
|
6
|
+
*/
|
|
7
|
+
export interface PluginSignatureConfig {
|
|
8
|
+
/**
|
|
9
|
+
* Map of publisher IDs to their trusted public keys
|
|
10
|
+
* Format: { 'com.objectstack': '-----BEGIN PUBLIC KEY-----...' }
|
|
11
|
+
*/
|
|
12
|
+
trustedPublicKeys: Map<string, string>;
|
|
13
|
+
/**
|
|
14
|
+
* Signature algorithm to use
|
|
15
|
+
* - RS256: RSA with SHA-256
|
|
16
|
+
* - ES256: ECDSA with SHA-256
|
|
17
|
+
*/
|
|
18
|
+
algorithm: 'RS256' | 'ES256';
|
|
19
|
+
/**
|
|
20
|
+
* Strict mode: reject plugins without signatures
|
|
21
|
+
* - true: All plugins must be signed
|
|
22
|
+
* - false: Unsigned plugins are allowed with warning
|
|
23
|
+
*/
|
|
24
|
+
strictMode: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Allow self-signed plugins in development
|
|
27
|
+
*/
|
|
28
|
+
allowSelfSigned?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Plugin Signature Verification Result
|
|
32
|
+
*/
|
|
33
|
+
export interface SignatureVerificationResult {
|
|
34
|
+
verified: boolean;
|
|
35
|
+
error?: string;
|
|
36
|
+
publisherId?: string;
|
|
37
|
+
algorithm?: string;
|
|
38
|
+
signedAt?: Date;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Plugin Signature Verifier
|
|
42
|
+
*
|
|
43
|
+
* Implements cryptographic verification of plugin signatures to ensure:
|
|
44
|
+
* 1. Plugin integrity - code hasn't been tampered with
|
|
45
|
+
* 2. Publisher authenticity - plugin comes from trusted source
|
|
46
|
+
* 3. Non-repudiation - publisher cannot deny signing
|
|
47
|
+
*
|
|
48
|
+
* Architecture:
|
|
49
|
+
* - Uses Node.js crypto module for signature verification
|
|
50
|
+
* - Supports RSA (RS256) and ECDSA (ES256) algorithms
|
|
51
|
+
* - Verifies against trusted public key registry
|
|
52
|
+
* - Computes hash of plugin code for integrity check
|
|
53
|
+
*
|
|
54
|
+
* Security Model:
|
|
55
|
+
* - Public keys are pre-registered and trusted
|
|
56
|
+
* - Plugin signature is verified before loading
|
|
57
|
+
* - Strict mode rejects unsigned plugins
|
|
58
|
+
* - Development mode allows self-signed plugins
|
|
59
|
+
*/
|
|
60
|
+
export declare class PluginSignatureVerifier {
|
|
61
|
+
private config;
|
|
62
|
+
private logger;
|
|
63
|
+
constructor(config: PluginSignatureConfig, logger: Logger);
|
|
64
|
+
/**
|
|
65
|
+
* Verify plugin signature
|
|
66
|
+
*
|
|
67
|
+
* @param plugin - Plugin metadata with signature
|
|
68
|
+
* @returns Verification result
|
|
69
|
+
* @throws Error if verification fails in strict mode
|
|
70
|
+
*/
|
|
71
|
+
verifyPluginSignature(plugin: PluginMetadata): Promise<SignatureVerificationResult>;
|
|
72
|
+
/**
|
|
73
|
+
* Register a trusted public key for a publisher
|
|
74
|
+
*/
|
|
75
|
+
registerPublicKey(publisherId: string, publicKey: string): void;
|
|
76
|
+
/**
|
|
77
|
+
* Remove a trusted public key
|
|
78
|
+
*/
|
|
79
|
+
revokePublicKey(publisherId: string): void;
|
|
80
|
+
/**
|
|
81
|
+
* Get list of trusted publishers
|
|
82
|
+
*/
|
|
83
|
+
getTrustedPublishers(): string[];
|
|
84
|
+
private handleUnsignedPlugin;
|
|
85
|
+
private extractPublisherId;
|
|
86
|
+
private computePluginHash;
|
|
87
|
+
private computePluginHashNode;
|
|
88
|
+
private computePluginHashBrowser;
|
|
89
|
+
private computePluginHashFallback;
|
|
90
|
+
private serializePluginCode;
|
|
91
|
+
private verifyCryptoSignature;
|
|
92
|
+
private verifyCryptoSignatureNode;
|
|
93
|
+
private verifyCryptoSignatureBrowser;
|
|
94
|
+
private validateConfig;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=plugin-signature-verifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-signature-verifier.d.ts","sourceRoot":"","sources":["../../src/security/plugin-signature-verifier.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,6BAA6B,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAc1D;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEvC;;;;OAIG;IACH,SAAS,EAAE,OAAO,GAAG,OAAO,CAAC;IAE7B;;;;OAIG;IACH,UAAU,EAAE,OAAO,CAAC;IAEpB;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,IAAI,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,uBAAuB;IAClC,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,MAAM,CAAS;gBAEX,MAAM,EAAE,qBAAqB,EAAE,MAAM,EAAE,MAAM;IAOzD;;;;;;OAMG;IACG,qBAAqB,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,2BAA2B,CAAC;IAqEzF;;OAEG;IACH,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAK/D;;OAEG;IACH,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAK1C;;OAEG;IACH,oBAAoB,IAAI,MAAM,EAAE;IAMhC,OAAO,CAAC,oBAAoB;IAkB5B,OAAO,CAAC,kBAAkB;IAa1B,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,qBAAqB;IAY7B,OAAO,CAAC,wBAAwB;IAOhC,OAAO,CAAC,yBAAyB;IAcjC,OAAO,CAAC,mBAAmB;YAoBb,qBAAqB;IAcnC,OAAO,CAAC,yBAAyB;YAqCnB,4BAA4B;IAW1C,OAAO,CAAC,cAAc;CAavB"}
|