Compare commits

...

22 Commits
4.1.2 ... 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
Min RK
42191672ac Bump to 4.1.4 2024-03-30 09:58:35 +01:00
Min RK
669d8d7b65 Merge pull request #4764 from minrk/414
changelog for 4.1.4
2024-03-30 09:58:09 +01:00
Min RK
171026583c changelog for 4.1.4 2024-03-30 09:55:16 +01:00
Min RK
78a3dc5b01 Merge pull request #4759 from minrk/xsrf-no-navigate
avoid xsrf check on navigate GET requests
2024-03-30 09:53:09 +01:00
Min RK
21c37309a5 avoid xsrf check on navigate GET requests
sevices/auth prevents calling check_xsrf_cookie,
but if the Handler itself called it the newly strict check would still be applied

this ensures the check is actually allowed for navigate GET requests
2024-03-29 09:55:49 +01:00
Min RK
3d40be5890 Bump to 4.1.3 2024-03-26 10:07:04 +01:00
Min RK
ac72c60cb3 Merge pull request #4754 from minrk/413
changelog for 4.1.3
2024-03-26 10:06:38 +01:00
Min RK
92264696b1 changelog for 4.1.3 2024-03-26 09:44:07 +01:00
Min RK
f2b7b69c3e Merge pull request #4753 from minrk/server-xsrf-config
respect jupyter-server disable_check_xsrf setting
2024-03-26 09:42:54 +01:00
Min RK
e0f001271b respect jupyter-server disable_check_xsrf setting
allows global disable of xsrf checks in single-user servers
2024-03-26 08:55:15 +01:00
11 changed files with 227 additions and 47 deletions

View File

@@ -6,7 +6,7 @@ info:
description: The REST API for JupyterHub
license:
name: BSD-3-Clause
version: 4.1.2
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,68 @@ 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))
#### Bugs fixed
- avoid xsrf check on navigate GET requests [#4759](https://github.com/jupyterhub/jupyterhub/pull/4759) ([@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-26&to=2024-03-30&type=c))
@consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2024-03-26..2024-03-30&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2024-03-26..2024-03-30&type=Issues))
### 4.1.3 - 2024-03-26
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/4.1.2...4.1.3))
#### Bugs fixed
- respect jupyter-server disable_check_xsrf setting [#4753](https://github.com/jupyterhub/jupyterhub/pull/4753) ([@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-25&to=2024-03-26&type=c))
@consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2024-03-25..2024-03-26&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2024-03-25..2024-03-26&type=Issues))
### 4.1.2 - 2024-03-25
4.1.2 fixes a regression in 4.1.0 affecting named servers.

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, 2, "", "")
version_info = (4, 1, 6, "", "")
# pep 440 version: no dot before beta/rc, but before .dev
# 0.1.0rc1

View File

@@ -178,10 +178,35 @@ def get_xsrf_token(handler, cookie_path=""):
return xsrf_token
def _needs_check_xsrf(handler):
"""Does the given cookie-authenticated request need to check xsrf?"""
if getattr(handler, "_token_authenticated", False):
return False
fetch_mode = handler.request.headers.get("Sec-Fetch-Mode", "unspecified")
if fetch_mode in {"websocket", "no-cors"} or (
fetch_mode in {"navigate", "unspecified"}
and handler.request.method.lower() in {"get", "head", "options"}
):
# no xsrf check needed for regular page views or no-cors
# or websockets after allow_websocket_cookie_auth passes
if fetch_mode == "unspecified":
app_log.warning(
f"Skipping XSRF check for insecure request {handler.request.method} {handler.request.path}"
)
return False
else:
return True
def check_xsrf_cookie(handler):
"""Check that xsrf cookie matches xsrf token in request"""
# overrides tornado's implementation
# because we changed what a correct value should be in xsrf_token
if not _needs_check_xsrf(handler):
# don't require XSRF for regular page views
return
token = (
handler.get_argument("_xsrf", None)

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

@@ -62,6 +62,7 @@ from traitlets.config import SingletonConfigurable
from .._xsrf_utils import (
_anonymous_xsrf_id,
_needs_check_xsrf,
_set_xsrf_cookie,
check_xsrf_cookie,
get_xsrf_token,
@@ -931,7 +932,9 @@ class HubOAuth(HubAuth):
Applies JupyterHub check_xsrf_cookie if not token authenticated
"""
if getattr(handler, '_token_authenticated', False):
if getattr(handler, '_token_authenticated', False) or handler.settings.get(
"disable_check_xsrf", False
):
return
check_xsrf_cookie(handler)
@@ -944,31 +947,11 @@ class HubOAuth(HubAuth):
kwargs["secure"] = True
return handler.clear_cookie(cookie_name, **kwargs)
def _needs_check_xsrf(self, handler):
"""Does the given cookie-authenticated request need to check xsrf?"""
if getattr(handler, "_token_authenticated", False):
return False
fetch_mode = handler.request.headers.get("Sec-Fetch-Mode", "unspecified")
if fetch_mode in {"websocket", "no-cors"} or (
fetch_mode in {"navigate", "unspecified"}
and handler.request.method.lower() in {"get", "head", "options"}
):
# no xsrf check needed for regular page views or no-cors
# or websockets after allow_websocket_cookie_auth passes
if fetch_mode == "unspecified":
self.log.warning(
f"Skipping XSRF check for insecure request {handler.request.method} {handler.request.path}"
)
return False
else:
return True
async def _get_user_cookie(self, handler):
# check xsrf if needed
token = self._get_token_cookie(handler)
session_id = self.get_session_id(handler)
if token and self._needs_check_xsrf(handler):
if token and _needs_check_xsrf(handler):
# call handler.check_xsrf_cookie instead of self.check_xsrf_cookie
# to allow subclass overrides
try:

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

@@ -157,7 +157,7 @@ async def test_permission_error_messages(app, user, auth, expected_message):
params["_xsrf"] = cookies["_xsrf"]
if auth == "cookie_xsrf_mismatch":
params["_xsrf"] = "somethingelse"
headers['Sec-Fetch-Mode'] = 'cors'
r = await async_requests.get(url, **kwargs)
assert r.status_code == 403
response = r.json()
@@ -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.2"
current = "4.1.6"
# Example of a semver regexp.
# Make sure this matches current_version before