Deprecate and remove some old auth bits

- remove long-deprecated `POST /api/authorizations/token` for creating tokens
- deprecate but do not remove `GET /api/authorizations/token/:token` in favor of GET /api/user
- remove shared-cookie auth for services from HubAuth, rely on OAuth for browser-auth instead
- use `/hub/api/user` to resolve user instead of `/authorizations/token` which is now deprecated
This commit is contained in:
Min RK
2021-04-23 14:02:50 +02:00
parent f45f1c250f
commit 863b4c7d50
12 changed files with 112 additions and 427 deletions

View File

@@ -203,8 +203,6 @@ To use HubAuth, you must set the `.api_token`, either programmatically when cons
or via the `JUPYTERHUB_API_TOKEN` environment variable. or via the `JUPYTERHUB_API_TOKEN` environment variable.
Most of the logic for authentication implementation is found in the Most of the logic for authentication implementation is found in the
[`HubAuth.user_for_cookie`][hubauth.user_for_cookie]
and in the
[`HubAuth.user_for_token`][hubauth.user_for_token] [`HubAuth.user_for_token`][hubauth.user_for_token]
methods, which makes a request of the Hub, and returns: methods, which makes a request of the Hub, and returns:
@@ -241,11 +239,11 @@ from urllib.parse import quote
from flask import Flask, redirect, request, Response from flask import Flask, redirect, request, Response
from jupyterhub.services.auth import HubAuth from jupyterhub.services.auth import HubOAuth
prefix = os.environ.get('JUPYTERHUB_SERVICE_PREFIX', '/') prefix = os.environ.get('JUPYTERHUB_SERVICE_PREFIX', '/')
auth = HubAuth( auth = HubOAuth(
api_token=os.environ['JUPYTERHUB_API_TOKEN'], api_token=os.environ['JUPYTERHUB_API_TOKEN'],
cache_max_age=60, cache_max_age=60,
) )
@@ -257,11 +255,8 @@ def authenticated(f):
"""Decorator for authenticating with the Hub""" """Decorator for authenticating with the Hub"""
@wraps(f) @wraps(f)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
cookie = request.cookies.get(auth.cookie_name)
token = request.headers.get(auth.auth_header_name) token = request.headers.get(auth.auth_header_name)
if cookie: if token:
user = auth.user_for_cookie(cookie)
elif token:
user = auth.user_for_token(token) user = auth.user_for_token(token)
else: else:
user = None user = None

View File

@@ -1,6 +1,6 @@
# Authenticating a flask service with JupyterHub # Authenticating a flask service with JupyterHub
Uses `jupyterhub.services.HubAuth` to authenticate requests with the Hub in a [flask][] application. Uses `jupyterhub.services.HubOAuth` to authenticate requests with the Hub in a [flask][] application.
## Run ## Run
@@ -8,7 +8,7 @@ Uses `jupyterhub.services.HubAuth` to authenticate requests with the Hub in a [f
jupyterhub --ip=127.0.0.1 jupyterhub --ip=127.0.0.1
2. Visit http://127.0.0.1:8000/services/whoami/ or http://127.0.0.1:8000/services/whoami-oauth/ 2. Visit http://127.0.0.1:8000/services/whoami/
After logging in with your local-system credentials, you should see a JSON dump of your user info: After logging in with your local-system credentials, you should see a JSON dump of your user info:

View File

@@ -5,10 +5,12 @@ c.JupyterHub.services = [
'command': ['flask', 'run', '--port=10101'], 'command': ['flask', 'run', '--port=10101'],
'environment': {'FLASK_APP': 'whoami-flask.py'}, 'environment': {'FLASK_APP': 'whoami-flask.py'},
}, },
{
'name': 'whoami-oauth',
'url': 'http://127.0.0.1:10201',
'command': ['flask', 'run', '--port=10201'],
'environment': {'FLASK_APP': 'whoami-oauth.py'},
},
] ]
# dummy auth and simple spawner for testing
# any username and password will work
c.JupyterHub.spawner_class = 'simple'
c.JupyterHub.authenticator_class = 'dummy'
# listen only on localhost while testing with wide-open auth
c.JupyterHub.ip = '127.0.0.1'

View File

@@ -4,42 +4,48 @@ whoami service authentication with the Hub
""" """
import json import json
import os import os
import secrets
from functools import wraps from functools import wraps
from urllib.parse import quote
from flask import Flask from flask import Flask
from flask import make_response
from flask import redirect from flask import redirect
from flask import request from flask import request
from flask import Response from flask import Response
from flask import session
from jupyterhub.services.auth import HubAuth from jupyterhub.services.auth import HubOAuth
prefix = os.environ.get('JUPYTERHUB_SERVICE_PREFIX', '/') prefix = os.environ.get('JUPYTERHUB_SERVICE_PREFIX', '/')
auth = HubAuth(api_token=os.environ['JUPYTERHUB_API_TOKEN'], cache_max_age=60) auth = HubOAuth(api_token=os.environ['JUPYTERHUB_API_TOKEN'], cache_max_age=60)
app = Flask(__name__) app = Flask(__name__)
# encryption key for session cookies
app.secret_key = secrets.token_bytes(32)
def authenticated(f): def authenticated(f):
"""Decorator for authenticating with the Hub""" """Decorator for authenticating with the Hub via OAuth"""
@wraps(f) @wraps(f)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
cookie = request.cookies.get(auth.cookie_name) token = session.get("token")
token = request.headers.get(auth.auth_header_name)
if cookie: if token:
user = auth.user_for_cookie(cookie)
elif token:
user = auth.user_for_token(token) user = auth.user_for_token(token)
else: else:
user = None user = None
if user: if user:
return f(user, *args, **kwargs) return f(user, *args, **kwargs)
else: else:
# redirect to login url on failed auth # redirect to login url on failed auth
return redirect(auth.login_url + '?next=%s' % quote(request.path)) state = auth.generate_state(next_url=request.path)
response = make_response(redirect(auth.login_url + '&state=%s' % state))
response.set_cookie(auth.state_cookie_name, state)
return response
return decorated return decorated
@@ -50,3 +56,24 @@ def whoami(user):
return Response( return Response(
json.dumps(user, indent=1, sort_keys=True), mimetype='application/json' json.dumps(user, indent=1, sort_keys=True), mimetype='application/json'
) )
@app.route(prefix + 'oauth_callback')
def oauth_callback():
code = request.args.get('code', None)
if code is None:
return 403
# validate state field
arg_state = request.args.get('state', None)
cookie_state = request.cookies.get(auth.state_cookie_name)
if arg_state is None or arg_state != cookie_state:
# state doesn't match
return 403
token = auth.token_for_code(code)
# store token in session cookie
session["token"] = token
next_url = auth.get_next_url(cookie_state) or prefix
response = make_response(redirect(next_url))
return response

View File

@@ -1,72 +0,0 @@
#!/usr/bin/env python3
"""
whoami service authentication with the Hub
"""
import json
import os
from functools import wraps
from flask import Flask
from flask import make_response
from flask import redirect
from flask import request
from flask import Response
from jupyterhub.services.auth import HubOAuth
prefix = os.environ.get('JUPYTERHUB_SERVICE_PREFIX', '/')
auth = HubOAuth(api_token=os.environ['JUPYTERHUB_API_TOKEN'], cache_max_age=60)
app = Flask(__name__)
def authenticated(f):
"""Decorator for authenticating with the Hub via OAuth"""
@wraps(f)
def decorated(*args, **kwargs):
token = request.cookies.get(auth.cookie_name)
if token:
user = auth.user_for_token(token)
else:
user = None
if user:
return f(user, *args, **kwargs)
else:
# redirect to login url on failed auth
state = auth.generate_state(next_url=request.path)
response = make_response(redirect(auth.login_url + '&state=%s' % state))
response.set_cookie(auth.state_cookie_name, state)
return response
return decorated
@app.route(prefix)
@authenticated
def whoami(user):
return Response(
json.dumps(user, indent=1, sort_keys=True), mimetype='application/json'
)
@app.route(prefix + 'oauth_callback')
def oauth_callback():
code = request.args.get('code', None)
if code is None:
return 403
# validate state field
arg_state = request.args.get('state', None)
cookie_state = request.cookies.get(auth.state_cookie_name)
if arg_state is None or arg_state != cookie_state:
# state doesn't match
return 403
token = auth.token_for_code(code)
next_url = auth.get_next_url(cookie_state) or prefix
response = make_response(redirect(next_url))
response.set_cookie(auth.cookie_name, token)
return response

View File

@@ -14,7 +14,6 @@ from tornado import web
from .. import orm from .. import orm
from .. import scopes from .. import scopes
from ..user import User
from ..utils import token_authenticated from ..utils import token_authenticated
from .base import APIHandler from .base import APIHandler
from .base import BaseHandler from .base import BaseHandler
@@ -24,7 +23,7 @@ class TokenAPIHandler(APIHandler):
@token_authenticated @token_authenticated
def get(self, token): def get(self, token):
# FIXME: deprecate this API for oauth token resolution, in favor of using /api/user # FIXME: deprecate this API for oauth token resolution, in favor of using /api/user
# TODO: require specific scope for this deprecated API, applied to oauth client secrets only? # TODO: require specific scope for this deprecated API, applied to service tokens only?
self.log.warning( self.log.warning(
"/authorizations/token/:token endpoint is deprecated in JupyterHub 2.0. Use /api/user" "/authorizations/token/:token endpoint is deprecated in JupyterHub 2.0. Use /api/user"
) )
@@ -55,53 +54,20 @@ class TokenAPIHandler(APIHandler):
self.write(json.dumps(model)) self.write(json.dumps(model))
async def post(self): async def post(self):
warn_msg = (
"Using deprecated token creation endpoint %s."
" Use /hub/api/users/:user/tokens instead."
) % self.request.uri
self.log.warning(warn_msg)
requester = user = self.current_user
if user is None:
# allow requesting a token with username and password
# for authenticators where that's possible
data = self.get_json_body()
try:
requester = user = await self.login_user(data)
except Exception as e:
self.log.error("Failure trying to authenticate with form data: %s" % e)
user = None
if user is None:
raise web.HTTPError(403)
else:
data = self.get_json_body()
# admin users can request tokens for other users
if data and data.get('username'):
user = self.find_user(data['username'])
if user is not requester and not requester.admin:
raise web.HTTPError( raise web.HTTPError(
403, "Only admins can request tokens for other users." 404,
) "Deprecated endpoint /hub/api/authorizations/token is removed in JupyterHub 2.0."
if requester.admin and user is None: " Use /hub/api/users/:user/tokens instead.",
raise web.HTTPError(400, "No such user '%s'" % data['username'])
note = (data or {}).get('note')
if not note:
note = "Requested via deprecated api"
if requester is not user:
kind = 'user' if isinstance(user, User) else 'service'
note += " by %s %s" % (kind, requester.name)
api_token = user.new_api_token(note=note)
self.write(
json.dumps(
{'token': api_token, 'warning': warn_msg, 'user': self.user_model(user)}
)
) )
class CookieAPIHandler(APIHandler): class CookieAPIHandler(APIHandler):
@token_authenticated @token_authenticated
def get(self, cookie_name, cookie_value=None): def get(self, cookie_name, cookie_value=None):
self.log.warning(
"/authorizations/cookie endpoint is deprecated in JupyterHub 2.0. Use /api/user with OAuth tokens."
)
cookie_name = quote(cookie_name, safe='') cookie_name = quote(cookie_name, safe='')
if cookie_value is None: if cookie_value is None:
self.log.warning( self.log.warning(

View File

@@ -44,6 +44,11 @@ class SelfAPIHandler(APIHandler):
self.raw_scopes.update(scopes.identify_scopes(user.orm_user)) self.raw_scopes.update(scopes.identify_scopes(user.orm_user))
self.parsed_scopes = scopes.parse_scopes(self.raw_scopes) self.parsed_scopes = scopes.parse_scopes(self.raw_scopes)
model = self.user_model(user) model = self.user_model(user)
# validate return, should have at least kind and name,
# otherwise our filters did something wrong
for key in ("kind", "name"):
if key not in model:
raise ValueError(f"Missing identify model for {user}: {model}")
self.write(json.dumps(model)) self.write(json.dumps(model))

View File

@@ -269,6 +269,9 @@ def identify_scopes(obj):
for field in {"name", "admin", "groups"} for field in {"name", "admin", "groups"}
} }
elif isinstance(obj, orm.Service): elif isinstance(obj, orm.Service):
# FIXME: need sub-scopes for services
# until then, we have just one service scope:
return {f"read:services!service={obj.name}"}
return { return {
f"read:services:{field}!service={obj.name}" for field in {"name", "admin"} f"read:services:{field}!service={obj.name}" for field in {"name", "admin"}
} }

View File

@@ -1,7 +1,7 @@
"""Authenticating services with JupyterHub. """Authenticating services with JupyterHub.
Cookies are sent to the Hub for verification. The Hub replies with a JSON Tokens are sent to the Hub for verification.
model describing the authenticated user. The Hub replies with a JSON model describing the authenticated user.
``HubAuth`` can be used in any application, even outside tornado. ``HubAuth`` can be used in any application, even outside tornado.
@@ -10,6 +10,7 @@ authenticate with the Hub.
""" """
import base64 import base64
import hashlib
import json import json
import os import os
import random import random
@@ -20,7 +21,6 @@ import time
import uuid import uuid
import warnings import warnings
from unittest import mock from unittest import mock
from urllib.parse import quote
from urllib.parse import urlencode from urllib.parse import urlencode
import requests import requests
@@ -113,9 +113,15 @@ class HubAuth(SingletonConfigurable):
This can be used by any application. This can be used by any application.
Use this base class only for direct, token-authenticated applications
(web APIs).
For applications that support direct visits from browsers,
use HubOAuth to enable OAuth redirect-based authentication.
If using tornado, use via :class:`HubAuthenticated` mixin. If using tornado, use via :class:`HubAuthenticated` mixin.
If using manually, use the ``.user_for_cookie(cookie_value)`` method If using manually, use the ``.user_for_token(token_value)`` method
to identify the user corresponding to a given cookie value. to identify the user owning a given token.
The following config must be set: The following config must be set:
@@ -129,9 +135,6 @@ class HubAuth(SingletonConfigurable):
- cookie_cache_max_age: the number of seconds responses - cookie_cache_max_age: the number of seconds responses
from the Hub should be cached. from the Hub should be cached.
- login_url (the *public* ``/hub/login`` URL of the Hub). - login_url (the *public* ``/hub/login`` URL of the Hub).
- cookie_name: the name of the cookie I should be using,
if different from the default (unlikely).
""" """
hub_host = Unicode( hub_host = Unicode(
@@ -239,10 +242,6 @@ class HubAuth(SingletonConfigurable):
""", """,
).tag(config=True) ).tag(config=True)
cookie_name = Unicode(
'jupyterhub-services', help="""The name of the cookie I should be looking for"""
).tag(config=True)
cookie_options = Dict( cookie_options = Dict(
help="""Additional options to pass when setting cookies. help="""Additional options to pass when setting cookies.
@@ -286,12 +285,12 @@ class HubAuth(SingletonConfigurable):
def _default_cache(self): def _default_cache(self):
return _ExpiringDict(self.cache_max_age) return _ExpiringDict(self.cache_max_age)
def _check_hub_authorization(self, url, cache_key=None, use_cache=True): def _check_hub_authorization(self, url, api_token, cache_key=None, use_cache=True):
"""Identify a user with the Hub """Identify a user with the Hub
Args: Args:
url (str): The API URL to check the Hub for authorization url (str): The API URL to check the Hub for authorization
(e.g. http://127.0.0.1:8081/hub/api/authorizations/token/abc-def) (e.g. http://127.0.0.1:8081/hub/api/user)
cache_key (str): The key for checking the cache cache_key (str): The key for checking the cache
use_cache (bool): Specify use_cache=False to skip cached cookie values (default: True) use_cache (bool): Specify use_cache=False to skip cached cookie values (default: True)
@@ -309,7 +308,12 @@ class HubAuth(SingletonConfigurable):
except KeyError: except KeyError:
app_log.debug("HubAuth cache miss: %s", cache_key) app_log.debug("HubAuth cache miss: %s", cache_key)
data = self._api_request('GET', url, allow_404=True) data = self._api_request(
'GET',
url,
headers={"Authorization": "token " + api_token},
allow_403=True,
)
if data is None: if data is None:
app_log.warning("No Hub user identified for request") app_log.warning("No Hub user identified for request")
else: else:
@@ -321,7 +325,7 @@ class HubAuth(SingletonConfigurable):
def _api_request(self, method, url, **kwargs): def _api_request(self, method, url, **kwargs):
"""Make an API request""" """Make an API request"""
allow_404 = kwargs.pop('allow_404', False) allow_403 = kwargs.pop('allow_403', False)
headers = kwargs.setdefault('headers', {}) headers = kwargs.setdefault('headers', {})
headers.setdefault('Authorization', 'token %s' % self.api_token) headers.setdefault('Authorization', 'token %s' % self.api_token)
if "cert" not in kwargs and self.certfile and self.keyfile: if "cert" not in kwargs and self.certfile and self.keyfile:
@@ -345,7 +349,7 @@ class HubAuth(SingletonConfigurable):
raise HTTPError(500, msg) raise HTTPError(500, msg)
data = None data = None
if r.status_code == 404 and allow_404: if r.status_code == 403 and allow_403:
pass pass
elif r.status_code == 403: elif r.status_code == 403:
app_log.error( app_log.error(
@@ -389,26 +393,9 @@ class HubAuth(SingletonConfigurable):
return data return data
def user_for_cookie(self, encrypted_cookie, use_cache=True, session_id=''): def user_for_cookie(self, encrypted_cookie, use_cache=True, session_id=''):
"""Ask the Hub to identify the user for a given cookie. """Deprecated and removed. Use HubOAuth to authenticate browsers."""
raise RuntimeError(
Args: "Identifying users by shared cookie is removed in JupyterHub 2.0. Use OAuth tokens."
encrypted_cookie (str): the cookie value (not decrypted, the Hub will do that)
use_cache (bool): Specify use_cache=False to skip cached cookie values (default: True)
Returns:
user_model (dict): The user model, if a user is identified, None if authentication fails.
The 'name' field contains the user's name.
"""
return self._check_hub_authorization(
url=url_path_join(
self.api_url,
"authorizations/cookie",
self.cookie_name,
quote(encrypted_cookie, safe=''),
),
cache_key='cookie:{}:{}'.format(session_id, encrypted_cookie),
use_cache=use_cache,
) )
def user_for_token(self, token, use_cache=True, session_id=''): def user_for_token(self, token, use_cache=True, session_id=''):
@@ -425,14 +412,19 @@ class HubAuth(SingletonConfigurable):
""" """
return self._check_hub_authorization( return self._check_hub_authorization(
url=url_path_join( url=url_path_join(
self.api_url, "authorizations/token", quote(token, safe='') self.api_url,
"user",
),
api_token=token,
cache_key='token:{}:{}'.format(
session_id,
hashlib.sha256(token.encode("utf8", "replace")).hexdigest(),
), ),
cache_key='token:{}:{}'.format(session_id, token),
use_cache=use_cache, use_cache=use_cache,
) )
auth_header_name = 'Authorization' auth_header_name = 'Authorization'
auth_header_pat = re.compile(r'token\s+(.+)', re.IGNORECASE) auth_header_pat = re.compile(r'(?:token|bearer)\s+(.+)', re.IGNORECASE)
def get_token(self, handler): def get_token(self, handler):
"""Get the user token from a request """Get the user token from a request
@@ -453,10 +445,8 @@ class HubAuth(SingletonConfigurable):
def _get_user_cookie(self, handler): def _get_user_cookie(self, handler):
"""Get the user model from a cookie""" """Get the user model from a cookie"""
encrypted_cookie = handler.get_cookie(self.cookie_name) # overridden in HubOAuth to store the access token after oauth
session_id = self.get_session_id(handler) return None
if encrypted_cookie:
return self.user_for_cookie(encrypted_cookie, session_id=session_id)
def get_session_id(self, handler): def get_session_id(self, handler):
"""Get the jupyterhub session id """Get the jupyterhub session id
@@ -509,6 +499,9 @@ class HubAuth(SingletonConfigurable):
class HubOAuth(HubAuth): class HubOAuth(HubAuth):
"""HubAuth using OAuth for login instead of cookies set by the Hub. """HubAuth using OAuth for login instead of cookies set by the Hub.
Use this class if you want users to be able to visit your service with a browser.
They will be authenticated via OAuth with the Hub.
.. versionadded: 0.8 .. versionadded: 0.8
""" """

View File

@@ -141,7 +141,6 @@ class OAuthCallbackHandlerMixin(HubOAuthCallbackHandler):
aliases = { aliases = {
'user': 'SingleUserNotebookApp.user', 'user': 'SingleUserNotebookApp.user',
'group': 'SingleUserNotebookApp.group', 'group': 'SingleUserNotebookApp.group',
'cookie-name': 'HubAuth.cookie_name',
'hub-prefix': 'SingleUserNotebookApp.hub_prefix', 'hub-prefix': 'SingleUserNotebookApp.hub_prefix',
'hub-host': 'SingleUserNotebookApp.hub_host', 'hub-host': 'SingleUserNotebookApp.hub_host',
'hub-api-url': 'SingleUserNotebookApp.hub_api_url', 'hub-api-url': 'SingleUserNotebookApp.hub_api_url',

View File

@@ -1176,76 +1176,13 @@ async def test_check_token(app):
assert r.status_code == 404 assert r.status_code == 404
@mark.parametrize("headers, status", [({}, 200), ({'Authorization': 'token bad'}, 403)]) @mark.parametrize("headers, status", [({}, 404), ({'Authorization': 'token bad'}, 404)])
async def test_get_new_token_deprecated(app, headers, status): async def test_get_new_token_deprecated(app, headers, status):
# request a new token # request a new token
r = await api_request( r = await api_request(
app, 'authorizations', 'token', method='post', headers=headers app, 'authorizations', 'token', method='post', headers=headers
) )
assert r.status_code == status assert r.status_code == status
if status != 200:
return
reply = r.json()
assert 'token' in reply
r = await api_request(app, 'authorizations', 'token', reply['token'])
r.raise_for_status()
reply = r.json()
assert reply['name'] == 'admin'
async def test_token_formdata_deprecated(app):
"""Create a token for a user with formdata and no auth header"""
data = {'username': 'fake', 'password': 'fake'}
r = await api_request(
app,
'authorizations',
'token',
method='post',
data=json.dumps(data) if data else None,
noauth=True,
)
assert r.status_code == 200
reply = r.json()
assert 'token' in reply
r = await api_request(app, 'authorizations', 'token', reply['token'])
r.raise_for_status()
reply = r.json()
assert reply['name'] == data['username']
@mark.parametrize(
"as_user, for_user, status",
[
('admin', 'other', 200),
('admin', 'missing', 400),
('user', 'other', 403),
('user', 'user', 200),
],
)
async def test_token_as_user_deprecated(app, as_user, for_user, status):
# ensure both users exist
u = add_user(app.db, app, name=as_user)
if for_user != 'missing':
for_user_obj = add_user(app.db, app, name=for_user)
data = {'username': for_user}
headers = {'Authorization': 'token %s' % u.new_api_token()}
r = await api_request(
app,
'authorizations',
'token',
method='post',
data=json.dumps(data),
headers=headers,
)
assert r.status_code == status
reply = r.json()
if status != 200:
return
assert 'token' in reply
r = await api_request(app, 'authorizations', 'token', reply['token'])
r.raise_for_status()
reply = r.json()
assert reply['name'] == data['username']
@mark.parametrize( @mark.parametrize(

View File

@@ -26,8 +26,8 @@ from tornado.web import RequestHandler
from .. import orm from .. import orm
from ..services.auth import _ExpiringDict from ..services.auth import _ExpiringDict
from ..services.auth import HubAuth from ..services.auth import HubOAuth
from ..services.auth import HubAuthenticated from ..services.auth import HubOAuthenticated
from ..utils import url_path_join from ..utils import url_path_join
from .mocking import public_host from .mocking import public_host
from .mocking import public_url from .mocking import public_url
@@ -76,178 +76,6 @@ def test_expiring_dict():
assert cache.get('key', 'default') == 'cached value' assert cache.get('key', 'default') == 'cached value'
def test_hub_auth():
auth = HubAuth(cookie_name='foo')
mock_model = {'name': 'onyxia'}
url = url_path_join(auth.api_url, "authorizations/cookie/foo/bar")
with requests_mock.Mocker() as m:
m.get(url, text=json.dumps(mock_model))
user_model = auth.user_for_cookie('bar')
assert user_model == mock_model
# check cache
user_model = auth.user_for_cookie('bar')
assert user_model == mock_model
with requests_mock.Mocker() as m:
m.get(url, status_code=404)
user_model = auth.user_for_cookie('bar', use_cache=False)
assert user_model is None
# invalidate cache with timer
mock_model = {'name': 'willow'}
with monotonic_future, requests_mock.Mocker() as m:
m.get(url, text=json.dumps(mock_model))
user_model = auth.user_for_cookie('bar')
assert user_model == mock_model
with requests_mock.Mocker() as m:
m.get(url, status_code=500)
with raises(HTTPError) as exc_info:
user_model = auth.user_for_cookie('bar', use_cache=False)
assert exc_info.value.status_code == 502
with requests_mock.Mocker() as m:
m.get(url, status_code=400)
with raises(HTTPError) as exc_info:
user_model = auth.user_for_cookie('bar', use_cache=False)
assert exc_info.value.status_code == 500
def test_hub_authenticated(request):
auth = HubAuth(cookie_name='jubal')
mock_model = {'name': 'jubalearly', 'groups': ['lions']}
cookie_url = url_path_join(auth.api_url, "authorizations/cookie", auth.cookie_name)
good_url = url_path_join(cookie_url, "early")
bad_url = url_path_join(cookie_url, "late")
class TestHandler(HubAuthenticated, RequestHandler):
hub_auth = auth
@authenticated
def get(self):
self.finish(self.get_current_user())
# start hub-authenticated service in a thread:
port = 50505
q = Queue()
def run():
asyncio.set_event_loop(asyncio.new_event_loop())
app = Application([('/*', TestHandler)], login_url=auth.login_url)
http_server = HTTPServer(app)
http_server.listen(port)
loop = IOLoop.current()
loop.add_callback(lambda: q.put(loop))
loop.start()
t = Thread(target=run)
t.start()
def finish_thread():
loop.add_callback(loop.stop)
t.join(timeout=30)
assert not t.is_alive()
request.addfinalizer(finish_thread)
# wait for thread to start
loop = q.get(timeout=10)
with requests_mock.Mocker(real_http=True) as m:
# no cookie
r = requests.get('http://127.0.0.1:%i' % port, allow_redirects=False)
r.raise_for_status()
assert r.status_code == 302
assert auth.login_url in r.headers['Location']
# wrong cookie
m.get(bad_url, status_code=404)
r = requests.get(
'http://127.0.0.1:%i' % port,
cookies={'jubal': 'late'},
allow_redirects=False,
)
r.raise_for_status()
assert r.status_code == 302
assert auth.login_url in r.headers['Location']
# clear the cache because we are going to request
# the same url again with a different result
auth.cache.clear()
# upstream 403
m.get(bad_url, status_code=403)
r = requests.get(
'http://127.0.0.1:%i' % port,
cookies={'jubal': 'late'},
allow_redirects=False,
)
assert r.status_code == 500
m.get(good_url, text=json.dumps(mock_model))
# no specific allowed user
r = requests.get(
'http://127.0.0.1:%i' % port,
cookies={'jubal': 'early'},
allow_redirects=False,
)
r.raise_for_status()
assert r.status_code == 200
# pass allowed user
TestHandler.hub_users = {'jubalearly'}
r = requests.get(
'http://127.0.0.1:%i' % port,
cookies={'jubal': 'early'},
allow_redirects=False,
)
r.raise_for_status()
assert r.status_code == 200
# no pass allowed ser
TestHandler.hub_users = {'kaylee'}
r = requests.get(
'http://127.0.0.1:%i' % port,
cookies={'jubal': 'early'},
allow_redirects=False,
)
assert r.status_code == 403
# pass allowed group
TestHandler.hub_groups = {'lions'}
r = requests.get(
'http://127.0.0.1:%i' % port,
cookies={'jubal': 'early'},
allow_redirects=False,
)
r.raise_for_status()
assert r.status_code == 200
# no pass allowed group
TestHandler.hub_groups = {'tigers'}
r = requests.get(
'http://127.0.0.1:%i' % port,
cookies={'jubal': 'early'},
allow_redirects=False,
)
assert r.status_code == 403
async def test_hubauth_cookie(app, mockservice_url):
"""Test HubAuthenticated service with user cookies"""
cookies = await app.login_user('badger')
r = await async_requests.get(
public_url(app, mockservice_url) + '/whoami/', cookies=cookies
)
r.raise_for_status()
print(r.text)
reply = r.json()
sub_reply = {key: reply.get(key, 'missing') for key in ['name', 'admin']}
assert sub_reply == {'name': 'badger', 'admin': False}
async def test_hubauth_token(app, mockservice_url): async def test_hubauth_token(app, mockservice_url):
"""Test HubAuthenticated service with user API tokens""" """Test HubAuthenticated service with user API tokens"""
u = add_user(app.db, name='river') u = add_user(app.db, name='river')
@@ -295,8 +123,10 @@ async def test_hubauth_service_token(app, mockservice_url):
r = await async_requests.get( r = await async_requests.get(
public_url(app, mockservice_url) + '/whoami/', public_url(app, mockservice_url) + '/whoami/',
headers={'Authorization': 'token %s' % token}, headers={'Authorization': 'token %s' % token},
allow_redirects=False,
) )
r.raise_for_status() r.raise_for_status()
assert r.status_code == 200
reply = r.json() reply = r.json()
assert reply == {'kind': 'service', 'name': name, 'admin': False, 'roles': []} assert reply == {'kind': 'service', 'name': name, 'admin': False, 'roles': []}
assert not r.cookies assert not r.cookies