---
# System prepended metadata

title: Current Permissions model in Nebari

---

<!-- Example of when and how to create a RFD or RFC (request for comments) -->
<!-- https://gitpod.notion.site/Decision-Making-RFCs-eb4a57f3a34f40f1afbd95e05322af70 -->

<!-- Use this guide to set the status: Draft 🚧 / Open for comments 💬/ Accepted ✅ /Implemented 🚀/ Obsolete 🗃 /  Rejected ⛔️ -->

| 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:
- https://github.com/nebari-dev/nebari-docker-images/pull/85/
- https://github.com/nebari-dev/nebari/pull/1856

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:

```python
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

<img width="1863" alt="a figure showing how services, components and scopes are related" src="https://github.com/nebari-dev/governance/assets/5647941/e02b9dd0-aca2-4627-a9e6-526e8ce72be9">

*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`     |

<img width="1599" alt="shared-directory-role" src="https://github.com/nebari-dev/governance/assets/5647941/88128c9c-daa4-45be-9e63-9ee49c8893d0">

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](https://jupyterhub.readthedocs.io/en/latest/rbac/scopes.html#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)

<!-- Make sure to discuss the relative merits of alternatives to your proposal. -->

### Best practices

<!-- Does this proposal change best practices for some aspect of using/developing JupyterLab or other project? How will these changes be communicated/enforced?
 -->
 
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

<!-- What are the user-facing changes? How will this feature be rolled out? -->

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

<!-- Seed this with open questions you require feedback on from the RFD process. -->
