diff --git a/jupyter_server/files/handlers.py b/jupyter_server/files/handlers.py index 5d20df3940..bf2be2be75 100644 --- a/jupyter_server/files/handlers.py +++ b/jupyter_server/files/handlers.py @@ -28,12 +28,12 @@ def content_security_policy(self): "; sandbox allow-scripts" @web.authenticated - @authorized('read') + @authorized("read", resource="files") def head(self, path): self.get(path, include_body=False) @web.authenticated - @authorized('read') + @authorized("read", resource="files") async def get(self, path, include_body=True): cm = self.contents_manager diff --git a/jupyter_server/kernelspecs/handlers.py b/jupyter_server/kernelspecs/handlers.py index a5f5d7cf05..2e0e2a2922 100644 --- a/jupyter_server/kernelspecs/handlers.py +++ b/jupyter_server/kernelspecs/handlers.py @@ -11,7 +11,7 @@ def initialize(self): web.StaticFileHandler.initialize(self, path='') @web.authenticated - @authorized("read") + @authorized("read", resource="kernelspecs") def get(self, kernel_name, path, include_body=True): ksm = self.kernel_spec_manager try: @@ -23,7 +23,7 @@ def get(self, kernel_name, path, include_body=True): return web.StaticFileHandler.get(self, path, include_body=include_body) @web.authenticated - @authorized("read") + @authorized("read", resource="kernelspecs") def head(self, kernel_name, path): return self.get(kernel_name, path, include_body=False) diff --git a/jupyter_server/nbconvert/handlers.py b/jupyter_server/nbconvert/handlers.py index 550d7bace1..a4fe5d59dd 100644 --- a/jupyter_server/nbconvert/handlers.py +++ b/jupyter_server/nbconvert/handlers.py @@ -20,6 +20,9 @@ from ipython_genutils.py3compat import cast_bytes from ipython_genutils import text +from jupyter_server.utils import authorized + + def find_resource_files(output_files_dir): files = [] for dirpath, dirnames, filenames in os.walk(output_files_dir): @@ -81,6 +84,7 @@ class NbconvertFileHandler(JupyterHandler): SUPPORTED_METHODS = ('GET',) @web.authenticated + @authorized("read", resource="nbconvert") async def get(self, format, path): exporter = get_exporter(format, config=self.config, log=self.log) @@ -150,6 +154,7 @@ class NbconvertPostHandler(JupyterHandler): SUPPORTED_METHODS = ('POST',) @web.authenticated + @authorized("write", resource="nbconvert") def post(self, format): exporter = get_exporter(format, config=self.config) diff --git a/jupyter_server/services/api/handlers.py b/jupyter_server/services/api/handlers.py index 571c643d51..25c2ffe3f4 100644 --- a/jupyter_server/services/api/handlers.py +++ b/jupyter_server/services/api/handlers.py @@ -9,7 +9,7 @@ from tornado import web from ...base.handlers import JupyterHandler, APIHandler -from jupyter_server.utils import ensure_async +from jupyter_server.utils import ensure_async, authorized from jupyter_server._tz import utcfromtimestamp, isoformat @@ -19,6 +19,7 @@ def initialize(self): web.StaticFileHandler.initialize(self, path=os.path.dirname(__file__)) @web.authenticated + @authorized("read", resource="api") def get(self): self.log.warning("Serving api spec (experimental, incomplete)") return web.StaticFileHandler.get(self, 'api.yaml') @@ -32,6 +33,7 @@ class APIStatusHandler(APIHandler): _track_activity = False @web.authenticated + @authorized("read", resource="api") async def get(self): # if started was missing, use unix epoch started = self.settings.get('started', utcfromtimestamp(0)) diff --git a/jupyter_server/services/config/handlers.py b/jupyter_server/services/config/handlers.py index 6a42b151db..faf798b117 100644 --- a/jupyter_server/services/config/handlers.py +++ b/jupyter_server/services/config/handlers.py @@ -10,27 +10,25 @@ from ipython_genutils.py3compat import PY3 from ...base.handlers import APIHandler - from jupyter_server.utils import authorized - class ConfigHandler(APIHandler): @web.authenticated - @authorized('read') + @authorized("read", resource="config") def get(self, section_name): self.set_header("Content-Type", 'application/json') self.finish(json.dumps(self.config_manager.get(section_name))) @web.authenticated - @authorized('write') + @authorized("write", resource="config") def put(self, section_name): data = self.get_json_body() # Will raise 400 if content is not valid JSON self.config_manager.set(section_name, data) self.set_status(204) @web.authenticated - @authorized('write') + @authorized("write", resource="config") def patch(self, section_name): new_data = self.get_json_body() section = self.config_manager.update(section_name, new_data) diff --git a/jupyter_server/services/contents/handlers.py b/jupyter_server/services/contents/handlers.py index 6e149971e0..8736c275e3 100644 --- a/jupyter_server/services/contents/handlers.py +++ b/jupyter_server/services/contents/handlers.py @@ -16,9 +16,9 @@ from jupyter_server.base.handlers import ( JupyterHandler, APIHandler, path_regex, ) - from jupyter_server.utils import authorized + def validate_model(model, expect_content): """ Validate a model returned by a ContentsManager method. @@ -89,7 +89,7 @@ def _finish_model(self, model, location=True): self.finish(json.dumps(model, default=date_default)) @web.authenticated - @authorized('read', resource='contents') + @authorized("read", resource="contents") async def get(self, path=''): """Return a model for a file or directory. @@ -116,7 +116,7 @@ async def get(self, path=''): self._finish_model(model, location=False) @web.authenticated - @authorized('write', resource='contents') + @authorized("write", resource="contents") async def patch(self, path=''): """PATCH renames a file or directory without re-uploading content.""" cm = self.contents_manager @@ -165,7 +165,7 @@ async def _save(self, model, path): self._finish_model(model) @web.authenticated - @authorized('write', resource='contents') + @authorized("write", resource="contents") async def post(self, path=''): """Create a new file in the specified path. @@ -202,7 +202,7 @@ async def post(self, path=''): await self._new_untitled(path) @web.authenticated - @authorized('write', resource='contents') + @authorized("write", resource="contents") async def put(self, path=''): """Saves the file in the location specified by name and path. @@ -227,7 +227,7 @@ async def put(self, path=''): await self._new_untitled(path) @web.authenticated - @authorized('write', resource='contents') + @authorized("write", resource="contents") async def delete(self, path=''): """delete a file in the given path""" cm = self.contents_manager @@ -240,7 +240,7 @@ async def delete(self, path=''): class CheckpointsHandler(APIHandler): @web.authenticated - @authorized('read', resource='checkpoints') + @authorized("read", resource="contents") async def get(self, path=''): """get lists checkpoints for a file""" cm = self.contents_manager @@ -249,7 +249,7 @@ async def get(self, path=''): self.finish(data) @web.authenticated - @authorized('write', resource='checkpoints') + @authorized("write", resource="contents") async def post(self, path=''): """post creates a new checkpoint""" cm = self.contents_manager @@ -265,7 +265,7 @@ async def post(self, path=''): class ModifyCheckpointsHandler(APIHandler): @web.authenticated - @authorized('write', resource='checkpoints') + @authorized("write", resource="contents") async def post(self, path, checkpoint_id): """post restores a file from a checkpoint""" cm = self.contents_manager @@ -274,7 +274,7 @@ async def post(self, path, checkpoint_id): self.finish() @web.authenticated - @authorized('write', resource='checkpoints') + @authorized("write", resource="contents") async def delete(self, path, checkpoint_id): """delete clears a checkpoint for a given file""" cm = self.contents_manager @@ -302,13 +302,12 @@ class TrustNotebooksHandler(JupyterHandler): """ Handles trust/signing of notebooks """ @web.authenticated - @authorized('write', resource='trust_notebook') + @authorized("write", resource="contents") async def post(self,path=''): cm = self.contents_manager await ensure_async(cm.trust_notebook(path)) self.set_status(201) self.finish() - #----------------------------------------------------------------------------- # URL to handler mappings #----------------------------------------------------------------------------- diff --git a/jupyter_server/services/kernels/handlers.py b/jupyter_server/services/kernels/handlers.py index fcb1ea9cb5..4e3008bedd 100644 --- a/jupyter_server/services/kernels/handlers.py +++ b/jupyter_server/services/kernels/handlers.py @@ -27,14 +27,14 @@ class MainKernelHandler(APIHandler): @web.authenticated - @authorized('read', resource='kernels') + @authorized("read", resource="kernels") async def get(self): km = self.kernel_manager kernels = await ensure_async(km.list_kernels()) self.finish(json.dumps(kernels, default=date_default)) @web.authenticated - @authorized('write', resource='kernels') + @authorized("write", resource="kernels") async def post(self): km = self.kernel_manager model = self.get_json_body() @@ -57,14 +57,14 @@ async def post(self): class KernelHandler(APIHandler): @web.authenticated - @authorized('read', resource='kernels') + @authorized("read", resource="kernels") async def get(self, kernel_id): km = self.kernel_manager model = await ensure_async(km.kernel_model(kernel_id)) self.finish(json.dumps(model, default=date_default)) @web.authenticated - @authorized('write', resource='kernels') + @authorized("write", resource="kernels") async def delete(self, kernel_id): km = self.kernel_manager await ensure_async(km.shutdown_kernel(kernel_id)) @@ -75,7 +75,7 @@ async def delete(self, kernel_id): class KernelActionHandler(APIHandler): @web.authenticated - @authorized('write', resource='kernels') + @authorized("write", resource="kernels") async def post(self, kernel_id, action): km = self.kernel_manager if action == 'interrupt': diff --git a/jupyter_server/services/kernelspecs/handlers.py b/jupyter_server/services/kernelspecs/handlers.py index 19afda888c..3c93919430 100644 --- a/jupyter_server/services/kernelspecs/handlers.py +++ b/jupyter_server/services/kernelspecs/handlers.py @@ -18,6 +18,7 @@ from jupyter_server.utils import authorized + def kernelspec_model(handler, name, spec_dict, resource_dir): """Load a KernelSpec by name and return the REST API model""" d = { @@ -56,7 +57,7 @@ def is_kernelspec_model(spec_dict): class MainKernelSpecHandler(APIHandler): @web.authenticated - @authorized('read', resource='kernelspecs') + @authorized("read", resource="kernelspecs") async def get(self): ksm = self.kernel_spec_manager km = self.kernel_manager @@ -81,7 +82,7 @@ async def get(self): class KernelSpecHandler(APIHandler): @web.authenticated - @authorized('read', resource='kernelspecs') + @authorized("read", resource="kernelspecs") async def get(self, kernel_name): ksm = self.kernel_spec_manager kernel_name = url_unescape(kernel_name) diff --git a/jupyter_server/services/nbconvert/handlers.py b/jupyter_server/services/nbconvert/handlers.py index 97886f1878..9efab314b1 100644 --- a/jupyter_server/services/nbconvert/handlers.py +++ b/jupyter_server/services/nbconvert/handlers.py @@ -3,11 +3,13 @@ from tornado import web from ...base.handlers import APIHandler +from jupyter_server.utils import authorized class NbconvertRootHandler(APIHandler): @web.authenticated + @authorized("read", resource="nbconvert") def get(self): try: from nbconvert.exporters import base diff --git a/jupyter_server/services/security/handlers.py b/jupyter_server/services/security/handlers.py index 82a00d234b..0539e8e7af 100644 --- a/jupyter_server/services/security/handlers.py +++ b/jupyter_server/services/security/handlers.py @@ -6,6 +6,7 @@ from tornado import web from ...base.handlers import APIHandler +from jupyter_server.utils import authorized from . import csp_report_uri class CSPReportHandler(APIHandler): @@ -22,6 +23,7 @@ def check_xsrf_cookie(self): return @web.authenticated + @authorized("write", resource="csp") def post(self): '''Log a content security policy violation report''' self.log.warning("Content security violation: %s", diff --git a/jupyter_server/services/sessions/handlers.py b/jupyter_server/services/sessions/handlers.py index dda6ea9892..927b930fdc 100644 --- a/jupyter_server/services/sessions/handlers.py +++ b/jupyter_server/services/sessions/handlers.py @@ -14,14 +14,13 @@ from jupyter_client.jsonutil import date_default from jupyter_server.utils import url_path_join, ensure_async from jupyter_client.kernelspec import NoSuchKernel - from jupyter_server.utils import authorized class SessionRootHandler(APIHandler): @web.authenticated - @authorized('read', resource='sessions') + @authorized("read", resource="sessions") async def get(self): # Return a list of running sessions sm = self.session_manager @@ -29,7 +28,7 @@ async def get(self): self.finish(json.dumps(sessions, default=date_default)) @web.authenticated - @authorized('write', resource='sessions') + @authorized("write", resource="sessions") async def post(self): # Creates a new session #(unless a session already exists for the named session) @@ -90,7 +89,7 @@ async def post(self): class SessionHandler(APIHandler): @web.authenticated - @authorized('read', resource='sessions') + @authorized("read", resource="sessions") async def get(self, session_id): # Returns the JSON model for a single session sm = self.session_manager @@ -98,7 +97,7 @@ async def get(self, session_id): self.finish(json.dumps(model, default=date_default)) @web.authenticated - @authorized('write', resource='sessions') + @authorized("write", resource="sessions") async def patch(self, session_id): """Patch updates sessions: @@ -149,7 +148,7 @@ async def patch(self, session_id): self.finish(json.dumps(model, default=date_default)) @web.authenticated - @authorized('write', resource='sessions') + @authorized("write", resource="sessions") async def delete(self, session_id): # Deletes the session with given session_id sm = self.session_manager diff --git a/jupyter_server/services/shutdown.py b/jupyter_server/services/shutdown.py index c7f5361c42..d62eb9237f 100644 --- a/jupyter_server/services/shutdown.py +++ b/jupyter_server/services/shutdown.py @@ -2,10 +2,12 @@ """ from tornado import web, ioloop from jupyter_server.base.handlers import JupyterHandler +from jupyter_server.utils import authorized class ShutdownHandler(JupyterHandler): @web.authenticated + @authorized("write", resource="shutdown") def post(self): self.log.info("Shutting down on /api/shutdown request.") ioloop.IOLoop.current().stop() diff --git a/jupyter_server/terminal/api_handlers.py b/jupyter_server/terminal/api_handlers.py index 92bb624289..6a6dd00789 100644 --- a/jupyter_server/terminal/api_handlers.py +++ b/jupyter_server/terminal/api_handlers.py @@ -1,16 +1,19 @@ import json from tornado import web from ..base.handlers import APIHandler +from jupyter_server.utils import authorized class TerminalRootHandler(APIHandler): @web.authenticated + @authorized("read", resource="terminal") def get(self): models = self.terminal_manager.list() self.finish(json.dumps(models)) @web.authenticated + @authorized("write", resource="terminal") def post(self): """POST /terminals creates a new terminal and redirects to it""" data = self.get_json_body() or {} @@ -23,11 +26,13 @@ class TerminalHandler(APIHandler): SUPPORTED_METHODS = ('GET', 'DELETE') @web.authenticated + @authorized("read", resource="terminal") def get(self, name): model = self.terminal_manager.get(name) self.finish(json.dumps(model)) @web.authenticated + @authorized("write", resource="terminal") async def delete(self, name): await self.terminal_manager.terminate(name, force=True) self.set_status(204) diff --git a/jupyter_server/utils.py b/jupyter_server/utils.py index 41a49c6202..b12192a929 100644 --- a/jupyter_server/utils.py +++ b/jupyter_server/utils.py @@ -8,8 +8,6 @@ import inspect import os import sys -import functools - from distutils.version import LooseVersion from urllib.parse import quote, unquote, urlparse, urljoin @@ -188,7 +186,6 @@ async def ensure_async(obj): return obj -<<<<<<< HEAD def run_sync(maybe_async): """If async, runs maybe_async and blocks until it has executed, possibly creating an event loop. @@ -231,7 +228,6 @@ def wrapped(): raise e return result return wrapped() -======= def authorized(action, resource=None, message=None): """A decorator for tornado.web.RequestHandler methods @@ -276,4 +272,3 @@ def inner(self, *args, **kwargs): return inner return wrapper ->>>>>>> add authorization layer to request handlers diff --git a/jupyter_server/view/handlers.py b/jupyter_server/view/handlers.py index 76f5a65b29..2508759ef0 100644 --- a/jupyter_server/view/handlers.py +++ b/jupyter_server/view/handlers.py @@ -6,12 +6,13 @@ from tornado import web from ..base.handlers import JupyterHandler, path_regex -from ..utils import ensure_async, url_escape, url_path_join +from ..utils import ensure_async, url_escape, url_path_join, authorized class ViewHandler(JupyterHandler): """Render HTML files within an iframe.""" @web.authenticated + @authorized("read", resource="view") async def get(self, path): path = path.strip('/') if not await ensure_async(self.contents_manager.file_exists(path)):