Skip to content

Commit

Permalink
Control access by roles
Browse files Browse the repository at this point in the history
  • Loading branch information
trungleduc committed Mar 14, 2024
1 parent e84432e commit 3b1eb4f
Show file tree
Hide file tree
Showing 8 changed files with 49 additions and 22 deletions.
14 changes: 6 additions & 8 deletions jupyterhub_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"

Expand All @@ -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"],
},
]
2 changes: 2 additions & 0 deletions tljh_repo2docker/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
34 changes: 29 additions & 5 deletions tljh_repo2docker/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -48,23 +70,25 @@ 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
**kwargs: Template arguments
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)
Expand Down
4 changes: 3 additions & 1 deletion tljh_repo2docker/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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-_]+$"
Expand All @@ -16,6 +16,7 @@ class BuildHandler(BaseHandler):
"""

@web.authenticated
@require_admin_role
async def delete(self):
data = self.get_json_body()
name = data["name"]
Expand All @@ -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"]
Expand Down
7 changes: 3 additions & 4 deletions tljh_repo2docker/environments.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions tljh_repo2docker/logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions tljh_repo2docker/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class UserModel:
name: str
admin: bool
servers: dict
roles: list

@classmethod
def from_dict(self, kwargs_dict: dict):
Expand All @@ -30,3 +31,4 @@ def all_spawners(self) -> list:
}
)
return sp

4 changes: 2 additions & 2 deletions tljh_repo2docker/templates/page.html
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -86,7 +86,7 @@
{% block login_widget %}
<span id="login_widget">
{% if user %}
<p class="navbar-text">{{user['name']}}</p>
<p class="navbar-text">{{user.name}}</p>
<a
id="logout"
role="button"
Expand Down

0 comments on commit 3b1eb4f

Please sign in to comment.