Try   HackMD
Status Open for comments 💬
Author(s) @aktech
Date Created 04-04-2024
Date Last updated 04-04-2024
Decision deadline dd-MM-YYY

Summary

Nebari doesn't have a proper RBAC model yet. As a consequence, providing fine grained control of access to Nebari's services to users and groups is not possible. This poses a risk of the user might inadvertently accessing and modifying any data or service within Nebari that violates the principle of least privilege.

User benefit

Role Based Access Control (RBAC) in Nebari will provides fine grained control of access to Nebari’s services.

Design Proposal

Current Permissions model in Nebari

To understand the proposal for the new RBAC, its important to understand current permissions model, we will go through this very briefly here. Also see https://github.com/nebari-dev/nebari/issues/2304 for more context.

JupyterHub

RBAC came in JupyterHub in 2.x and we upgraded from 1.5 in Aug, 2023:

This means we never got to implement JupyterHub's RBAC and have our limited in-house baked permissions model.

Which is basically getting server options for the given user from keycloak

Only two levels of permissions available at this point:

  • jupyterhub_developer
  • jupyterhub_admin

If the user is not in any group, the user will get 403 on accessing Nebari.

Conda Store

At the moment we have the following roles:

  • conda_store_superadmin
  • conda_store_admin
  • conda_store_developer
  • conda_store_viewer

This is set in conda-store configuration:

c.CondaStoreServer.authentication_class = KeyCloakAuthentication

The KeyCloakAuthentication class fetches the user data from keycloak
via keycloak's conda-store client and finds the roles user have and based on that, it returns user's role binding such that user have corresponding permissions on conda-store.

This also fetches user's groups and creates conda-store namespaces (adding them to the conda-store db).

Grafana

Nebari has following Grafana roles, which in code maps to corresponding Grafana roles (1-to-1 mapping):

Nebari Roles Grafana roles
grafana_admin Admin
grafana_developer Editor, Viewer
grafana_viewer Viewer

Dask

  • dask_gateway_admin
  • dask_gateway_developer

This uses NebariAuthentication(JupyterHubAuthenticator) to define custom authentication, which checks if either of the above are present on user's roles

If none of these are present, user has no access to create dask clusters.
This makes calls to JupyterHub's API to get user roles and groups from the following keys in JupyterHub's /user endpoint.

  • auth_state.oauth_user.roles
  • auth_state.oauth_user.groups

Argo

Argo has following roles:

  • argo-admin
  • argo-developer
  • argo-viewer

Three k8s service accounts are created with the above three levels of permissions and based on that user is assigned permissions. These roles are assigned in keycloak.

Problems

  • Fine grained access cannot be provided to users/groups.
  • Roles are static. New roles cannot be created and existing roles cannot be updated (In theory they can be updated and created, but it will not have the desired affect on Nebari services)

Idea

The idea is to not re-invent the wheel but rather try to use existing RBAC frameworks wherever possible with little or no modifications to support wide range of fine-grained control. We will use JupyterHub's RBAC as a motivation to implement RBAC in Nebari. We'll also try to use similar conventions to avoid confusion and reduce the learning curve of yet another rbac system.

JupyterHub RBAC: brief overview

Read more about it here: https://jupyterhub.readthedocs.io/en/latest/rbac/index.html

JupyterHub defines the following fundamental concepts:

  • Role: Roles are collections of scopes.
  • Scope: Permission - specific permissions used to evaluate API requests.
  • Group / User: single or a set of users

Nebari RBAC: Proposal

The idea is to be able to manage roles and permissions from a central place, in this case keycloak. An admin or anyone who has permissions to create a role in keycloak will create role(s) with assigned scopes (permissions) to it and attach it to user(s) or group(s). We define the following concepts (some of them already exist):

Service

This represents the main services in Nebari. There is a keycloak client created for most nebari services. The idea here is those services will call keycloak API (they already do at the moment for authentication) to fetch roles from a particular client for a user and using the roles's attributes it will decide what permissions the user have on that service. Here are some of the main core services:

  • Jupyterhub
  • Grafana
  • Conda Store
  • Argo

Component

A service can have several components and each component can have the need for a user or group to have different levels of access. We call them as component. For example, the JupyterHub service can have the following components:

  • JupyterHub itself (JupyterHub API).
  • shared-directory (e.g. creating a shared nfs for a particular group), it is categorized under JupyterHub service because it is created by jupyterhub hooks.
  • Dask: dask is accessed from JupyterHub.

Roles and Scopes

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

This figure depicts how services, components and scopes are related. Note that the scopes for grafana and argo are only for demonstration purpose.

Role is a collection of scopes (permissions).

Scope is a permissions to resource in a component of a service. We're borrowing the syntax for defining scopes (or permissions) from JupyterHub's RBAC. See https://jupyterhub.readthedocs.io/en/latest/rbac/scopes.html#scope-conventions for reference.

In a nutshell, it looks something like this:

<access-level><resource>:<subresource>!<object>=<objectname>
  • <resource>:<subresource> - vertical filtering
  • <resource>!<object>=<objectname> - horizontal filtering.
  • If <access-level> is not provided, we assume maximum permissions.
  • <subresource> is optional
  • !<object>=<objectname> is optional

Examples:

1. Create a role such that when attached to a group it will create shared nfs directory in /shared.

You'll go to keycloak client jupyterhub and create a role with a meaningful name and add the following attributes to the role:

Role: create-shared-directory

Key Value
component shared-directory
scopes create:shared
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

When you'd attach this role to a group, then jupyterhub will make sure to create a shared directory for the group.

Next, we want to have two sets of permissions:

  • group of people (pycon-shared-read-group) with read access to a shared directory.
  • group of people (pycon-shared-write-group) with write access to a shared directory.

We will create two roles:

  • Role: read-access-to-pycon-shared-directory-role
    This role will be attached to pycon-read-group

    Key Value
    component shared-directory
    scopes read:shared!shared=pycon
  • Role: write-access-to-pycon-shared-directory-role
    This role will be attached to pycon-read-group

    Key Value
    component shared-directory
    scopes write:shared!shared=pycon

Note: The names of roles and groups are arbitrary and can be anything.

2. Roles to control access to conda store namespace

  • Create a role such that when attached to a group name read-conda-pycon-group, the group members will have read access to conda environments in pycon namespace.
  • Create a role such that when attached to a group name write-conda-pycon-group, the group members will have read access to conda environment in pycon namespace.

Role: read-access-conda-pycon-namespace-role
This role will be attached to read-conda-pycon-group

Key Value
component conda-store
scopes read:conda-store!namespace=quansight

Role: write-access-conda-pycon-namespace-role
This role will be attached to write-conda-pycon-group

Key Value
component conda-store
scopes write:conda-store!namespace=quansight

3. App sharing permission

Create a role to allow everyone to share apps in a particular group.

Since we're using JupyterHub's RBAC, we can use scopes convention directly here.

Role: allow-app-sharing-role
This role will be attached to allow-app-sharing-group

Key Value
component jupyterhub
scopes shares!user,read:users:name,read:groups:name

See https://jupyterhub.readthedocs.io/en/latest/reference/sharing.html#enable-sharing

By default, it will be disabled for everyone.

Implementation steps:

Since this is a complex piece of functionality, we would need to implement this in small modular steps. The steps are mentioned below:

  • Fetch keycloak groups and roles from Keycloak and sync them in JupyterHub. https://github.com/nebari-dev/nebari/issues/2308. At the moment we do have these available from the /user endpoint in the Hub API, under following keys. These needs to to be synced with JupyterHub roles and groups so that the permissions are actually applied at JupyterHub level.
    • auth_state.oauth_user.roles
    • auth_state.oauth_user.groups
  • Update the render_profiles functionality in profiles in jupyterhub config, such that it creates shared directory only if the group has permissions. This would also require us to implement a way to parse role scopes, e.g: parsing read:shared!shared=pycon for shared directory, when the component is shared-directory.
  • Implement scopes parsing can evolve as we implement rbac for each service. I don't expect us to write perfect scope parsing in one go. We should start with a service. I would suggest JupyterHub and make scope parsing for shared-directory work for it. For jupyterhub component it would work without parsing, after they are synced from keycloak into jupyterhub.
  • Implement scope parsing for conda-store and based on that incorporate conda-store's new permissions model.
  • Follow the same for Argo and Grafana.

Notes

  • The RBAC system described above gives us the flexibility to create fine-grained permissions to specific resources and groups. While this is useful, we also need to make sure we set sensible defaults. Every user of Nebari might not need this level of fine-grain ability so we need to create some roles and groups by default when deploying a Nebari to facilitate basic permissions.
  • JupyterHub also has a concept of custom scopes, but it's not very flexible and it does not enforce that your services apply them, so might not be very useful, but still worth exploring.

Alternatives or approaches considered (if any)

Best practices

The goal of this proposal is to implement the following best practices:

  • Principle of least privilege.
  • Adopting from an existing RBAC system in the Jupyter Ecosystem.

User impact

The implementation would change the default permissions of a user so it would affect the user, but we can set up with sensisble defaults to reduce the impact.

Unresolved questions