mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-13 13:03:01 +00:00
Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a1824a4b3a | ||
![]() |
e28b234cc5 | ||
![]() |
88be9eda05 | ||
![]() |
916632fcc8 | ||
![]() |
0737e6f581 | ||
![]() |
02cbb5f337 | ||
![]() |
99e2720b0f | ||
![]() |
b405361674 | ||
![]() |
0d53ead186 | ||
![]() |
cedd176a34 | ||
![]() |
58e5022d28 | ||
![]() |
f395acd9ca |
@@ -6,7 +6,7 @@ info:
|
||||
description: The REST API for JupyterHub
|
||||
license:
|
||||
name: BSD-3-Clause
|
||||
version: 4.1.4
|
||||
version: 4.1.6
|
||||
servers:
|
||||
- url: /hub/api
|
||||
security:
|
||||
@@ -1461,8 +1461,9 @@ components:
|
||||
Access the admin page. Permission to take actions via the admin
|
||||
page granted separately.
|
||||
admin:users:
|
||||
Read, write, create and delete users and their authentication
|
||||
state, not including their servers or tokens.
|
||||
Read, modify, create, and delete users and their authentication
|
||||
state, not including their servers or tokens. This is an extremely privileged
|
||||
scope and should be considered tantamount to superuser.
|
||||
admin:auth_state: Read a user’s authentication state.
|
||||
users:
|
||||
Read and write permissions to user models (excluding servers, tokens
|
||||
@@ -1493,8 +1494,8 @@ components:
|
||||
read:tokens: Read user tokens.
|
||||
admin:groups: Read and write group information, create and delete groups.
|
||||
groups:
|
||||
Read and write group information, including adding/removing users
|
||||
to/from groups.
|
||||
"Read and write group information, including adding/removing any
|
||||
users to/from groups. Note: adding users to groups may affect permissions."
|
||||
list:groups: List groups, including at least their names.
|
||||
read:groups: Read group models.
|
||||
read:groups:name: Read group names.
|
||||
|
@@ -178,6 +178,32 @@ Note that only the {ref}`horizontal filtering <horizontal-filtering-target>` can
|
||||
Metascopes `self` and `all`, `<resource>`, `<resource>:<subresource>`, `read:<resource>`, `admin:<resource>`, and `access:<resource>` scopes are predefined and cannot be changed otherwise.
|
||||
```
|
||||
|
||||
(granting-scopes)=
|
||||
|
||||
### Considerations when allowing users to grant permissions via the `groups` scope
|
||||
|
||||
In general, permissions are fixed by role assignments in configuration (or via Authenticator-managed roles in JupyterHub 5) and can only be modified by administrators who can modify the Hub configuration.
|
||||
|
||||
There is only one scope that allows users to modify permissions of themselves or others at runtime instead of via configuration:
|
||||
the `groups` scope, which allows adding and removing users from one or more groups.
|
||||
With the `groups` scope, a user can add or remove any users to/from any group.
|
||||
With the `groups!group=name` filtered scope, a user can add or remove any users to/from a specific group.
|
||||
There are two ways in which adding a user to a group may affect their permissions:
|
||||
|
||||
- if the group is assigned one or more roles, adding a user to the group may increase their permissions (this is usually the point!)
|
||||
- if the group is the _target_ of a filter on this or another group, such as `access:servers!group=students`, adding a user to the group can grant _other_ users elevated access to that user's resources.
|
||||
|
||||
With these in mind, when designing your roles, do not grant users the `groups` scope for any groups which:
|
||||
|
||||
- have roles the user should not have authority over, or
|
||||
- would grant them access they shouldn't have for _any_ user (e.g. don't grant `teachers` both `access:servers!group=students` and `groups!group=students` which is tantamount to the unrestricted `access:servers` because they control which users the `group=students` filter applies to).
|
||||
|
||||
If a group does not have role assignments and the group is not present in any `!group=` filter, there should be no permissions-related consequences for adding users to groups.
|
||||
|
||||
:::{note}
|
||||
The legacy `admin` property of users, which grants extreme superuser permissions and is generally discouraged in favor of more specific roles and scopes, may be modified only by other users with the `admin` property (e.g. added via `admin_users`).
|
||||
:::
|
||||
|
||||
(custom-scopes)=
|
||||
|
||||
### Custom scopes
|
||||
|
@@ -10,6 +10,34 @@ command line for details.
|
||||
|
||||
## 4.1
|
||||
|
||||
### 4.1.6 - 2024-07-31
|
||||
|
||||
4.1.6 is a **security release**, fixing [CVE-2024-41942].
|
||||
All JupyterHub deployments are encouraged to upgrade,
|
||||
but only those with users having the `admin:users` scope are affected.
|
||||
The [full advisory][CVE-2024-41942] will be published 7 days after the release.
|
||||
|
||||
[CVE-2024-41942]: https://github.com/jupyterhub/jupyterhub/security/advisories/GHSA-9x4q-3gxw-849f
|
||||
|
||||
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/4.1.5...4.1.6))
|
||||
|
||||
### 4.1.5 - 2024-04-04
|
||||
|
||||
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/4.1.4...4.1.5))
|
||||
|
||||
#### Bugs fixed
|
||||
|
||||
- singleuser mixin: include check_xsrf_cookie in overrides [#4771](https://github.com/jupyterhub/jupyterhub/pull/4771) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
|
||||
#### Contributors to this release
|
||||
|
||||
The following people contributed discussions, new ideas, code and documentation contributions, and review.
|
||||
See [our definition of contributors](https://github-activity.readthedocs.io/en/latest/#how-does-this-tool-define-contributions-in-the-reports).
|
||||
|
||||
([GitHub contributors page for this release](https://github.com/jupyterhub/jupyterhub/graphs/contributors?from=2024-03-30&to=2024-04-04&type=c))
|
||||
|
||||
@consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2024-03-30..2024-04-04&type=Issues)) | @manics ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amanics+updated%3A2024-03-30..2024-04-04&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2024-03-30..2024-04-04&type=Issues))
|
||||
|
||||
### 4.1.4 - 2024-03-30
|
||||
|
||||
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/4.1.3...4.1.4))
|
||||
|
@@ -3,7 +3,7 @@
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
# version_info updated by running `tbump`
|
||||
version_info = (4, 1, 4, "", "")
|
||||
version_info = (4, 1, 6, "", "")
|
||||
|
||||
# pep 440 version: no dot before beta/rc, but before .dev
|
||||
# 0.1.0rc1
|
||||
|
@@ -186,6 +186,8 @@ class UserListAPIHandler(APIHandler):
|
||||
# admin is set for all users
|
||||
# to create admin and non-admin users requires at least two API requests
|
||||
admin = data.get('admin', False)
|
||||
if admin and not self.current_user.admin:
|
||||
raise web.HTTPError(403, "Only admins can grant admin permissions")
|
||||
|
||||
to_create = []
|
||||
invalid_names = []
|
||||
@@ -259,12 +261,16 @@ class UserAPIHandler(APIHandler):
|
||||
if user is not None:
|
||||
raise web.HTTPError(409, "User %s already exists" % user_name)
|
||||
|
||||
user = self.user_from_username(user_name)
|
||||
if data:
|
||||
self._check_user_model(data)
|
||||
if 'admin' in data:
|
||||
user.admin = data['admin']
|
||||
assign_default_roles(self.db, entity=user)
|
||||
if data.get('admin', False) and not self.current_user.admin:
|
||||
raise web.HTTPError(403, "Only admins can grant admin permissions")
|
||||
|
||||
# create the user
|
||||
user = self.user_from_username(user_name)
|
||||
if data and data.get('admin', False):
|
||||
user.admin = data['admin']
|
||||
assign_default_roles(self.db, entity=user)
|
||||
self.db.commit()
|
||||
|
||||
try:
|
||||
@@ -322,7 +328,17 @@ class UserAPIHandler(APIHandler):
|
||||
400,
|
||||
"User %s already exists, username must be unique" % data['name'],
|
||||
)
|
||||
|
||||
if not self.current_user.admin:
|
||||
if user.admin:
|
||||
raise web.HTTPError(403, "Only admins can modify other admins")
|
||||
if 'admin' in data and data['admin']:
|
||||
raise web.HTTPError(403, "Only admins can grant admin permissions")
|
||||
for key, value in data.items():
|
||||
value_s = "..." if key == "auth_state" else repr(value)
|
||||
self.log.info(
|
||||
f"{self.current_user.name} setting {key}={value_s} for {user.name}"
|
||||
)
|
||||
if key == 'auth_state':
|
||||
await user.save_auth_state(value)
|
||||
else:
|
||||
|
@@ -48,7 +48,7 @@ scope_definitions = {
|
||||
'doc_description': 'Access the admin page. Permission to take actions via the admin page granted separately.',
|
||||
},
|
||||
'admin:users': {
|
||||
'description': 'Read, write, create and delete users and their authentication state, not including their servers or tokens.',
|
||||
'description': 'Read, modify, create, and delete users and their authentication state, not including their servers or tokens. This is an extremely privileged scope and should be considered tantamount to superuser.',
|
||||
'subscopes': ['admin:auth_state', 'users', 'read:roles:users', 'delete:users'],
|
||||
},
|
||||
'admin:auth_state': {'description': 'Read a user’s authentication state.'},
|
||||
@@ -109,7 +109,7 @@ scope_definitions = {
|
||||
'subscopes': ['groups', 'read:roles:groups', 'delete:groups'],
|
||||
},
|
||||
'groups': {
|
||||
'description': 'Read and write group information, including adding/removing users to/from groups.',
|
||||
'description': 'Read and write group information, including adding/removing any users to/from groups. Note: adding users to groups may affect permissions.',
|
||||
'subscopes': ['read:groups', 'list:groups'],
|
||||
},
|
||||
'list:groups': {
|
||||
|
@@ -825,7 +825,7 @@ def patch_base_handler(BaseHandler, log=None):
|
||||
# but we also need to ensure BaseHandler *itself* doesn't
|
||||
# override the public tornado API methods we have inserted.
|
||||
# If they are defined in BaseHandler, explicitly replace them with our methods.
|
||||
for name in ("get_current_user", "get_login_url"):
|
||||
for name in ("get_current_user", "get_login_url", "check_xsrf_cookie"):
|
||||
if name in BaseHandler.__dict__:
|
||||
log.debug(
|
||||
f"Overriding {BaseHandler}.{name} with HubAuthenticatedHandler.{name}"
|
||||
|
@@ -665,16 +665,25 @@ async def test_add_multi_user(app):
|
||||
|
||||
@mark.user
|
||||
@mark.role
|
||||
async def test_add_multi_user_admin(app):
|
||||
@mark.parametrize("is_admin", [True, False])
|
||||
async def test_add_multi_user_admin(app, create_user_with_scopes, is_admin):
|
||||
db = app.db
|
||||
requester = create_user_with_scopes("admin:users")
|
||||
requester.admin = is_admin
|
||||
db.commit()
|
||||
names = ['c', 'd']
|
||||
r = await api_request(
|
||||
app,
|
||||
'users',
|
||||
method='post',
|
||||
data=json.dumps({'usernames': names, 'admin': True}),
|
||||
name=requester.name,
|
||||
)
|
||||
assert r.status_code == 201
|
||||
if is_admin:
|
||||
assert r.status_code == 201
|
||||
else:
|
||||
assert r.status_code == 403
|
||||
return
|
||||
reply = r.json()
|
||||
r_names = [user['name'] for user in reply]
|
||||
assert names == r_names
|
||||
@@ -712,13 +721,26 @@ async def test_add_user_duplicate(app):
|
||||
|
||||
@mark.user
|
||||
@mark.role
|
||||
async def test_add_admin(app):
|
||||
@mark.parametrize("is_admin", [True, False])
|
||||
async def test_add_admin(app, create_user_with_scopes, is_admin):
|
||||
db = app.db
|
||||
name = 'newadmin'
|
||||
user = create_user_with_scopes("admin:users")
|
||||
user.admin = is_admin
|
||||
db.commit()
|
||||
r = await api_request(
|
||||
app, 'users', name, method='post', data=json.dumps({'admin': True})
|
||||
app,
|
||||
'users',
|
||||
name,
|
||||
method='post',
|
||||
data=json.dumps({'admin': True}),
|
||||
name=user.name,
|
||||
)
|
||||
assert r.status_code == 201
|
||||
if is_admin:
|
||||
assert r.status_code == 201
|
||||
else:
|
||||
assert r.status_code == 403
|
||||
return
|
||||
user = find_user(db, name)
|
||||
assert user is not None
|
||||
assert user.name == name
|
||||
@@ -738,9 +760,14 @@ async def test_delete_user(app):
|
||||
|
||||
@mark.user
|
||||
@mark.role
|
||||
async def test_make_admin(app):
|
||||
@mark.parametrize("is_admin", [True, False])
|
||||
async def test_user_make_admin(app, create_user_with_scopes, is_admin):
|
||||
db = app.db
|
||||
name = 'admin2'
|
||||
requester = create_user_with_scopes('admin:users')
|
||||
requester.admin = is_admin
|
||||
db.commit()
|
||||
|
||||
name = new_username("make_admin")
|
||||
r = await api_request(app, 'users', name, method='post')
|
||||
assert r.status_code == 201
|
||||
user = find_user(db, name)
|
||||
@@ -751,10 +778,18 @@ async def test_make_admin(app):
|
||||
assert orm.Role.find(db, 'admin') not in user.roles
|
||||
|
||||
r = await api_request(
|
||||
app, 'users', name, method='patch', data=json.dumps({'admin': True})
|
||||
app,
|
||||
'users',
|
||||
name,
|
||||
method='patch',
|
||||
data=json.dumps({'admin': True}),
|
||||
name=requester.name,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
if is_admin:
|
||||
assert r.status_code == 200
|
||||
else:
|
||||
assert r.status_code == 403
|
||||
return
|
||||
user = find_user(db, name)
|
||||
assert user is not None
|
||||
assert user.name == name
|
||||
@@ -763,6 +798,38 @@ async def test_make_admin(app):
|
||||
assert orm.Role.find(db, 'admin') in user.roles
|
||||
|
||||
|
||||
@mark.user
|
||||
@mark.parametrize("requester_is_admin", [True, False])
|
||||
@mark.parametrize("user_is_admin", [True, False])
|
||||
async def test_user_set_name(
|
||||
app, user, create_user_with_scopes, requester_is_admin, user_is_admin
|
||||
):
|
||||
db = app.db
|
||||
requester = create_user_with_scopes('admin:users')
|
||||
requester.admin = requester_is_admin
|
||||
user.admin = user_is_admin
|
||||
db.commit()
|
||||
new_name = new_username()
|
||||
|
||||
r = await api_request(
|
||||
app,
|
||||
'users',
|
||||
user.name,
|
||||
method='patch',
|
||||
data=json.dumps({'name': new_name}),
|
||||
name=requester.name,
|
||||
)
|
||||
if requester_is_admin or not user_is_admin:
|
||||
assert r.status_code == 200
|
||||
else:
|
||||
assert r.status_code == 403
|
||||
return
|
||||
renamed = find_user(db, new_name)
|
||||
assert renamed is not None
|
||||
assert renamed.name == new_name
|
||||
assert renamed.id == user.id
|
||||
|
||||
|
||||
@mark.user
|
||||
async def test_set_auth_state(app, auth_state_enabled):
|
||||
auth_state = {'secret': 'hello'}
|
||||
|
@@ -43,7 +43,7 @@ target_version = [
|
||||
github_url = "https://github.com/jupyterhub/jupyterhub"
|
||||
|
||||
[tool.tbump.version]
|
||||
current = "4.1.4"
|
||||
current = "4.1.6"
|
||||
|
||||
# Example of a semver regexp.
|
||||
# Make sure this matches current_version before
|
||||
|
Reference in New Issue
Block a user