diff --git a/README.md b/README.md index 44f66d4..b40deb1 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Here's the config if you set it up as an external service, say, in another Docke ] You have to specify the API token for the hub and the announcement service to share here. +Starting with JupyterHub 2.0, you will need to set user access through appropriate definition of `c.JupyterHub.load_roles`. The service also has its own configuration file, by default `announcement_config.py` is what it is called. The configuration text can be generated with a `--generate-config` option. @@ -55,16 +56,11 @@ If you're running a hub with internal SSL turned on, you'll want to take advanta What does it actually look like when it runs? Start up the hub. -If you're running this locally on port 8000 (or in a Docker container with that port exposed), go to +Log in as an admin user, then go to http://localhost:8000/services/announcement/ -If all goes well you'll see a mostly blank JupyterHub-style page with "None" for the "Latest Announcement." - -![Unauthenticated view](docs/resources/01-unauthenticated-view.png "Unauthenticated view") - -Now go ahead and click the convenient login button. -Log in as an admin user, then go back to the above URL. +You should see: ![Admin view uninitialized](docs/resources/02-admin-view-uninitialized.png "Admin view uninitialized") diff --git a/announcement_config.py b/announcement_config.py index ee13057..727a28c 100644 --- a/announcement_config.py +++ b/announcement_config.py @@ -3,67 +3,109 @@ #------------------------------------------------------------------------------ # Application(SingletonConfigurable) configuration #------------------------------------------------------------------------------ - ## This is an application. ## The date format used by logging formatters for %(asctime)s -#c.Application.log_datefmt = '%Y-%m-%d %H:%M:%S' +# Default: '%Y-%m-%d %H:%M:%S' +# c.Application.log_datefmt = '%Y-%m-%d %H:%M:%S' ## The Logging format template -#c.Application.log_format = '[%(name)s]%(highlevel)s %(message)s' +# Default: '[%(name)s]%(highlevel)s %(message)s' +# c.Application.log_format = '[%(name)s]%(highlevel)s %(message)s' ## Set the log level by value or name. -#c.Application.log_level = 30 +# Choices: any of [0, 10, 20, 30, 40, 50, 'DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL'] +# Default: 30 +# c.Application.log_level = 30 + +## Instead of starting the Application, dump configuration to stdout +# Default: False +# c.Application.show_config = False + +## Instead of starting the Application, dump configuration to stdout (as JSON) +# Default: False +# c.Application.show_config_json = False #------------------------------------------------------------------------------ # AnnouncementService(Application) configuration #------------------------------------------------------------------------------ - ## This is an application. +## Allow access from subdomains +# Default: False +# c.AnnouncementService.allow_origin = False + ## Config file to load -#c.AnnouncementService.config_file = 'announcement_config.py' +# Default: 'announcement_config.py' +# c.AnnouncementService.config_file = 'announcement_config.py' + +## File in which to store the cookie secret. +# Default: 'jupyterhub-announcement-cookie-secret' +# c.AnnouncementService.cookie_secret_file = 'jupyterhub-announcement-cookie-secret' ## Fixed message to show at the top of the page. # -# A good use for this parameter would be a link to a more general live system -# status page or MOTD. -#c.AnnouncementService.fixed_message = '' +# A good use for this parameter would be a link to a more general +# live system status page or MOTD. +# Default: '' +# c.AnnouncementService.fixed_message = '' ## Generate default config file -#c.AnnouncementService.generate_config = False +# Default: False +# c.AnnouncementService.generate_config = False + +## The date format used by logging formatters for %(asctime)s +# See also: Application.log_datefmt +# c.AnnouncementService.log_datefmt = '%Y-%m-%d %H:%M:%S' + +## The Logging format template +# See also: Application.log_format +# c.AnnouncementService.log_format = '[%(name)s]%(highlevel)s %(message)s' + +## Set the log level by value or name. +# See also: Application.log_level +# c.AnnouncementService.log_level = 30 ## Logo path, can be used to override JupyterHub one -#c.AnnouncementService.logo_file = '' +# Default: '' +# c.AnnouncementService.logo_file = '' ## Port this service will listen on -#c.AnnouncementService.port = 8888 +# Default: 8888 +# c.AnnouncementService.port = 8888 ## Announcement service prefix -#c.AnnouncementService.service_prefix = '/services/announcement/' +# Default: '/services/announcement/' +# c.AnnouncementService.service_prefix = '/services/announcement/' -## Allow access from subdomains -#c.AnnouncementService.allow_origin = False +## Instead of starting the Application, dump configuration to stdout +# See also: Application.show_config +# c.AnnouncementService.show_config = False + +## Instead of starting the Application, dump configuration to stdout (as JSON) +# See also: Application.show_config_json +# c.AnnouncementService.show_config_json = False ## Search paths for jinja templates, coming before default ones -#c.AnnouncementService.template_paths = [] +# Default: [] +# c.AnnouncementService.template_paths = [] #------------------------------------------------------------------------------ # AnnouncementQueue(LoggingConfigurable) configuration #------------------------------------------------------------------------------ - ## Number of days to retain announcements. # -# Announcements that have been in the queue for this many days are purged from -# the queue. -#c.AnnouncementQueue.lifetime_days = 7.0 +# Announcements that have been in the queue for this many days are +# purged from the queue. +# Default: 7.0 +# c.AnnouncementQueue.lifetime_days = 7.0 ## File path where announcements persist as JSON. # -# For a persistent announcement queue, this parameter must be set to a non-empty -# value and correspond to a read+write-accessible path. The announcement queue -# is stored as a list of JSON objects. If this parameter is set to a non-empty -# value: +# For a persistent announcement queue, this parameter must be set to +# a non-empty value and correspond to a read+write-accessible path. +# The announcement queue is stored as a list of JSON objects. If this +# parameter is set to a non-empty value: # # * The persistence file is used to initialize the announcement queue # at start-up. This is the only time the persistence file is read. @@ -72,21 +114,23 @@ # * The persistence file is over-written with the contents of the # announcement queue each time a new announcement is added. # -# If this parameter is set to an empty value (the default) then the queue is -# just empty at initialization and the queue is ephemeral; announcements will -# not be persisted on updates to the queue. -#c.AnnouncementQueue.persist_path = '' +# If this parameter is set to an empty value (the default) then the +# queue is just empty at initialization and the queue is ephemeral; +# announcements will not be persisted on updates to the queue. +# Default: '' +# c.AnnouncementQueue.persist_path = '' #------------------------------------------------------------------------------ # SSLContext(Configurable) configuration #------------------------------------------------------------------------------ - ## SSL CA, use with keyfile and certfile -#c.SSLContext.cafile = '' +# Default: '' +# c.SSLContext.cafile = '' ## SSL cert, use with keyfile -#c.SSLContext.certfile = '' +# Default: '' +# c.SSLContext.certfile = '' ## SSL key, use with certfile -#c.SSLContext.keyfile = '' - +# Default: '' +# c.SSLContext.keyfile = '' diff --git a/jupyterhub_announcement/announcement.py b/jupyterhub_announcement/announcement.py index de2e423..c11d13b 100644 --- a/jupyterhub_announcement/announcement.py +++ b/jupyterhub_announcement/announcement.py @@ -1,11 +1,12 @@ +import binascii import datetime import json import os import sys from jinja2 import Environment, ChoiceLoader, FileSystemLoader, PrefixLoader -from jupyterhub.services.auth import HubAuthenticated +from jupyterhub.services.auth import HubOAuthenticated, HubOAuthCallbackHandler from jupyterhub.handlers.static import LogoHandler from jupyterhub.utils import url_path_join, make_ssl_context from jupyterhub._data import DATA_FILES_PATH @@ -37,31 +38,31 @@ class AnnouncementQueue(LoggingConfigurable): announcements = List() persist_path = Unicode( - "", - help="""File path where announcements persist as JSON. - - For a persistent announcement queue, this parameter must be set to - a non-empty value and correspond to a read+write-accessible path. - The announcement queue is stored as a list of JSON objects. If this - parameter is set to a non-empty value: - - * The persistence file is used to initialize the announcement queue - at start-up. This is the only time the persistence file is read. - * If the persistence file does not exist at start-up, it is - created when an announcement is added to the queue. - * The persistence file is over-written with the contents of the - announcement queue each time a new announcement is added. - - If this parameter is set to an empty value (the default) then the - queue is just empty at initialization and the queue is ephemeral; - announcements will not be persisted on updates to the queue.""" + "", + help="""File path where announcements persist as JSON. + + For a persistent announcement queue, this parameter must be set to + a non-empty value and correspond to a read+write-accessible path. + The announcement queue is stored as a list of JSON objects. If this + parameter is set to a non-empty value: + + * The persistence file is used to initialize the announcement queue + at start-up. This is the only time the persistence file is read. + * If the persistence file does not exist at start-up, it is + created when an announcement is added to the queue. + * The persistence file is over-written with the contents of the + announcement queue each time a new announcement is added. + + If this parameter is set to an empty value (the default) then the + queue is just empty at initialization and the queue is ephemeral; + announcements will not be persisted on updates to the queue.""" ).tag(config=True) lifetime_days = Float(7.0, - help="""Number of days to retain announcements. + help="""Number of days to retain announcements. - Announcements that have been in the queue for this many days are - purged from the queue.""" + Announcements that have been in the queue for this many days are + purged from the queue.""" ).tag(config=True) def __init__(self, **kwargs): @@ -108,14 +109,14 @@ def purge(self): max_age = datetime.timedelta(days=self.lifetime_days) now = datetime.datetime.now() old_count = len(self.announcements) - self.announcements = [a for a in self.announcements + self.announcements = [a for a in self.announcements if now - a["timestamp"] < max_age] if self.persist_path and len(self.announcements) < old_count: self.log.info(f"persisting queue to {self.persist_path}") self._handle_persist() -class AnnouncementHandler(HubAuthenticated, web.RequestHandler): +class AnnouncementHandler(HubOAuthenticated, web.RequestHandler): def initialize(self, queue): self.queue = queue @@ -131,16 +132,16 @@ def initialize(self, queue, fixed_message, loader): self.env = Environment(loader=self.loader) self.template = self.env.get_template("index.html") - + @web.authenticated def get(self): user = self.get_current_user() prefix = self.hub_auth.hub_prefix logout_url = url_path_join(prefix, "logout") - self.write(self.template.render(user=user, + self.write(self.template.render(user=user, fixed_message=self.fixed_message, announcements=self.queue.announcements, static_url=self.static_url, - login_url=self.hub_auth.login_url, + login_url=self.hub_auth.login_url, logout_url=logout_url, base_url=prefix, no_spawner_check=True)) @@ -184,7 +185,7 @@ def post(self): class SSLContext(Configurable): - + keyfile = Unicode( os.getenv("JUPYTERHUB_SSL_KEYFILE", ""), help="SSL key, use with certfile" @@ -200,7 +201,7 @@ class SSLContext(Configurable): help="SSL CA, use with keyfile and certfile" ).tag(config=True) - def ssl_context(self): + def ssl_context(self): if self.keyfile and self.certfile and self.cafile: return make_ssl_context(self.keyfile, self.certfile, cafile=self.cafile, check_hostname=False) @@ -219,24 +220,23 @@ class AnnouncementService(Application): )}) generate_config = Bool( - False, - help="Generate default config file" + False, + help="Generate default config file" ).tag(config=True) config_file = Unicode( - "announcement_config.py", - help="Config file to load" + "announcement_config.py", + help="Config file to load" ).tag(config=True) service_prefix = Unicode( - os.environ.get("JUPYTERHUB_SERVICE_PREFIX", - "/services/announcement/"), - help="Announcement service prefix" + os.environ.get("JUPYTERHUB_SERVICE_PREFIX", "/services/announcement/"), + help="Announcement service prefix" ).tag(config=True) port = Integer( - 8888, - help="Port this service will listen on" + 8888, + help="Port this service will listen on" ).tag(config=True) allow_origin = Bool( @@ -245,12 +245,12 @@ class AnnouncementService(Application): ).tag(config=True) data_files_path = Unicode( - DATA_FILES_PATH, - help="Location of JupyterHub data files" + DATA_FILES_PATH, + help="Location of JupyterHub data files" ) template_paths = List( - help="Search paths for jinja templates, coming before default ones" + help="Search paths for jinja templates, coming before default ones" ).tag(config=True) @default('template_paths') @@ -259,8 +259,8 @@ def _template_paths_default(self): os.path.join(self.data_files_path, 'templates')] logo_file = Unicode( - "", - help="Logo path, can be used to override JupyterHub one", + "", + help="Logo path, can be used to override JupyterHub one", ).tag(config=True) @default('logo_file') @@ -270,15 +270,20 @@ def _logo_file_default(self): ) fixed_message = Unicode( - "", - help="""Fixed message to show at the top of the page. + "", + help="""Fixed message to show at the top of the page. - A good use for this parameter would be a link to a more general - live system status page or MOTD.""" + A good use for this parameter would be a link to a more general + live system status page or MOTD.""" ).tag(config=True) ssl_context = Any() + cookie_secret_file = Unicode( + "jupyterhub-announcement-cookie-secret", + help="File in which we store the cookie secret." + ).tag(config=True) + def initialize(self, argv=None): super().initialize(argv) @@ -305,13 +310,19 @@ def initialize(self, argv=None): ] ) + with open(self.cookie_secret_file) as f: + cookie_secret_text = f.read().strip() + cookie_secret = binascii.a2b_hex(cookie_secret_text) + self.settings = { + "cookie_secret": cookie_secret, "static_path": os.path.join(self.data_files_path, "static"), "static_url_prefix": url_path_join(self.service_prefix, "static/") } self.app = web.Application([ (self.service_prefix, AnnouncementViewHandler, dict(queue=self.queue, fixed_message=self.fixed_message, loader=loader), "view"), + (self.service_prefix + r"oauth_callback", HubOAuthCallbackHandler), (self.service_prefix + r"latest", AnnouncementLatestHandler, dict(queue=self.queue, allow_origin=self.allow_origin)), (self.service_prefix + r"update", AnnouncementUpdateHandler, dict(queue=self.queue)), (self.service_prefix + r"static/(.*)", web.StaticFileHandler, dict(path=self.settings["static_path"])), diff --git a/setup.py b/setup.py index 4a9fe01..438d1c1 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name='jupyterhub-announcement', - version='0.6.0', + version='0.7.0', description='JupyterHub Announcement Service', author='R. C. Thomas, François Tessier', author_email='rcthomas@lbl.gov',