From af0d81436d36692631f683e5431506a15ed6f71b Mon Sep 17 00:00:00 2001 From: Min RK Date: Mon, 22 Mar 2021 12:17:31 +0100 Subject: [PATCH 1/4] alpine dockerfile: avoid compilation by getting some deps from apk cryptography is the big one, which needs rust and is a huge pain --- dockerfiles/Dockerfile.alpine | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) 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"] From 665e5c7427f57d307da63ed896493f89c7185d02 Mon Sep 17 00:00:00 2001 From: Min RK Date: Mon, 22 Mar 2021 13:10:03 +0100 Subject: [PATCH 2/4] ensure /authorizations/token can read the owner model of the token itself --- jupyterhub/apihandlers/auth.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/jupyterhub/apihandlers/auth.py b/jupyterhub/apihandlers/auth.py index 76fcd8b8..7e28fd2d 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,6 +23,11 @@ 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) @@ -32,9 +37,15 @@ class TokenAPIHandler(APIHandler): # record activity whenever we see a token now = orm_token.last_activity = datetime.utcnow() if orm_token.user: + # 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.add(f'read:users!user={orm_token.user.name}') + self.parsed_scopes = scopes.parse_scopes(self.raw_scopes) orm_token.user.last_activity = now model = self.user_model(self.users[orm_token.user]) elif orm_token.service: + self.raw_scopes.add(f'read:services!service={orm_token.service.name}') + self.parsed_scopes = scopes.parse_scopes(self.raw_scopes) model = self.service_model(orm_token.service) else: self.log.warning("%s has no user or service. Deleting..." % orm_token) From 58a80e50505478d4b08174e02e26bf2205d45dde Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 23 Mar 2021 13:27:00 +0100 Subject: [PATCH 3/4] ensure MockAPIHandler has request.path defined --- jupyterhub/tests/test_scopes.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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): From 97e1a5cb2690d3bf8e11625aaf8bce87c415329f Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 23 Mar 2021 13:56:46 +0100 Subject: [PATCH 4/4] add scopes.identify_scopes helper --- jupyterhub/apihandlers/auth.py | 13 +++++++------ jupyterhub/scopes.py | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/jupyterhub/apihandlers/auth.py b/jupyterhub/apihandlers/auth.py index 7e28fd2d..938d88ec 100644 --- a/jupyterhub/apihandlers/auth.py +++ b/jupyterhub/apihandlers/auth.py @@ -34,18 +34,19 @@ class TokenAPIHandler(APIHandler): 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: - # 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.add(f'read:users!user={orm_token.user.name}') - self.parsed_scopes = scopes.parse_scopes(self.raw_scopes) orm_token.user.last_activity = now model = self.user_model(self.users[orm_token.user]) elif orm_token.service: - self.raw_scopes.add(f'read:services!service={orm_token.service.name}') - self.parsed_scopes = scopes.parse_scopes(self.raw_scopes) model = self.service_model(orm_token.service) else: self.log.warning("%s has no user or service. Deleting..." % orm_token) 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}")