add notes on API tokens when they are allocated

This commit is contained in:
Min RK
2018-01-12 17:42:14 -08:00
parent d82de98001
commit aa23b01a57
4 changed files with 40 additions and 24 deletions

View File

@@ -11,6 +11,7 @@ from oauth2.web.tornado import OAuth2Handler
from tornado import web, gen from tornado import web, gen
from .. import orm from .. import orm
from ..user import User
from ..utils import token_authenticated from ..utils import token_authenticated
from .base import BaseHandler, APIHandler from .base import BaseHandler, APIHandler
@@ -39,13 +40,13 @@ class TokenAPIHandler(APIHandler):
@gen.coroutine @gen.coroutine
def post(self): def post(self):
user = self.get_current_user() requester = user = self.get_current_user()
if user is None: if user is None:
# allow requesting a token with username and password # allow requesting a token with username and password
# for authenticators where that's possible # for authenticators where that's possible
data = self.get_json_body() data = self.get_json_body()
try: try:
user = yield self.login_user(data) requester = user = yield self.login_user(data)
except Exception as e: except Exception as e:
self.log.error("Failure trying to authenticate with form data: %s" % e) self.log.error("Failure trying to authenticate with form data: %s" % e)
user = None user = None
@@ -53,15 +54,22 @@ class TokenAPIHandler(APIHandler):
raise web.HTTPError(403) raise web.HTTPError(403)
else: else:
data = self.get_json_body() data = self.get_json_body()
# admin users can request tokens for other usrs # admin users can request tokens for other users
if data and data.get('username') != user.name: if data and data.get('username'):
if user.admin: user = self.find_user(data['username'])
user = self.find_user(data['username']) if user is not requester and not requester.admin:
if user is None:
raise web.HTTPError(400, "No such user '%s'" % data['username'])
else:
raise web.HTTPError(403, "Only admins can request tokens for other users.") 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({ self.write(json.dumps({
'token': api_token, 'token': api_token,
'user': self.user_model(user), 'user': self.user_model(user),

View File

@@ -153,7 +153,7 @@ class NewToken(Application):
if user is None: if user is None:
print("No such user: %s" % self.name, file=sys.stderr) print("No such user: %s" % self.name, file=sys.stderr)
self.exit(1) self.exit(1)
token = user.new_api_token() token = user.new_api_token(note="command-line generated")
print(token) print(token)
@@ -1119,7 +1119,7 @@ class JupyterHub(Application):
try: try:
# set generated=False to ensure that user-provided tokens # set generated=False to ensure that user-provided tokens
# get extra hashing (don't trust entropy of 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: except Exception:
if created: if created:
# don't allow bad tokens to create users # don't allow bad tokens to create users
@@ -1173,7 +1173,8 @@ class JupyterHub(Application):
if service.managed: if service.managed:
if not service.api_token: if not service.api_token:
# generate new 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: else:
# ensure provided token is registered # ensure provided token is registered
self.service_tokens[service.api_token] = service.name self.service_tokens[service.api_token] = service.name

View File

@@ -155,12 +155,12 @@ class User(Base):
running=sum(bool(s.server) for s in self._orm_spawners), 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 """Create a new API token
If `token` is given, load that 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 @classmethod
def find(cls, db, name): def find(cls, db, name):
@@ -216,11 +216,11 @@ class Service(Base):
server = relationship(Server, primaryjoin=_server_id == Server.id) server = relationship(Server, primaryjoin=_server_id == Server.id)
pid = Column(Integer) 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 """Create a new API token
If `token` is given, load that 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 @classmethod
def find(cls, db, name): def find(cls, db, name):
@@ -230,6 +230,7 @@ class Service(Base):
""" """
return db.query(cls).filter(cls.name == name).first() return db.query(cls).filter(cls.name == name).first()
class Hashed(object): class Hashed(object):
"""Mixin for tables with hashed tokens""" """Mixin for tables with hashed tokens"""
prefix_length = 4 prefix_length = 4
@@ -266,7 +267,7 @@ class Hashed(object):
def match(self, token): def match(self, token):
"""Is this my token?""" """Is this my token?"""
return compare_token(self.hashed, token) return compare_token(self.hashed, token)
@classmethod @classmethod
def check_token(cls, db, token): def check_token(cls, db, token):
"""Check if a token is acceptable""" """Check if a token is acceptable"""
@@ -281,7 +282,7 @@ class Hashed(object):
@classmethod @classmethod
def find_prefix(cls, db, token): def find_prefix(cls, db, token):
"""Start the query for matching token. """Start the query for matching token.
Returns an SQLAlchemy query already filtered by prefix-matches. Returns an SQLAlchemy query already filtered by prefix-matches.
""" """
prefix = token[:cls.prefix_length] prefix = token[:cls.prefix_length]
@@ -303,6 +304,7 @@ class Hashed(object):
if orm_token.match(token): if orm_token.match(token):
return orm_token return orm_token
class APIToken(Hashed, Base): class APIToken(Hashed, Base):
"""An API token""" """An API token"""
__tablename__ = 'api_tokens' __tablename__ = 'api_tokens'
@@ -363,7 +365,7 @@ class APIToken(Hashed, Base):
return orm_token return orm_token
@classmethod @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""" """Generate a new API token for a user or service"""
assert user or service assert user or service
assert not (user and service) assert not (user and service)
@@ -377,7 +379,7 @@ class APIToken(Hashed, Base):
cls.check_token(db, token) cls.check_token(db, token)
# two stages to ensure orm_token.generated has been set # two stages to ensure orm_token.generated has been set
# before token setter is called # before token setter is called
orm_token = cls(generated=generated) orm_token = cls(generated=generated, note=note or '')
orm_token.token = token orm_token.token = token
if user: if user:
assert user.id is not None assert user.id is not None

View File

@@ -344,8 +344,10 @@ class User:
base_url=base_url, base_url=base_url,
) )
db.add(orm_server) db.add(orm_server)
note = "server token"
api_token = self.new_api_token() if server_name:
note += " for server %s" % server_name
api_token = self.new_api_token(note=note)
db.commit() db.commit()
@@ -420,7 +422,10 @@ class User:
) )
# use generated=False because we don't trust this token # use generated=False because we don't trust this token
# to have been generated properly # 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 # update OAuth client secret with updated API token
if oauth_provider: if oauth_provider:
client_store = oauth_provider.client_authenticator.client_store client_store = oauth_provider.client_authenticator.client_store