@objectstack/plugin-security 4.0.1 → 4.0.3
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/.turbo/turbo-build.log +11 -11
- package/CHANGELOG.md +15 -0
- package/dist/index.d.mts +14 -62
- package/dist/index.d.ts +14 -62
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +15 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/security-plugin.test.ts +21 -4
- package/src/security-plugin.ts +19 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/plugin-security@4.0.
|
|
2
|
+
> @objectstack/plugin-security@4.0.3 build /home/runner/work/framework/framework/packages/plugins/plugin-security
|
|
3
3
|
> tsup --config ../../../tsup.config.ts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
6
6
|
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
7
|
[34mCLI[39m tsup v8.5.1
|
|
8
|
-
[34mCLI[39m Using tsup config: /home/runner/work/
|
|
8
|
+
[34mCLI[39m Using tsup config: /home/runner/work/framework/framework/tsup.config.ts
|
|
9
9
|
[34mCLI[39m Target: es2020
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
[34mCJS[39m Build start
|
|
13
|
-
[32mESM[39m [1mdist/index.mjs [22m[
|
|
14
|
-
[32mESM[39m [1mdist/index.mjs.map [22m[
|
|
15
|
-
[32mESM[39m ⚡️ Build success in
|
|
16
|
-
[32mCJS[39m [1mdist/index.js [22m[32m15.
|
|
17
|
-
[32mCJS[39m [1mdist/index.js.map [22m[
|
|
18
|
-
[32mCJS[39m ⚡️ Build success in
|
|
13
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m14.18 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m29.65 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 94ms
|
|
16
|
+
[32mCJS[39m [1mdist/index.js [22m[32m15.57 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m30.28 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 102ms
|
|
19
19
|
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in
|
|
21
|
-
[32mDTS[39m [1mdist/index.d.mts [22m[
|
|
22
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 16680ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m164.76 KB[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m164.76 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @objectstack/plugin-security
|
|
2
2
|
|
|
3
|
+
## 4.0.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @objectstack/spec@4.0.3
|
|
8
|
+
- @objectstack/core@4.0.3
|
|
9
|
+
|
|
10
|
+
## 4.0.2
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies [5f659e9]
|
|
15
|
+
- @objectstack/spec@4.0.2
|
|
16
|
+
- @objectstack/core@4.0.2
|
|
17
|
+
|
|
3
18
|
## 4.0.0
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -441,12 +441,8 @@ declare const SysRole: Omit<{
|
|
|
441
441
|
keyPrefix?: string | undefined;
|
|
442
442
|
actions?: {
|
|
443
443
|
name: string;
|
|
444
|
-
label: string
|
|
445
|
-
|
|
446
|
-
defaultValue?: string | undefined;
|
|
447
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
448
|
-
};
|
|
449
|
-
type: "url" | "script" | "modal" | "flow" | "api";
|
|
444
|
+
label: string;
|
|
445
|
+
type: "url" | "flow" | "api" | "script" | "modal";
|
|
450
446
|
refreshAfter: boolean;
|
|
451
447
|
objectName?: string | undefined;
|
|
452
448
|
icon?: string | undefined;
|
|
@@ -456,44 +452,24 @@ declare const SysRole: Omit<{
|
|
|
456
452
|
execute?: string | undefined;
|
|
457
453
|
params?: {
|
|
458
454
|
name: string;
|
|
459
|
-
label: string
|
|
460
|
-
key: string;
|
|
461
|
-
defaultValue?: string | undefined;
|
|
462
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
463
|
-
};
|
|
455
|
+
label: string;
|
|
464
456
|
type: "number" | "boolean" | "date" | "lookup" | "file" | "url" | "json" | "text" | "textarea" | "email" | "phone" | "password" | "markdown" | "html" | "richtext" | "currency" | "percent" | "datetime" | "time" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "formula" | "summary" | "autonumber" | "location" | "address" | "code" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector";
|
|
465
457
|
required: boolean;
|
|
466
458
|
options?: {
|
|
467
|
-
label: string
|
|
468
|
-
key: string;
|
|
469
|
-
defaultValue?: string | undefined;
|
|
470
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
471
|
-
};
|
|
459
|
+
label: string;
|
|
472
460
|
value: string;
|
|
473
461
|
}[] | undefined;
|
|
474
462
|
}[] | undefined;
|
|
475
463
|
variant?: "link" | "primary" | "secondary" | "danger" | "ghost" | undefined;
|
|
476
|
-
confirmText?: string |
|
|
477
|
-
|
|
478
|
-
defaultValue?: string | undefined;
|
|
479
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
480
|
-
} | undefined;
|
|
481
|
-
successMessage?: string | {
|
|
482
|
-
key: string;
|
|
483
|
-
defaultValue?: string | undefined;
|
|
484
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
485
|
-
} | undefined;
|
|
464
|
+
confirmText?: string | undefined;
|
|
465
|
+
successMessage?: string | undefined;
|
|
486
466
|
visible?: string | undefined;
|
|
487
467
|
disabled?: string | boolean | undefined;
|
|
488
468
|
shortcut?: string | undefined;
|
|
489
469
|
bulkEnabled?: boolean | undefined;
|
|
490
470
|
timeout?: number | undefined;
|
|
491
471
|
aria?: {
|
|
492
|
-
ariaLabel?: string |
|
|
493
|
-
key: string;
|
|
494
|
-
defaultValue?: string | undefined;
|
|
495
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
496
|
-
} | undefined;
|
|
472
|
+
ariaLabel?: string | undefined;
|
|
497
473
|
ariaDescribedBy?: string | undefined;
|
|
498
474
|
role?: string | undefined;
|
|
499
475
|
} | undefined;
|
|
@@ -2224,12 +2200,8 @@ declare const SysPermissionSet: Omit<{
|
|
|
2224
2200
|
keyPrefix?: string | undefined;
|
|
2225
2201
|
actions?: {
|
|
2226
2202
|
name: string;
|
|
2227
|
-
label: string
|
|
2228
|
-
|
|
2229
|
-
defaultValue?: string | undefined;
|
|
2230
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
2231
|
-
};
|
|
2232
|
-
type: "url" | "script" | "modal" | "flow" | "api";
|
|
2203
|
+
label: string;
|
|
2204
|
+
type: "url" | "flow" | "api" | "script" | "modal";
|
|
2233
2205
|
refreshAfter: boolean;
|
|
2234
2206
|
objectName?: string | undefined;
|
|
2235
2207
|
icon?: string | undefined;
|
|
@@ -2239,44 +2211,24 @@ declare const SysPermissionSet: Omit<{
|
|
|
2239
2211
|
execute?: string | undefined;
|
|
2240
2212
|
params?: {
|
|
2241
2213
|
name: string;
|
|
2242
|
-
label: string
|
|
2243
|
-
key: string;
|
|
2244
|
-
defaultValue?: string | undefined;
|
|
2245
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
2246
|
-
};
|
|
2214
|
+
label: string;
|
|
2247
2215
|
type: "number" | "boolean" | "date" | "lookup" | "file" | "url" | "json" | "text" | "textarea" | "email" | "phone" | "password" | "markdown" | "html" | "richtext" | "currency" | "percent" | "datetime" | "time" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "formula" | "summary" | "autonumber" | "location" | "address" | "code" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector";
|
|
2248
2216
|
required: boolean;
|
|
2249
2217
|
options?: {
|
|
2250
|
-
label: string
|
|
2251
|
-
key: string;
|
|
2252
|
-
defaultValue?: string | undefined;
|
|
2253
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
2254
|
-
};
|
|
2218
|
+
label: string;
|
|
2255
2219
|
value: string;
|
|
2256
2220
|
}[] | undefined;
|
|
2257
2221
|
}[] | undefined;
|
|
2258
2222
|
variant?: "link" | "primary" | "secondary" | "danger" | "ghost" | undefined;
|
|
2259
|
-
confirmText?: string |
|
|
2260
|
-
|
|
2261
|
-
defaultValue?: string | undefined;
|
|
2262
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
2263
|
-
} | undefined;
|
|
2264
|
-
successMessage?: string | {
|
|
2265
|
-
key: string;
|
|
2266
|
-
defaultValue?: string | undefined;
|
|
2267
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
2268
|
-
} | undefined;
|
|
2223
|
+
confirmText?: string | undefined;
|
|
2224
|
+
successMessage?: string | undefined;
|
|
2269
2225
|
visible?: string | undefined;
|
|
2270
2226
|
disabled?: string | boolean | undefined;
|
|
2271
2227
|
shortcut?: string | undefined;
|
|
2272
2228
|
bulkEnabled?: boolean | undefined;
|
|
2273
2229
|
timeout?: number | undefined;
|
|
2274
2230
|
aria?: {
|
|
2275
|
-
ariaLabel?: string |
|
|
2276
|
-
key: string;
|
|
2277
|
-
defaultValue?: string | undefined;
|
|
2278
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
2279
|
-
} | undefined;
|
|
2231
|
+
ariaLabel?: string | undefined;
|
|
2280
2232
|
ariaDescribedBy?: string | undefined;
|
|
2281
2233
|
role?: string | undefined;
|
|
2282
2234
|
} | undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -441,12 +441,8 @@ declare const SysRole: Omit<{
|
|
|
441
441
|
keyPrefix?: string | undefined;
|
|
442
442
|
actions?: {
|
|
443
443
|
name: string;
|
|
444
|
-
label: string
|
|
445
|
-
|
|
446
|
-
defaultValue?: string | undefined;
|
|
447
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
448
|
-
};
|
|
449
|
-
type: "url" | "script" | "modal" | "flow" | "api";
|
|
444
|
+
label: string;
|
|
445
|
+
type: "url" | "flow" | "api" | "script" | "modal";
|
|
450
446
|
refreshAfter: boolean;
|
|
451
447
|
objectName?: string | undefined;
|
|
452
448
|
icon?: string | undefined;
|
|
@@ -456,44 +452,24 @@ declare const SysRole: Omit<{
|
|
|
456
452
|
execute?: string | undefined;
|
|
457
453
|
params?: {
|
|
458
454
|
name: string;
|
|
459
|
-
label: string
|
|
460
|
-
key: string;
|
|
461
|
-
defaultValue?: string | undefined;
|
|
462
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
463
|
-
};
|
|
455
|
+
label: string;
|
|
464
456
|
type: "number" | "boolean" | "date" | "lookup" | "file" | "url" | "json" | "text" | "textarea" | "email" | "phone" | "password" | "markdown" | "html" | "richtext" | "currency" | "percent" | "datetime" | "time" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "formula" | "summary" | "autonumber" | "location" | "address" | "code" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector";
|
|
465
457
|
required: boolean;
|
|
466
458
|
options?: {
|
|
467
|
-
label: string
|
|
468
|
-
key: string;
|
|
469
|
-
defaultValue?: string | undefined;
|
|
470
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
471
|
-
};
|
|
459
|
+
label: string;
|
|
472
460
|
value: string;
|
|
473
461
|
}[] | undefined;
|
|
474
462
|
}[] | undefined;
|
|
475
463
|
variant?: "link" | "primary" | "secondary" | "danger" | "ghost" | undefined;
|
|
476
|
-
confirmText?: string |
|
|
477
|
-
|
|
478
|
-
defaultValue?: string | undefined;
|
|
479
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
480
|
-
} | undefined;
|
|
481
|
-
successMessage?: string | {
|
|
482
|
-
key: string;
|
|
483
|
-
defaultValue?: string | undefined;
|
|
484
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
485
|
-
} | undefined;
|
|
464
|
+
confirmText?: string | undefined;
|
|
465
|
+
successMessage?: string | undefined;
|
|
486
466
|
visible?: string | undefined;
|
|
487
467
|
disabled?: string | boolean | undefined;
|
|
488
468
|
shortcut?: string | undefined;
|
|
489
469
|
bulkEnabled?: boolean | undefined;
|
|
490
470
|
timeout?: number | undefined;
|
|
491
471
|
aria?: {
|
|
492
|
-
ariaLabel?: string |
|
|
493
|
-
key: string;
|
|
494
|
-
defaultValue?: string | undefined;
|
|
495
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
496
|
-
} | undefined;
|
|
472
|
+
ariaLabel?: string | undefined;
|
|
497
473
|
ariaDescribedBy?: string | undefined;
|
|
498
474
|
role?: string | undefined;
|
|
499
475
|
} | undefined;
|
|
@@ -2224,12 +2200,8 @@ declare const SysPermissionSet: Omit<{
|
|
|
2224
2200
|
keyPrefix?: string | undefined;
|
|
2225
2201
|
actions?: {
|
|
2226
2202
|
name: string;
|
|
2227
|
-
label: string
|
|
2228
|
-
|
|
2229
|
-
defaultValue?: string | undefined;
|
|
2230
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
2231
|
-
};
|
|
2232
|
-
type: "url" | "script" | "modal" | "flow" | "api";
|
|
2203
|
+
label: string;
|
|
2204
|
+
type: "url" | "flow" | "api" | "script" | "modal";
|
|
2233
2205
|
refreshAfter: boolean;
|
|
2234
2206
|
objectName?: string | undefined;
|
|
2235
2207
|
icon?: string | undefined;
|
|
@@ -2239,44 +2211,24 @@ declare const SysPermissionSet: Omit<{
|
|
|
2239
2211
|
execute?: string | undefined;
|
|
2240
2212
|
params?: {
|
|
2241
2213
|
name: string;
|
|
2242
|
-
label: string
|
|
2243
|
-
key: string;
|
|
2244
|
-
defaultValue?: string | undefined;
|
|
2245
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
2246
|
-
};
|
|
2214
|
+
label: string;
|
|
2247
2215
|
type: "number" | "boolean" | "date" | "lookup" | "file" | "url" | "json" | "text" | "textarea" | "email" | "phone" | "password" | "markdown" | "html" | "richtext" | "currency" | "percent" | "datetime" | "time" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "formula" | "summary" | "autonumber" | "location" | "address" | "code" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector";
|
|
2248
2216
|
required: boolean;
|
|
2249
2217
|
options?: {
|
|
2250
|
-
label: string
|
|
2251
|
-
key: string;
|
|
2252
|
-
defaultValue?: string | undefined;
|
|
2253
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
2254
|
-
};
|
|
2218
|
+
label: string;
|
|
2255
2219
|
value: string;
|
|
2256
2220
|
}[] | undefined;
|
|
2257
2221
|
}[] | undefined;
|
|
2258
2222
|
variant?: "link" | "primary" | "secondary" | "danger" | "ghost" | undefined;
|
|
2259
|
-
confirmText?: string |
|
|
2260
|
-
|
|
2261
|
-
defaultValue?: string | undefined;
|
|
2262
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
2263
|
-
} | undefined;
|
|
2264
|
-
successMessage?: string | {
|
|
2265
|
-
key: string;
|
|
2266
|
-
defaultValue?: string | undefined;
|
|
2267
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
2268
|
-
} | undefined;
|
|
2223
|
+
confirmText?: string | undefined;
|
|
2224
|
+
successMessage?: string | undefined;
|
|
2269
2225
|
visible?: string | undefined;
|
|
2270
2226
|
disabled?: string | boolean | undefined;
|
|
2271
2227
|
shortcut?: string | undefined;
|
|
2272
2228
|
bulkEnabled?: boolean | undefined;
|
|
2273
2229
|
timeout?: number | undefined;
|
|
2274
2230
|
aria?: {
|
|
2275
|
-
ariaLabel?: string |
|
|
2276
|
-
key: string;
|
|
2277
|
-
defaultValue?: string | undefined;
|
|
2278
|
-
params?: Record<string, string | number | boolean> | undefined;
|
|
2279
|
-
} | undefined;
|
|
2231
|
+
ariaLabel?: string | undefined;
|
|
2280
2232
|
ariaDescribedBy?: string | undefined;
|
|
2281
2233
|
role?: string | undefined;
|
|
2282
2234
|
} | undefined;
|
package/dist/index.js
CHANGED
|
@@ -392,7 +392,7 @@ var SecurityPlugin = class {
|
|
|
392
392
|
ctx.registerService("security.permissions", this.permissionEvaluator);
|
|
393
393
|
ctx.registerService("security.rls", this.rlsCompiler);
|
|
394
394
|
ctx.registerService("security.fieldMasker", this.fieldMasker);
|
|
395
|
-
ctx.
|
|
395
|
+
ctx.getService("manifest").register({
|
|
396
396
|
id: "com.objectstack.security",
|
|
397
397
|
name: "Security",
|
|
398
398
|
version: "1.0.0",
|
|
@@ -400,6 +400,20 @@ var SecurityPlugin = class {
|
|
|
400
400
|
namespace: "sys",
|
|
401
401
|
objects: [SysRole, SysPermissionSet]
|
|
402
402
|
});
|
|
403
|
+
try {
|
|
404
|
+
const setupNav = ctx.getService("setupNav");
|
|
405
|
+
if (setupNav) {
|
|
406
|
+
setupNav.contribute({
|
|
407
|
+
areaId: "area_administration",
|
|
408
|
+
items: [
|
|
409
|
+
{ id: "nav_roles", type: "object", label: "Roles", objectName: "role", icon: "shield-check", order: 60 },
|
|
410
|
+
{ id: "nav_permission_sets", type: "object", label: "Permission Sets", objectName: "permission_set", icon: "lock", order: 70 }
|
|
411
|
+
]
|
|
412
|
+
});
|
|
413
|
+
ctx.logger.info("Security navigation items contributed to Setup App");
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
}
|
|
403
417
|
ctx.logger.info("Security Plugin initialized");
|
|
404
418
|
}
|
|
405
419
|
async start(ctx) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/permission-evaluator.ts","../src/rls-compiler.ts","../src/field-masker.ts","../src/objects/sys-role.object.ts","../src/objects/sys-permission-set.object.ts","../src/security-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/plugin-security\n * \n * Security Plugin for ObjectStack\n * Provides RBAC, Row-Level Security (RLS), and Field-Level Security runtime.\n */\n\nexport { SecurityPlugin } from './security-plugin.js';\nexport { PermissionEvaluator } from './permission-evaluator.js';\nexport { RLSCompiler } from './rls-compiler.js';\nexport { FieldMasker } from './field-masker.js';\n\n// System Object Definitions (sys namespace)\nexport { SysRole, SysPermissionSet } from './objects/index.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { PermissionSet, ObjectPermission, FieldPermission } from '@objectstack/spec/security';\n\n/**\n * Operation type mapping to permission checks\n */\nconst OPERATION_TO_PERMISSION: Record<string, keyof ObjectPermission> = {\n find: 'allowRead',\n findOne: 'allowRead',\n count: 'allowRead',\n aggregate: 'allowRead',\n insert: 'allowCreate',\n update: 'allowEdit',\n delete: 'allowDelete',\n};\n\n/**\n * PermissionEvaluator\n * \n * Runtime evaluator for PermissionSet definitions.\n * Resolves aggregated permissions from roles to concrete allow/deny decisions.\n */\nexport class PermissionEvaluator {\n /**\n * Check if an operation is allowed on an object for the given permission sets.\n * Uses \"most permissive\" merging: if ANY permission set allows, it's allowed.\n */\n checkObjectPermission(\n operation: string,\n objectName: string,\n permissionSets: PermissionSet[]\n ): boolean {\n const permKey = OPERATION_TO_PERMISSION[operation];\n if (!permKey) return true; // Unknown operations are allowed by default\n\n for (const ps of permissionSets) {\n const objPerm = ps.objects?.[objectName];\n if (objPerm) {\n // Check if modifyAllRecords is set (super-user bypass for write ops)\n if (['allowEdit', 'allowDelete'].includes(permKey) && objPerm.modifyAllRecords) {\n return true;\n }\n // Check if viewAllRecords is set (super-user bypass for read ops)\n if (permKey === 'allowRead' && (objPerm.viewAllRecords || objPerm.modifyAllRecords)) {\n return true;\n }\n // Check the specific permission\n if (objPerm[permKey]) {\n return true;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Get the merged field permissions for an object.\n * Returns a map of field names to their effective permissions.\n * Uses \"most permissive\" merging.\n */\n getFieldPermissions(\n objectName: string,\n permissionSets: PermissionSet[]\n ): Record<string, FieldPermission> {\n const result: Record<string, FieldPermission> = {};\n\n for (const ps of permissionSets) {\n if (!ps.fields) continue;\n\n for (const [key, perm] of Object.entries(ps.fields)) {\n // Field keys are in format: \"object_name.field_name\"\n if (!key.startsWith(`${objectName}.`)) continue;\n const fieldName = key.substring(objectName.length + 1);\n\n if (!result[fieldName]) {\n result[fieldName] = { readable: false, editable: false };\n }\n\n // Most permissive merge\n if (perm.readable) result[fieldName].readable = true;\n if (perm.editable) result[fieldName].editable = true;\n }\n }\n\n return result;\n }\n\n /**\n * Resolve permission sets for a list of role names from metadata.\n */\n resolvePermissionSets(\n roles: string[],\n metadataService: any\n ): PermissionSet[] {\n const result: PermissionSet[] = [];\n\n // Get all permission sets from metadata\n const allPermSets = metadataService.list?.('permissions') || [];\n\n for (const ps of allPermSets) {\n // A permission set is relevant if it's a profile assigned to any of the user's roles,\n // or if the role name matches the permission set name\n if (roles.includes(ps.name)) {\n result.push(ps);\n }\n }\n\n return result;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { RowLevelSecurityPolicy } from '@objectstack/spec/security';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\n\n/**\n * RLS User Context\n * Variables available for RLS expression evaluation.\n */\ninterface RLSUserContext {\n id?: string;\n tenant_id?: string;\n roles?: string[];\n [key: string]: unknown;\n}\n\n/**\n * RLSCompiler\n * \n * Compiles Row-Level Security policy expressions into query filters.\n * Converts `using` / `check` expressions into ObjectQL-compatible filter conditions.\n */\nexport class RLSCompiler {\n /**\n * Compile RLS policies into a query filter for the given user context.\n * Multiple policies for the same object/operation are OR-combined (any match allows access).\n */\n compileFilter(\n policies: RowLevelSecurityPolicy[],\n executionContext?: ExecutionContext\n ): Record<string, unknown> | null {\n if (policies.length === 0) return null;\n\n const userCtx: RLSUserContext = {\n id: executionContext?.userId,\n tenant_id: executionContext?.tenantId,\n roles: executionContext?.roles,\n };\n\n const filters: Record<string, unknown>[] = [];\n\n for (const policy of policies) {\n if (!policy.using) continue;\n const filter = this.compileExpression(policy.using, userCtx);\n if (filter) {\n filters.push(filter);\n }\n }\n\n if (filters.length === 0) return null;\n if (filters.length === 1) return filters[0];\n\n // Multiple policies: OR-combine (any policy allows access)\n return { $or: filters };\n }\n\n /**\n * Compile a single RLS expression into a query filter.\n * \n * Supports simple expressions like:\n * - \"field_name = current_user.property\"\n * - \"field_name IN (current_user.array_property)\"\n * - \"field_name = 'literal_value'\"\n */\n compileExpression(\n expression: string,\n userCtx: RLSUserContext\n ): Record<string, unknown> | null {\n if (!expression) return null;\n\n // Handle simple equality: \"field = current_user.property\"\n const eqMatch = expression.match(/^\\s*(\\w+)\\s*=\\s*current_user\\.(\\w+)\\s*$/);\n if (eqMatch) {\n const [, field, prop] = eqMatch;\n const value = userCtx[prop];\n if (value === undefined) return null;\n return { [field]: value };\n }\n\n // Handle literal equality: \"field = 'value'\"\n const litMatch = expression.match(/^\\s*(\\w+)\\s*=\\s*'([^']*)'\\s*$/);\n if (litMatch) {\n const [, field, value] = litMatch;\n return { [field]: value };\n }\n\n // Handle IN: \"field IN (current_user.array_property)\"\n const inMatch = expression.match(/^\\s*(\\w+)\\s+IN\\s+\\(\\s*current_user\\.(\\w+)\\s*\\)\\s*$/i);\n if (inMatch) {\n const [, field, prop] = inMatch;\n const value = userCtx[prop];\n if (!Array.isArray(value)) return null;\n return { [field]: { $in: value } };\n }\n\n // Unsupported expression: return null (no additional RLS filter applied).\n // Note: callers should treat absence of RLS policies as \"allow all\" only when\n // no policies are defined. If policies exist but cannot be compiled, the caller\n // may want to deny access as a safety measure.\n return null;\n }\n\n /**\n * Get applicable RLS policies for a given object and operation.\n */\n getApplicablePolicies(\n objectName: string,\n operation: string,\n allPolicies: RowLevelSecurityPolicy[]\n ): RowLevelSecurityPolicy[] {\n // Map engine operation to RLS operation type\n const rlsOp = this.mapOperationToRLS(operation);\n\n return allPolicies.filter(policy => {\n // Check object match\n if (policy.object !== objectName && policy.object !== '*') return false;\n\n // Check operation match\n if (policy.operation === 'all') return true;\n if (policy.operation === rlsOp) return true;\n\n return false;\n });\n }\n\n private mapOperationToRLS(operation: string): string {\n switch (operation) {\n case 'find':\n case 'findOne':\n case 'count':\n case 'aggregate':\n return 'select';\n case 'insert':\n return 'insert';\n case 'update':\n return 'update';\n case 'delete':\n return 'delete';\n default:\n return 'select';\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { FieldPermission } from '@objectstack/spec/security';\n\n/**\n * FieldMasker\n * \n * Applies field-level security by stripping restricted fields from query results.\n */\nexport class FieldMasker {\n /**\n * Mask fields in query results based on field permissions.\n * Removes fields that the user does not have read access to.\n */\n maskResults(\n results: any | any[],\n fieldPermissions: Record<string, FieldPermission>,\n _objectName: string\n ): any | any[] {\n // If no field permissions defined, return results as-is\n if (Object.keys(fieldPermissions).length === 0) return results;\n\n // Get list of non-readable fields\n const hiddenFields = Object.entries(fieldPermissions)\n .filter(([, perm]) => !perm.readable)\n .map(([field]) => field);\n\n if (hiddenFields.length === 0) return results;\n\n if (Array.isArray(results)) {\n return results.map(record => this.maskRecord(record, hiddenFields));\n }\n\n return this.maskRecord(results, hiddenFields);\n }\n\n /**\n * Get non-editable fields for use in write operations.\n * Returns a list of field names that should be stripped from incoming data.\n */\n getNonEditableFields(\n fieldPermissions: Record<string, FieldPermission>\n ): string[] {\n return Object.entries(fieldPermissions)\n .filter(([, perm]) => !perm.editable)\n .map(([field]) => field);\n }\n\n /**\n * Strip non-editable fields from write data.\n */\n stripNonEditableFields(\n data: Record<string, any>,\n fieldPermissions: Record<string, FieldPermission>\n ): Record<string, any> {\n const nonEditable = this.getNonEditableFields(fieldPermissions);\n if (nonEditable.length === 0) return data;\n\n const result = { ...data };\n for (const field of nonEditable) {\n delete result[field];\n }\n return result;\n }\n\n private maskRecord(record: any, hiddenFields: string[]): any {\n if (!record || typeof record !== 'object') return record;\n\n const result = { ...record };\n for (const field of hiddenFields) {\n delete result[field];\n }\n return result;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { ObjectSchema, Field } from '@objectstack/spec/data';\n\n/**\n * sys_role — System Role Object\n *\n * RBAC role definition for the ObjectStack platform.\n * Roles group permissions and are assigned to users or members.\n *\n * @namespace sys\n */\nexport const SysRole = ObjectSchema.create({\n namespace: 'sys',\n name: 'role',\n label: 'Role',\n pluralLabel: 'Roles',\n icon: 'shield',\n isSystem: true,\n description: 'Role definitions for RBAC access control',\n titleFormat: '{name}',\n compactLayout: ['name', 'label', 'active'],\n \n fields: {\n id: Field.text({\n label: 'Role ID',\n required: true,\n readonly: true,\n }),\n \n created_at: Field.datetime({\n label: 'Created At',\n defaultValue: 'NOW()',\n readonly: true,\n }),\n \n updated_at: Field.datetime({\n label: 'Updated At',\n defaultValue: 'NOW()',\n readonly: true,\n }),\n \n name: Field.text({\n label: 'API Name',\n required: true,\n searchable: true,\n maxLength: 100,\n description: 'Unique machine name for the role (e.g. admin, editor, viewer)',\n }),\n \n label: Field.text({\n label: 'Display Name',\n required: true,\n maxLength: 255,\n }),\n \n description: Field.textarea({\n label: 'Description',\n required: false,\n }),\n \n permissions: Field.textarea({\n label: 'Permissions',\n required: false,\n description: 'JSON-serialized array of permission strings',\n }),\n \n active: Field.boolean({\n label: 'Active',\n defaultValue: true,\n }),\n \n is_default: Field.boolean({\n label: 'Default Role',\n defaultValue: false,\n description: 'Automatically assigned to new users',\n }),\n },\n \n indexes: [\n { fields: ['name'], unique: true },\n { fields: ['active'] },\n ],\n \n enable: {\n trackHistory: true,\n searchable: true,\n apiEnabled: true,\n apiMethods: ['get', 'list', 'create', 'update', 'delete'],\n trash: true,\n mru: true,\n },\n});\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { ObjectSchema, Field } from '@objectstack/spec/data';\n\n/**\n * sys_permission_set — System Permission Set Object\n *\n * Named groupings of fine-grained permissions.\n * Permission sets can be assigned to roles or directly to users\n * for granular access control.\n *\n * @namespace sys\n */\nexport const SysPermissionSet = ObjectSchema.create({\n namespace: 'sys',\n name: 'permission_set',\n label: 'Permission Set',\n pluralLabel: 'Permission Sets',\n icon: 'lock',\n isSystem: true,\n description: 'Named permission groupings for fine-grained access control',\n titleFormat: '{name}',\n compactLayout: ['name', 'label', 'active'],\n \n fields: {\n id: Field.text({\n label: 'Permission Set ID',\n required: true,\n readonly: true,\n }),\n \n created_at: Field.datetime({\n label: 'Created At',\n defaultValue: 'NOW()',\n readonly: true,\n }),\n \n updated_at: Field.datetime({\n label: 'Updated At',\n defaultValue: 'NOW()',\n readonly: true,\n }),\n \n name: Field.text({\n label: 'API Name',\n required: true,\n searchable: true,\n maxLength: 100,\n description: 'Unique machine name for the permission set',\n }),\n \n label: Field.text({\n label: 'Display Name',\n required: true,\n maxLength: 255,\n }),\n \n description: Field.textarea({\n label: 'Description',\n required: false,\n }),\n \n object_permissions: Field.textarea({\n label: 'Object Permissions',\n required: false,\n description: 'JSON-serialized object-level CRUD permissions',\n }),\n \n field_permissions: Field.textarea({\n label: 'Field Permissions',\n required: false,\n description: 'JSON-serialized field-level read/write permissions',\n }),\n \n active: Field.boolean({\n label: 'Active',\n defaultValue: true,\n }),\n },\n \n indexes: [\n { fields: ['name'], unique: true },\n { fields: ['active'] },\n ],\n \n enable: {\n trackHistory: true,\n searchable: true,\n apiEnabled: true,\n apiMethods: ['get', 'list', 'create', 'update', 'delete'],\n trash: true,\n mru: true,\n },\n});\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { Plugin, PluginContext } from '@objectstack/core';\nimport type { PermissionSet, RowLevelSecurityPolicy } from '@objectstack/spec/security';\nimport { PermissionEvaluator } from './permission-evaluator.js';\nimport { RLSCompiler } from './rls-compiler.js';\nimport { FieldMasker } from './field-masker.js';\nimport { SysRole, SysPermissionSet } from './objects/index.js';\n\n/**\n * SecurityPlugin\n * \n * Provides RBAC, Row-Level Security, and Field-Level Security runtime.\n * Registers as an engine middleware on the ObjectQL engine.\n * \n * This plugin is fully optional — without it, the system operates\n * without permission checks (same as current behavior).\n * \n * Dependencies:\n * - objectql service (ObjectQL engine with middleware support)\n * - metadata service (MetadataFacade for reading permission sets and RLS policies)\n */\nexport class SecurityPlugin implements Plugin {\n name = 'com.objectstack.security';\n type = 'standard';\n version = '1.0.0';\n dependencies = ['com.objectstack.engine.objectql'];\n\n private permissionEvaluator = new PermissionEvaluator();\n private rlsCompiler = new RLSCompiler();\n private fieldMasker = new FieldMasker();\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Initializing Security Plugin...');\n\n // Register security services\n ctx.registerService('security.permissions', this.permissionEvaluator);\n ctx.registerService('security.rls', this.rlsCompiler);\n ctx.registerService('security.fieldMasker', this.fieldMasker);\n\n // Register security system objects so ObjectQLPlugin auto-discovers them\n ctx.registerService('app.com.objectstack.security', {\n id: 'com.objectstack.security',\n name: 'Security',\n version: '1.0.0',\n type: 'plugin',\n namespace: 'sys',\n objects: [SysRole, SysPermissionSet],\n });\n\n ctx.logger.info('Security Plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Starting Security Plugin...');\n\n // Get required services\n let ql: any;\n let metadata: any;\n\n try {\n ql = ctx.getService('objectql');\n metadata = ctx.getService('metadata');\n } catch (e) {\n ctx.logger.warn('ObjectQL or metadata service not available, security middleware not registered');\n return;\n }\n\n if (!ql || typeof ql.registerMiddleware !== 'function') {\n ctx.logger.warn('ObjectQL engine does not support middleware, security middleware not registered');\n return;\n }\n\n // Register security middleware\n ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {\n // System operations bypass security\n if (opCtx.context?.isSystem) {\n return next();\n }\n\n const roles = opCtx.context?.roles ?? [];\n\n // Skip security checks if no roles (anonymous/unauthenticated)\n // The auth middleware should handle authentication separately\n if (roles.length === 0 && !opCtx.context?.userId) {\n return next();\n }\n\n // 1. Resolve permission sets for the user's roles\n let permissionSets: PermissionSet[] = [];\n try {\n permissionSets = this.permissionEvaluator.resolvePermissionSets(roles, metadata);\n } catch (e) {\n // If metadata service is misconfigured, log and continue without permission checks\n // rather than blocking all operations\n return next();\n }\n\n // 2. CRUD permission check\n if (permissionSets.length > 0) {\n const allowed = this.permissionEvaluator.checkObjectPermission(\n opCtx.operation,\n opCtx.object,\n permissionSets\n );\n\n if (!allowed) {\n throw new Error(\n `[Security] Access denied: operation '${opCtx.operation}' on object '${opCtx.object}' ` +\n `is not permitted for roles [${roles.join(', ')}]`\n );\n }\n }\n\n // 3. RLS filter injection\n const allRlsPolicies = this.collectRLSPolicies(permissionSets, opCtx.object, opCtx.operation);\n if (allRlsPolicies.length > 0 && opCtx.ast) {\n const rlsFilter = this.rlsCompiler.compileFilter(allRlsPolicies, opCtx.context);\n if (rlsFilter) {\n if (opCtx.ast.where) {\n opCtx.ast.where = { $and: [opCtx.ast.where, rlsFilter] };\n } else {\n opCtx.ast.where = rlsFilter;\n }\n }\n }\n\n await next();\n\n // 4. Field-level security: mask restricted fields in read results\n if (opCtx.result && ['find', 'findOne'].includes(opCtx.operation)) {\n const fieldPerms = this.permissionEvaluator.getFieldPermissions(opCtx.object, permissionSets);\n if (Object.keys(fieldPerms).length > 0) {\n opCtx.result = this.fieldMasker.maskResults(opCtx.result, fieldPerms, opCtx.object);\n }\n }\n });\n\n ctx.logger.info('Security middleware registered on ObjectQL engine');\n }\n\n async destroy(): Promise<void> {\n // No cleanup needed\n }\n\n /**\n * Collect all RLS policies from permission sets applicable to the given object/operation.\n */\n private collectRLSPolicies(\n permissionSets: PermissionSet[],\n objectName: string,\n operation: string\n ): RowLevelSecurityPolicy[] {\n const allPolicies: RowLevelSecurityPolicy[] = [];\n\n for (const ps of permissionSets) {\n if (ps.rowLevelSecurity) {\n allPolicies.push(...ps.rowLevelSecurity);\n }\n }\n\n return this.rlsCompiler.getApplicablePolicies(objectName, operation, allPolicies);\n }\n}\n"],"mappings":"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","names":["import_data"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/permission-evaluator.ts","../src/rls-compiler.ts","../src/field-masker.ts","../src/objects/sys-role.object.ts","../src/objects/sys-permission-set.object.ts","../src/security-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/plugin-security\n * \n * Security Plugin for ObjectStack\n * Provides RBAC, Row-Level Security (RLS), and Field-Level Security runtime.\n */\n\nexport { SecurityPlugin } from './security-plugin.js';\nexport { PermissionEvaluator } from './permission-evaluator.js';\nexport { RLSCompiler } from './rls-compiler.js';\nexport { FieldMasker } from './field-masker.js';\n\n// System Object Definitions (sys namespace)\nexport { SysRole, SysPermissionSet } from './objects/index.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { PermissionSet, ObjectPermission, FieldPermission } from '@objectstack/spec/security';\n\n/**\n * Operation type mapping to permission checks\n */\nconst OPERATION_TO_PERMISSION: Record<string, keyof ObjectPermission> = {\n find: 'allowRead',\n findOne: 'allowRead',\n count: 'allowRead',\n aggregate: 'allowRead',\n insert: 'allowCreate',\n update: 'allowEdit',\n delete: 'allowDelete',\n};\n\n/**\n * PermissionEvaluator\n * \n * Runtime evaluator for PermissionSet definitions.\n * Resolves aggregated permissions from roles to concrete allow/deny decisions.\n */\nexport class PermissionEvaluator {\n /**\n * Check if an operation is allowed on an object for the given permission sets.\n * Uses \"most permissive\" merging: if ANY permission set allows, it's allowed.\n */\n checkObjectPermission(\n operation: string,\n objectName: string,\n permissionSets: PermissionSet[]\n ): boolean {\n const permKey = OPERATION_TO_PERMISSION[operation];\n if (!permKey) return true; // Unknown operations are allowed by default\n\n for (const ps of permissionSets) {\n const objPerm = ps.objects?.[objectName];\n if (objPerm) {\n // Check if modifyAllRecords is set (super-user bypass for write ops)\n if (['allowEdit', 'allowDelete'].includes(permKey) && objPerm.modifyAllRecords) {\n return true;\n }\n // Check if viewAllRecords is set (super-user bypass for read ops)\n if (permKey === 'allowRead' && (objPerm.viewAllRecords || objPerm.modifyAllRecords)) {\n return true;\n }\n // Check the specific permission\n if (objPerm[permKey]) {\n return true;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Get the merged field permissions for an object.\n * Returns a map of field names to their effective permissions.\n * Uses \"most permissive\" merging.\n */\n getFieldPermissions(\n objectName: string,\n permissionSets: PermissionSet[]\n ): Record<string, FieldPermission> {\n const result: Record<string, FieldPermission> = {};\n\n for (const ps of permissionSets) {\n if (!ps.fields) continue;\n\n for (const [key, perm] of Object.entries(ps.fields)) {\n // Field keys are in format: \"object_name.field_name\"\n if (!key.startsWith(`${objectName}.`)) continue;\n const fieldName = key.substring(objectName.length + 1);\n\n if (!result[fieldName]) {\n result[fieldName] = { readable: false, editable: false };\n }\n\n // Most permissive merge\n if (perm.readable) result[fieldName].readable = true;\n if (perm.editable) result[fieldName].editable = true;\n }\n }\n\n return result;\n }\n\n /**\n * Resolve permission sets for a list of role names from metadata.\n */\n resolvePermissionSets(\n roles: string[],\n metadataService: any\n ): PermissionSet[] {\n const result: PermissionSet[] = [];\n\n // Get all permission sets from metadata\n const allPermSets = metadataService.list?.('permissions') || [];\n\n for (const ps of allPermSets) {\n // A permission set is relevant if it's a profile assigned to any of the user's roles,\n // or if the role name matches the permission set name\n if (roles.includes(ps.name)) {\n result.push(ps);\n }\n }\n\n return result;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { RowLevelSecurityPolicy } from '@objectstack/spec/security';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\n\n/**\n * RLS User Context\n * Variables available for RLS expression evaluation.\n */\ninterface RLSUserContext {\n id?: string;\n tenant_id?: string;\n roles?: string[];\n [key: string]: unknown;\n}\n\n/**\n * RLSCompiler\n * \n * Compiles Row-Level Security policy expressions into query filters.\n * Converts `using` / `check` expressions into ObjectQL-compatible filter conditions.\n */\nexport class RLSCompiler {\n /**\n * Compile RLS policies into a query filter for the given user context.\n * Multiple policies for the same object/operation are OR-combined (any match allows access).\n */\n compileFilter(\n policies: RowLevelSecurityPolicy[],\n executionContext?: ExecutionContext\n ): Record<string, unknown> | null {\n if (policies.length === 0) return null;\n\n const userCtx: RLSUserContext = {\n id: executionContext?.userId,\n tenant_id: executionContext?.tenantId,\n roles: executionContext?.roles,\n };\n\n const filters: Record<string, unknown>[] = [];\n\n for (const policy of policies) {\n if (!policy.using) continue;\n const filter = this.compileExpression(policy.using, userCtx);\n if (filter) {\n filters.push(filter);\n }\n }\n\n if (filters.length === 0) return null;\n if (filters.length === 1) return filters[0];\n\n // Multiple policies: OR-combine (any policy allows access)\n return { $or: filters };\n }\n\n /**\n * Compile a single RLS expression into a query filter.\n * \n * Supports simple expressions like:\n * - \"field_name = current_user.property\"\n * - \"field_name IN (current_user.array_property)\"\n * - \"field_name = 'literal_value'\"\n */\n compileExpression(\n expression: string,\n userCtx: RLSUserContext\n ): Record<string, unknown> | null {\n if (!expression) return null;\n\n // Handle simple equality: \"field = current_user.property\"\n const eqMatch = expression.match(/^\\s*(\\w+)\\s*=\\s*current_user\\.(\\w+)\\s*$/);\n if (eqMatch) {\n const [, field, prop] = eqMatch;\n const value = userCtx[prop];\n if (value === undefined) return null;\n return { [field]: value };\n }\n\n // Handle literal equality: \"field = 'value'\"\n const litMatch = expression.match(/^\\s*(\\w+)\\s*=\\s*'([^']*)'\\s*$/);\n if (litMatch) {\n const [, field, value] = litMatch;\n return { [field]: value };\n }\n\n // Handle IN: \"field IN (current_user.array_property)\"\n const inMatch = expression.match(/^\\s*(\\w+)\\s+IN\\s+\\(\\s*current_user\\.(\\w+)\\s*\\)\\s*$/i);\n if (inMatch) {\n const [, field, prop] = inMatch;\n const value = userCtx[prop];\n if (!Array.isArray(value)) return null;\n return { [field]: { $in: value } };\n }\n\n // Unsupported expression: return null (no additional RLS filter applied).\n // Note: callers should treat absence of RLS policies as \"allow all\" only when\n // no policies are defined. If policies exist but cannot be compiled, the caller\n // may want to deny access as a safety measure.\n return null;\n }\n\n /**\n * Get applicable RLS policies for a given object and operation.\n */\n getApplicablePolicies(\n objectName: string,\n operation: string,\n allPolicies: RowLevelSecurityPolicy[]\n ): RowLevelSecurityPolicy[] {\n // Map engine operation to RLS operation type\n const rlsOp = this.mapOperationToRLS(operation);\n\n return allPolicies.filter(policy => {\n // Check object match\n if (policy.object !== objectName && policy.object !== '*') return false;\n\n // Check operation match\n if (policy.operation === 'all') return true;\n if (policy.operation === rlsOp) return true;\n\n return false;\n });\n }\n\n private mapOperationToRLS(operation: string): string {\n switch (operation) {\n case 'find':\n case 'findOne':\n case 'count':\n case 'aggregate':\n return 'select';\n case 'insert':\n return 'insert';\n case 'update':\n return 'update';\n case 'delete':\n return 'delete';\n default:\n return 'select';\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { FieldPermission } from '@objectstack/spec/security';\n\n/**\n * FieldMasker\n * \n * Applies field-level security by stripping restricted fields from query results.\n */\nexport class FieldMasker {\n /**\n * Mask fields in query results based on field permissions.\n * Removes fields that the user does not have read access to.\n */\n maskResults(\n results: any | any[],\n fieldPermissions: Record<string, FieldPermission>,\n _objectName: string\n ): any | any[] {\n // If no field permissions defined, return results as-is\n if (Object.keys(fieldPermissions).length === 0) return results;\n\n // Get list of non-readable fields\n const hiddenFields = Object.entries(fieldPermissions)\n .filter(([, perm]) => !perm.readable)\n .map(([field]) => field);\n\n if (hiddenFields.length === 0) return results;\n\n if (Array.isArray(results)) {\n return results.map(record => this.maskRecord(record, hiddenFields));\n }\n\n return this.maskRecord(results, hiddenFields);\n }\n\n /**\n * Get non-editable fields for use in write operations.\n * Returns a list of field names that should be stripped from incoming data.\n */\n getNonEditableFields(\n fieldPermissions: Record<string, FieldPermission>\n ): string[] {\n return Object.entries(fieldPermissions)\n .filter(([, perm]) => !perm.editable)\n .map(([field]) => field);\n }\n\n /**\n * Strip non-editable fields from write data.\n */\n stripNonEditableFields(\n data: Record<string, any>,\n fieldPermissions: Record<string, FieldPermission>\n ): Record<string, any> {\n const nonEditable = this.getNonEditableFields(fieldPermissions);\n if (nonEditable.length === 0) return data;\n\n const result = { ...data };\n for (const field of nonEditable) {\n delete result[field];\n }\n return result;\n }\n\n private maskRecord(record: any, hiddenFields: string[]): any {\n if (!record || typeof record !== 'object') return record;\n\n const result = { ...record };\n for (const field of hiddenFields) {\n delete result[field];\n }\n return result;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { ObjectSchema, Field } from '@objectstack/spec/data';\n\n/**\n * sys_role — System Role Object\n *\n * RBAC role definition for the ObjectStack platform.\n * Roles group permissions and are assigned to users or members.\n *\n * @namespace sys\n */\nexport const SysRole = ObjectSchema.create({\n namespace: 'sys',\n name: 'role',\n label: 'Role',\n pluralLabel: 'Roles',\n icon: 'shield',\n isSystem: true,\n description: 'Role definitions for RBAC access control',\n titleFormat: '{name}',\n compactLayout: ['name', 'label', 'active'],\n \n fields: {\n id: Field.text({\n label: 'Role ID',\n required: true,\n readonly: true,\n }),\n \n created_at: Field.datetime({\n label: 'Created At',\n defaultValue: 'NOW()',\n readonly: true,\n }),\n \n updated_at: Field.datetime({\n label: 'Updated At',\n defaultValue: 'NOW()',\n readonly: true,\n }),\n \n name: Field.text({\n label: 'API Name',\n required: true,\n searchable: true,\n maxLength: 100,\n description: 'Unique machine name for the role (e.g. admin, editor, viewer)',\n }),\n \n label: Field.text({\n label: 'Display Name',\n required: true,\n maxLength: 255,\n }),\n \n description: Field.textarea({\n label: 'Description',\n required: false,\n }),\n \n permissions: Field.textarea({\n label: 'Permissions',\n required: false,\n description: 'JSON-serialized array of permission strings',\n }),\n \n active: Field.boolean({\n label: 'Active',\n defaultValue: true,\n }),\n \n is_default: Field.boolean({\n label: 'Default Role',\n defaultValue: false,\n description: 'Automatically assigned to new users',\n }),\n },\n \n indexes: [\n { fields: ['name'], unique: true },\n { fields: ['active'] },\n ],\n \n enable: {\n trackHistory: true,\n searchable: true,\n apiEnabled: true,\n apiMethods: ['get', 'list', 'create', 'update', 'delete'],\n trash: true,\n mru: true,\n },\n});\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { ObjectSchema, Field } from '@objectstack/spec/data';\n\n/**\n * sys_permission_set — System Permission Set Object\n *\n * Named groupings of fine-grained permissions.\n * Permission sets can be assigned to roles or directly to users\n * for granular access control.\n *\n * @namespace sys\n */\nexport const SysPermissionSet = ObjectSchema.create({\n namespace: 'sys',\n name: 'permission_set',\n label: 'Permission Set',\n pluralLabel: 'Permission Sets',\n icon: 'lock',\n isSystem: true,\n description: 'Named permission groupings for fine-grained access control',\n titleFormat: '{name}',\n compactLayout: ['name', 'label', 'active'],\n \n fields: {\n id: Field.text({\n label: 'Permission Set ID',\n required: true,\n readonly: true,\n }),\n \n created_at: Field.datetime({\n label: 'Created At',\n defaultValue: 'NOW()',\n readonly: true,\n }),\n \n updated_at: Field.datetime({\n label: 'Updated At',\n defaultValue: 'NOW()',\n readonly: true,\n }),\n \n name: Field.text({\n label: 'API Name',\n required: true,\n searchable: true,\n maxLength: 100,\n description: 'Unique machine name for the permission set',\n }),\n \n label: Field.text({\n label: 'Display Name',\n required: true,\n maxLength: 255,\n }),\n \n description: Field.textarea({\n label: 'Description',\n required: false,\n }),\n \n object_permissions: Field.textarea({\n label: 'Object Permissions',\n required: false,\n description: 'JSON-serialized object-level CRUD permissions',\n }),\n \n field_permissions: Field.textarea({\n label: 'Field Permissions',\n required: false,\n description: 'JSON-serialized field-level read/write permissions',\n }),\n \n active: Field.boolean({\n label: 'Active',\n defaultValue: true,\n }),\n },\n \n indexes: [\n { fields: ['name'], unique: true },\n { fields: ['active'] },\n ],\n \n enable: {\n trackHistory: true,\n searchable: true,\n apiEnabled: true,\n apiMethods: ['get', 'list', 'create', 'update', 'delete'],\n trash: true,\n mru: true,\n },\n});\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { Plugin, PluginContext } from '@objectstack/core';\nimport type { PermissionSet, RowLevelSecurityPolicy } from '@objectstack/spec/security';\nimport { PermissionEvaluator } from './permission-evaluator.js';\nimport { RLSCompiler } from './rls-compiler.js';\nimport { FieldMasker } from './field-masker.js';\nimport { SysRole, SysPermissionSet } from './objects/index.js';\n\n/**\n * SecurityPlugin\n * \n * Provides RBAC, Row-Level Security, and Field-Level Security runtime.\n * Registers as an engine middleware on the ObjectQL engine.\n * \n * This plugin is fully optional — without it, the system operates\n * without permission checks (same as current behavior).\n * \n * Dependencies:\n * - objectql service (ObjectQL engine with middleware support)\n * - metadata service (MetadataFacade for reading permission sets and RLS policies)\n */\nexport class SecurityPlugin implements Plugin {\n name = 'com.objectstack.security';\n type = 'standard';\n version = '1.0.0';\n dependencies = ['com.objectstack.engine.objectql'];\n\n private permissionEvaluator = new PermissionEvaluator();\n private rlsCompiler = new RLSCompiler();\n private fieldMasker = new FieldMasker();\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Initializing Security Plugin...');\n\n // Register security services\n ctx.registerService('security.permissions', this.permissionEvaluator);\n ctx.registerService('security.rls', this.rlsCompiler);\n ctx.registerService('security.fieldMasker', this.fieldMasker);\n\n // Register security system objects via the manifest service.\n ctx.getService<{ register(m: any): void }>('manifest').register({\n id: 'com.objectstack.security',\n name: 'Security',\n version: '1.0.0',\n type: 'plugin',\n namespace: 'sys',\n objects: [SysRole, SysPermissionSet],\n });\n\n // Contribute navigation items to the Setup App (if SetupPlugin is loaded).\n try {\n const setupNav = ctx.getService<{ contribute(c: any): void }>('setupNav');\n if (setupNav) {\n setupNav.contribute({\n areaId: 'area_administration',\n items: [\n { id: 'nav_roles', type: 'object', label: 'Roles', objectName: 'role', icon: 'shield-check', order: 60 },\n { id: 'nav_permission_sets', type: 'object', label: 'Permission Sets', objectName: 'permission_set', icon: 'lock', order: 70 },\n ],\n });\n ctx.logger.info('Security navigation items contributed to Setup App');\n }\n } catch {\n // SetupPlugin not loaded — skip silently\n }\n\n ctx.logger.info('Security Plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Starting Security Plugin...');\n\n // Get required services\n let ql: any;\n let metadata: any;\n\n try {\n ql = ctx.getService('objectql');\n metadata = ctx.getService('metadata');\n } catch (e) {\n ctx.logger.warn('ObjectQL or metadata service not available, security middleware not registered');\n return;\n }\n\n if (!ql || typeof ql.registerMiddleware !== 'function') {\n ctx.logger.warn('ObjectQL engine does not support middleware, security middleware not registered');\n return;\n }\n\n // Register security middleware\n ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {\n // System operations bypass security\n if (opCtx.context?.isSystem) {\n return next();\n }\n\n const roles = opCtx.context?.roles ?? [];\n\n // Skip security checks if no roles (anonymous/unauthenticated)\n // The auth middleware should handle authentication separately\n if (roles.length === 0 && !opCtx.context?.userId) {\n return next();\n }\n\n // 1. Resolve permission sets for the user's roles\n let permissionSets: PermissionSet[] = [];\n try {\n permissionSets = this.permissionEvaluator.resolvePermissionSets(roles, metadata);\n } catch (e) {\n // If metadata service is misconfigured, log and continue without permission checks\n // rather than blocking all operations\n return next();\n }\n\n // 2. CRUD permission check\n if (permissionSets.length > 0) {\n const allowed = this.permissionEvaluator.checkObjectPermission(\n opCtx.operation,\n opCtx.object,\n permissionSets\n );\n\n if (!allowed) {\n throw new Error(\n `[Security] Access denied: operation '${opCtx.operation}' on object '${opCtx.object}' ` +\n `is not permitted for roles [${roles.join(', ')}]`\n );\n }\n }\n\n // 3. RLS filter injection\n const allRlsPolicies = this.collectRLSPolicies(permissionSets, opCtx.object, opCtx.operation);\n if (allRlsPolicies.length > 0 && opCtx.ast) {\n const rlsFilter = this.rlsCompiler.compileFilter(allRlsPolicies, opCtx.context);\n if (rlsFilter) {\n if (opCtx.ast.where) {\n opCtx.ast.where = { $and: [opCtx.ast.where, rlsFilter] };\n } else {\n opCtx.ast.where = rlsFilter;\n }\n }\n }\n\n await next();\n\n // 4. Field-level security: mask restricted fields in read results\n if (opCtx.result && ['find', 'findOne'].includes(opCtx.operation)) {\n const fieldPerms = this.permissionEvaluator.getFieldPermissions(opCtx.object, permissionSets);\n if (Object.keys(fieldPerms).length > 0) {\n opCtx.result = this.fieldMasker.maskResults(opCtx.result, fieldPerms, opCtx.object);\n }\n }\n });\n\n ctx.logger.info('Security middleware registered on ObjectQL engine');\n }\n\n async destroy(): Promise<void> {\n // No cleanup needed\n }\n\n /**\n * Collect all RLS policies from permission sets applicable to the given object/operation.\n */\n private collectRLSPolicies(\n permissionSets: PermissionSet[],\n objectName: string,\n operation: string\n ): RowLevelSecurityPolicy[] {\n const allPolicies: RowLevelSecurityPolicy[] = [];\n\n for (const ps of permissionSets) {\n if (ps.rowLevelSecurity) {\n allPolicies.push(...ps.rowLevelSecurity);\n }\n }\n\n return this.rlsCompiler.getApplicablePolicies(objectName, operation, allPolicies);\n }\n}\n"],"mappings":"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","names":["import_data"]}
|
package/dist/index.mjs
CHANGED
|
@@ -361,7 +361,7 @@ var SecurityPlugin = class {
|
|
|
361
361
|
ctx.registerService("security.permissions", this.permissionEvaluator);
|
|
362
362
|
ctx.registerService("security.rls", this.rlsCompiler);
|
|
363
363
|
ctx.registerService("security.fieldMasker", this.fieldMasker);
|
|
364
|
-
ctx.
|
|
364
|
+
ctx.getService("manifest").register({
|
|
365
365
|
id: "com.objectstack.security",
|
|
366
366
|
name: "Security",
|
|
367
367
|
version: "1.0.0",
|
|
@@ -369,6 +369,20 @@ var SecurityPlugin = class {
|
|
|
369
369
|
namespace: "sys",
|
|
370
370
|
objects: [SysRole, SysPermissionSet]
|
|
371
371
|
});
|
|
372
|
+
try {
|
|
373
|
+
const setupNav = ctx.getService("setupNav");
|
|
374
|
+
if (setupNav) {
|
|
375
|
+
setupNav.contribute({
|
|
376
|
+
areaId: "area_administration",
|
|
377
|
+
items: [
|
|
378
|
+
{ id: "nav_roles", type: "object", label: "Roles", objectName: "role", icon: "shield-check", order: 60 },
|
|
379
|
+
{ id: "nav_permission_sets", type: "object", label: "Permission Sets", objectName: "permission_set", icon: "lock", order: 70 }
|
|
380
|
+
]
|
|
381
|
+
});
|
|
382
|
+
ctx.logger.info("Security navigation items contributed to Setup App");
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
372
386
|
ctx.logger.info("Security Plugin initialized");
|
|
373
387
|
}
|
|
374
388
|
async start(ctx) {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/permission-evaluator.ts","../src/rls-compiler.ts","../src/field-masker.ts","../src/objects/sys-role.object.ts","../src/objects/sys-permission-set.object.ts","../src/security-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { PermissionSet, ObjectPermission, FieldPermission } from '@objectstack/spec/security';\n\n/**\n * Operation type mapping to permission checks\n */\nconst OPERATION_TO_PERMISSION: Record<string, keyof ObjectPermission> = {\n find: 'allowRead',\n findOne: 'allowRead',\n count: 'allowRead',\n aggregate: 'allowRead',\n insert: 'allowCreate',\n update: 'allowEdit',\n delete: 'allowDelete',\n};\n\n/**\n * PermissionEvaluator\n * \n * Runtime evaluator for PermissionSet definitions.\n * Resolves aggregated permissions from roles to concrete allow/deny decisions.\n */\nexport class PermissionEvaluator {\n /**\n * Check if an operation is allowed on an object for the given permission sets.\n * Uses \"most permissive\" merging: if ANY permission set allows, it's allowed.\n */\n checkObjectPermission(\n operation: string,\n objectName: string,\n permissionSets: PermissionSet[]\n ): boolean {\n const permKey = OPERATION_TO_PERMISSION[operation];\n if (!permKey) return true; // Unknown operations are allowed by default\n\n for (const ps of permissionSets) {\n const objPerm = ps.objects?.[objectName];\n if (objPerm) {\n // Check if modifyAllRecords is set (super-user bypass for write ops)\n if (['allowEdit', 'allowDelete'].includes(permKey) && objPerm.modifyAllRecords) {\n return true;\n }\n // Check if viewAllRecords is set (super-user bypass for read ops)\n if (permKey === 'allowRead' && (objPerm.viewAllRecords || objPerm.modifyAllRecords)) {\n return true;\n }\n // Check the specific permission\n if (objPerm[permKey]) {\n return true;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Get the merged field permissions for an object.\n * Returns a map of field names to their effective permissions.\n * Uses \"most permissive\" merging.\n */\n getFieldPermissions(\n objectName: string,\n permissionSets: PermissionSet[]\n ): Record<string, FieldPermission> {\n const result: Record<string, FieldPermission> = {};\n\n for (const ps of permissionSets) {\n if (!ps.fields) continue;\n\n for (const [key, perm] of Object.entries(ps.fields)) {\n // Field keys are in format: \"object_name.field_name\"\n if (!key.startsWith(`${objectName}.`)) continue;\n const fieldName = key.substring(objectName.length + 1);\n\n if (!result[fieldName]) {\n result[fieldName] = { readable: false, editable: false };\n }\n\n // Most permissive merge\n if (perm.readable) result[fieldName].readable = true;\n if (perm.editable) result[fieldName].editable = true;\n }\n }\n\n return result;\n }\n\n /**\n * Resolve permission sets for a list of role names from metadata.\n */\n resolvePermissionSets(\n roles: string[],\n metadataService: any\n ): PermissionSet[] {\n const result: PermissionSet[] = [];\n\n // Get all permission sets from metadata\n const allPermSets = metadataService.list?.('permissions') || [];\n\n for (const ps of allPermSets) {\n // A permission set is relevant if it's a profile assigned to any of the user's roles,\n // or if the role name matches the permission set name\n if (roles.includes(ps.name)) {\n result.push(ps);\n }\n }\n\n return result;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { RowLevelSecurityPolicy } from '@objectstack/spec/security';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\n\n/**\n * RLS User Context\n * Variables available for RLS expression evaluation.\n */\ninterface RLSUserContext {\n id?: string;\n tenant_id?: string;\n roles?: string[];\n [key: string]: unknown;\n}\n\n/**\n * RLSCompiler\n * \n * Compiles Row-Level Security policy expressions into query filters.\n * Converts `using` / `check` expressions into ObjectQL-compatible filter conditions.\n */\nexport class RLSCompiler {\n /**\n * Compile RLS policies into a query filter for the given user context.\n * Multiple policies for the same object/operation are OR-combined (any match allows access).\n */\n compileFilter(\n policies: RowLevelSecurityPolicy[],\n executionContext?: ExecutionContext\n ): Record<string, unknown> | null {\n if (policies.length === 0) return null;\n\n const userCtx: RLSUserContext = {\n id: executionContext?.userId,\n tenant_id: executionContext?.tenantId,\n roles: executionContext?.roles,\n };\n\n const filters: Record<string, unknown>[] = [];\n\n for (const policy of policies) {\n if (!policy.using) continue;\n const filter = this.compileExpression(policy.using, userCtx);\n if (filter) {\n filters.push(filter);\n }\n }\n\n if (filters.length === 0) return null;\n if (filters.length === 1) return filters[0];\n\n // Multiple policies: OR-combine (any policy allows access)\n return { $or: filters };\n }\n\n /**\n * Compile a single RLS expression into a query filter.\n * \n * Supports simple expressions like:\n * - \"field_name = current_user.property\"\n * - \"field_name IN (current_user.array_property)\"\n * - \"field_name = 'literal_value'\"\n */\n compileExpression(\n expression: string,\n userCtx: RLSUserContext\n ): Record<string, unknown> | null {\n if (!expression) return null;\n\n // Handle simple equality: \"field = current_user.property\"\n const eqMatch = expression.match(/^\\s*(\\w+)\\s*=\\s*current_user\\.(\\w+)\\s*$/);\n if (eqMatch) {\n const [, field, prop] = eqMatch;\n const value = userCtx[prop];\n if (value === undefined) return null;\n return { [field]: value };\n }\n\n // Handle literal equality: \"field = 'value'\"\n const litMatch = expression.match(/^\\s*(\\w+)\\s*=\\s*'([^']*)'\\s*$/);\n if (litMatch) {\n const [, field, value] = litMatch;\n return { [field]: value };\n }\n\n // Handle IN: \"field IN (current_user.array_property)\"\n const inMatch = expression.match(/^\\s*(\\w+)\\s+IN\\s+\\(\\s*current_user\\.(\\w+)\\s*\\)\\s*$/i);\n if (inMatch) {\n const [, field, prop] = inMatch;\n const value = userCtx[prop];\n if (!Array.isArray(value)) return null;\n return { [field]: { $in: value } };\n }\n\n // Unsupported expression: return null (no additional RLS filter applied).\n // Note: callers should treat absence of RLS policies as \"allow all\" only when\n // no policies are defined. If policies exist but cannot be compiled, the caller\n // may want to deny access as a safety measure.\n return null;\n }\n\n /**\n * Get applicable RLS policies for a given object and operation.\n */\n getApplicablePolicies(\n objectName: string,\n operation: string,\n allPolicies: RowLevelSecurityPolicy[]\n ): RowLevelSecurityPolicy[] {\n // Map engine operation to RLS operation type\n const rlsOp = this.mapOperationToRLS(operation);\n\n return allPolicies.filter(policy => {\n // Check object match\n if (policy.object !== objectName && policy.object !== '*') return false;\n\n // Check operation match\n if (policy.operation === 'all') return true;\n if (policy.operation === rlsOp) return true;\n\n return false;\n });\n }\n\n private mapOperationToRLS(operation: string): string {\n switch (operation) {\n case 'find':\n case 'findOne':\n case 'count':\n case 'aggregate':\n return 'select';\n case 'insert':\n return 'insert';\n case 'update':\n return 'update';\n case 'delete':\n return 'delete';\n default:\n return 'select';\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { FieldPermission } from '@objectstack/spec/security';\n\n/**\n * FieldMasker\n * \n * Applies field-level security by stripping restricted fields from query results.\n */\nexport class FieldMasker {\n /**\n * Mask fields in query results based on field permissions.\n * Removes fields that the user does not have read access to.\n */\n maskResults(\n results: any | any[],\n fieldPermissions: Record<string, FieldPermission>,\n _objectName: string\n ): any | any[] {\n // If no field permissions defined, return results as-is\n if (Object.keys(fieldPermissions).length === 0) return results;\n\n // Get list of non-readable fields\n const hiddenFields = Object.entries(fieldPermissions)\n .filter(([, perm]) => !perm.readable)\n .map(([field]) => field);\n\n if (hiddenFields.length === 0) return results;\n\n if (Array.isArray(results)) {\n return results.map(record => this.maskRecord(record, hiddenFields));\n }\n\n return this.maskRecord(results, hiddenFields);\n }\n\n /**\n * Get non-editable fields for use in write operations.\n * Returns a list of field names that should be stripped from incoming data.\n */\n getNonEditableFields(\n fieldPermissions: Record<string, FieldPermission>\n ): string[] {\n return Object.entries(fieldPermissions)\n .filter(([, perm]) => !perm.editable)\n .map(([field]) => field);\n }\n\n /**\n * Strip non-editable fields from write data.\n */\n stripNonEditableFields(\n data: Record<string, any>,\n fieldPermissions: Record<string, FieldPermission>\n ): Record<string, any> {\n const nonEditable = this.getNonEditableFields(fieldPermissions);\n if (nonEditable.length === 0) return data;\n\n const result = { ...data };\n for (const field of nonEditable) {\n delete result[field];\n }\n return result;\n }\n\n private maskRecord(record: any, hiddenFields: string[]): any {\n if (!record || typeof record !== 'object') return record;\n\n const result = { ...record };\n for (const field of hiddenFields) {\n delete result[field];\n }\n return result;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { ObjectSchema, Field } from '@objectstack/spec/data';\n\n/**\n * sys_role — System Role Object\n *\n * RBAC role definition for the ObjectStack platform.\n * Roles group permissions and are assigned to users or members.\n *\n * @namespace sys\n */\nexport const SysRole = ObjectSchema.create({\n namespace: 'sys',\n name: 'role',\n label: 'Role',\n pluralLabel: 'Roles',\n icon: 'shield',\n isSystem: true,\n description: 'Role definitions for RBAC access control',\n titleFormat: '{name}',\n compactLayout: ['name', 'label', 'active'],\n \n fields: {\n id: Field.text({\n label: 'Role ID',\n required: true,\n readonly: true,\n }),\n \n created_at: Field.datetime({\n label: 'Created At',\n defaultValue: 'NOW()',\n readonly: true,\n }),\n \n updated_at: Field.datetime({\n label: 'Updated At',\n defaultValue: 'NOW()',\n readonly: true,\n }),\n \n name: Field.text({\n label: 'API Name',\n required: true,\n searchable: true,\n maxLength: 100,\n description: 'Unique machine name for the role (e.g. admin, editor, viewer)',\n }),\n \n label: Field.text({\n label: 'Display Name',\n required: true,\n maxLength: 255,\n }),\n \n description: Field.textarea({\n label: 'Description',\n required: false,\n }),\n \n permissions: Field.textarea({\n label: 'Permissions',\n required: false,\n description: 'JSON-serialized array of permission strings',\n }),\n \n active: Field.boolean({\n label: 'Active',\n defaultValue: true,\n }),\n \n is_default: Field.boolean({\n label: 'Default Role',\n defaultValue: false,\n description: 'Automatically assigned to new users',\n }),\n },\n \n indexes: [\n { fields: ['name'], unique: true },\n { fields: ['active'] },\n ],\n \n enable: {\n trackHistory: true,\n searchable: true,\n apiEnabled: true,\n apiMethods: ['get', 'list', 'create', 'update', 'delete'],\n trash: true,\n mru: true,\n },\n});\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { ObjectSchema, Field } from '@objectstack/spec/data';\n\n/**\n * sys_permission_set — System Permission Set Object\n *\n * Named groupings of fine-grained permissions.\n * Permission sets can be assigned to roles or directly to users\n * for granular access control.\n *\n * @namespace sys\n */\nexport const SysPermissionSet = ObjectSchema.create({\n namespace: 'sys',\n name: 'permission_set',\n label: 'Permission Set',\n pluralLabel: 'Permission Sets',\n icon: 'lock',\n isSystem: true,\n description: 'Named permission groupings for fine-grained access control',\n titleFormat: '{name}',\n compactLayout: ['name', 'label', 'active'],\n \n fields: {\n id: Field.text({\n label: 'Permission Set ID',\n required: true,\n readonly: true,\n }),\n \n created_at: Field.datetime({\n label: 'Created At',\n defaultValue: 'NOW()',\n readonly: true,\n }),\n \n updated_at: Field.datetime({\n label: 'Updated At',\n defaultValue: 'NOW()',\n readonly: true,\n }),\n \n name: Field.text({\n label: 'API Name',\n required: true,\n searchable: true,\n maxLength: 100,\n description: 'Unique machine name for the permission set',\n }),\n \n label: Field.text({\n label: 'Display Name',\n required: true,\n maxLength: 255,\n }),\n \n description: Field.textarea({\n label: 'Description',\n required: false,\n }),\n \n object_permissions: Field.textarea({\n label: 'Object Permissions',\n required: false,\n description: 'JSON-serialized object-level CRUD permissions',\n }),\n \n field_permissions: Field.textarea({\n label: 'Field Permissions',\n required: false,\n description: 'JSON-serialized field-level read/write permissions',\n }),\n \n active: Field.boolean({\n label: 'Active',\n defaultValue: true,\n }),\n },\n \n indexes: [\n { fields: ['name'], unique: true },\n { fields: ['active'] },\n ],\n \n enable: {\n trackHistory: true,\n searchable: true,\n apiEnabled: true,\n apiMethods: ['get', 'list', 'create', 'update', 'delete'],\n trash: true,\n mru: true,\n },\n});\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { Plugin, PluginContext } from '@objectstack/core';\nimport type { PermissionSet, RowLevelSecurityPolicy } from '@objectstack/spec/security';\nimport { PermissionEvaluator } from './permission-evaluator.js';\nimport { RLSCompiler } from './rls-compiler.js';\nimport { FieldMasker } from './field-masker.js';\nimport { SysRole, SysPermissionSet } from './objects/index.js';\n\n/**\n * SecurityPlugin\n * \n * Provides RBAC, Row-Level Security, and Field-Level Security runtime.\n * Registers as an engine middleware on the ObjectQL engine.\n * \n * This plugin is fully optional — without it, the system operates\n * without permission checks (same as current behavior).\n * \n * Dependencies:\n * - objectql service (ObjectQL engine with middleware support)\n * - metadata service (MetadataFacade for reading permission sets and RLS policies)\n */\nexport class SecurityPlugin implements Plugin {\n name = 'com.objectstack.security';\n type = 'standard';\n version = '1.0.0';\n dependencies = ['com.objectstack.engine.objectql'];\n\n private permissionEvaluator = new PermissionEvaluator();\n private rlsCompiler = new RLSCompiler();\n private fieldMasker = new FieldMasker();\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Initializing Security Plugin...');\n\n // Register security services\n ctx.registerService('security.permissions', this.permissionEvaluator);\n ctx.registerService('security.rls', this.rlsCompiler);\n ctx.registerService('security.fieldMasker', this.fieldMasker);\n\n // Register security system objects so ObjectQLPlugin auto-discovers them\n ctx.registerService('app.com.objectstack.security', {\n id: 'com.objectstack.security',\n name: 'Security',\n version: '1.0.0',\n type: 'plugin',\n namespace: 'sys',\n objects: [SysRole, SysPermissionSet],\n });\n\n ctx.logger.info('Security Plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Starting Security Plugin...');\n\n // Get required services\n let ql: any;\n let metadata: any;\n\n try {\n ql = ctx.getService('objectql');\n metadata = ctx.getService('metadata');\n } catch (e) {\n ctx.logger.warn('ObjectQL or metadata service not available, security middleware not registered');\n return;\n }\n\n if (!ql || typeof ql.registerMiddleware !== 'function') {\n ctx.logger.warn('ObjectQL engine does not support middleware, security middleware not registered');\n return;\n }\n\n // Register security middleware\n ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {\n // System operations bypass security\n if (opCtx.context?.isSystem) {\n return next();\n }\n\n const roles = opCtx.context?.roles ?? [];\n\n // Skip security checks if no roles (anonymous/unauthenticated)\n // The auth middleware should handle authentication separately\n if (roles.length === 0 && !opCtx.context?.userId) {\n return next();\n }\n\n // 1. Resolve permission sets for the user's roles\n let permissionSets: PermissionSet[] = [];\n try {\n permissionSets = this.permissionEvaluator.resolvePermissionSets(roles, metadata);\n } catch (e) {\n // If metadata service is misconfigured, log and continue without permission checks\n // rather than blocking all operations\n return next();\n }\n\n // 2. CRUD permission check\n if (permissionSets.length > 0) {\n const allowed = this.permissionEvaluator.checkObjectPermission(\n opCtx.operation,\n opCtx.object,\n permissionSets\n );\n\n if (!allowed) {\n throw new Error(\n `[Security] Access denied: operation '${opCtx.operation}' on object '${opCtx.object}' ` +\n `is not permitted for roles [${roles.join(', ')}]`\n );\n }\n }\n\n // 3. RLS filter injection\n const allRlsPolicies = this.collectRLSPolicies(permissionSets, opCtx.object, opCtx.operation);\n if (allRlsPolicies.length > 0 && opCtx.ast) {\n const rlsFilter = this.rlsCompiler.compileFilter(allRlsPolicies, opCtx.context);\n if (rlsFilter) {\n if (opCtx.ast.where) {\n opCtx.ast.where = { $and: [opCtx.ast.where, rlsFilter] };\n } else {\n opCtx.ast.where = rlsFilter;\n }\n }\n }\n\n await next();\n\n // 4. Field-level security: mask restricted fields in read results\n if (opCtx.result && ['find', 'findOne'].includes(opCtx.operation)) {\n const fieldPerms = this.permissionEvaluator.getFieldPermissions(opCtx.object, permissionSets);\n if (Object.keys(fieldPerms).length > 0) {\n opCtx.result = this.fieldMasker.maskResults(opCtx.result, fieldPerms, opCtx.object);\n }\n }\n });\n\n ctx.logger.info('Security middleware registered on ObjectQL engine');\n }\n\n async destroy(): Promise<void> {\n // No cleanup needed\n }\n\n /**\n * Collect all RLS policies from permission sets applicable to the given object/operation.\n */\n private collectRLSPolicies(\n permissionSets: PermissionSet[],\n objectName: string,\n operation: string\n ): RowLevelSecurityPolicy[] {\n const allPolicies: RowLevelSecurityPolicy[] = [];\n\n for (const ps of permissionSets) {\n if (ps.rowLevelSecurity) {\n allPolicies.push(...ps.rowLevelSecurity);\n }\n }\n\n return this.rlsCompiler.getApplicablePolicies(objectName, operation, allPolicies);\n }\n}\n"],"mappings":"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","names":["ObjectSchema","Field"]}
|
|
1
|
+
{"version":3,"sources":["../src/permission-evaluator.ts","../src/rls-compiler.ts","../src/field-masker.ts","../src/objects/sys-role.object.ts","../src/objects/sys-permission-set.object.ts","../src/security-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { PermissionSet, ObjectPermission, FieldPermission } from '@objectstack/spec/security';\n\n/**\n * Operation type mapping to permission checks\n */\nconst OPERATION_TO_PERMISSION: Record<string, keyof ObjectPermission> = {\n find: 'allowRead',\n findOne: 'allowRead',\n count: 'allowRead',\n aggregate: 'allowRead',\n insert: 'allowCreate',\n update: 'allowEdit',\n delete: 'allowDelete',\n};\n\n/**\n * PermissionEvaluator\n * \n * Runtime evaluator for PermissionSet definitions.\n * Resolves aggregated permissions from roles to concrete allow/deny decisions.\n */\nexport class PermissionEvaluator {\n /**\n * Check if an operation is allowed on an object for the given permission sets.\n * Uses \"most permissive\" merging: if ANY permission set allows, it's allowed.\n */\n checkObjectPermission(\n operation: string,\n objectName: string,\n permissionSets: PermissionSet[]\n ): boolean {\n const permKey = OPERATION_TO_PERMISSION[operation];\n if (!permKey) return true; // Unknown operations are allowed by default\n\n for (const ps of permissionSets) {\n const objPerm = ps.objects?.[objectName];\n if (objPerm) {\n // Check if modifyAllRecords is set (super-user bypass for write ops)\n if (['allowEdit', 'allowDelete'].includes(permKey) && objPerm.modifyAllRecords) {\n return true;\n }\n // Check if viewAllRecords is set (super-user bypass for read ops)\n if (permKey === 'allowRead' && (objPerm.viewAllRecords || objPerm.modifyAllRecords)) {\n return true;\n }\n // Check the specific permission\n if (objPerm[permKey]) {\n return true;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Get the merged field permissions for an object.\n * Returns a map of field names to their effective permissions.\n * Uses \"most permissive\" merging.\n */\n getFieldPermissions(\n objectName: string,\n permissionSets: PermissionSet[]\n ): Record<string, FieldPermission> {\n const result: Record<string, FieldPermission> = {};\n\n for (const ps of permissionSets) {\n if (!ps.fields) continue;\n\n for (const [key, perm] of Object.entries(ps.fields)) {\n // Field keys are in format: \"object_name.field_name\"\n if (!key.startsWith(`${objectName}.`)) continue;\n const fieldName = key.substring(objectName.length + 1);\n\n if (!result[fieldName]) {\n result[fieldName] = { readable: false, editable: false };\n }\n\n // Most permissive merge\n if (perm.readable) result[fieldName].readable = true;\n if (perm.editable) result[fieldName].editable = true;\n }\n }\n\n return result;\n }\n\n /**\n * Resolve permission sets for a list of role names from metadata.\n */\n resolvePermissionSets(\n roles: string[],\n metadataService: any\n ): PermissionSet[] {\n const result: PermissionSet[] = [];\n\n // Get all permission sets from metadata\n const allPermSets = metadataService.list?.('permissions') || [];\n\n for (const ps of allPermSets) {\n // A permission set is relevant if it's a profile assigned to any of the user's roles,\n // or if the role name matches the permission set name\n if (roles.includes(ps.name)) {\n result.push(ps);\n }\n }\n\n return result;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { RowLevelSecurityPolicy } from '@objectstack/spec/security';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\n\n/**\n * RLS User Context\n * Variables available for RLS expression evaluation.\n */\ninterface RLSUserContext {\n id?: string;\n tenant_id?: string;\n roles?: string[];\n [key: string]: unknown;\n}\n\n/**\n * RLSCompiler\n * \n * Compiles Row-Level Security policy expressions into query filters.\n * Converts `using` / `check` expressions into ObjectQL-compatible filter conditions.\n */\nexport class RLSCompiler {\n /**\n * Compile RLS policies into a query filter for the given user context.\n * Multiple policies for the same object/operation are OR-combined (any match allows access).\n */\n compileFilter(\n policies: RowLevelSecurityPolicy[],\n executionContext?: ExecutionContext\n ): Record<string, unknown> | null {\n if (policies.length === 0) return null;\n\n const userCtx: RLSUserContext = {\n id: executionContext?.userId,\n tenant_id: executionContext?.tenantId,\n roles: executionContext?.roles,\n };\n\n const filters: Record<string, unknown>[] = [];\n\n for (const policy of policies) {\n if (!policy.using) continue;\n const filter = this.compileExpression(policy.using, userCtx);\n if (filter) {\n filters.push(filter);\n }\n }\n\n if (filters.length === 0) return null;\n if (filters.length === 1) return filters[0];\n\n // Multiple policies: OR-combine (any policy allows access)\n return { $or: filters };\n }\n\n /**\n * Compile a single RLS expression into a query filter.\n * \n * Supports simple expressions like:\n * - \"field_name = current_user.property\"\n * - \"field_name IN (current_user.array_property)\"\n * - \"field_name = 'literal_value'\"\n */\n compileExpression(\n expression: string,\n userCtx: RLSUserContext\n ): Record<string, unknown> | null {\n if (!expression) return null;\n\n // Handle simple equality: \"field = current_user.property\"\n const eqMatch = expression.match(/^\\s*(\\w+)\\s*=\\s*current_user\\.(\\w+)\\s*$/);\n if (eqMatch) {\n const [, field, prop] = eqMatch;\n const value = userCtx[prop];\n if (value === undefined) return null;\n return { [field]: value };\n }\n\n // Handle literal equality: \"field = 'value'\"\n const litMatch = expression.match(/^\\s*(\\w+)\\s*=\\s*'([^']*)'\\s*$/);\n if (litMatch) {\n const [, field, value] = litMatch;\n return { [field]: value };\n }\n\n // Handle IN: \"field IN (current_user.array_property)\"\n const inMatch = expression.match(/^\\s*(\\w+)\\s+IN\\s+\\(\\s*current_user\\.(\\w+)\\s*\\)\\s*$/i);\n if (inMatch) {\n const [, field, prop] = inMatch;\n const value = userCtx[prop];\n if (!Array.isArray(value)) return null;\n return { [field]: { $in: value } };\n }\n\n // Unsupported expression: return null (no additional RLS filter applied).\n // Note: callers should treat absence of RLS policies as \"allow all\" only when\n // no policies are defined. If policies exist but cannot be compiled, the caller\n // may want to deny access as a safety measure.\n return null;\n }\n\n /**\n * Get applicable RLS policies for a given object and operation.\n */\n getApplicablePolicies(\n objectName: string,\n operation: string,\n allPolicies: RowLevelSecurityPolicy[]\n ): RowLevelSecurityPolicy[] {\n // Map engine operation to RLS operation type\n const rlsOp = this.mapOperationToRLS(operation);\n\n return allPolicies.filter(policy => {\n // Check object match\n if (policy.object !== objectName && policy.object !== '*') return false;\n\n // Check operation match\n if (policy.operation === 'all') return true;\n if (policy.operation === rlsOp) return true;\n\n return false;\n });\n }\n\n private mapOperationToRLS(operation: string): string {\n switch (operation) {\n case 'find':\n case 'findOne':\n case 'count':\n case 'aggregate':\n return 'select';\n case 'insert':\n return 'insert';\n case 'update':\n return 'update';\n case 'delete':\n return 'delete';\n default:\n return 'select';\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { FieldPermission } from '@objectstack/spec/security';\n\n/**\n * FieldMasker\n * \n * Applies field-level security by stripping restricted fields from query results.\n */\nexport class FieldMasker {\n /**\n * Mask fields in query results based on field permissions.\n * Removes fields that the user does not have read access to.\n */\n maskResults(\n results: any | any[],\n fieldPermissions: Record<string, FieldPermission>,\n _objectName: string\n ): any | any[] {\n // If no field permissions defined, return results as-is\n if (Object.keys(fieldPermissions).length === 0) return results;\n\n // Get list of non-readable fields\n const hiddenFields = Object.entries(fieldPermissions)\n .filter(([, perm]) => !perm.readable)\n .map(([field]) => field);\n\n if (hiddenFields.length === 0) return results;\n\n if (Array.isArray(results)) {\n return results.map(record => this.maskRecord(record, hiddenFields));\n }\n\n return this.maskRecord(results, hiddenFields);\n }\n\n /**\n * Get non-editable fields for use in write operations.\n * Returns a list of field names that should be stripped from incoming data.\n */\n getNonEditableFields(\n fieldPermissions: Record<string, FieldPermission>\n ): string[] {\n return Object.entries(fieldPermissions)\n .filter(([, perm]) => !perm.editable)\n .map(([field]) => field);\n }\n\n /**\n * Strip non-editable fields from write data.\n */\n stripNonEditableFields(\n data: Record<string, any>,\n fieldPermissions: Record<string, FieldPermission>\n ): Record<string, any> {\n const nonEditable = this.getNonEditableFields(fieldPermissions);\n if (nonEditable.length === 0) return data;\n\n const result = { ...data };\n for (const field of nonEditable) {\n delete result[field];\n }\n return result;\n }\n\n private maskRecord(record: any, hiddenFields: string[]): any {\n if (!record || typeof record !== 'object') return record;\n\n const result = { ...record };\n for (const field of hiddenFields) {\n delete result[field];\n }\n return result;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { ObjectSchema, Field } from '@objectstack/spec/data';\n\n/**\n * sys_role — System Role Object\n *\n * RBAC role definition for the ObjectStack platform.\n * Roles group permissions and are assigned to users or members.\n *\n * @namespace sys\n */\nexport const SysRole = ObjectSchema.create({\n namespace: 'sys',\n name: 'role',\n label: 'Role',\n pluralLabel: 'Roles',\n icon: 'shield',\n isSystem: true,\n description: 'Role definitions for RBAC access control',\n titleFormat: '{name}',\n compactLayout: ['name', 'label', 'active'],\n \n fields: {\n id: Field.text({\n label: 'Role ID',\n required: true,\n readonly: true,\n }),\n \n created_at: Field.datetime({\n label: 'Created At',\n defaultValue: 'NOW()',\n readonly: true,\n }),\n \n updated_at: Field.datetime({\n label: 'Updated At',\n defaultValue: 'NOW()',\n readonly: true,\n }),\n \n name: Field.text({\n label: 'API Name',\n required: true,\n searchable: true,\n maxLength: 100,\n description: 'Unique machine name for the role (e.g. admin, editor, viewer)',\n }),\n \n label: Field.text({\n label: 'Display Name',\n required: true,\n maxLength: 255,\n }),\n \n description: Field.textarea({\n label: 'Description',\n required: false,\n }),\n \n permissions: Field.textarea({\n label: 'Permissions',\n required: false,\n description: 'JSON-serialized array of permission strings',\n }),\n \n active: Field.boolean({\n label: 'Active',\n defaultValue: true,\n }),\n \n is_default: Field.boolean({\n label: 'Default Role',\n defaultValue: false,\n description: 'Automatically assigned to new users',\n }),\n },\n \n indexes: [\n { fields: ['name'], unique: true },\n { fields: ['active'] },\n ],\n \n enable: {\n trackHistory: true,\n searchable: true,\n apiEnabled: true,\n apiMethods: ['get', 'list', 'create', 'update', 'delete'],\n trash: true,\n mru: true,\n },\n});\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { ObjectSchema, Field } from '@objectstack/spec/data';\n\n/**\n * sys_permission_set — System Permission Set Object\n *\n * Named groupings of fine-grained permissions.\n * Permission sets can be assigned to roles or directly to users\n * for granular access control.\n *\n * @namespace sys\n */\nexport const SysPermissionSet = ObjectSchema.create({\n namespace: 'sys',\n name: 'permission_set',\n label: 'Permission Set',\n pluralLabel: 'Permission Sets',\n icon: 'lock',\n isSystem: true,\n description: 'Named permission groupings for fine-grained access control',\n titleFormat: '{name}',\n compactLayout: ['name', 'label', 'active'],\n \n fields: {\n id: Field.text({\n label: 'Permission Set ID',\n required: true,\n readonly: true,\n }),\n \n created_at: Field.datetime({\n label: 'Created At',\n defaultValue: 'NOW()',\n readonly: true,\n }),\n \n updated_at: Field.datetime({\n label: 'Updated At',\n defaultValue: 'NOW()',\n readonly: true,\n }),\n \n name: Field.text({\n label: 'API Name',\n required: true,\n searchable: true,\n maxLength: 100,\n description: 'Unique machine name for the permission set',\n }),\n \n label: Field.text({\n label: 'Display Name',\n required: true,\n maxLength: 255,\n }),\n \n description: Field.textarea({\n label: 'Description',\n required: false,\n }),\n \n object_permissions: Field.textarea({\n label: 'Object Permissions',\n required: false,\n description: 'JSON-serialized object-level CRUD permissions',\n }),\n \n field_permissions: Field.textarea({\n label: 'Field Permissions',\n required: false,\n description: 'JSON-serialized field-level read/write permissions',\n }),\n \n active: Field.boolean({\n label: 'Active',\n defaultValue: true,\n }),\n },\n \n indexes: [\n { fields: ['name'], unique: true },\n { fields: ['active'] },\n ],\n \n enable: {\n trackHistory: true,\n searchable: true,\n apiEnabled: true,\n apiMethods: ['get', 'list', 'create', 'update', 'delete'],\n trash: true,\n mru: true,\n },\n});\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { Plugin, PluginContext } from '@objectstack/core';\nimport type { PermissionSet, RowLevelSecurityPolicy } from '@objectstack/spec/security';\nimport { PermissionEvaluator } from './permission-evaluator.js';\nimport { RLSCompiler } from './rls-compiler.js';\nimport { FieldMasker } from './field-masker.js';\nimport { SysRole, SysPermissionSet } from './objects/index.js';\n\n/**\n * SecurityPlugin\n * \n * Provides RBAC, Row-Level Security, and Field-Level Security runtime.\n * Registers as an engine middleware on the ObjectQL engine.\n * \n * This plugin is fully optional — without it, the system operates\n * without permission checks (same as current behavior).\n * \n * Dependencies:\n * - objectql service (ObjectQL engine with middleware support)\n * - metadata service (MetadataFacade for reading permission sets and RLS policies)\n */\nexport class SecurityPlugin implements Plugin {\n name = 'com.objectstack.security';\n type = 'standard';\n version = '1.0.0';\n dependencies = ['com.objectstack.engine.objectql'];\n\n private permissionEvaluator = new PermissionEvaluator();\n private rlsCompiler = new RLSCompiler();\n private fieldMasker = new FieldMasker();\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Initializing Security Plugin...');\n\n // Register security services\n ctx.registerService('security.permissions', this.permissionEvaluator);\n ctx.registerService('security.rls', this.rlsCompiler);\n ctx.registerService('security.fieldMasker', this.fieldMasker);\n\n // Register security system objects via the manifest service.\n ctx.getService<{ register(m: any): void }>('manifest').register({\n id: 'com.objectstack.security',\n name: 'Security',\n version: '1.0.0',\n type: 'plugin',\n namespace: 'sys',\n objects: [SysRole, SysPermissionSet],\n });\n\n // Contribute navigation items to the Setup App (if SetupPlugin is loaded).\n try {\n const setupNav = ctx.getService<{ contribute(c: any): void }>('setupNav');\n if (setupNav) {\n setupNav.contribute({\n areaId: 'area_administration',\n items: [\n { id: 'nav_roles', type: 'object', label: 'Roles', objectName: 'role', icon: 'shield-check', order: 60 },\n { id: 'nav_permission_sets', type: 'object', label: 'Permission Sets', objectName: 'permission_set', icon: 'lock', order: 70 },\n ],\n });\n ctx.logger.info('Security navigation items contributed to Setup App');\n }\n } catch {\n // SetupPlugin not loaded — skip silently\n }\n\n ctx.logger.info('Security Plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Starting Security Plugin...');\n\n // Get required services\n let ql: any;\n let metadata: any;\n\n try {\n ql = ctx.getService('objectql');\n metadata = ctx.getService('metadata');\n } catch (e) {\n ctx.logger.warn('ObjectQL or metadata service not available, security middleware not registered');\n return;\n }\n\n if (!ql || typeof ql.registerMiddleware !== 'function') {\n ctx.logger.warn('ObjectQL engine does not support middleware, security middleware not registered');\n return;\n }\n\n // Register security middleware\n ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {\n // System operations bypass security\n if (opCtx.context?.isSystem) {\n return next();\n }\n\n const roles = opCtx.context?.roles ?? [];\n\n // Skip security checks if no roles (anonymous/unauthenticated)\n // The auth middleware should handle authentication separately\n if (roles.length === 0 && !opCtx.context?.userId) {\n return next();\n }\n\n // 1. Resolve permission sets for the user's roles\n let permissionSets: PermissionSet[] = [];\n try {\n permissionSets = this.permissionEvaluator.resolvePermissionSets(roles, metadata);\n } catch (e) {\n // If metadata service is misconfigured, log and continue without permission checks\n // rather than blocking all operations\n return next();\n }\n\n // 2. CRUD permission check\n if (permissionSets.length > 0) {\n const allowed = this.permissionEvaluator.checkObjectPermission(\n opCtx.operation,\n opCtx.object,\n permissionSets\n );\n\n if (!allowed) {\n throw new Error(\n `[Security] Access denied: operation '${opCtx.operation}' on object '${opCtx.object}' ` +\n `is not permitted for roles [${roles.join(', ')}]`\n );\n }\n }\n\n // 3. RLS filter injection\n const allRlsPolicies = this.collectRLSPolicies(permissionSets, opCtx.object, opCtx.operation);\n if (allRlsPolicies.length > 0 && opCtx.ast) {\n const rlsFilter = this.rlsCompiler.compileFilter(allRlsPolicies, opCtx.context);\n if (rlsFilter) {\n if (opCtx.ast.where) {\n opCtx.ast.where = { $and: [opCtx.ast.where, rlsFilter] };\n } else {\n opCtx.ast.where = rlsFilter;\n }\n }\n }\n\n await next();\n\n // 4. Field-level security: mask restricted fields in read results\n if (opCtx.result && ['find', 'findOne'].includes(opCtx.operation)) {\n const fieldPerms = this.permissionEvaluator.getFieldPermissions(opCtx.object, permissionSets);\n if (Object.keys(fieldPerms).length > 0) {\n opCtx.result = this.fieldMasker.maskResults(opCtx.result, fieldPerms, opCtx.object);\n }\n }\n });\n\n ctx.logger.info('Security middleware registered on ObjectQL engine');\n }\n\n async destroy(): Promise<void> {\n // No cleanup needed\n }\n\n /**\n * Collect all RLS policies from permission sets applicable to the given object/operation.\n */\n private collectRLSPolicies(\n permissionSets: PermissionSet[],\n objectName: string,\n operation: string\n ): RowLevelSecurityPolicy[] {\n const allPolicies: RowLevelSecurityPolicy[] = [];\n\n for (const ps of permissionSets) {\n if (ps.rowLevelSecurity) {\n allPolicies.push(...ps.rowLevelSecurity);\n }\n }\n\n return this.rlsCompiler.getApplicablePolicies(objectName, operation, allPolicies);\n }\n}\n"],"mappings":"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","names":["ObjectSchema","Field"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/plugin-security",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.3",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Security Plugin for ObjectStack — RBAC, RLS, and Field-Level Security Runtime",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@objectstack/core": "4.0.
|
|
17
|
-
"@objectstack/spec": "4.0.
|
|
16
|
+
"@objectstack/core": "4.0.3",
|
|
17
|
+
"@objectstack/spec": "4.0.3"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@types/node": "^25.
|
|
20
|
+
"@types/node": "^25.6.0",
|
|
21
21
|
"typescript": "^6.0.2",
|
|
22
|
-
"vitest": "^4.1.
|
|
22
|
+
"vitest": "^4.1.4"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsup --config ../../../tsup.config.ts",
|
|
@@ -21,23 +21,32 @@ describe('SecurityPlugin', () => {
|
|
|
21
21
|
|
|
22
22
|
it('should register services during init', async () => {
|
|
23
23
|
const plugin = new SecurityPlugin();
|
|
24
|
+
const manifestService = { register: vi.fn() };
|
|
24
25
|
const ctx: any = {
|
|
25
26
|
logger: { info: vi.fn(), warn: vi.fn() },
|
|
26
27
|
registerService: vi.fn(),
|
|
27
|
-
getService: vi.fn()
|
|
28
|
+
getService: vi.fn().mockImplementation((name: string) => {
|
|
29
|
+
if (name === 'manifest') return manifestService;
|
|
30
|
+
return undefined;
|
|
31
|
+
}),
|
|
28
32
|
};
|
|
29
33
|
await plugin.init(ctx);
|
|
30
34
|
expect(ctx.registerService).toHaveBeenCalledWith('security.permissions', expect.any(PermissionEvaluator));
|
|
31
35
|
expect(ctx.registerService).toHaveBeenCalledWith('security.rls', expect.any(RLSCompiler));
|
|
32
36
|
expect(ctx.registerService).toHaveBeenCalledWith('security.fieldMasker', expect.any(FieldMasker));
|
|
37
|
+
expect(manifestService.register).toHaveBeenCalled();
|
|
33
38
|
});
|
|
34
39
|
|
|
35
40
|
it('should warn and return when objectql service is missing', async () => {
|
|
36
41
|
const plugin = new SecurityPlugin();
|
|
42
|
+
const manifestService = { register: vi.fn() };
|
|
37
43
|
const ctx: any = {
|
|
38
44
|
logger: { info: vi.fn(), warn: vi.fn() },
|
|
39
45
|
registerService: vi.fn(),
|
|
40
|
-
getService: vi.fn().mockImplementation(() => {
|
|
46
|
+
getService: vi.fn().mockImplementation((name: string) => {
|
|
47
|
+
if (name === 'manifest') return manifestService;
|
|
48
|
+
throw new Error('not found');
|
|
49
|
+
}),
|
|
41
50
|
};
|
|
42
51
|
await plugin.init(ctx);
|
|
43
52
|
await plugin.start(ctx);
|
|
@@ -46,10 +55,14 @@ describe('SecurityPlugin', () => {
|
|
|
46
55
|
|
|
47
56
|
it('should warn when objectql does not support middleware', async () => {
|
|
48
57
|
const plugin = new SecurityPlugin();
|
|
58
|
+
const manifestService = { register: vi.fn() };
|
|
49
59
|
const ctx: any = {
|
|
50
60
|
logger: { info: vi.fn(), warn: vi.fn() },
|
|
51
61
|
registerService: vi.fn(),
|
|
52
|
-
getService: vi.fn().
|
|
62
|
+
getService: vi.fn().mockImplementation((name: string) => {
|
|
63
|
+
if (name === 'manifest') return manifestService;
|
|
64
|
+
return {}; // objectql without registerMiddleware
|
|
65
|
+
}),
|
|
53
66
|
};
|
|
54
67
|
await plugin.init(ctx);
|
|
55
68
|
await plugin.start(ctx);
|
|
@@ -59,10 +72,14 @@ describe('SecurityPlugin', () => {
|
|
|
59
72
|
it('should register middleware when objectql supports it', async () => {
|
|
60
73
|
const plugin = new SecurityPlugin();
|
|
61
74
|
const registerMiddleware = vi.fn();
|
|
75
|
+
const manifestService = { register: vi.fn() };
|
|
62
76
|
const ctx: any = {
|
|
63
77
|
logger: { info: vi.fn(), warn: vi.fn() },
|
|
64
78
|
registerService: vi.fn(),
|
|
65
|
-
getService: vi.fn().
|
|
79
|
+
getService: vi.fn().mockImplementation((name: string) => {
|
|
80
|
+
if (name === 'manifest') return manifestService;
|
|
81
|
+
return { registerMiddleware };
|
|
82
|
+
}),
|
|
66
83
|
};
|
|
67
84
|
await plugin.init(ctx);
|
|
68
85
|
await plugin.start(ctx);
|
package/src/security-plugin.ts
CHANGED
|
@@ -38,8 +38,8 @@ export class SecurityPlugin implements Plugin {
|
|
|
38
38
|
ctx.registerService('security.rls', this.rlsCompiler);
|
|
39
39
|
ctx.registerService('security.fieldMasker', this.fieldMasker);
|
|
40
40
|
|
|
41
|
-
// Register security system objects
|
|
42
|
-
ctx.
|
|
41
|
+
// Register security system objects via the manifest service.
|
|
42
|
+
ctx.getService<{ register(m: any): void }>('manifest').register({
|
|
43
43
|
id: 'com.objectstack.security',
|
|
44
44
|
name: 'Security',
|
|
45
45
|
version: '1.0.0',
|
|
@@ -48,6 +48,23 @@ export class SecurityPlugin implements Plugin {
|
|
|
48
48
|
objects: [SysRole, SysPermissionSet],
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
// Contribute navigation items to the Setup App (if SetupPlugin is loaded).
|
|
52
|
+
try {
|
|
53
|
+
const setupNav = ctx.getService<{ contribute(c: any): void }>('setupNav');
|
|
54
|
+
if (setupNav) {
|
|
55
|
+
setupNav.contribute({
|
|
56
|
+
areaId: 'area_administration',
|
|
57
|
+
items: [
|
|
58
|
+
{ id: 'nav_roles', type: 'object', label: 'Roles', objectName: 'role', icon: 'shield-check', order: 60 },
|
|
59
|
+
{ id: 'nav_permission_sets', type: 'object', label: 'Permission Sets', objectName: 'permission_set', icon: 'lock', order: 70 },
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
ctx.logger.info('Security navigation items contributed to Setup App');
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// SetupPlugin not loaded — skip silently
|
|
66
|
+
}
|
|
67
|
+
|
|
51
68
|
ctx.logger.info('Security Plugin initialized');
|
|
52
69
|
}
|
|
53
70
|
|