diff --git a/jupyterhub/apihandlers/base.py b/jupyterhub/apihandlers/base.py index 91e7f6e1..753af60a 100644 --- a/jupyterhub/apihandlers/base.py +++ b/jupyterhub/apihandlers/base.py @@ -2,8 +2,8 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. +from datetime import datetime import json -import datetime from http.client import responses @@ -14,7 +14,17 @@ from .. import orm from ..handlers import BaseHandler from ..utils import isoformat, url_path_join + class APIHandler(BaseHandler): + """Base class for API endpoints + + Differences from page handlers: + + - JSON responses and errors + - strict referer checking for Cookie-authenticated requests + - strict content-security-policy + - methods for REST API models + """ @property def content_security_policy(self): @@ -137,7 +147,7 @@ class APIHandler(BaseHandler): 'oauth_client': token.client.description or token.client.client_id, } if token.expires_at: - expires_at = datetime.datetime.fromtimestamp(token.expires_at) + expires_at = datetime.fromtimestamp(token.expires_at) else: raise TypeError( "token must be an APIToken or OAuthAccessToken, not %s" @@ -157,6 +167,7 @@ class APIHandler(BaseHandler): 'kind': kind, 'created': isoformat(token.created), 'last_activity': isoformat(token.last_activity), + 'expires_at': isoformat(expires_at), } model.update(extra) return model diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index d069f903..8bdce794 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -247,9 +247,11 @@ class TokenPageHandler(BaseHandler): api_tokens.append(token) # group oauth client tokens by client id + # AccessTokens have expires_at as an integer timestamp + now_timestamp = now.timestamp() oauth_tokens = defaultdict(list) for token in user.oauth_tokens: - if token.expires_at and token.expires_at < now: + if token.expires_at and token.expires_at < now_timestamp: self.log.warning("Deleting expired token") self.db.delete(token) self.db.commit() diff --git a/jupyterhub/tests/test_api.py b/jupyterhub/tests/test_api.py index 4e8b1875..4699a40e 100644 --- a/jupyterhub/tests/test_api.py +++ b/jupyterhub/tests/test_api.py @@ -1214,14 +1214,19 @@ def test_token_as_user_deprecated(app, as_user, for_user, status): @mark.gen_test -@mark.parametrize("headers, status, note", [ - ({}, 200, 'test note'), - ({}, 200, ''), - ({'Authorization': 'token bad'}, 403, ''), +@mark.parametrize("headers, status, note, expires_in", [ + ({}, 200, 'test note', None), + ({}, 200, '', 100), + ({'Authorization': 'token bad'}, 403, '', None), ]) -def test_get_new_token(app, headers, status, note): +def test_get_new_token(app, headers, status, note, expires_in): + options = {} if note: - body = json.dumps({'note': note}) + options['note'] = note + if expires_in: + options['expires_in'] = expires_in + if options: + body = json.dumps(options) else: body = '' # request a new token @@ -1239,6 +1244,10 @@ def test_get_new_token(app, headers, status, note): assert reply['user'] == 'admin' assert reply['created'] assert 'last_activity' in reply + if expires_in: + assert isinstance(reply['expires_at'], str) + else: + assert reply['expires_at'] is None if note: assert reply['note'] == note else: diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py index 62a74106..e9f66bd0 100644 --- a/jupyterhub/tests/test_pages.py +++ b/jupyterhub/tests/test_pages.py @@ -598,6 +598,29 @@ def test_announcements(app, announcements): assert_announcement("logout", r.text) +@pytest.mark.gen_test +def test_token_page(app): + name = "cake" + cookies = yield app.login_user(name) + r = yield get_page("token", app, cookies=cookies) + r.raise_for_status() + assert urlparse(r.url).path.endswith('/hub/token') + assert "Request new API token" in r.text + assert "API Tokens" in r.text + assert "Server at %s" % app.users[name].url in r.text + # no oauth tokens yet, shouldn't have that section + assert "Authorized Applications" not in r.text + + # spawn the user to trigger oauth, etc. + r = yield get_page("spawn", app, cookies=cookies) + r.raise_fo_status() + + r = yield get_page("token", app, cookies=cookies) + r.raise_for_status() + assert "API Tokens" in r.text + assert "Authorized Applications" not in r.text + + @pytest.mark.gen_test def test_server_not_running_api_request(app): cookies = yield app.login_user("bees")