Goal
An entitled user grants or revokes access for roles on a CMS path, at runtime, and it takes effect immediately. There is no UI for this today, and the mechanism behind it is a workaround.
What exists today
(no UI) -> api/constraints.js -> CMS __internal/roles-access.json
-> Quartz job, cron "0 * * * * ?" (= every MINUTE)
-> rewrites the registry artefact documents/security/roles.access
-> AccessSynchronizer -> DIRIGIBLE_SECURITY_ACCESS
-> AccessVerifier (cache refreshed on a 5s modified-check)
-> CmisFacade.isAllowed(path, method) [called only from cmis.ts]
Six hops; worst-case propagation ≈ one minute + a synchronization round + 5s; and a runtime job that rewrites a synchronizer-managed artefact, which inverts the platform's own line that synchronizers reconcile declared artefacts. Since nothing calls api/constraints.js any more, the loop currently maintains an empty {"constraints":[]} forever and churns the artefact for nothing.
Two further defects:
- The ACL store is global while the content it protects is per-tenant.
Access is a BaseSynchronizer over the shared DIRIGIBLE_SECURITY_ACCESS, but every CMS path is tenant-resolved by TenantPathResolver — so a rule set by one tenant governs every tenant's identically-named folder.
- Enforcement lives in the GraalJS wrapper (separate issue).
Proposal — a CMS-only ACL: own store, own guard, own UI
The runtime ACL is user data, not an authoring artifact — which is exactly why the current design had to smuggle its state through a job. Modelling it as tenant data (the tenant-configuration shape) removes the whole contraption: no job, no artefact rewrite, no registry mirror, no scope: CMIS coupling. The generic .access stays what it is good at — static HTTP constraints authored by developers.
| Concern |
Design |
| Storage |
new per-tenant DIRIGIBLE_CMS_ACCESS (PATH, METHOD READ|WRITE, ROLE, CREATED_BY, CREATED_AT), raw SqlFactory against the tenant-routed default datasource, create-if-absent — the DocumentNumberStore / TenantConfigurationStore pattern. A JPA entity would be wrong: JPA resolves against the default schema and is not tenant-routed |
| Resolution |
one service over a per-tenant cache invalidated on write — a grant takes effect on the next request, no polling window |
| Enforcement |
a guard consulted for CMS paths in the Java layer, so every consumer inherits it. Safe to apply platform-wide because no rules for a path = open: nothing changes until an admin writes a rule |
| Editing |
/services/documents/access (ADMINISTRATOR/OPERATOR): GET ?path= effective + inherited grants, PUT grant, DELETE revoke |
| UI |
a "Manage access" dialog on a folder — a role × {read, write} matrix with inherited rows shown greyed — in both surfaces: the AngularJS perspective (<bk-dialog> off the folder context menu) and the shared Harmonia Documents page (which covers the application / my / partner / admin shells at once) |
Semantics, specified once and unit-tested: inheritance, most-specific-path-wins, roles OR, no-rules-is-open, WRITE independent of READ, __internal neither listable nor grantable. The three system-context escapes must be preserved (see the enforcement issue).
Migration: on first start per tenant, import any rows from the legacy __internal/roles-access.json (and any scope: CMIS rows in DIRIGIBLE_SECURITY_ACCESS) once, logged; then stop reading them and delete the job, constraintsProcessor and api/constraints.js.
Deliberate trade-off: without a declaration artefact a project cannot ship baseline folder ACLs. Nothing does today (the artefact is runtime-written and empty), so nothing real is lost; if it is ever wanted it returns as a .cmsaccess declaration with the .numbers semantics — insert-if-absent, never overwrite a user's row.
Tests that must exist, because these are the failure modes of what is being replaced: a grant/revoke takes effect on the next request with no sleep; a rule in tenant A does not affect tenant B; each of the three system-context escapes still succeeds under a restrictive rule; a restricted folder disappears from a listing and its children are unreachable by direct path (today's exact-path bug means it does not); DIRIGIBLE_CMS_ROLES_ENABLED=false disables enforcement wholesale.
Goal
An entitled user grants or revokes access for roles on a CMS path, at runtime, and it takes effect immediately. There is no UI for this today, and the mechanism behind it is a workaround.
What exists today
Six hops; worst-case propagation ≈ one minute + a synchronization round + 5s; and a runtime job that rewrites a synchronizer-managed artefact, which inverts the platform's own line that synchronizers reconcile declared artefacts. Since nothing calls
api/constraints.jsany more, the loop currently maintains an empty{"constraints":[]}forever and churns the artefact for nothing.Two further defects:
Accessis aBaseSynchronizerover the sharedDIRIGIBLE_SECURITY_ACCESS, but every CMS path is tenant-resolved byTenantPathResolver— so a rule set by one tenant governs every tenant's identically-named folder.Proposal — a CMS-only ACL: own store, own guard, own UI
The runtime ACL is user data, not an authoring artifact — which is exactly why the current design had to smuggle its state through a job. Modelling it as tenant data (the tenant-configuration shape) removes the whole contraption: no job, no artefact rewrite, no registry mirror, no
scope: CMIScoupling. The generic.accessstays what it is good at — static HTTP constraints authored by developers.DIRIGIBLE_CMS_ACCESS(PATH,METHODREAD|WRITE,ROLE,CREATED_BY,CREATED_AT), rawSqlFactoryagainst the tenant-routed default datasource, create-if-absent — theDocumentNumberStore/TenantConfigurationStorepattern. A JPA entity would be wrong: JPA resolves against the default schema and is not tenant-routed/services/documents/access(ADMINISTRATOR/OPERATOR):GET ?path=effective + inherited grants,PUTgrant,DELETErevoke<bk-dialog>off the folder context menu) and the shared Harmonia Documents page (which covers the application / my / partner / admin shells at once)Semantics, specified once and unit-tested: inheritance, most-specific-path-wins, roles OR, no-rules-is-open, WRITE independent of READ,
__internalneither listable nor grantable. The three system-context escapes must be preserved (see the enforcement issue).Migration: on first start per tenant, import any rows from the legacy
__internal/roles-access.json(and anyscope: CMISrows inDIRIGIBLE_SECURITY_ACCESS) once, logged; then stop reading them and delete the job,constraintsProcessorandapi/constraints.js.Deliberate trade-off: without a declaration artefact a project cannot ship baseline folder ACLs. Nothing does today (the artefact is runtime-written and empty), so nothing real is lost; if it is ever wanted it returns as a
.cmsaccessdeclaration with the.numberssemantics — insert-if-absent, never overwrite a user's row.Tests that must exist, because these are the failure modes of what is being replaced: a grant/revoke takes effect on the next request with no sleep; a rule in tenant A does not affect tenant B; each of the three system-context escapes still succeeds under a restrictive rule; a restricted folder disappears from a listing and its children are unreachable by direct path (today's exact-path bug means it does not);
DIRIGIBLE_CMS_ROLES_ENABLED=falsedisables enforcement wholesale.