mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-12 20:43:02 +00:00
test coverage for auth expiry
This commit is contained in:
@@ -236,7 +236,7 @@ class BaseHandler(RequestHandler):
|
|||||||
self.db.commit()
|
self.db.commit()
|
||||||
return self._user_from_orm(orm_token.user)
|
return self._user_from_orm(orm_token.user)
|
||||||
|
|
||||||
async def refresh_user_auth(self, user, force=False):
|
async def refresh_auth(self, user, force=False):
|
||||||
"""Refresh user authentication info
|
"""Refresh user authentication info
|
||||||
|
|
||||||
Calls `authenticator.refresh_user(user)`
|
Calls `authenticator.refresh_user(user)`
|
||||||
@@ -257,7 +257,6 @@ class BaseHandler(RequestHandler):
|
|||||||
if not force and user._auth_refreshed and (now - user._auth_refreshed < refresh_age):
|
if not force and user._auth_refreshed and (now - user._auth_refreshed < refresh_age):
|
||||||
# auth up-to-date
|
# auth up-to-date
|
||||||
return user
|
return user
|
||||||
user._auth_refreshed = now
|
|
||||||
|
|
||||||
# refresh a user at most once per request
|
# refresh a user at most once per request
|
||||||
if not hasattr(self, '_refreshed_users'):
|
if not hasattr(self, '_refreshed_users'):
|
||||||
@@ -277,6 +276,8 @@ class BaseHandler(RequestHandler):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
user._auth_refreshed = now
|
||||||
|
|
||||||
if auth_info == True:
|
if auth_info == True:
|
||||||
# refresh_user confirmed that it's up-to-date,
|
# refresh_user confirmed that it's up-to-date,
|
||||||
# nothing to refresh
|
# nothing to refresh
|
||||||
@@ -358,7 +359,7 @@ class BaseHandler(RequestHandler):
|
|||||||
if user is None:
|
if user is None:
|
||||||
user = self.get_current_user_cookie()
|
user = self.get_current_user_cookie()
|
||||||
if user:
|
if user:
|
||||||
user = await self.refresh_user_auth(user)
|
user = await self.refresh_auth(user)
|
||||||
self._jupyterhub_user = user
|
self._jupyterhub_user = user
|
||||||
except Exception:
|
except Exception:
|
||||||
# don't let errors here raise more than once
|
# don't let errors here raise more than once
|
||||||
@@ -612,6 +613,7 @@ class BaseHandler(RequestHandler):
|
|||||||
self.statsd.incr('login.success')
|
self.statsd.incr('login.success')
|
||||||
self.statsd.timing('login.authenticate.success', auth_timer.ms)
|
self.statsd.timing('login.authenticate.success', auth_timer.ms)
|
||||||
self.log.info("User logged in: %s", user.name)
|
self.log.info("User logged in: %s", user.name)
|
||||||
|
user._auth_refreshed = time.monotonic()
|
||||||
return user
|
return user
|
||||||
else:
|
else:
|
||||||
self.statsd.incr('login.failure')
|
self.statsd.incr('login.failure')
|
||||||
@@ -646,9 +648,9 @@ class BaseHandler(RequestHandler):
|
|||||||
async def spawn_single_user(self, user, server_name='', options=None):
|
async def spawn_single_user(self, user, server_name='', options=None):
|
||||||
# in case of error, include 'try again from /hub/home' message
|
# in case of error, include 'try again from /hub/home' message
|
||||||
if self.authenticator.refresh_pre_spawn:
|
if self.authenticator.refresh_pre_spawn:
|
||||||
auth_user = await self.refresh_user(user, force=True)
|
auth_user = await self.refresh_auth(user, force=True)
|
||||||
if auth_user is None:
|
if auth_user is None:
|
||||||
raise web.HTTPError(403, "auth has expired for %s, login again", auth_user.name)
|
raise web.HTTPError(403, "auth has expired for %s, login again", user.name)
|
||||||
|
|
||||||
spawn_start_time = time.perf_counter()
|
spawn_start_time = time.perf_counter()
|
||||||
self.extra_error_html = self.spawn_home_error
|
self.extra_error_html = self.spawn_home_error
|
||||||
|
179
jupyterhub/tests/test_auth_expiry.py
Normal file
179
jupyterhub/tests/test_auth_expiry.py
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
"""
|
||||||
|
test authentication expiry
|
||||||
|
|
||||||
|
authentication can expire in a number of ways:
|
||||||
|
|
||||||
|
- needs refresh and can be refreshed
|
||||||
|
- doesn't need refresh
|
||||||
|
- needs refresh and cannot be refreshed without new login
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from unittest import mock
|
||||||
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from .utils import api_request, get_page
|
||||||
|
|
||||||
|
|
||||||
|
async def refresh_expired(authenticator, user):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def disable_refresh(app):
|
||||||
|
"""Fixture disabling auth refresh"""
|
||||||
|
with mock.patch.object(app.authenticator, 'refresh_user', refresh_expired):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def refresh_pre_spawn(app):
|
||||||
|
"""Fixture enabling auth refresh pre spawn"""
|
||||||
|
app.authenticator.refresh_pre_spawn = True
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
app.authenticator.refresh_pre_spawn = False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_auth_refresh_at_login(app, user):
|
||||||
|
# auth_refreshed starts unset:
|
||||||
|
assert not user._auth_refreshed
|
||||||
|
# login sets auth_refreshed timestamp
|
||||||
|
await app.login_user(user.name)
|
||||||
|
assert user._auth_refreshed
|
||||||
|
user._auth_refreshed -= 10
|
||||||
|
before = user._auth_refreshed
|
||||||
|
# login again updates auth_refreshed timestamp
|
||||||
|
# even when auth is fresh
|
||||||
|
await app.login_user(user.name)
|
||||||
|
assert user._auth_refreshed > before
|
||||||
|
|
||||||
|
|
||||||
|
async def test_auth_refresh_page(app, user):
|
||||||
|
cookies = await app.login_user(user.name)
|
||||||
|
assert user._auth_refreshed
|
||||||
|
user._auth_refreshed -= 10
|
||||||
|
before = user._auth_refreshed
|
||||||
|
|
||||||
|
# get a page with auth not expired
|
||||||
|
# doesn't trigger refresh
|
||||||
|
r = await get_page('home', app, cookies=cookies)
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert user._auth_refreshed == before
|
||||||
|
|
||||||
|
# get a page with stale auth, refreshes auth
|
||||||
|
user._auth_refreshed -= app.authenticator.auth_refresh_age
|
||||||
|
r = await get_page('home', app, cookies=cookies)
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert user._auth_refreshed > before
|
||||||
|
|
||||||
|
|
||||||
|
async def test_auth_expired_page(app, user, disable_refresh):
|
||||||
|
cookies = await app.login_user(user.name)
|
||||||
|
assert user._auth_refreshed
|
||||||
|
user._auth_refreshed -= 10
|
||||||
|
before = user._auth_refreshed
|
||||||
|
|
||||||
|
# auth is fresh, doesn't trigger expiry
|
||||||
|
r = await get_page('home', app, cookies=cookies)
|
||||||
|
assert user._auth_refreshed == before
|
||||||
|
assert r.status_code == 200
|
||||||
|
|
||||||
|
# get a page with stale auth, triggers expiry
|
||||||
|
user._auth_refreshed -= app.authenticator.auth_refresh_age
|
||||||
|
before = user._auth_refreshed
|
||||||
|
r = await get_page('home', app, cookies=cookies, allow_redirects=False)
|
||||||
|
|
||||||
|
# verify that we redirect to login with ?next=requested page
|
||||||
|
assert r.status_code == 302
|
||||||
|
redirect_url = urlparse(r.headers['Location'])
|
||||||
|
assert redirect_url.path.endswith('/login')
|
||||||
|
query = parse_qs(redirect_url.query)
|
||||||
|
assert query['next']
|
||||||
|
next_url = urlparse(query['next'][0])
|
||||||
|
assert next_url.path == urlparse(r.url).path
|
||||||
|
|
||||||
|
# make sure refresh didn't get updated
|
||||||
|
assert user._auth_refreshed == before
|
||||||
|
|
||||||
|
|
||||||
|
async def test_auth_expired_api(app, user, disable_refresh):
|
||||||
|
cookies = await app.login_user(user.name)
|
||||||
|
assert user._auth_refreshed
|
||||||
|
user._auth_refreshed -= 10
|
||||||
|
before = user._auth_refreshed
|
||||||
|
|
||||||
|
# auth is fresh, doesn't trigger expiry
|
||||||
|
r = await api_request(app, 'users/' + user.name, name=user.name)
|
||||||
|
assert user._auth_refreshed == before
|
||||||
|
assert r.status_code == 200
|
||||||
|
|
||||||
|
# get a page with stale auth, triggers expiry
|
||||||
|
user._auth_refreshed -= app.authenticator.auth_refresh_age
|
||||||
|
r = await api_request(app, 'users/' + user.name, name=user.name)
|
||||||
|
# api requests can't do login redirects
|
||||||
|
assert r.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
async def test_refresh_pre_spawn(app, user, refresh_pre_spawn):
|
||||||
|
cookies = await app.login_user(user.name)
|
||||||
|
assert user._auth_refreshed
|
||||||
|
user._auth_refreshed -= 10
|
||||||
|
before = user._auth_refreshed
|
||||||
|
|
||||||
|
# auth is fresh, but should be forced to refresh by spawn
|
||||||
|
r = await api_request(
|
||||||
|
app, 'users/{}/server'.format(user.name), method='post', name=user.name
|
||||||
|
)
|
||||||
|
assert 200 <= r.status_code < 300
|
||||||
|
assert user._auth_refreshed > before
|
||||||
|
|
||||||
|
|
||||||
|
async def test_refresh_pre_spawn_expired(app, user, refresh_pre_spawn, disable_refresh):
|
||||||
|
cookies = await app.login_user(user.name)
|
||||||
|
assert user._auth_refreshed
|
||||||
|
user._auth_refreshed -= 10
|
||||||
|
before = user._auth_refreshed
|
||||||
|
|
||||||
|
# auth is fresh, doesn't trigger expiry
|
||||||
|
r = await api_request(
|
||||||
|
app, 'users/{}/server'.format(user.name), method='post', name=user.name
|
||||||
|
)
|
||||||
|
assert r.status_code == 403
|
||||||
|
assert user._auth_refreshed == before
|
||||||
|
|
||||||
|
|
||||||
|
async def test_refresh_pre_spawn_admin_request(
|
||||||
|
app, user, admin_user, refresh_pre_spawn
|
||||||
|
):
|
||||||
|
await app.login_user(user.name)
|
||||||
|
await app.login_user(admin_user.name)
|
||||||
|
user._auth_refreshed -= 10
|
||||||
|
before = user._auth_refreshed
|
||||||
|
|
||||||
|
# admin request, auth is fresh. Should still refresh user auth.
|
||||||
|
r = await api_request(
|
||||||
|
app, 'users', user.name, 'server', method='post', name=admin_user.name
|
||||||
|
)
|
||||||
|
assert 200 <= r.status_code < 300
|
||||||
|
assert user._auth_refreshed > before
|
||||||
|
|
||||||
|
|
||||||
|
async def test_refresh_pre_spawn_expired_admin_request(
|
||||||
|
app, user, admin_user, refresh_pre_spawn, disable_refresh
|
||||||
|
):
|
||||||
|
await app.login_user(user.name)
|
||||||
|
await app.login_user(admin_user.name)
|
||||||
|
user._auth_refreshed -= 10
|
||||||
|
|
||||||
|
# auth needs refresh but can't without a new login; spawn should fail
|
||||||
|
user._auth_refreshed -= app.authenticator.auth_refresh_age
|
||||||
|
r = await api_request(
|
||||||
|
app, 'users', user.name, 'server', method='post', name=admin_user.name
|
||||||
|
)
|
||||||
|
# api requests can't do login redirects
|
||||||
|
assert r.status_code == 403
|
@@ -129,7 +129,7 @@ async def api_request(app, *api_path, **kwargs):
|
|||||||
# make a copy to avoid modifying arg in-place
|
# make a copy to avoid modifying arg in-place
|
||||||
kwargs['headers'] = h = {}
|
kwargs['headers'] = h = {}
|
||||||
h.update(headers)
|
h.update(headers)
|
||||||
h.update(auth_header(app.db, 'admin'))
|
h.update(auth_header(app.db, kwargs.pop('name', 'admin')))
|
||||||
|
|
||||||
url = ujoin(base_url, 'api', *api_path)
|
url = ujoin(base_url, 'api', *api_path)
|
||||||
method = kwargs.pop('method', 'get')
|
method = kwargs.pop('method', 'get')
|
||||||
|
@@ -138,7 +138,7 @@ class User:
|
|||||||
orm_user = None
|
orm_user = None
|
||||||
log = app_log
|
log = app_log
|
||||||
settings = None
|
settings = None
|
||||||
auth_refreshed = None
|
_auth_refreshed = None
|
||||||
|
|
||||||
def __init__(self, orm_user, settings=None, db=None):
|
def __init__(self, orm_user, settings=None, db=None):
|
||||||
self.db = db or inspect(orm_user).session
|
self.db = db or inspect(orm_user).session
|
||||||
|
Reference in New Issue
Block a user