Compare commits

...

29 Commits
1.4.1 ... 1.5.0

Author SHA1 Message Date
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
17 changed files with 251 additions and 44 deletions

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.0
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,41 @@ 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.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 +59,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 +114,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 +1132,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

@@ -4,8 +4,8 @@
version_info = (
1,
4,
1,
5,
0,
"", # 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

@@ -369,7 +369,7 @@ class JupyterHub(Application):
even if your Hub authentication is still valid.
If your Hub authentication is valid,
logging in may be a transparent redirect as you refresh the page.
This does not affect JupyterHub API tokens in general,
which do not expire by default.
Only tokens issued during the oauth flow
@@ -862,7 +862,7 @@ class JupyterHub(Application):
"/",
help="""
The routing prefix for the Hub itself.
Override to send only a subset of traffic to the Hub.
Default is to use the Hub as the default route for all requests.
@@ -874,7 +874,7 @@ class JupyterHub(Application):
may want to handle these events themselves,
in which case they can register their own default target with the proxy
and set e.g. `hub_routespec = /hub/` to serve only the hub's own pages, or even `/hub/api/` for api-only operation.
Note: hub_routespec must include the base_url, if any.
.. versionadded:: 1.4
@@ -1448,7 +1448,7 @@ class JupyterHub(Application):
Can be a Unicode string (e.g. '/hub/home') or a callable based on the handler object:
::
def default_url_fn(handler):
user = handler.current_user
if user and user.admin:
@@ -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
@@ -2042,18 +2062,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)

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
@@ -952,8 +972,8 @@ class PAMAuthenticator(LocalAuthenticator):
help="""
Whether to check the user's account status via PAM during authentication.
The PAM account stack performs non-authentication based account
management. It is typically used to restrict/permit access to a
The PAM account stack performs non-authentication based account
management. It is typically used to restrict/permit access to a
service and this step is needed to access the host's user access control.
Disabling this can be dangerous as authenticated but unauthorized users may

View File

@@ -1330,7 +1330,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 +1349,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="")
@@ -1514,15 +1521,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(

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

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

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

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