From aa23b01a5722759c85810e29340a8d9c5e78cb1e Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 12 Jan 2018 17:42:14 -0800 Subject: [PATCH] add notes on API tokens when they are allocated --- jupyterhub/apihandlers/auth.py | 28 ++++++++++++++++++---------- jupyterhub/app.py | 7 ++++--- jupyterhub/orm.py | 18 ++++++++++-------- jupyterhub/user.py | 11 ++++++++--- 4 files changed, 40 insertions(+), 24 deletions(-) diff --git a/jupyterhub/apihandlers/auth.py b/jupyterhub/apihandlers/auth.py index 39ffa3e8..7d1c8bde 100644 --- a/jupyterhub/apihandlers/auth.py +++ b/jupyterhub/apihandlers/auth.py @@ -11,6 +11,7 @@ from oauth2.web.tornado import OAuth2Handler from tornado import web, gen from .. import orm +from ..user import User from ..utils import token_authenticated from .base import BaseHandler, APIHandler @@ -39,13 +40,13 @@ class TokenAPIHandler(APIHandler): @gen.coroutine def post(self): - user = self.get_current_user() + requester = user = self.get_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: - user = yield self.login_user(data) + requester = user = yield self.login_user(data) except Exception as e: self.log.error("Failure trying to authenticate with form data: %s" % e) user = None @@ -53,15 +54,22 @@ class TokenAPIHandler(APIHandler): raise web.HTTPError(403) else: data = self.get_json_body() - # admin users can request tokens for other usrs - if data and data.get('username') != user.name: - if user.admin: - user = self.find_user(data['username']) - if user is None: - raise web.HTTPError(400, "No such user '%s'" % data['username']) - else: + # 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(403, "Only admins can request tokens for other users.") - api_token = user.new_api_token() + if requester.admin and user is None: + raise web.HTTPError(400, "No such user '%s'" % data['username']) + + note = (data or {}).get('note') + if not note: + note = "via 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, 'user': self.user_model(user), diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 5e973d98..79ba1050 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -153,7 +153,7 @@ class NewToken(Application): if user is None: print("No such user: %s" % self.name, file=sys.stderr) self.exit(1) - token = user.new_api_token() + token = user.new_api_token(note="command-line generated") print(token) @@ -1119,7 +1119,7 @@ class JupyterHub(Application): try: # set generated=False to ensure that user-provided tokens # get extra hashing (don't trust entropy of user-provided tokens) - obj.new_api_token(token, generated=self.trust_user_provided_tokens) + obj.new_api_token(token, note="from config", generated=self.trust_user_provided_tokens) except Exception: if created: # don't allow bad tokens to create users @@ -1173,7 +1173,8 @@ class JupyterHub(Application): if service.managed: if not service.api_token: # generate new token - service.api_token = service.orm.new_api_token() + # TODO: revoke old tokens? + service.api_token = service.orm.new_api_token(note="generated at startup") else: # ensure provided token is registered self.service_tokens[service.api_token] = service.name diff --git a/jupyterhub/orm.py b/jupyterhub/orm.py index 8ec52a75..1d4d6b07 100644 --- a/jupyterhub/orm.py +++ b/jupyterhub/orm.py @@ -155,12 +155,12 @@ class User(Base): running=sum(bool(s.server) for s in self._orm_spawners), ) - def new_api_token(self, token=None, generated=True): + def new_api_token(self, token=None, generated=True, note=''): """Create a new API token If `token` is given, load that token. """ - return APIToken.new(token=token, user=self, generated=generated) + return APIToken.new(token=token, user=self, note='', generated=generated) @classmethod def find(cls, db, name): @@ -216,11 +216,11 @@ class Service(Base): server = relationship(Server, primaryjoin=_server_id == Server.id) pid = Column(Integer) - def new_api_token(self, token=None, generated=True): + def new_api_token(self, token=None, generated=True, note=''): """Create a new API token If `token` is given, load that token. """ - return APIToken.new(token=token, service=self, generated=generated) + return APIToken.new(token=token, service=self, note=note, generated=generated) @classmethod def find(cls, db, name): @@ -230,6 +230,7 @@ class Service(Base): """ return db.query(cls).filter(cls.name == name).first() + class Hashed(object): """Mixin for tables with hashed tokens""" prefix_length = 4 @@ -266,7 +267,7 @@ class Hashed(object): def match(self, token): """Is this my token?""" return compare_token(self.hashed, token) - + @classmethod def check_token(cls, db, token): """Check if a token is acceptable""" @@ -281,7 +282,7 @@ class Hashed(object): @classmethod def find_prefix(cls, db, token): """Start the query for matching token. - + Returns an SQLAlchemy query already filtered by prefix-matches. """ prefix = token[:cls.prefix_length] @@ -303,6 +304,7 @@ class Hashed(object): if orm_token.match(token): return orm_token + class APIToken(Hashed, Base): """An API token""" __tablename__ = 'api_tokens' @@ -363,7 +365,7 @@ class APIToken(Hashed, Base): return orm_token @classmethod - def new(cls, token=None, user=None, service=None, generated=True): + def new(cls, token=None, user=None, service=None, note='', generated=True): """Generate a new API token for a user or service""" assert user or service assert not (user and service) @@ -377,7 +379,7 @@ class APIToken(Hashed, Base): cls.check_token(db, token) # two stages to ensure orm_token.generated has been set # before token setter is called - orm_token = cls(generated=generated) + orm_token = cls(generated=generated, note=note or '') orm_token.token = token if user: assert user.id is not None diff --git a/jupyterhub/user.py b/jupyterhub/user.py index 882ce1b1..aa94d635 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -344,8 +344,10 @@ class User: base_url=base_url, ) db.add(orm_server) - - api_token = self.new_api_token() + note = "server token" + if server_name: + note += " for server %s" % server_name + api_token = self.new_api_token(note=note) db.commit() @@ -420,7 +422,10 @@ class User: ) # use generated=False because we don't trust this token # to have been generated properly - self.new_api_token(spawner.api_token, generated=False) + self.new_api_token(spawner.api_token, + generated=False, + note="retrieved from spawner %s" % server_name, + ) # update OAuth client secret with updated API token if oauth_provider: client_store = oauth_provider.client_authenticator.client_store