diff --git a/dockerfiles/Dockerfile.alpine b/dockerfiles/Dockerfile.alpine index be7f64b0..79ccd774 100644 --- a/dockerfiles/Dockerfile.alpine +++ b/dockerfiles/Dockerfile.alpine @@ -1,9 +1,14 @@ -FROM python:3.6.3-alpine3.6 - -ARG JUPYTERHUB_VERSION=0.8.1 - -RUN pip3 install --no-cache jupyterhub==${JUPYTERHUB_VERSION} +FROM alpine:3.13 ENV LANG=en_US.UTF-8 +RUN apk add --no-cache \ + python3 \ + py3-pip \ + py3-ruamel.yaml \ + py3-cryptography \ + py3-sqlalchemy + +ARG JUPYTERHUB_VERSION=1.3.0 +RUN pip3 install --no-cache jupyterhub==${JUPYTERHUB_VERSION} USER nobody CMD ["jupyterhub"] diff --git a/jupyterhub/apihandlers/auth.py b/jupyterhub/apihandlers/auth.py index 76fcd8b8..938d88ec 100644 --- a/jupyterhub/apihandlers/auth.py +++ b/jupyterhub/apihandlers/auth.py @@ -13,8 +13,8 @@ from oauthlib import oauth2 from tornado import web from .. import orm +from .. import scopes from ..user import User -from ..utils import compare_token from ..utils import token_authenticated from .base import APIHandler from .base import BaseHandler @@ -23,12 +23,24 @@ from .base import BaseHandler class TokenAPIHandler(APIHandler): @token_authenticated def get(self, token): + # 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? + self.log.warning( + "/authorizations/token/:token endpoint is deprecated in JupyterHub 2.0. Use /api/user" + ) orm_token = orm.APIToken.find(self.db, token) if orm_token is None: orm_token = orm.OAuthAccessToken.find(self.db, token) if orm_token is None: raise web.HTTPError(404) + owner = orm_token.user or orm_token.service + if owner: + # having a token means we should be able to read the owner's model + # (this is the only thing this handler is for) + self.raw_scopes.update(scopes.identify_scopes(owner)) + self.parsed_scopes = scopes.parse_scopes(self.raw_scopes) + # record activity whenever we see a token now = orm_token.last_activity = datetime.utcnow() if orm_token.user: diff --git a/jupyterhub/scopes.py b/jupyterhub/scopes.py index 838cd10b..0819cc08 100644 --- a/jupyterhub/scopes.py +++ b/jupyterhub/scopes.py @@ -185,3 +185,25 @@ def needs_scope(*scopes): return _auth_func return scope_decorator + + +def identify_scopes(obj): + """Return 'identify' scopes for an orm object + + Arguments: + obj: orm.User or orm.Service + + Returns: + scopes (set): set of scopes needed for 'identify' endpoints + """ + if isinstance(obj, orm.User): + return { + f"read:users:{field}!user={obj.name}" + for field in {"name", "admin", "groups"} + } + elif isinstance(obj, orm.Service): + return { + f"read:services:{field}!service={obj.name}" for field in {"name", "admin"} + } + else: + raise TypeError(f"Expected orm.User or orm.Service, got {obj!r}") diff --git a/jupyterhub/tests/test_scopes.py b/jupyterhub/tests/test_scopes.py index abc1c835..ecc2af22 100644 --- a/jupyterhub/tests/test_scopes.py +++ b/jupyterhub/tests/test_scopes.py @@ -85,6 +85,8 @@ class MockAPIHandler: def __init__(self): self.raw_scopes = {'users'} self.parsed_scopes = {} + self.request = mock.Mock(spec=HTTPServerRequest) + self.request.path = '/path' @needs_scope('users') def user_thing(self, user_name): @@ -169,7 +171,6 @@ class MockAPIHandler: def test_scope_method_access(scopes, method, arguments, is_allowed): obj = MockAPIHandler() obj.current_user = mock.Mock(name=arguments[0]) - obj.request = mock.Mock(spec=HTTPServerRequest) obj.raw_scopes = set(scopes) obj.parsed_scopes = parse_scopes(obj.raw_scopes) api_call = getattr(obj, method) @@ -183,7 +184,6 @@ def test_scope_method_access(scopes, method, arguments, is_allowed): def test_double_scoped_method_succeeds(): obj = MockAPIHandler() obj.current_user = mock.Mock(name='lucille') - obj.request = mock.Mock(spec=HTTPServerRequest) obj.raw_scopes = {'users', 'read:services'} obj.parsed_scopes = parse_scopes(obj.raw_scopes) assert obj.secret_thing() @@ -192,7 +192,6 @@ def test_double_scoped_method_succeeds(): def test_double_scoped_method_denials(): obj = MockAPIHandler() obj.current_user = mock.Mock(name='lucille2') - obj.request = mock.Mock(spec=HTTPServerRequest) obj.raw_scopes = {'users', 'read:groups'} obj.parsed_scopes = parse_scopes(obj.raw_scopes) with pytest.raises(web.HTTPError):