Compare commits

...

8 Commits
1.4.1 ... 1.4.2

Author SHA1 Message Date
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
8 changed files with 88 additions and 17 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

@@ -3,7 +3,7 @@
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
https://github.com/jupyterhub/autodoc-traits/archive/d22282c1c18c6865436e06d8b329c06fe12a07f8.zip
pydata-sphinx-theme
pytablewriter>=0.56
recommonmark>=0.6

View File

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

@@ -24,6 +24,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 +79,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))
@@ -1072,6 +1098,7 @@ 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
[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

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

View File

@@ -2042,18 +2042,14 @@ class JupyterHub(Application):
raise AttributeError("No such service field: %s" % key)
setattr(service, key, value)
if service.managed:
if not service.api_token:
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"
)
else:
# ensure provided token is registered
self.service_tokens[service.api_token] = service.name
else:
self.service_tokens[service.api_token] = service.name
if service.url:
parsed = urlparse(service.url)

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

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