mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-13 04:53:01 +00:00
add notes on API tokens when they are allocated
This commit is contained in:
@@ -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),
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user