New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
user-initiated sharing #4594
user-initiated sharing #4594
Conversation
Adding some notes from today's meeting (thanks everyone!)
|
I've got some time to put into this again before getting buried in the end of the semester, and I'm wondering if the Removing Another question is about modifying permissions. Normally, permissions are a single dict per user (i.e. a user has X permissions) and don't expire. But as written, a Server can be shared with a User more than once with different permissions and different expiration. This enables the possibility of e.g. "alice has read-only permissions to bob's server, but bob can grant alice elevated permissions for one hour, which will then revert to before." Again, this is a weird situation, and I don't really like being in the business of weird situations, permissions-wise. So I see two main options in terms of permissions modification:
So it seems to me that permissions should be a single record per
Expiration is really the only thing that complicates this, because otherwise they are just additions/subtractions from a list. I'm thinking 3 makes the most sense, because there's no guessing involved. It's also not common for permissions to expire. Finally, one last option is a bit in-between, but more complex to implement, which is to remove the concept of expiration from user permissions entirely, and only apply them to invitation codes. I.e. I can use the InviteCode approach to share permissions, and those permissions can expire, and they are tied to the expiration of the invite code. But only permissions granted that way. Right now, I have invitations as a temporary mechanism that exchanges a code for a real change in permissions, but once exchanged, there is no relationship between the code and the permissions granted (i.e. it's exactly the same as sharing with a specific user, you just didn't need to know the username beforehand). Changing this, such that permissions-from-invitations are more substantially different from permissions-granted-directly, would I think make expiration sensible (it makes a bit of sense to be 'invited' a few times), such that when a code expires or is revoked, all permissions granted by that code are also revoked. I really want to make this first version as simple as possible, but combining invite codes and permission expiration is making this hairier than I'd like. I think it should be on the table to drop expiration of permissions entirely. It's quite a rare feature, and much more common for tokens than users. Yet another possible simplification is to make the invite code singular per server, again matching Google Docs - where "people with the link" permissions are singular, and can't vary (they also don't expire, but can be revoked). So here's a proposal that I think simplifies things, assuming we want to preserve the desire for sharing to expire:
I'm on the fence about whether a given server should be allowed to have more than one code at a time. It makes sense structurally and in principle, but again this is something I haven't seen elsewhere and I don't like being in the business of unique things permissions-wise. |
I agree with starting with something simple, especially if it's designed so that other features can be added without breaking changes. Token sharing with automatic expiry is nice, how much demand is there for it? Is it essential for a first release? Looking at the overall plan for sharing, does all of it need to be in core JupyterHub or could some of it be done in extensions/services? |
I think it needs to be in Hub, because there is currently no mechanism by which an extension can grant permissions at runtime. If I were to make the simplest user-initiated sharing proposal, it wouldn't include expiration:
That is: permissions are permanent (can be revoked, but do not expire automatically), and sharing via code can permanently expand a user's permissions, but not grant anything temporarily. I believe that solves most of the challenges. The other simpler alternative is:
I think both make sense and are similarly simple. The first is more of an "invite code" model, where you are inviting folks to a server to stay, whereas the second is a "people with the link" model. The latter is easier to revoke permissions, while the former is easier to have short-term invite links. I don't know what folks would find most comfortable / familiar. Another possibly simpler approach from the Hub perspective would be to implement a generic API for roles (#3858), then a 'sharing service' could layer on top the higher-level representation of what a 'share' is. That's not simpler for me, who needs to implement both, but it might be simpler to implement in the Hub (roles in general present their own challenges if they can be user-created). This is ultimately more complex overall. |
Updating notes from today's meeting:
I think this is simple and clear enough that I can get something working before needing to bug folks for more feedback. |
- create share via api - revoke share via api - permissions granted and tested use pydantic for request validation
Still some work to do, but I now have sharing fully working where a user can grant and revoke access to their server to another user. Still todo:
The fact that shares can be referenced both by the server and the user it's granted to has been surprisingly hard to make sense of in the REST API. As it is now, I have: By server:
By user/group:
|
Making progress, working on sharing codes, and came up with another question: Is a sharing code more like a token, which should be stored hashed and irretrievable (must issue new code if you need access) or a more permanent, retrievable value. I think how folks use are expected to use them informs this. If we're thinking of slack or otherwise invitation-style links, I think the token format is better. If we're thinking of Google docs style "share via code", then being able to retrieve the code so you don't reissue codes too often makes sense. Given that share codes are immediately exchanged for sharing permission, and permissions granted via the code cannot be readily tracked or revoked, I'm inclined to go with the token approach. |
only store hashed share codes now, same as tokens, client secrets
Still quite a few tests to write and things to update, but now I've tested the full process of issuing and accepting a new sharing code, and things are reasonably complete (not finished, but complete). For this PR, what I plan to finish:
What I don't plant to do in this PR:
UI is its own whole can of worms, and I'd like to be able to make a 5.0 release with sharing made possible via the API, and consider UI on a longer timescale (and via extensions in the meantime) |
without need for oauth_client to be predefined
…arsed_scopes dict accepts expanded_scopes as well, which is immediately parsed does not accept raw scopes
tuple, list, set in, tuple out
let's migrate to a JSONSet using FrozenSet later
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see a blocker. I've not reviewed all changes in detail due to limited capacity =/
I've glossed over changes in orm.py and scopes.py, and hardly looked at shares.py. Nothing seems off from what I've seen in all other code sections.
Amazing effort into this @minrk! There are some unaddressed reivew points above, but none is critical so I'll leave it to you to decide if you want to take action on them before going for a merge.
this was my confusion mixing internal variables with the rest API. |
and comment on why subset of fields is chosen
This is because unless we specify individual examples for everything, all timestamp fields use the same value (the current time). We can specify manual examples. I've expanded the description to state that expires_at is always in the future, and |
1. Warn that a read-only constraint is enforced in user environments
I think this info is critical info that should be surfaced very intentionally to admins.
Edit: handled via #4699 |
requires consolidating redirect validation into _validate_next_url to allow re-use
I'll think about where to highlight it. I don't think it's particularly relevant to sharing, so I wouldn't add anything to the sharing docs, but it is relevant to the custom_scopes definitions. The user-control over the environment is discussed already in the security doc, with the statement that:
including installing packages, modifying $PATH, etc., all of which enable users to compromise security of their own servers. |
/hub/ prefix is no longer helpful
Thanks for the reviews! I believe CI will be all green and all reivew has been addressed (at least that I didn't forget about). This is a big PR with lots of iterations and merges from main to preserve the review history, so I was planning to squash & merge when folks approve it, rather than merging all the iterations and testing in the history. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Amazing effort into this @minrk!!! 👍 For squash/merge/get this pr into main branch!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Amazing work !
Thanks so much for the reviews! |
Very happy to see this merged 🎉 What is the release plan for this feature? Is it backwards compatible and could land in say 4.1 or will it need to wait for 5.0? |
This is the main feature for 5.0, which I plan to start prereleases for shortly. Working on one more fix before that can start. |
Implements the possibility of users granting each other permission to access their servers.
Summary of the design for review:
shares
permission to all or a subset of usersread:users:name
orread:groups:name
permission in order to grant specific access)shares
permission, no other permissionsusers:shares
scope (part of default user scopes)Questions to consider:
read:users:name
limits who they can share with by name, but there is not a mechanism to limit sharing by name if the user has other reasons to haveread:users:name
shares,read:users:name!user=xyz
, then you can grantxyz
access to your server without their participation at all. This is a major avenue for abuse and spam in Google Docs / Calendar, but I'm not sure that applies to JupyterHub? There's nothing you can do with a shared server once you grant the permission until the shared-with user visits the server and proceeds through the oauth confirmation steps.Scope limitations:
Original PR description below
Absolutely not close to working or ready for review (I'll be rewriting history a bunch), but opening as a placeholder since folks are asking about it.
The closest to ready is the new doc explaining how sharing is meant to work.
The idea is that a 'share' is a granting of permission (usually access) associated with a specific server to
shares are essentially mini roles, which always grant permissions restricted to a single server (usually just access:servers!server, but potentially including custom scopes, e.g. for read-only access). See #4349 for my thoughts on why I think this is probably going to work better than the more powerful but much more complex full user-initiated roles in #3858.
A Share
And users should be able to:
Managing sharing permissions is tied only to the server being shared, there is no limit placed on who it can be shared with. This also means an admin can grant access and the user can revoke that same access.
Questions:
xyz
, I can know whether there is a user with that name. Is that a problem?)?user
role?I'd like to start as minimal as possible while still being actually useful.
As with other things, I plan to implement this as 'api-only' first, and UI can be its own task. Sharing UI is hard, and I'd like to try it in an extension first.
closes #4349