Files
lasuite-docs/src/yhub-server/permissions.js
T
Kevin JahnsandManuel Raynaud 4041d38db2 ⬆️(collaboration) upgrade yhub to 0.8.0 and adopt its permission model
yhub 0.8.0 retires the 'r' | 'rw' | null access vocabulary. The auth
plugin now answers a typed permission object stating, facet by facet,
what a subject may do with a document, and yhub enforces every facet
itself - on the websocket and on the REST routes alike. Three rules we
wanted but could not express under the old vocabulary become one-line
facets. Our whole access policy now lives in
src/yhub-server/permissions.js, apart from the server so that it can be
read and tested without standing up redis and postgres.

Read-only users no longer share their cursor #2544. A read-only
connection could still propagate awareness updates to everyone else in
the document, even though its document updates were already dropped.
Presence is now a permission of its own, separate from the right to
edit: a reader receives it and never publishes it. The collaboration
server enforces that rather than trusting the editor to stay quiet, so a
modified or stale client changes nothing. The frontend has to know it
too - the http fallback provider has no receive-only mode, so a reader's
provider is built with no awareness instance at all, or its first PATCH
would take a 403 and close it for good.

The browser is granted only the two routes it uses, the websocket and
ydoc for the http fallback. Everything else - history, rollback, prune,
and every backend-internal endpoint - is refused to it, as is any
endpoint a future release adds, because the grant names no wildcard.
create-ydoc in particular was reachable by any signed-in editor and is
now the backend's alone.

Anonymous visitors are given the userid "anonymous" rather than no
identity at all, which is what lets them keep editing public documents:
yhub refuses the upgrade of a caller that holds the write but cannot be
attributed. Their edits share one author.

Room is DocRef throughout, which is a rename of object keys and not only
of types: the worker event payload and the stream message lookup both
carry it, and both fail silently rather than loudly when missed.

Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
2026-09-22 16:01:41 +02:00

82 lines
3.6 KiB
JavaScript

/**
* Docs' access policy, as yhub 0.8 permission objects.
*
* Kept apart from `server.js` so it can be read — and tested — without standing
* up redis and postgres: these three tables *are* the policy, and they are the
* only thing between a reader and someone else's document.
*
* A permission object states, per facet, what a subject may do with one
* document; yhub enforces every facet itself, on the websocket and on the REST
* routes alike. Masks are positional `crud` strings where `-` denies, so
* `'-r--'` is read-only and `'----'` grants nothing.
*/
/**
* What a browser may do with a document, from the backend's verdict on it.
* `canEdit` is `abilities.update`; read access was settled by `abilities.retrieve`
* before this is reached.
*
* `awareness` is why Docs moved to yhub 0.8: a reader receives presence but
* never publishes it (suitenumerique/docs#2544 — a read-only connection used to
* propagate cursors even though its document updates were dropped). yhub enforces
* it on both transports: it drops a read-only connection's awareness message on
* the socket, and refuses the `awareness` field of `PATCH /ydoc`. Note this is a
* deliberate departure from yhub's own default, which grants a reader `'-ru-'`
* and documents read-only cursors as a feature.
*
* `ydoc` withholds `c`, which yhub's migration table would grant an editor: `c`
* is reserved for "may populate the initial content", and in Docs that is
* `create-ydoc` with the admin token. `u` alone already creates the document on
* first write.
*
* No `history` facet, and that is load-bearing rather than an omission: it is
* what makes yhub refuse a `gc=false` connection with a 403. Docs users are
* served the garbage-collected document; the full history is the backend's.
*
* No `delete` facet: deleting a document is Django's, through the admin token.
*
* No `'*'` endpoint fallback, so everything not named here is denied. The browser
* calls exactly two routes — the websocket, and `ydoc` for the http fallback.
* `activity`, `changeset`, `rollback`, `prune` and every custom endpoint are
* closed to it. Under 0.7 this fence was a `purpose != null` check in
* `getAccessType`, which `create-ydoc` slipped through by declaring no purpose.
*/
export const browserDocumentPermissions = (canEdit) => ({
type: 'permissions:document:v1',
ydoc: canEdit ? '-ru-' : '-r--',
awareness: canEdit ? '-ru-' : '-r--',
endpoint: {
// `r` opens the socket, `u` admits document updates over it
ws: canEdit ? '-ru-' : '-r--',
// GET is `r` and PATCH is `u`; DELETE (`d`) stays out — see `delete` above
ydoc: canEdit ? '-ru-' : '-r--',
},
});
/**
* Django's admin token: everything, with one deliberate hole. `delete: ['soft']`
* and not `'hard'` — yhub 0.8 made `DELETE /ydoc?hard=true` reachable over REST
* for the first time, and Docs keeps irreversible erasure programmatic, behind
* `reset-ydoc`, exactly as `yhub_services.delete_ydoc` describes.
*/
export const adminDocumentPermissions = {
type: 'permissions:document:v1',
ydoc: 'cru-',
awareness: '-ru-',
history: { from: 0 },
delete: ['soft'],
endpoint: { '*': 'crud' },
};
/**
* The global-scoped routes, served to anyone: the JWKS, which carries public keys
* and which the backend must read before it can authenticate anything we send it,
* and the two probes, which kubernetes calls with no cookie and no token. Read
* only, and named individually — a global endpoint added later is denied until it
* is listed here.
*/
export const publicGlobalPermissions = {
type: 'permissions:global:v1',
endpoint: { ping: '-r--', ready: '-r--', jwks: '-r--' },
};