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 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),
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user