Status | Open for comments 💬 |
---|---|
Author(s) | @aktech |
Date Created | 04-04-2024 |
Date Last updated | 04-04-2024 |
Decision deadline | dd-MM-YYY |
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.
Role Based Access Control (RBAC) in Nebari will provides fine grained control of access to Nebari’s services.
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.
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:
If the user is not in any group, the user will get 403 on accessing Nebari.
At the moment we have the following roles:
This is set in conda-store configuration:
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).
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 |
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 has following roles:
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.
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.
Read more about it here: https://jupyterhub.readthedocs.io/en/latest/rbac/index.html
JupyterHub defines the following fundamental concepts:
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):
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:
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:
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:
<resource>:<subresource>
- vertical filtering<resource>!<object>=<objectname>
- horizontal filtering.<access-level>
is not provided, we assume maximum permissions.<subresource>
is optional!<object>=<objectname>
is optional/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 |
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:
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.
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 |
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.
Since this is a complex piece of functionality, we would need to implement this in small modular steps. The steps are mentioned below:
/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
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
.shared-directory
work for it. For jupyterhub
component it would work without parsing, after they are synced from keycloak into jupyterhub.The goal of this proposal is to implement the following best practices:
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.