diff --git a/jupyterhub_config.py b/jupyterhub_config.py index dd838ea..bde26fe 100644 --- a/jupyterhub_config.py +++ b/jupyterhub_config.py @@ -3,8 +3,6 @@ and overrides some of the default values from the plugin. """ -import getpass - from jupyterhub.auth import DummyAuthenticator from tljh.configurer import apply_config, load_config from tljh_repo2docker import tljh_custom_jupyterhub_config @@ -31,8 +29,6 @@ c.JupyterHub.authenticator_class = DummyAuthenticator -user = getpass.getuser() -c.Authenticator.admin_users = {user, "alice"} c.JupyterHub.allow_named_servers = True c.JupyterHub.ip = "0.0.0.0" @@ -55,19 +51,21 @@ ] ) + c.JupyterHub.load_roles = [ { "description": "Role for tljh_repo2docker service", - "name": "tljh_repo2docker_role", + "name": "tljh-repo2docker-service", "scopes": ["read:users", "read:servers", "read:roles:users"], "services": ["tljh_repo2docker"], }, + {"name": "tljh-repo2docker-service-admin", "users": ["alice"]}, { - "name": "env-user", + "name": "user", "scopes": [ + "self", # access to the env page - "access:services" + "access:services!service=tljh_repo2docker", ], - "users": ["trung"], }, ] diff --git a/tljh_repo2docker/__init__.py b/tljh_repo2docker/__init__.py index edcedb1..3204a4a 100644 --- a/tljh_repo2docker/__init__.py +++ b/tljh_repo2docker/__init__.py @@ -14,6 +14,8 @@ # See: https://docs.docker.com/config/containers/resource_constraints/#limit-a-containers-access-to-memory#configure-the-default-cfs-scheduler CPU_PERIOD = 100_000 +TLJH_R2D_ADMIN_ROLE = 'tljh-repo2docker-service-admin' + class SpawnerMixin(Configurable): """ diff --git a/tljh_repo2docker/base.py b/tljh_repo2docker/base.py index d835e15..24d7e29 100644 --- a/tljh_repo2docker/base.py +++ b/tljh_repo2docker/base.py @@ -6,8 +6,22 @@ from jupyterhub.services.auth import HubOAuthenticated from jupyterhub.utils import url_path_join from tornado import web -from jupyterhub.scopes import needs_scope +from tljh_repo2docker import TLJH_R2D_ADMIN_ROLE +import functools from .model import UserModel +from jupyterhub.scopes import needs_scope + + +def require_admin_role(func): + """decorator to require admin role to perform an action""" + @functools.wraps(func) + async def wrapped_func(self, *args, **kwargs): + user = await self.fetch_user() + if not user.admin: + raise web.HTTPError(status_code=404, reason="Unauthorized.") + else: + return await func(self, *args, **kwargs) + return wrapped_func class BaseHandler(HubOAuthenticated, web.RequestHandler): @@ -35,8 +49,16 @@ async def fetch_user(self) -> UserModel: user_model: dict = response.json() user_model.setdefault("name", user["name"]) user_model.setdefault("servers", {}) + user_model.setdefault("roles", []) user_model.setdefault("admin", False) + if not user_model["admin"]: + if ( + "admin" in user_model["roles"] + or TLJH_R2D_ADMIN_ROLE in user_model["roles"] + ): + user_model["admin"] = True + return UserModel.from_dict(user_model) def get_template(self, name: str) -> Template: @@ -48,7 +70,7 @@ def get_template(self, name: str) -> Template: """ return self.settings["jinja2_env"].get_template(name) - def render_template(self, name: str, **kwargs) -> str: + async def render_template(self, name: str, **kwargs) -> str: """Render the given template with the provided arguments Args: name: Template name @@ -56,15 +78,17 @@ def render_template(self, name: str, **kwargs) -> str: Returns: The generated template """ - user = self.current_user + user = await self.fetch_user() + base_url = self.settings.get("base_url", "/") template_ns = dict( service_prefix=self.settings.get("service_prefix", "/"), hub_prefix=self.settings.get("hub_prefix", "/"), - base_url=self.settings.get("base_url", "/"), + base_url=base_url, + logout_url=self.settings.get("logout_url", url_path_join(base_url, 'logout')), static_url=self.static_url, xsrf_token=self.xsrf_token.decode("ascii"), user=user, - admin_access=user["admin"], + admin_access=user.admin, ) template_ns.update(kwargs) template = self.get_template(name) diff --git a/tljh_repo2docker/builder.py b/tljh_repo2docker/builder.py index ec7b1d9..013ffa9 100644 --- a/tljh_repo2docker/builder.py +++ b/tljh_repo2docker/builder.py @@ -4,7 +4,7 @@ from aiodocker import Docker, DockerError from tornado import web -from .base import BaseHandler +from .base import BaseHandler, require_admin_role from .docker import build_image IMAGE_NAME_RE = r"^[a-z0-9-_]+$" @@ -16,6 +16,7 @@ class BuildHandler(BaseHandler): """ @web.authenticated + @require_admin_role async def delete(self): data = self.get_json_body() name = data["name"] @@ -29,6 +30,7 @@ async def delete(self): self.finish(json.dumps({"status": "ok"})) @web.authenticated + @require_admin_role async def post(self): data = self.get_json_body() repo = data["repo"] diff --git a/tljh_repo2docker/environments.py b/tljh_repo2docker/environments.py index 1925c05..c934069 100644 --- a/tljh_repo2docker/environments.py +++ b/tljh_repo2docker/environments.py @@ -1,7 +1,8 @@ from inspect import isawaitable from tornado import web -from .base import BaseHandler + +from .base import BaseHandler, require_admin_role from .docker import list_containers, list_images @@ -11,11 +12,9 @@ class EnvironmentsHandler(BaseHandler): """ @web.authenticated + @require_admin_role async def get(self): - user = self.current_user - if not user["admin"]: - raise web.HTTPError(status_code=404, reason="Unauthorized.") images = await list_images() containers = await list_containers() result = self.render_template( diff --git a/tljh_repo2docker/logs.py b/tljh_repo2docker/logs.py index 2a1b4b4..e595a02 100644 --- a/tljh_repo2docker/logs.py +++ b/tljh_repo2docker/logs.py @@ -2,10 +2,9 @@ from aiodocker import Docker from tornado import web -from tornado.ioloop import IOLoop from tornado.iostream import StreamClosedError -from .base import BaseHandler +from .base import BaseHandler, require_admin_role class LogsHandler(BaseHandler): @@ -14,6 +13,7 @@ class LogsHandler(BaseHandler): """ @web.authenticated + @require_admin_role async def get(self, name): self.set_header("Content-Type", "text/event-stream") self.set_header("Cache-Control", "no-cache") diff --git a/tljh_repo2docker/model.py b/tljh_repo2docker/model.py index 04de93d..b06343e 100644 --- a/tljh_repo2docker/model.py +++ b/tljh_repo2docker/model.py @@ -7,6 +7,7 @@ class UserModel: name: str admin: bool servers: dict + roles: list @classmethod def from_dict(self, kwargs_dict: dict): @@ -30,3 +31,4 @@ def all_spawners(self) -> list: } ) return sp + diff --git a/tljh_repo2docker/templates/page.html b/tljh_repo2docker/templates/page.html index 4dcac67..3e2a194 100644 --- a/tljh_repo2docker/templates/page.html +++ b/tljh_repo2docker/templates/page.html @@ -18,7 +18,7 @@ base_url: "{{base_url}}", hub_prefix: "{{hub_prefix}}", {% if user %} - user: "{{ user['name'] | safe }}", + user: "{{ user.name | safe }}", {% endif %} {% if admin_access %} admin_access: true, @@ -86,7 +86,7 @@ {% block login_widget %} {% if user %} - +