Compare commits

...

39 Commits
1.4.1 ... 1.5.1

Author SHA1 Message Date
Min RK
f7903a78d1 release 1.5.1 2022-12-05 13:27:57 +01:00
Erik Sundell
edddacec1d Merge pull request #4234 from minrk/1.x
changelog for 1.5.1
2022-12-01 16:43:44 +01:00
Min RK
c5a33f227f changelog for 1.5.1 2022-11-30 10:56:00 +01:00
Min RK
aaa20e5787 fix flake8 url in pre-commit
repo moved
2022-11-30 10:55:52 +01:00
Min RK
c8ebf1ec90 Merge pull request #4076 from mriedem/1.x-db-fix
Backport PR #3566 to 1.x series
2022-10-12 12:37:22 +02:00
pre-commit-ci[bot]
89745c002b [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-10-10 15:44:36 +00:00
Matt Riedemann
3e0ee49ce8 Upgrade black to 22.3.0
Partial cherry-pick of f124f06c2 needed because of
https://github.com/psf/black/issues/2964.
2022-10-10 10:43:36 -05:00
pre-commit-ci[bot]
bbbeffb443 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-10-10 13:49:36 +00:00
Min RK
c29a5ca4ce finish up db rollback checks
- move catch_db_error to utils
- tidy catch/propagate errors in prepare, get_current_user

(cherry picked from commit 3bcc542e27)

Conflicts:
  jupyterhub/handlers/base.py

NOTE(mriedem): The conflict is due to e6845a68f not being
in 1.5.0.
2022-10-10 08:47:00 -05:00
SHAHN3
f5182fe349 add explicit db rollback
add context manager/decorator for db rollback

add db rollback in top level prepare method

Co-authored-by: Sarath Babu <sbreached@gmail.com>
(cherry picked from commit 044fb23a70)
2022-10-10 08:42:42 -05:00
Min RK
bf73e6f7b7 fix 1.5.0 link in changelog 2021-11-04 13:56:40 +01:00
Min RK
e2631b302a import commands from setuptools
importing build_py from distutils breaks in setuptools 58
2021-11-04 13:55:41 +01:00
Min RK
0d89241c9f release 1.5.0 2021-11-04 13:22:53 +01:00
Min RK
5ac9e7f73a Merge branch 'fix-set-cookie' into 1.4.x
Prepare to release 1.5.0

Fixes GHSA-cw7p-q79f-m2v7
2021-11-04 13:21:52 +01:00
Min RK
9672b534ec changelog for 1.5.0 2021-11-03 16:16:48 +01:00
Min RK
254365716d jupyterlab: don't use $JUPYTERHUB_API_TOKEN in PageConfig.token 2021-11-03 15:52:00 +01:00
Min RK
dcac8c4efe Revert "store tokens passed via url or header, not only url."
This reverts commit 53c3201c17.

Only tokens in URLs should be persisted in cookies.
Tokens in headers should not have any effect on cookies.
2021-11-03 15:52:00 +01:00
Min RK
0611169dea Merge pull request #3677 from minrk/doc-requirements-1x
1.4.x: update doc requirements
2021-11-02 10:38:16 +01:00
Min RK
a432fa3bb6 Merge pull request #3676 from manics/1-use_legacy_stopped_server_status_code
use_legacy_stopped_server_status_code: use 1.* language
2021-11-02 10:35:09 +01:00
Min RK
44141ae025 1.4.x: update doc requirements
pin down docutils, unpin autodoc-traits
2021-11-02 10:35:04 +01:00
Simon Li
04ae25d2c2 use_legacy_stopped_server_status_code: use 1.* language
Also fixes the JupyterHub 2.0 default: will be False not True
2021-11-01 22:14:59 +00:00
Min RK
69a1e97fbe Merge pull request #3639 from yuvipanda/404-1.4.x
Backport #3636 to 1.4.x
2021-10-06 16:08:00 +02:00
YuviPanda
eb0c6514af Set use_legacy_stopped_server_status_code to True for 1.4.x 2021-10-06 17:25:10 +05:30
YuviPanda
d03fc8c531 Update tests that were looking for 503s 2021-10-05 20:19:59 +05:30
YuviPanda
1c8dce533b Preserve older 503 behavior behind a flag 2021-10-05 20:19:59 +05:30
YuviPanda
bbfbc47bb3 Use 424 rather than 404 to indicate non-running server
404 is also used to identify that a particular resource
(like a kernel or terminal) is not present, maybe because
it is deleted. That comes from the notebook server, while
here we are responding from JupyterHub. Saying that the
user server they are trying to request the resource (kernel, etc)
from does not exist seems right.
2021-10-05 20:19:59 +05:30
YuviPanda
be6ec28dab Fail suspected API requests with 404, not 503
Non-running user servers making requests is a fairly
common occurance - user servers get culled while their
browser tabs are left open. So we now have a background level
of 503s responses on the hub *all* the time, making it
very difficult to detect *real* 503s, which should ideally
be closely monitored and alerted on.

I *think* 404 is a more appropriate response, as the resource
(API) being requested is no longer present.
2021-10-05 20:19:59 +05:30
Min RK
bd3a215c9e Merge pull request #3580 from meeseeksmachine/auto-backport-of-pr-3552-on-1.4.x
Backport PR #3552 on branch 1.4.x (Add expiration date dropdown to Token page)
2021-08-23 12:08:56 +02:00
Min RK
3783a1bc6c Backport PR #3552: Add expiration date dropdown to Token page 2021-08-23 09:42:15 +00:00
Min RK
7b0f29b340 Merge pull request #3579 from meeseeksmachine/auto-backport-of-pr-3488-on-1.4.x
Backport PR #3488 on branch 1.4.x (Support auto login when used as a OAuth2 provider)
2021-08-23 10:32:15 +02:00
Min RK
f63e810dfe Backport PR #3488: Support auto login when used as a OAuth2 provider 2021-08-20 08:30:04 +00:00
Min RK
909b3ad4d7 Merge pull request #3538 from consideRatio/pr/release-1.4.2
Release 1.4.2
2021-07-16 10:57:54 +00:00
Erik Sundell
114493be9b release 1.4.2 2021-07-15 16:57:54 +02:00
Erik Sundell
4c0ac5ba91 changelog for 1.4.2 2021-07-15 16:57:52 +02:00
Erik Sundell
52793d65bd Backport PR #3531: Fix regression where external services api_token became required
Issue background

Registering an external service means it won't be run as a process by JupyterHub or similar as I understand it, and such external services may be registered only to get a /services/<service_name> route registered with JupyterHub's configured proxy rather than to actually use an api_token and speak with JupyterHub.

In the past, it was okay for a external service without an api_token to be registered, but not it isn't. This PR fixes that.

The situation when I run into this is when I register grafana as an external service like this (but in reality via a z2jh config with slightly different syntax).

```python
c.JupyterHub.services = [
    {
        "name": "grafana",
        "url": "http://grafana",
    }
]
```

JupyterHub has a [documentation about Services](https://jupyterhub.readthedocs.io/en/stable/reference/services.html properties-of-a-service), where one can see that the default value of api_token is None.

    Issue details

This is an error me and  GeorgianaElena have run into using JupyterHub 1.4.1, but I'm not sure at what point the regression was introduced besides it was around in 1.4.1.

I wrote some notes tracking this issue down. This is the summary I wrote.

```
    This test was made to reproduce an error like this:

        ValueError: Tokens must be at least 8 characters, got ''

    The error had the following stack trace in 1.4.1:

        jupyterhub/app.py:2213: in init_api_tokens
            await self._add_tokens(self.service_tokens, kind='service')
        jupyterhub/app.py:2182: in _add_tokens
            obj.new_api_token(
        jupyterhub/orm.py:424: in new_api_token
            return APIToken.new(token=token, service=self, **kwargs)
        jupyterhub/orm.py:699: in new
            cls.check_token(db, token)

    This test also make _add_tokens receive a token_dict that is buggy:

        {"": "external_2"}

    It turned out that whatever passes token_dict to _add_tokens failed to
    ignore service's api_tokens that were None, and instead passes them as blank
    strings.

    It turned out that init_api_tokens was passing self.service_tokens, and that
    self.service_tokens had been populated with blank string tokens for external
    services registered with JupyterHub.
```

Signed-off-by: Erik Sundell <erik.i.sundell@gmail.com>
2021-07-15 10:16:18 +02:00
passer
320e1924a7 Backport PR #3521: Fix contributor documentation's link
Clicking the contributor documentation's link [https://jupyter.readthedocs.io/en/latest/contributor/content-contributor.html](https://jupyter.readthedocs.io/en/latest/contributor/content-contributor.html) will get an error

This link needs to be replaced with [https://jupyter.readthedocs.io/en/latest/contributing/content-contributor.html](https://jupyter.readthedocs.io/en/latest/contributing/content-contributor.html)

Signed-off-by: Erik Sundell <erik.i.sundell@gmail.com>
2021-07-15 10:16:16 +02:00
Min RK
2c90715c8d Backport PR #3510: bump autodoc-traits
for sphinx compatibility fix, to get docs building again

Signed-off-by: Erik Sundell <erik.i.sundell@gmail.com>
2021-07-15 10:16:13 +02:00
David Brochart
c99bb32e12 Backport PR #3494: Fix typo
Signed-off-by: Erik Sundell <erik.i.sundell@gmail.com>
2021-07-15 10:16:11 +02:00
Igor Beliakov
fee4ee23c0 Backport PR #3484: Bug: save_bearer_token (provider.py) passes a float value to the expires_at field (int)
**Environment**

* image: k8s-hub (`jupyterhub/k8s-hub:0.11.1`);
* `authenticator_class: dummy`;
* db: cocroachdb (`sqlalchemy-cocroachdb`).

**Description:**

`save_bearer_token` method (`provider.py`) passes a float value to the `expires_at` field (int).

A user can create a notebook, it gets successfully scheduled, and then, once the pod is up and ready, the user is unable to enter the notebook, because jupyterhub cannot save a token. In logs, we can see the following:

```
[I 2021-05-29 14:45:04.302 JupyterHub log:181] 302 GET /hub/api/oauth2/authorize?client_id=jupyterhub-user-user2&redirect_uri=%2Fuser%2Fuser2%2Foauth_callback&response_type=code&state=[secret] -> /user/user2/oauth_callback?code=[secret]&state=[secret] (user2 40.113.125.116) 73.98ms
[E 2021-05-29 14:45:04.424 JupyterHub web:1789] Uncaught exception POST /hub/api/oauth2/token (10.42.80.10)
    HTTPServerRequest(protocol='http', host='hub:8081', method='POST', uri='/hub/api/oauth2/token', version='HTTP/1.1', remote_ip='10.42.80.10')
    Traceback (most recent call last):
      File "/usr/local/lib/python3.8/dist-packages/tornado/web.py", line 1702, in _execute
        result = method(*self.path_args, **self.path_kwargs)
      File "/usr/local/lib/python3.8/dist-packages/jupyterhub/apihandlers/auth.py", line 324, in post
        headers, body, status = self.oauth_provider.create_token_response(
      File "/usr/local/lib/python3.8/dist-packages/oauthlib/oauth2/rfc6749/endpoints/base.py", line 116, in wrapper
        return f(endpoint, uri, *args, **kwargs)
      File "/usr/local/lib/python3.8/dist-packages/oauthlib/oauth2/rfc6749/endpoints/token.py", line 118, in create_token_response
        return grant_type_handler.create_token_response(
      File "/usr/local/lib/python3.8/dist-packages/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py", line 313, in create_token_response
        self.request_validator.save_token(token, request)
      File "/usr/local/lib/python3.8/dist-packages/jupyterhub/oauth/provider.py", line 281, in save_token
        return self.save_bearer_token(token, request, *args, **kwargs)
      File "/usr/local/lib/python3.8/dist-packages/jupyterhub/oauth/provider.py", line 354, in save_bearer_token
        self.db.commit()
      File "/usr/local/lib/python3.8/dist-packages/sqlalchemy/orm/session.py", line 1042, in commit
        self.transaction.commit()
      File "/usr/local/lib/python3.8/dist-packages/sqlalchemy/orm/session.py", line 504, in commit
        self._prepare_impl()
      File "/usr/local/lib/python3.8/dist-packages/sqlalchemy/orm/session.py", line 483, in _prepare_impl
        self.session.flush()
      File "/usr/local/lib/python3.8/dist-packages/sqlalchemy/orm/session.py", line 2536, in flush
        self._flush(objects)
      File "/usr/local/lib/python3.8/dist-packages/sqlalchemy/orm/session.py", line 2678, in _flush
        transaction.rollback(_capture_exception=True)
      File "/usr/local/lib/python3.8/dist-packages/sqlalchemy/util/langhelpers.py", line 68, in __exit__
        compat.raise_(
      File "/usr/local/lib/python3.8/dist-packages/sqlalchemy/util/compat.py", line 182, in raise_
        raise exception
      File "/usr/local/lib/python3.8/dist-packages/sqlalchemy/orm/session.py", line 2638, in _flush
        flush_context.execute()
      File "/usr/local/lib/python3.8/dist-packages/sqlalchemy/orm/unitofwork.py", line 422, in execute
        rec.execute(self)
      File "/usr/local/lib/python3.8/dist-packages/sqlalchemy/orm/unitofwork.py", line 586, in execute
        persistence.save_obj(
      File "/usr/local/lib/python3.8/dist-packages/sqlalchemy/orm/persistence.py", line 239, in save_obj
        _emit_insert_statements(
      File "/usr/local/lib/python3.8/dist-packages/sqlalchemy/orm/persistence.py", line 1135, in _emit_insert_statements
        result = cached_connections[connection].execute(
      File "/usr/local/lib/python3.8/dist-packages/sqlalchemy/engine/base.py", line 1011, in execute
        return meth(self, multiparams, params)
      File "/usr/local/lib/python3.8/dist-packages/sqlalchemy/sql/elements.py", line 298, in _execute_on_connection
        return connection._execute_clauseelement(self, multiparams, params)
      File "/usr/local/lib/python3.8/dist-packages/sqlalchemy/engine/base.py", line 1124, in _execute_clauseelement
        ret = self._execute_context(
      File "/usr/local/lib/python3.8/dist-packages/sqlalchemy/engine/base.py", line 1316, in _execute_context
        self._handle_dbapi_exception(
      File "/usr/local/lib/python3.8/dist-packages/sqlalchemy/engine/base.py", line 1510, in _handle_dbapi_exception
        util.raise_(
      File "/usr/local/lib/python3.8/dist-packages/sqlalchemy/util/compat.py", line 182, in raise_
        raise exception
      File "/usr/local/lib/python3.8/dist-packages/sqlalchemy/engine/base.py", line 1276, in _execute_context
        self.dialect.do_execute(
      File "/usr/local/lib/python3.8/dist-packages/sqlalchemy/engine/default.py", line 593, in do_execute
        cursor.execute(statement, parameters)
    sqlalchemy.exc.ProgrammingError: (psycopg2.errors.DatatypeMismatch) value type decimal doesn't match type int of column "expires_at"
    HINT:  you will need to rewrite or cast the expression

    [SQL: INSERT INTO oauth_access_tokens (client_id, grant_type, expires_at, refresh_token, refresh_expires_at, user_id, session_id, hashed, prefix, created, last_activity) VALUES (%(client_id)s, %(grant_type)s, %(expires_at)s, %(refresh_token)s, %(refresh_expires_at)s, %(user_id)s, %(session_id)s, %(hashed)s, %(prefix)s, %(created)s, %(last_activity)s) RETURNING oauth_access_tokens.id]
    [parameters: {'client_id': 'jupyterhub-user-user2', 'grant_type': 'authorization_code', 'expires_at': 1622303104.418992, 'refresh_token': 'FVJ8S4is0367LlEMnxIiEIoTOeoxhf', 'refresh_expires_at': None, 'user_id': 662636890939424770, 'session_id': '4e041a2bfcb34a34a00033a281bc1236', 'hashed': 'sha512:1:3b18deae37fbf50a:03df035736960af14e19196e1d13fd74f55c21f17405119f80e75817ff37c7567fab089a3d40b97a57f94b54065ee56f7260895352516b9facb989d656f05be8', 'prefix': 't11z', 'created': datetime.datetime(2021, 5, 29, 14, 45, 4, 421305), 'last_activity': None}]
    (Background on this error at: http://sqlalche.me/e/13/f405)

[W 2021-05-29 14:45:04.430 JupyterHub base:110] Rolling back session due to database error (psycopg2.errors.DatatypeMismatch) value type decimal doesn't match type int of column "expires_at"
    HINT:  you will need to rewrite or cast the expression

    [SQL: INSERT INTO oauth_access_tokens (client_id, grant_type, expires_at, refresh_token, refresh_expires_at, user_id, session_id, hashed, prefix, created, last_activity) VALUES (%(client_id)s, %(grant_type)s, %(expires_at)s, %(refresh_token)s, %(refresh_expires_at)s, %(user_id)s, %(session_id)s, %(hashed)s, %(prefix)s, %(created)s, %(last_activity)s) RETURNING oauth_access_tokens.id]
    [parameters: {'client_id': 'jupyterhub-user-user2', 'grant_type': 'authorization_code', 'expires_at': 1622303104.418992, 'refresh_token': 'FVJ8S4is0367LlEMnxIiEIoTOeoxhf', 'refresh_expires_at': None, 'user_id': 662636890939424770, 'session_id': '4e041a2bfcb34a34a00033a281bc1236', 'hashed': 'sha512:1:3b18deae37fbf50a:03df035736960af14e19196e1d13fd74f55c21f17405119f80e75817ff37c7567fab089a3d40b97a57f94b54065ee56f7260895352516b9facb989d656f05be8', 'prefix': 't11z', 'created': datetime.datetime(2021, 5, 29, 14, 45, 4, 421305), 'last_activity': None}]
    (Background on this error at: http://sqlalche.me/e/13/f405)
[E 2021-05-29 14:45:04.443 JupyterHub log:173] {
      "Host": "hub:8081",
      "User-Agent": "python-requests/2.25.1",
      "Accept-Encoding": "gzip, deflate",
      "Accept": "*/*",
      "Connection": "keep-alive",
      "Content-Type": "application/x-www-form-urlencoded",
      "Authorization": "token [secret]",
      "Content-Length": "190"
    }
[E 2021-05-29 14:45:04.444 JupyterHub log:181] 500 POST /hub/api/oauth2/token (user2 10.42.80.10) 63.28ms
```

Everything went well, when I changed:
`expires_at=orm.OAuthAccessToken.now() + token['expires_in'],`
to:
`expires_at=int(orm.OAuthAccessToken.now() + token['expires_in']),`
That's what this PR is about.

As a sidenote, `black` formatter adjusted the `orm_client = orm.OAuthClient(identifier=client_id,)` line, but I guess it should be fine. Please, feel free to revert this change if needed.

(Upd): added the missing `int` conversion.

Signed-off-by: Erik Sundell <erik.i.sundell@gmail.com>
2021-07-15 10:16:08 +02:00
23 changed files with 312 additions and 66 deletions

View File

@@ -4,14 +4,14 @@ repos:
hooks:
- id: reorder-python-imports
- repo: https://github.com/psf/black
rev: 20.8b1
rev: 22.3.0
hooks:
- id: black
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.2.1
hooks:
- id: prettier
- repo: https://gitlab.com/pycqa/flake8
- repo: https://github.com/pycqa/flake8
rev: "3.8.4"
hooks:
- id: flake8

View File

@@ -201,7 +201,7 @@ These accounts will be used for authentication in JupyterHub's default configura
## Contributing
If you would like to contribute to the project, please read our
[contributor documentation](http://jupyter.readthedocs.io/en/latest/contributor/content-contributor.html)
[contributor documentation](https://jupyter.readthedocs.io/en/latest/contributing/content-contributor.html)
and the [`CONTRIBUTING.md`](CONTRIBUTING.md). The `CONTRIBUTING.md` file
explains how to set up a development installation, how to run the test suite,
and how to contribute to documentation.

View File

@@ -1,9 +1,8 @@
-r ../requirements.txt
alabaster_jupyterhub
# Temporary fix of #3021. Revert back to released autodoc-traits when
# 0.1.0 released.
https://github.com/jupyterhub/autodoc-traits/archive/75885ee24636efbfebfceed1043459715049cd84.zip
autodoc-traits
docutils<0.18
pydata-sphinx-theme
pytablewriter>=0.56
recommonmark>=0.6

View File

@@ -3,7 +3,7 @@ swagger: "2.0"
info:
title: JupyterHub
description: The REST API for JupyterHub
version: 1.4.0
version: 1.5.1
license:
name: BSD-3-Clause
schemes: [http, https]
@@ -873,7 +873,7 @@ definitions:
description: The user that owns a token (undefined if owned by a service)
service:
type: string
description: The service that owns the token (undefined of owned by a user)
description: The service that owns the token (undefined if owned by a user)
note:
type: string
description: A note about the token, typically describing what it was created for.

View File

@@ -6,6 +6,57 @@ command line for details.
## [Unreleased]
## 1.5
JupyterHub 1.5 is a **security release**,
fixing a vulnerability [ghsa-cw7p-q79f-m2v7][] where JupyterLab users
with multiple tabs open could fail to logout completely,
leaving their browser with valid credentials until they logout again.
A few fully backward-compatible features have been backported from 2.0.
[ghsa-cw7p-q79f-m2v7]: https://github.com/jupyterhub/jupyterhub/security/advisories/GHSA-cw7p-q79f-m2v7
### 1.5.1 2022-12-01
This is a patch release, improving db resiliency when certain errors occur, without requiring a jupyterhub restart.
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/1.5.0...1.5.1))
#### Merged PRs
- Backport db rollback fixes to 1.x [#4076](https://github.com/jupyterhub/jupyterhub/pull/4076) ([@mriedem](https://github.com/mriedem), [@minrk](https://github.com/minrk)), [@nsshah1288](https://github.com/nsshah1288)
#### Contributors to this release
([GitHub contributors page for this release](https://github.com/jupyterhub/jupyterhub/graphs/contributors?from=2022-09-09&to=2022-11-30&type=c))
[@mriedem](https://github.com/mriedem) | [@minrk](https://github.com/minrk) | [@nsshah1288](https://github.com/nsshah1288)
### [1.5.0] 2021-11-04
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/1.4.2...1.5.0))
#### New features added
- Backport #3636 to 1.4.x (opt-in support for JupyterHub.use_legacy_stopped_server_status_code) [#3639](https://github.com/jupyterhub/jupyterhub/pull/3639) ([@yuvipanda](https://github.com/yuvipanda))
- Backport PR #3552 on branch 1.4.x (Add expiration date dropdown to Token page) [#3580](https://github.com/jupyterhub/jupyterhub/pull/3580) ([@meeseeksmachine](https://github.com/meeseeksmachine))
- Backport PR #3488 on branch 1.4.x (Support auto login when used as a OAuth2 provider) [#3579](https://github.com/jupyterhub/jupyterhub/pull/3579) ([@meeseeksmachine](https://github.com/meeseeksmachine))
#### Maintenance and upkeep improvements
- 1.4.x: update doc requirements [#3677](https://github.com/jupyterhub/jupyterhub/pull/3677) ([@minrk](https://github.com/minrk))
#### Documentation improvements
- use_legacy_stopped_server_status_code: use 1.\* language [#3676](https://github.com/jupyterhub/jupyterhub/pull/3676) ([@manics](https://github.com/manics))
#### Contributors to this release
([GitHub contributors page for this release](https://github.com/jupyterhub/jupyterhub/graphs/contributors?from=2021-07-16&to=2021-11-03&type=c))
[@choldgraf](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Acholdgraf+updated%3A2021-07-16..2021-11-03&type=Issues) | [@consideRatio](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2021-07-16..2021-11-03&type=Issues) | [@manics](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amanics+updated%3A2021-07-16..2021-11-03&type=Issues) | [@meeseeksmachine](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ameeseeksmachine+updated%3A2021-07-16..2021-11-03&type=Issues) | [@minrk](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2021-07-16..2021-11-03&type=Issues) | [@support](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Asupport+updated%3A2021-07-16..2021-11-03&type=Issues) | [@welcome](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Awelcome+updated%3A2021-07-16..2021-11-03&type=Issues) | [@yuvipanda](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ayuvipanda+updated%3A2021-07-16..2021-11-03&type=Issues)
## 1.4
JupyterHub 1.4 is a small release, with several enhancements, bug fixes,
@@ -24,6 +75,32 @@ This is now also configurable via `JupyterHub.oauth_token_expires_in`.
The result is that it should be much less likely for auth tokens stored in cookies
to expire during the lifetime of a server.
### [1.4.2] 2021-06-15
1.4.2 is a small bugfix release for 1.4.
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/1.4.1...d9860aa98cc537cf685022f81b8f725bfef41304))
#### Bugs fixed
- Fix regression where external services api_token became required [#3531](https://github.com/jupyterhub/jupyterhub/pull/3531) ([@consideRatio](https://github.com/consideRatio))
- Bug: save_bearer_token (provider.py) passes a float value to the expires_at field (int) [#3484](https://github.com/jupyterhub/jupyterhub/pull/3484) ([@weisdd](https://github.com/weisdd))
#### Maintenance and upkeep improvements
- bump autodoc-traits [#3510](https://github.com/jupyterhub/jupyterhub/pull/3510) ([@minrk](https://github.com/minrk))
#### Documentation improvements
- Fix contributor documentation's link [#3521](https://github.com/jupyterhub/jupyterhub/pull/3521) ([@icankeep](https://github.com/icankeep))
- Fix typo [#3494](https://github.com/jupyterhub/jupyterhub/pull/3494) ([@davidbrochart](https://github.com/davidbrochart))
#### Contributors to this release
([GitHub contributors page for this release](https://github.com/jupyterhub/jupyterhub/graphs/contributors?from=2021-05-12&to=2021-07-15&type=c))
[@consideRatio](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2021-05-12..2021-07-15&type=Issues) | [@davidbrochart](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Adavidbrochart+updated%3A2021-05-12..2021-07-15&type=Issues) | [@icankeep](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aicankeep+updated%3A2021-05-12..2021-07-15&type=Issues) | [@minrk](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2021-05-12..2021-07-15&type=Issues) | [@weisdd](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aweisdd+updated%3A2021-05-12..2021-07-15&type=Issues)
### [1.4.1] 2021-05-12
1.4.1 is a small bugfix release for 1.4.
@@ -53,7 +130,7 @@ to expire during the lifetime of a server.
[@0mar](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3A0mar+updated%3A2021-04-19..2021-05-12&type=Issues) | [@betatim](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Abetatim+updated%3A2021-04-19..2021-05-12&type=Issues) | [@consideRatio](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2021-04-19..2021-05-12&type=Issues) | [@danlester](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Adanlester+updated%3A2021-04-19..2021-05-12&type=Issues) | [@davidbrochart](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Adavidbrochart+updated%3A2021-04-19..2021-05-12&type=Issues) | [@IvanaH8](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AIvanaH8+updated%3A2021-04-19..2021-05-12&type=Issues) | [@manics](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amanics+updated%3A2021-04-19..2021-05-12&type=Issues) | [@minrk](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2021-04-19..2021-05-12&type=Issues) | [@naatebarber](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Anaatebarber+updated%3A2021-04-19..2021-05-12&type=Issues) | [@OrnithOrtion](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AOrnithOrtion+updated%3A2021-04-19..2021-05-12&type=Issues) | [@support](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Asupport+updated%3A2021-04-19..2021-05-12&type=Issues) | [@welcome](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Awelcome+updated%3A2021-04-19..2021-05-12&type=Issues)
### 1.4.0 2021-04-19
### [1.4.0] 2021-04-19
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/1.3.0...1.4.0))
@@ -1071,7 +1148,9 @@ Fix removal of `/login` page in 0.4.0, breaking some OAuth providers.
First preview release
[unreleased]: https://github.com/jupyterhub/jupyterhub/compare/1.4.1...HEAD
[unreleased]: https://github.com/jupyterhub/jupyterhub/compare/1.5.0...HEAD
[1.5.0]: https://github.com/jupyterhub/jupyterhub/compare/1.4.2...1.5.0
[1.4.2]: https://github.com/jupyterhub/jupyterhub/compare/1.4.1...1.4.2
[1.4.1]: https://github.com/jupyterhub/jupyterhub/compare/1.4.0...1.4.1
[1.4.0]: https://github.com/jupyterhub/jupyterhub/compare/1.3.0...1.4.0
[1.3.0]: https://github.com/jupyterhub/jupyterhub/compare/1.2.1...1.3.0

View File

@@ -10,7 +10,7 @@ from jupyter_client.localinterfaces import public_ips
def create_dir_hook(spawner):
""" Create directory """
"""Create directory"""
username = spawner.user.name # get the username
volume_path = os.path.join('/volumes/jupyterhub', username)
if not os.path.exists(volume_path):
@@ -20,7 +20,7 @@ def create_dir_hook(spawner):
def clean_dir_hook(spawner):
""" Delete directory """
"""Delete directory"""
username = spawner.user.name # get the username
temp_path = os.path.join('/volumes/jupyterhub', username, 'temp')
if os.path.exists(temp_path) and os.path.isdir(temp_path):

View File

@@ -4,7 +4,7 @@
version_info = (
1,
4,
5,
1,
"", # release (b1, rc1, or "" for final or dev)
# "dev", # dev or nothing for beta/rc/stable releases

View File

@@ -222,6 +222,14 @@ class OAuthAuthorizeHandler(OAuthHandler, BaseHandler):
# default: require confirmation
return True
def get_login_url(self):
"""
Support automatically logging in when JupyterHub is used as auth provider
"""
if self.authenticator.auto_login_oauth2_authorize:
return self.authenticator.login_url(self.hub.base_url)
return super().get_login_url()
@web.authenticated
async def get(self):
"""GET /oauth/authorization

View File

@@ -42,7 +42,7 @@ class GroupListAPIHandler(_GroupAPIHandler):
@admin_only
async def post(self):
"""POST creates Multiple groups """
"""POST creates Multiple groups"""
model = self.get_json_body()
if not model or not isinstance(model, dict) or not model.get('groups'):
raise web.HTTPError(400, "Must specify at least one group to create")

View File

@@ -5,7 +5,6 @@
import asyncio
import atexit
import binascii
import json
import logging
import os
import re
@@ -90,6 +89,7 @@ from .pagination import Pagination
from .proxy import Proxy, ConfigurableHTTPProxy
from .traitlets import URLPrefix, Command, EntryPointType, Callable
from .utils import (
catch_db_error,
maybe_future,
url_path_join,
print_stacks,
@@ -1476,6 +1476,26 @@ class JupyterHub(Application):
""",
).tag(config=True)
use_legacy_stopped_server_status_code = Bool(
True,
help="""
Return 503 rather than 424 when request comes in for a non-running server.
Prior to JupyterHub 2.0, this returns a 503 when any request comes in for
a user server that is currently not running. By default, JupyterHub 2.0
will return a 424 - this makes operational metric dashboards more useful.
JupyterLab < 3.2 expected the 503 to know if the user server is no longer
running, and prompted the user to start their server. Set this config to
true to retain the old behavior, so JupyterLab < 3.2 can continue to show
the appropriate UI when the user server is stopped.
This option will default to False in JupyterHub 2.0, and be removed in a
future release.
""",
config=True,
)
def init_handlers(self):
h = []
# load handlers from the authenticator
@@ -1980,6 +2000,7 @@ class JupyterHub(Application):
# purge expired tokens hourly
purge_expired_tokens_interval = 3600
@catch_db_error
def purge_expired_tokens(self):
"""purge all expiring token objects from the database
@@ -1995,7 +2016,7 @@ class JupyterHub(Application):
await self._add_tokens(self.service_tokens, kind='service')
await self._add_tokens(self.api_tokens, kind='user')
self.purge_expired_tokens()
await self.purge_expired_tokens()
# purge expired tokens hourly
# we don't need to be prompt about this
# because expired tokens cannot be used anyway
@@ -2042,18 +2063,14 @@ class JupyterHub(Application):
raise AttributeError("No such service field: %s" % key)
setattr(service, key, value)
if service.managed:
if not service.api_token:
# generate new token
# TODO: revoke old tokens?
service.api_token = service.orm.new_api_token(
note="generated at startup"
)
else:
# ensure provided token is registered
self.service_tokens[service.api_token] = service.name
else:
if service.api_token:
self.service_tokens[service.api_token] = service.name
elif service.managed:
# generate new token
# TODO: revoke old tokens?
service.api_token = service.orm.new_api_token(
note="generated at startup"
)
if service.url:
parsed = urlparse(service.url)
@@ -2647,6 +2664,7 @@ class JupyterHub(Application):
with open(self.config_file, mode='w') as f:
f.write(config_text)
@catch_db_error
async def update_last_activity(self):
"""Update User.last_activity timestamps from the proxy"""
routes = await self.proxy.get_all_routes()

View File

@@ -646,6 +646,26 @@ class Authenticator(LoggingConfigurable):
""",
)
auto_login_oauth2_authorize = Bool(
False,
config=True,
help="""
Automatically begin login process for OAuth2 authorization requests
When another application is using JupyterHub as OAuth2 provider, it
sends users to `/hub/api/oauth2/authorize`. If the user isn't logged
in already, and auto_login is not set, the user will be dumped on the
hub's home page, without any context on what to do next.
Setting this to true will automatically redirect users to login if
they aren't logged in *only* on the `/hub/api/oauth2/authorize`
endpoint.
.. versionadded:: 1.5
""",
)
def login_url(self, base_url):
"""Override this when registering a custom login handler

View File

@@ -81,9 +81,14 @@ class BaseHandler(RequestHandler):
"""
try:
await self.get_current_user()
except Exception:
self.log.exception("Failed to get current user")
except Exception as e:
# ensure get_current_user is never called again for this handler,
# since it failed
self._jupyterhub_user = None
self.log.exception("Failed to get current user")
if isinstance(e, SQLAlchemyError):
self.log.error("Rolling back session due to database error")
self.db.rollback()
return await maybe_future(super().prepare())
@@ -426,7 +431,8 @@ class BaseHandler(RequestHandler):
except Exception:
# don't let errors here raise more than once
self._jupyterhub_user = None
self.log.exception("Error getting current user")
# but still raise, which will get handled in .prepare()
raise
return self._jupyterhub_user
@property
@@ -1330,7 +1336,7 @@ class UserUrlHandler(BaseHandler):
**Changed Behavior as of 1.0** This handler no longer triggers a spawn. Instead, it checks if:
1. server is not active, serve page prompting for spawn (status: 503)
1. server is not active, serve page prompting for spawn (status: 424)
2. server is ready (This shouldn't happen! Proxy isn't updated yet. Wait a bit and redirect.)
3. server is active, redirect to /hub/spawn-pending to monitor launch progress
(will redirect back when finished)
@@ -1349,7 +1355,14 @@ class UserUrlHandler(BaseHandler):
self.log.warning(
"Failing suspected API request to not-running server: %s", self.request.path
)
self.set_status(503)
# If we got here, the server is not running. To differentiate
# that the *server* itself is not running, rather than just the particular
# resource *in* the server is not found, we return a 424 instead of a 404.
# We allow retaining the old behavior to support older JupyterLab versions
self.set_status(
424 if not self.app.use_legacy_stopped_server_status_code else 503
)
self.set_header("Content-Type", "application/json")
spawn_url = urlparse(self.request.full_url())._replace(query="")
@@ -1492,14 +1505,10 @@ class UserUrlHandler(BaseHandler):
# if request is expecting JSON, assume it's an API request and fail with 503
# because it won't like the redirect to the pending page
if (
get_accepted_mimetype(
self.request.headers.get('Accept', ''),
choices=['application/json', 'text/html'],
)
== 'application/json'
or 'api' in user_path.split('/')
):
if get_accepted_mimetype(
self.request.headers.get('Accept', ''),
choices=['application/json', 'text/html'],
) == 'application/json' or 'api' in user_path.split('/'):
self._fail_api_request(user_name, server_name)
return
@@ -1514,15 +1523,17 @@ class UserUrlHandler(BaseHandler):
self.redirect(pending_url, status=303)
return
# if we got here, the server is not running
# serve a page prompting for spawn and 503 error
# visiting /user/:name no longer triggers implicit spawn
# without explicit user action
# If we got here, the server is not running. To differentiate
# that the *server* itself is not running, rather than just the particular
# page *in* the server is not found, we return a 424 instead of a 404.
# We allow retaining the old behavior to support older JupyterLab versions
spawn_url = url_concat(
url_path_join(self.hub.base_url, "spawn", user.escaped_name, server_name),
{"next": self.request.uri},
)
self.set_status(503)
self.set_status(
424 if not self.app.use_legacy_stopped_server_status_code else 503
)
auth_state = await user.get_auth_state()
html = await self.render_template(
@@ -1579,7 +1590,7 @@ class UserUrlHandler(BaseHandler):
if redirects:
self.log.warning("Redirect loop detected on %s", self.request.uri)
# add capped exponential backoff where cap is 10s
await asyncio.sleep(min(1 * (2 ** redirects), 10))
await asyncio.sleep(min(1 * (2**redirects), 10))
# rewrite target url with new `redirects` query value
url_parts = urlparse(target)
query_parts = parse_qs(url_parts.query)

View File

@@ -342,7 +342,7 @@ class JupyterHubRequestValidator(RequestValidator):
orm_access_token = orm.OAuthAccessToken(
client=client,
grant_type=orm.GrantType.authorization_code,
expires_at=orm.OAuthAccessToken.now() + token['expires_in'],
expires_at=int(orm.OAuthAccessToken.now() + token['expires_in']),
refresh_token=token['refresh_token'],
# TODO: save scopes,
# scopes=scopes,

View File

@@ -223,7 +223,7 @@ class User(Base):
class Spawner(Base):
""""State about a Spawner"""
""" "State about a Spawner"""
__tablename__ = 'spawners'

View File

@@ -927,8 +927,8 @@ class HubAuthenticated(object):
self._hub_auth_user_cache = None
raise
# store tokens passed via url or header in a cookie for future requests
url_token = self.hub_auth.get_token(self)
# store ?token=... tokens passed via url in a cookie for future requests
url_token = self.get_argument('token', '')
if (
user_model
and url_token

View File

@@ -452,7 +452,7 @@ class SingleUserNotebookAppMixin(Configurable):
i,
RETRIES,
)
await asyncio.sleep(min(2 ** i, 16))
await asyncio.sleep(min(2**i, 16))
else:
break
else:
@@ -675,6 +675,18 @@ class SingleUserNotebookAppMixin(Configurable):
orig_loader = env.loader
env.loader = ChoiceLoader([FunctionLoader(get_page), orig_loader])
def load_server_extensions(self):
# Loading LabApp sets $JUPYTERHUB_API_TOKEN on load, which is incorrect
r = super().load_server_extensions()
# clear the token in PageConfig at this step
# so that cookie auth is used
# FIXME: in the future,
# it would probably make sense to set page_config.token to the token
# from the current request.
if 'page_config_data' in self.web_app.settings:
self.web_app.settings['page_config_data']['token'] = ''
return r
def detect_base_package(App):
"""Detect the base package for an App class

View File

@@ -1008,6 +1008,13 @@ async def test_server_not_running_api_request(app):
assert " /user/bees" in message
async def test_server_not_running_api_request_legacy_status(app):
app.use_legacy_stopped_server_status_code = False
cookies = await app.login_user("bees")
r = await get_page("user/bees/api/status", app, hub=False, cookies=cookies)
assert r.status_code == 424
async def test_metrics_no_auth(app):
r = await get_page("metrics", app)
assert r.status_code == 403

View File

@@ -102,3 +102,51 @@ async def test_external_service(app):
assert len(resp) >= 1
assert isinstance(resp[0], dict)
assert 'name' in resp[0]
async def test_external_services_without_api_token_set(app):
"""
This test was made to reproduce an error like this:
ValueError: Tokens must be at least 8 characters, got ''
The error had the following stack trace in 1.4.1:
jupyterhub/app.py:2213: in init_api_tokens
await self._add_tokens(self.service_tokens, kind='service')
jupyterhub/app.py:2182: in _add_tokens
obj.new_api_token(
jupyterhub/orm.py:424: in new_api_token
return APIToken.new(token=token, service=self, **kwargs)
jupyterhub/orm.py:699: in new
cls.check_token(db, token)
This test also make _add_tokens receive a token_dict that is buggy:
{"": "external_2"}
It turned out that whatever passes token_dict to _add_tokens failed to
ignore service's api_tokens that were None, and instead passes them as blank
strings.
It turned out that init_api_tokens was passing self.service_tokens, and that
self.service_tokens had been populated with blank string tokens for external
services registered with JupyterHub.
"""
name_1 = 'external_1'
name_2 = 'external_2'
async with external_service(app, name=name_1) as env_1, external_service(
app, name=name_2
) as env_2:
app.services = [
{
'name': name_1,
'url': "http://irrelevant",
},
{
'name': name_2,
'url': "http://irrelevant",
},
]
await maybe_future(app.init_services())
await app.init_api_tokens()

View File

@@ -4,9 +4,9 @@
import asyncio
import concurrent.futures
import errno
import functools
import hashlib
import inspect
import os
import random
import secrets
import socket
@@ -22,6 +22,7 @@ from hmac import compare_digest
from operator import itemgetter
from async_generator import aclosing
from sqlalchemy.exc import SQLAlchemyError
from tornado import ioloop
from tornado import web
from tornado.httpclient import AsyncHTTPClient
@@ -642,3 +643,21 @@ def get_accepted_mimetype(accept_header, choices=None):
else:
return mime
return None
def catch_db_error(f):
"""Catch and rollback database errors"""
@functools.wraps(f)
async def catching(self, *args, **kwargs):
try:
r = f(self, *args, **kwargs)
if inspect.isawaitable(r):
r = await r
except SQLAlchemyError:
self.log.exception("Rolling back session due to database error")
self.db.rollback()
else:
return r
return catching

View File

@@ -12,8 +12,11 @@ import shutil
import sys
from subprocess import check_call
from setuptools import Command
from setuptools import setup
from setuptools.command.bdist_egg import bdist_egg
from setuptools.command.build_py import build_py
from setuptools.command.sdist import sdist
v = sys.version_info
@@ -132,14 +135,9 @@ setup_args = dict(
)
# ---------------------------------------------------------------------------
# custom distutils commands
# custom setuptools commands
# ---------------------------------------------------------------------------
# imports here, so they are after setuptools import if there was one
from distutils.cmd import Command
from distutils.command.build_py import build_py
from distutils.command.sdist import sdist
def mtime(path):
"""shorthand for mtime"""

View File

@@ -20,9 +20,14 @@ require(["jquery", "jhapi", "moment"], function ($, JHAPI, moment) {
if (!note.length) {
note = "Requested via token page";
}
var expiration_seconds =
parseInt($("#token-expiration-seconds").val()) || null;
api.request_token(
user,
{ note: note },
{
note: note,
expires_in: expiration_seconds,
},
{
success: function (reply) {
$("#token-result").text(reply.token);

View File

@@ -19,6 +19,20 @@
<small id="note-note" class="form-text text-muted">
This note will help you keep track of what your tokens are for.
</small>
<br><br>
<label for="token-expiration-seconds">Token expires</label>
{% block expiration_options %}
<select id="token-expiration-seconds"
class="form-control">
<option value="3600">1 Day</option>
<option value="86400">1 Week</option>
<option value="604800">1 Month</option>
<option value="" selected="selected">Never</option>
</select>
{% endblock expiration_options %}
<small id="note-expires-at" class="form-text text-muted">
You can configure when your token will be expired.
</small>
</div>
</form>
</div>
@@ -56,6 +70,7 @@
<td>Note</td>
<td>Last used</td>
<td>Created</td>
<td>Expires at</td>
</tr>
</thead>
<tbody>
@@ -77,6 +92,13 @@
N/A
{%- endif -%}
</td>
<td class="time-col col-sm-3">
{%- if token.expires_at -%}
{{ token.expires_at.isoformat() + 'Z' }}
{%- else -%}
Never
{%- endif -%}
</td>
<td class="col-sm-1 text-center">
<button class="revoke-token-btn btn btn-xs btn-danger">revoke</button>
</td>

View File

@@ -26,8 +26,8 @@ import os
import pipes
import shutil
from contextlib import contextmanager
from distutils.version import LooseVersion as V
from distutils.version import LooseVersion as V
from invoke import run as invoke_run
from invoke import task