mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-17 15:03:02 +00:00
Don't persist proxy auth token to db
removes last need for encrypted database fields, so db_secret is removed as well.
This commit is contained in:
@@ -8,6 +8,7 @@ import binascii
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
from datetime import datetime
|
||||
from subprocess import Popen
|
||||
|
||||
@@ -24,6 +25,7 @@ from sqlalchemy.exc import OperationalError
|
||||
|
||||
import tornado.httpserver
|
||||
import tornado.options
|
||||
from tornado.httpclient import HTTPError
|
||||
from tornado.ioloop import IOLoop, PeriodicCallback
|
||||
from tornado.log import LogFormatter, app_log, access_log, gen_log
|
||||
from tornado import gen, web
|
||||
@@ -170,7 +172,16 @@ class JupyterHubApp(Application):
|
||||
"""
|
||||
)
|
||||
def _proxy_auth_token_default(self):
|
||||
return os.environ.get('CONFIGPROXY_AUTH_TOKEN', orm.new_token())
|
||||
token = os.environ.get('CONFIGPROXY_AUTH_TOKEN', None)
|
||||
if not token:
|
||||
self.log.warn('\n'.join([
|
||||
"",
|
||||
"Generating CONFIGPROXY_AUTH_TOKEN. Restarting the Hub will require restarting the proxy.",
|
||||
"Set CONFIGPROXY_AUTH_TOKEN env or JupyterHubApp.proxy_auth_token config to avoid this message.",
|
||||
"",
|
||||
]))
|
||||
token = orm.new_token()
|
||||
return token
|
||||
|
||||
proxy_api_ip = Unicode('localhost', config=True,
|
||||
help="The ip for the proxy API handlers"
|
||||
@@ -218,17 +229,6 @@ class JupyterHubApp(Application):
|
||||
help="""File in which to store the cookie secret."""
|
||||
)
|
||||
|
||||
db_secret = Bytes(config=True, env='JPY_DB_SECRET',
|
||||
help="""The database secret to use to encrypt sensitive information in the database.
|
||||
|
||||
Loaded from the JPY_DB_SECRET env variable by default.
|
||||
"""
|
||||
)
|
||||
|
||||
db_secret_file = Unicode('jupyterhub_db_secret', config=True,
|
||||
help="""File in which to store the database secret."""
|
||||
)
|
||||
|
||||
authenticator_class = Type(PAMAuthenticator, Authenticator,
|
||||
config=True,
|
||||
help="""Class for authenticating users.
|
||||
@@ -378,58 +378,55 @@ class JupyterHubApp(Application):
|
||||
self.log.error("%s cannot edit %s", user, path)
|
||||
|
||||
def init_secrets(self):
|
||||
traits = self.traits()
|
||||
for key in ('cookie', 'db'):
|
||||
trait_name = '{}_secret'.format(key)
|
||||
env_name = traits[trait_name].get_metadata('env')
|
||||
file_attr_name = '{}_secret_file'.format(key)
|
||||
secret_file = os.path.abspath(
|
||||
os.path.expanduser(getattr(self, file_attr_name))
|
||||
)
|
||||
secret = getattr(self, trait_name)
|
||||
secret_from = 'config'
|
||||
# load priority: 1. config, 2. env, 3. file
|
||||
if not secret and os.environ.get(env_name):
|
||||
secret_from = 'env'
|
||||
self.log.info("Loading %s from env[%s]", trait_name, env_name)
|
||||
secret = binascii.a2b_hex(os.environ[env_name])
|
||||
if not secret and os.path.exists(secret_file):
|
||||
secret_from = 'file'
|
||||
perm = os.stat(secret_file).st_mode
|
||||
if perm & 0o077:
|
||||
self.log.error("Bad permissions on %s", secret_file)
|
||||
else:
|
||||
self.log.info("Loading %s from %s", trait_name, secret_file)
|
||||
with io.open(secret_file) as f:
|
||||
b64_secret = f.read()
|
||||
try:
|
||||
secret = binascii.a2b_base64(b64_secret)
|
||||
except Exception as e:
|
||||
self.log.error("%s does not contain b64 key: %s", secret_file, e)
|
||||
if not secret:
|
||||
secret_from = 'new'
|
||||
self.log.debug("Generating new %s", trait_name)
|
||||
secret = os.urandom(SECRET_BYTES)
|
||||
|
||||
if secret_file and secret_from == 'new':
|
||||
# if we generated a new secret, store it in the secret_file
|
||||
self.log.info("Writing %s to %s", trait_name, secret_file)
|
||||
b64_secret = text_type(binascii.b2a_base64(secret))
|
||||
with io.open(secret_file, 'w', encoding='utf8') as f:
|
||||
f.write(b64_secret)
|
||||
trait_name = 'cookie_secret'
|
||||
trait = self.traits()[trait_name]
|
||||
env_name = trait.get_metadata('env')
|
||||
secret_file = os.path.abspath(
|
||||
os.path.expanduser(self.cookie_secret_file)
|
||||
)
|
||||
secret = self.cookie_secret
|
||||
secret_from = 'config'
|
||||
# load priority: 1. config, 2. env, 3. file
|
||||
if not secret and os.environ.get(env_name):
|
||||
secret_from = 'env'
|
||||
self.log.info("Loading %s from env[%s]", trait_name, env_name)
|
||||
secret = binascii.a2b_hex(os.environ[env_name])
|
||||
if not secret and os.path.exists(secret_file):
|
||||
secret_from = 'file'
|
||||
perm = os.stat(secret_file).st_mode
|
||||
if perm & 0o077:
|
||||
self.log.error("Bad permissions on %s", secret_file)
|
||||
else:
|
||||
self.log.info("Loading %s from %s", trait_name, secret_file)
|
||||
with io.open(secret_file) as f:
|
||||
b64_secret = f.read()
|
||||
try:
|
||||
os.chmod(secret_file, 0o600)
|
||||
except OSError:
|
||||
self.log.warn("Failed to set permissions on %s", secret_file)
|
||||
# store the loaded trait value
|
||||
setattr(self, trait_name, secret)
|
||||
secret = binascii.a2b_base64(b64_secret)
|
||||
except Exception as e:
|
||||
self.log.error("%s does not contain b64 key: %s", secret_file, e)
|
||||
if not secret:
|
||||
secret_from = 'new'
|
||||
self.log.debug("Generating new %s", trait_name)
|
||||
secret = os.urandom(SECRET_BYTES)
|
||||
|
||||
if secret_file and secret_from == 'new':
|
||||
# if we generated a new secret, store it in the secret_file
|
||||
self.log.info("Writing %s to %s", trait_name, secret_file)
|
||||
b64_secret = text_type(binascii.b2a_base64(secret))
|
||||
with io.open(secret_file, 'w', encoding='utf8') as f:
|
||||
f.write(b64_secret)
|
||||
try:
|
||||
os.chmod(secret_file, 0o600)
|
||||
except OSError:
|
||||
self.log.warn("Failed to set permissions on %s", secret_file)
|
||||
# store the loaded trait value
|
||||
self.cookie_secret = secret
|
||||
|
||||
def init_db(self):
|
||||
"""Create the database connection"""
|
||||
self.log.debug("Connecting to db: %s", self.db_url)
|
||||
try:
|
||||
self.db = orm.new_session(self.db_url, reset=self.reset_db, echo=self.debug_db,
|
||||
crypto_key=self.db_secret,
|
||||
**self.db_kwargs
|
||||
)
|
||||
except OperationalError as e:
|
||||
@@ -565,28 +562,39 @@ class JupyterHubApp(Application):
|
||||
self.proxy = orm.Proxy(
|
||||
public_server=orm.Server(),
|
||||
api_server=orm.Server(),
|
||||
auth_token = self.proxy_auth_token,
|
||||
)
|
||||
self.db.add(self.proxy)
|
||||
self.db.commit()
|
||||
self.proxy.auth_token = self.proxy_auth_token # not persisted
|
||||
self.proxy.log = self.log
|
||||
self.proxy.public_server.ip = self.ip
|
||||
self.proxy.public_server.port = self.port
|
||||
self.proxy.api_server.ip = self.proxy_api_ip
|
||||
self.proxy.api_server.port = self.proxy_api_port
|
||||
self.proxy.api_server.base_url = u'/api/routes/'
|
||||
if self.proxy.auth_token is None:
|
||||
self.proxy.auth_token = self.proxy_auth_token
|
||||
self.db.commit()
|
||||
|
||||
@gen.coroutine
|
||||
def start_proxy(self):
|
||||
"""Actually start the configurable-http-proxy"""
|
||||
if self.proxy.public_server.is_up() and \
|
||||
self.proxy.api_server.is_up():
|
||||
self.log.warn("Proxy already running at: %s", self.proxy.public_server.url)
|
||||
self.proxy_process = None
|
||||
return
|
||||
# check for proxy
|
||||
if self.proxy.public_server.is_up() or self.proxy.api_server.is_up():
|
||||
# check for *authenticated* access to the proxy (auth token can change)
|
||||
try:
|
||||
yield self.proxy.get_routes()
|
||||
except (HTTPError, OSError, socket.error) as e:
|
||||
if isinstance(e, HTTPError) and e.code == 403:
|
||||
msg = "Did CONFIGPROXY_AUTH_TOKEN change?"
|
||||
else:
|
||||
msg = "Is something else using %s?" % self.proxy.public_server.url
|
||||
self.log.error("Proxy appears to be running at %s, but I can't access it (%s)\n%s",
|
||||
self.proxy.public_server.url, e, msg)
|
||||
self.exit(1)
|
||||
return
|
||||
else:
|
||||
self.log.info("Proxy already running at: %s", self.proxy.public_server.url)
|
||||
self.proxy_process = None
|
||||
return
|
||||
|
||||
env = os.environ.copy()
|
||||
env['CONFIGPROXY_AUTH_TOKEN'] = self.proxy.auth_token
|
||||
@@ -604,7 +612,8 @@ class JupyterHubApp(Application):
|
||||
cmd.extend(['--ssl-key', self.ssl_key])
|
||||
if self.ssl_cert:
|
||||
cmd.extend(['--ssl-cert', self.ssl_cert])
|
||||
self.log.info("Starting proxy: %s", cmd)
|
||||
self.log.info("Starting proxy @ %s", self.proxy.public_server.url)
|
||||
self.log.debug("Proxy cmd: %s", cmd)
|
||||
self.proxy_process = Popen(cmd, env=env)
|
||||
def _check():
|
||||
status = self.proxy_process.poll()
|
||||
@@ -763,7 +772,7 @@ class JupyterHubApp(Application):
|
||||
@gen.coroutine
|
||||
def update_last_activity(self):
|
||||
"""Update User.last_activity timestamps from the proxy"""
|
||||
routes = yield self.proxy.fetch_routes()
|
||||
routes = yield self.proxy.get_routes()
|
||||
for prefix, route in routes.items():
|
||||
if 'user' not in route:
|
||||
# not a user route, ignore it
|
||||
|
@@ -25,7 +25,7 @@ from sqlalchemy.ext.declarative import declarative_base, declared_attr
|
||||
from sqlalchemy.orm import sessionmaker, relationship, backref
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy_utils.types import EncryptedType, PasswordType
|
||||
from sqlalchemy_utils.types import PasswordType
|
||||
|
||||
from .utils import random_port, url_path_join, wait_for_server, wait_for_http_server
|
||||
|
||||
@@ -125,7 +125,7 @@ class Proxy(Base):
|
||||
"""
|
||||
__tablename__ = 'proxies'
|
||||
id = Column(Integer, primary_key=True)
|
||||
auth_token = Column(EncryptedType(Unicode, key=b''), default=new_token)
|
||||
auth_token = None
|
||||
_public_server_id = Column(Integer, ForeignKey('servers.id'))
|
||||
public_server = relationship(Server, primaryjoin=_public_server_id == Server.id)
|
||||
_api_server_id = Column(Integer, ForeignKey('servers.id'))
|
||||
@@ -197,7 +197,7 @@ class Proxy(Base):
|
||||
yield f
|
||||
|
||||
@gen.coroutine
|
||||
def fetch_routes(self, client=None):
|
||||
def get_routes(self, client=None):
|
||||
"""Fetch the proxy's routes"""
|
||||
resp = yield self.api_request('', client=client)
|
||||
raise gen.Return(json.loads(resp.body.decode('utf8', 'replace')))
|
||||
@@ -411,7 +411,7 @@ class CookieToken(Token, Base):
|
||||
__tablename__ = 'cookie_tokens'
|
||||
|
||||
|
||||
def new_session(url="sqlite:///:memory:", reset=False, crypto_key=None, **kwargs):
|
||||
def new_session(url="sqlite:///:memory:", reset=False, **kwargs):
|
||||
"""Create a new session at url"""
|
||||
if url.startswith('sqlite'):
|
||||
kwargs.setdefault('connect_args', {'check_same_thread': False})
|
||||
@@ -421,12 +421,6 @@ def new_session(url="sqlite:///:memory:", reset=False, crypto_key=None, **kwargs
|
||||
session = Session()
|
||||
if reset:
|
||||
Base.metadata.drop_all(engine)
|
||||
# configure encryption key
|
||||
if crypto_key:
|
||||
for table in Base.metadata.tables.values():
|
||||
for column in table.columns.values():
|
||||
if isinstance(column.type, EncryptedType):
|
||||
column.type.key = crypto_key
|
||||
Base.metadata.create_all(engine)
|
||||
return session
|
||||
|
||||
|
@@ -4,7 +4,6 @@ jinja2
|
||||
simplepam
|
||||
sqlalchemy
|
||||
sqlalchemy-utils
|
||||
cryptography
|
||||
passlib
|
||||
requests
|
||||
six
|
||||
|
Reference in New Issue
Block a user