jupyter_server
jupyter_server copied to clipboard
Contents permissions and RTC
Problem
With RTC, the contents API is shifting to the YDocWebSocketHandler, which e.g. handles saving documents automatically after a change. There is no PUT /api/contents/{path} endpoint that we can restrict to users having a contents:write permission, for instance. I see two ways of handling contents permissions in RTC:
- The
YDocWebSocketHandlercan be protected withcontents:write, but it means that the WebSocket should be accepted for a user who has onlycontents:read, and the Y updates which change the document should be filtered out for this user. In other words, the authorization is moving from an HTTP protocol level to a Y protocol level. - Another direction would be to let the responsibility to front-ends to allow a user to change a document. Indeed, they know if a user has write permissions through the
/api/meendpoint. That way, no Y update would be emitted for a user lacking write permissions, and the server would not even need to care about authorization at the Y protocol level.
Note that even for solution 1, the front-end has to disable changes to the document, otherwise the user would see that they're changing the document while in reality it wouldn't be true. It would even be catastrophic because the local document would loose synchronization with the remote document. But relying on front-ends for authorization (solution 2) could potentially open the door to malicious apps that could give users more rights than they have, so there might be a security risk.
cc @dmonad @SylvainCorlay @ellisonbg @afshin @minrk
It definitely cannot ever be the front-end's responsibility to implement anything security related, so 1. is really the only option. The front-end has to do what it needs to accurately reflect its permissions (e.g. making documents read-only and/or handling rejected edits), so it may still need the information needed for 2 to have a good experience.
It definitely cannot ever be the front-end's responsibility to implement anything security related, so 1. is really the only option.
+1
I think our assumption that "execute" permission wraps all requests to WebSockets (see the documentation) doesn't hold for the YDocWebSocketHandler. Indeed, if that's the case, then a user with only "read" permission wouldn't be able to read a document in collaborative mode.
Right. And it doesn't really hold for kernels either (read-only access to kernels makes sense, too). That's just the current state of the implementation that we don't have per-message permissions. It's not a limitation of the spec - permissions can be applied anywhere, but it's where the boundaries are now. Implementing permission on the Yjs socket isn't blocked by anything - we need to:
- define what the permissions should be, and
- apply the checks in the right places
And it doesn't really hold for kernels either (read-only access to kernels makes sense, too)
I guess this would mean being able to read IOPub messages?
- define what the permissions should be, and
I think a user with read permission would have the Y updates of other users applied to their document, and a user with write permission would also have their Y updates accepted and applied to the document. I don't think execute permission makes sense for the YDoc WebSocket.
- apply the checks in the right places
This will have to be handled in ypy-websocket, probably through a per-client "read-only" attribute.
For terminals too, read-only access makes sense. It means being able to see the output of an already opened terminal, but not being able to write into it.
I guess this would mean being able to read IOPub messages?
Yes. If people think it's useful, we could apply more granular permissions, e.g. being able to make completion requests, widget messages, etc. The spec makes us free to define any verb on any resource. We started with the coarsest necessary to describe the existing permissions, and can continually refine as we see what level of granularity makes sense.
I don't think execute permission makes sense for the YDoc WebSocket.
I think that's right. Execute only really makes sense for kernels and terminals.
One thing I'm wondering, up to now HTTP endpoints are protected by a single resource-permission combination, e.g.:
GET /api/contents/{path}:contents:readPOST /api/contents/{path}:contents:write
But since permissions are handled inside WebSockets, should their endpoint be protected by several permissions? For instance, the YDoc WebSocket should be opened if the user has any of yjs:read or yjs:write. Or do we assume that a write permission implies read also?
But since permissions are handled inside WebSockets, should their endpoint be protected by several permissions?
There's 3 ways to do this, I think:
- make the check for the request handler an OR check for any permissions that can be used in the handler, or
- guard the request itself only with the minimal permission required, and define a permission hierarchy, such that having e.g. write permissions expands to ensuring you always have read permissions, too, and then guard the request itself only with the minimal permissions.
- define separate permission for connection to the socket, and then guard individual messages with the appropriate permissions
Options 2. and 3. are really the same, just depending on whether the base permission is one of the contents action permissions or not. You don't need to implement the permission hierarchy (JupyterHub does, and calls these 'sub scopes'), that could be left to the Authorizer, leaving it technically possible to e.g. have write without read, which wouldn't work. I don't think that's great, though.
For RTC, it makes sense to me to have the Yjs connection itself be a separate permission, because it still makes perfect sense to read and write documents without ever connecting to Yjs.
In https://github.com/jupyter-server/jupyverse/pull/211 I have implemented 1.
@davidbrochart nice! I'm working on porting that to jupyter-server. Do you have any pointers on reversing transactions? Skipping messages successfully prevents propagating changes to the document and other peers, but the client whose transactions failed doesn't seem to get any indication that its changes didn't happen.
You mean sending the author of the transaction a transaction that reverses their change? I don't know if it's possible in Ypy/Yjs, maybe @dmonad knows? Or should it be the responsibility of the client? If it behaves well, it should already know if it's allowed to make such a transaction, right? We're only making sure "bad clients" cannot do anything wrong by ignoring their transactions, but is it very important to be "polite" with them?
In Yjs, it's not possible to accept some changes and discard others. However, you can make a distinction between read-only clients (discard their updates on the backend) and read-write clients.
You can reverse a change using the undo manager (https://docs.yjs.dev/api/undo-manager). You can also undo remote changes. However, it is not meant for reversing changes from read-only clients. A malicious client could probably find a way around it.
Yeah, I guess that's a second change to make in jupyterlab to handle its own permissions, and subscribe as a read-only client when it doesn't have the required permissions. Is there already an Issue for jupyterlab understanding its own permissions, post server 2.0?