thatcher 1.0.68 → 1.0.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.js +2 -2
- package/src/lib/config-generator-engine.js +15 -0
- package/src/server/server.js +18 -1
- package/src/ui/entity-renderer.js +14 -2
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -305,9 +305,9 @@ export class Thatcher {
|
|
|
305
305
|
return list(entity, where, opts);
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
-
async get(entity, id) {
|
|
308
|
+
async get(entity, id, opts = {}) {
|
|
309
309
|
const { get } = await import(resolveModule('./lib/busybase/store.js'));
|
|
310
|
-
return get(entity, id);
|
|
310
|
+
return get(entity, id, opts);
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
async create(entity, data, user) {
|
|
@@ -513,6 +513,21 @@ export class ConfigGeneratorEngine {
|
|
|
513
513
|
};
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
+
// Field-level RBAC: a field's visible_to/editable_by (already present on
|
|
517
|
+
// spec.fields[key] via the `...field` spread above) is compiled into
|
|
518
|
+
// spec.fieldPermissions, the shape permission.service.js's checkFieldAccess
|
|
519
|
+
// already reads. A field with neither key contributes no entry here, so
|
|
520
|
+
// checkFieldAccess's `if (!perm) return true` keeps every role's access
|
|
521
|
+
// exactly as it was before this feature existed -- no regression for the
|
|
522
|
+
// overwhelming majority of fields that never set either key.
|
|
523
|
+
for (const [key, field] of Object.entries(spec.fields)) {
|
|
524
|
+
if (!field.visible_to && !field.editable_by) continue;
|
|
525
|
+
spec.fieldPermissions = spec.fieldPermissions || {};
|
|
526
|
+
spec.fieldPermissions[key] = {};
|
|
527
|
+
if (field.visible_to) spec.fieldPermissions[key].view = field.visible_to;
|
|
528
|
+
if (field.editable_by) spec.fieldPermissions[key].edit = field.editable_by;
|
|
529
|
+
}
|
|
530
|
+
|
|
516
531
|
if (entityDef.permission_template) {
|
|
517
532
|
const matrix = this.getPermissionTemplate(entityDef.permission_template);
|
|
518
533
|
const access = {};
|
package/src/server/server.js
CHANGED
|
@@ -382,22 +382,37 @@ async function handleGenericCrud(req, res, entity, id, action, thatcher, configE
|
|
|
382
382
|
let result;
|
|
383
383
|
let status = 200;
|
|
384
384
|
|
|
385
|
+
const { permissionService } = await import('../services/permission.service.js');
|
|
386
|
+
|
|
385
387
|
switch (req.method) {
|
|
386
388
|
case 'GET':
|
|
387
389
|
if (id) {
|
|
388
|
-
|
|
390
|
+
// get(...,{user}) applies the same row/org-access scoping every other
|
|
391
|
+
// read path in the codebase uses -- this generic-CRUD GET-by-id had
|
|
392
|
+
// none at all, so any authenticated user could read any record by id
|
|
393
|
+
// across organizations. filterFields then strips any field the
|
|
394
|
+
// caller's role isn't visible_to, the field-level half of this pass.
|
|
395
|
+
result = await thatcher.get(entity, id, { user });
|
|
389
396
|
if (!result) {
|
|
390
397
|
res.writeHead(404);
|
|
391
398
|
res.end(JSON.stringify({ error: 'Not found' }));
|
|
392
399
|
return;
|
|
393
400
|
}
|
|
401
|
+
result = permissionService.filterFields(user, spec, result);
|
|
394
402
|
} else {
|
|
395
403
|
result = await thatcher.list(entity, {}, { user });
|
|
404
|
+
result = result.map(r => permissionService.filterFields(user, spec, r));
|
|
396
405
|
}
|
|
397
406
|
break;
|
|
398
407
|
|
|
399
408
|
case 'POST':
|
|
409
|
+
// enforceEditPermissions is the authoritative field-level write check --
|
|
410
|
+
// it throws if the payload touches a field the caller's role isn't
|
|
411
|
+
// editable_by, so a hand-crafted request bypassing the rendered form
|
|
412
|
+
// cannot smuggle a restricted field in.
|
|
413
|
+
permissionService.enforceEditPermissions(user, spec, body);
|
|
400
414
|
result = await thatcher.create(entity, body, user);
|
|
415
|
+
result = permissionService.filterFields(user, spec, result);
|
|
401
416
|
status = 201;
|
|
402
417
|
break;
|
|
403
418
|
|
|
@@ -408,7 +423,9 @@ async function handleGenericCrud(req, res, entity, id, action, thatcher, configE
|
|
|
408
423
|
res.end(JSON.stringify({ error: 'ID required' }));
|
|
409
424
|
return;
|
|
410
425
|
}
|
|
426
|
+
permissionService.enforceEditPermissions(user, spec, body);
|
|
411
427
|
result = await thatcher.update(entity, id, body, user);
|
|
428
|
+
result = permissionService.filterFields(user, spec, result);
|
|
412
429
|
break;
|
|
413
430
|
|
|
414
431
|
case 'DELETE':
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { page, dataTable } from '@/ui/layout.js'
|
|
2
2
|
import { fmtVal, TOAST_SCRIPT, esc } from '@/ui/render-helpers.js'
|
|
3
3
|
import { canCreate, canEdit, canDelete } from '@/ui/permissions-ui.js'
|
|
4
|
+
import { permissionService } from '@/services/permission.service.js'
|
|
4
5
|
|
|
5
6
|
export function renderEntityList(entityName, items, spec, user, options = {}) {
|
|
6
7
|
const label = spec?.labelPlural || spec?.label || entityName
|
|
@@ -85,7 +86,10 @@ function formatFieldValue(k, v, entityName, f) {
|
|
|
85
86
|
|
|
86
87
|
export function renderEntityDetail(entityName, item, spec, user) {
|
|
87
88
|
const label = spec?.label || entityName
|
|
88
|
-
|
|
89
|
+
// A field the caller's role isn't visible_to must never reach rendered HTML,
|
|
90
|
+
// not just be CSS-hidden -- filterFields drops it from `fields` entirely so
|
|
91
|
+
// it can't appear in visibleFields below however this function evolves.
|
|
92
|
+
const fields = permissionService.filterFields(user, spec || {}, spec?.fields || {})
|
|
89
93
|
const userCanEdit = canEdit(user, entityName)
|
|
90
94
|
const userCanDelete = canDelete(user, entityName)
|
|
91
95
|
|
|
@@ -141,7 +145,15 @@ export function renderEntityDetail(entityName, item, spec, user) {
|
|
|
141
145
|
|
|
142
146
|
export function renderEntityForm(entityName, item, spec, user, isNew = false, refOptions = {}, templates = []) {
|
|
143
147
|
const label = spec?.label || entityName
|
|
144
|
-
|
|
148
|
+
// A field the caller's role isn't editable_by must not be offered in the
|
|
149
|
+
// form at all -- omitting it here is UX (a clean form), the real
|
|
150
|
+
// enforcement is enforceEditPermissions rejecting the field server-side if
|
|
151
|
+
// a raw request includes it anyway.
|
|
152
|
+
const allFields = spec?.fields || {}
|
|
153
|
+
const fields = {}
|
|
154
|
+
for (const [k, f] of Object.entries(allFields)) {
|
|
155
|
+
if (permissionService.checkFieldAccess(user, spec || {}, k, 'edit')) fields[k] = f
|
|
156
|
+
}
|
|
145
157
|
const lbl = (k, f, req) => `<label class="form-label" for="field-${k}">${esc(f.label||k)}${req ? '<span class="req">*</span>' : ''}</label>`
|
|
146
158
|
const formFields = Object.entries(fields).filter(([k, f]) => k !== 'id' && !f.auto && !f.readOnly && !f.auto_generate && k !== 'password_hash').map(([k, f]) => {
|
|
147
159
|
let val = item?.[k] ?? f.default ?? ''
|