Compare commits

...

12 Commits
4.1.4 ... 4.1.6

Author SHA1 Message Date
Min RK
a1824a4b3a Bump to 4.1.6 2024-07-31 10:44:50 +02:00
Min RK
e28b234cc5 remove 5.x link target from backported docs 2024-07-31 10:41:05 +02:00
Min RK
88be9eda05 regen rest-api 2024-07-31 10:34:13 +02:00
Min RK
916632fcc8 Merge commit from fork
only admins can modify admins (4.x)
2024-07-31 10:28:14 +02:00
Min RK
0737e6f581 changelog for 4.1.6 2024-07-31 09:16:51 +02:00
Min RK
02cbb5f337 add some docs on groups permissions 2024-07-03 09:29:04 +02:00
Min RK
99e2720b0f only admins can modify admins
- if not admin, cannot set admin=True anywhere
- if not admin, cannot modify any user where admin=True
2024-07-03 09:18:04 +02:00
Min RK
b405361674 Bump to 4.1.5 2024-04-04 22:13:51 +02:00
Min RK
0d53ead186 Merge pull request #4773 from minrk/cl415
changelog for 4.1.5
2024-04-04 22:13:21 +02:00
Min RK
cedd176a34 changelog for 4.1.5 2024-04-04 12:51:26 +02:00
Min RK
58e5022d28 Merge pull request #4771 from minrk/xsrf-mixin
singleuser mixin: include check_xsrf_cookie in overrides
2024-04-04 12:46:52 +02:00
Min RK
f395acd9ca singleuser mixin: include check_xsrf_cookie in overrides 2024-04-04 11:05:10 +02:00
9 changed files with 162 additions and 24 deletions

View File

@@ -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 users 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.

View File

@@ -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

View File

@@ -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))

View File

@@ -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

View File

@@ -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:

View File

@@ -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 users 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': {

View File

@@ -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}"

View File

@@ -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'}

View File

@@ -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