diff --git a/docs/docs/how-tos/fine-grained-permissions.md b/docs/docs/how-tos/fine-grained-permissions.md deleted file mode 100644 index b95c4813c..000000000 --- a/docs/docs/how-tos/fine-grained-permissions.md +++ /dev/null @@ -1,348 +0,0 @@ ---- -id: fine-grained-permissions -title: Creating and Managing Groups, Roles, and Directories -description: How to configure Keycloak's permissions, groups and roles, and manage group directories in JupyterHub. ---- - -Groups are a fundamental and vital part of the Nebari ecosystem. They are used to manage access to a wide range of services within Nebari, including JupyterHub instances, Keycloak realms, Conda environments, and computing resources. By grouping users based on roles, projects, or departments, Nebari simplifies the management of permissions and resource sharing. - -Beyond managing access, groups play a crucial role in organizing and sharing data across the JupyterHub ecosystem. Each group can have its own shared directory within JupyterHub, allowing users within the same group to collaborate seamlessly by sharing files and resources. This facilitates team collaboration and ensures that data is organized and accessible to those who need it. - -In this document, we will cover: - -- How to create and manage groups and subgroups in Keycloak -- How to assign roles and permissions to groups -- How groups and roles interact within Nebari's services, such as JupyterHub and Conda-Store -- How to manage group directories in JupyterHub - -## Managing Groups in Keycloak - -Keycloak is the identity and access management service used in Nebari. It allows you to create and manage users, groups, roles, and permissions. Groups in Keycloak are collections of users, and they can be assigned specific roles that grant permissions to access various services within Nebari. - -For detailed information on managing groups in Keycloak, refer to the [Keycloak documentation on Group Management](https://www.keycloak.org/docs/latest/server_admin/#_group_management). - -Below we outline the steps specific to Nebari. - -### Creating a New Group - -To create a new group in Keycloak: - -1. **Log in to Keycloak** as an administrator (usually the `root` user). -2. **Navigate to Groups**: Click on **Groups** in the left-hand menu. -3. **Create the Group**: Click the **New** button, enter an appropriate name for your new group (e.g., `conda-store-manager`), and save. - -If you wish to organize your groups hierarchically, you can create subgroups by selecting a parent group and adding a subgroup. - -# Fine Grained Permissions via Keycloak - -Nebari provides its users (particularly admins) a way to manage roles and permissions to -various services like `jupyterhub` and `conda-store` via Keycloak. 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). - -These roles are created and attached from keycloak's interface and scoped for a particular -client (i.e. a Nebari service such as `jupyterhub` or `conda-store`). This means the roles for a -particular service (say `jupyterhub`) should be created within the Keycloak client named -`jupyterhub`. - -By default, Nebari comes with several custom clients included in a fresh deployment. -These clients facilitate various services and integrations within the Nebari ecosystem. -The predefined clients are as follows: - -```yaml -clients: - - jupyterhub - - conda_store - - grafana (if monitoring is enabled) - - argo-server-sso (if argo is enabled) - - forwardauth -``` - -To manage and configure these clients, you can navigate to the `Clients` tab within the -Keycloak admin console, as illustrated in the image below. - -![Keycloak clients](/img/how-tos/fine_grainer_permissions_keycloak_clients.png) - -This can be accessed at `/auth/admin/master/console/#/realms/nebari/clients` - -## Creating a Role - -The process for creating a role is similar, irrespective of the service. To create a role for a -service - -1. Select the appropriate client and click on "Add Role". - -![Keycloak client add jupyterhub role](/img/how-tos/keycloak_jupyterhub_client.png) - -2. On the "Add Role" form, write a meaningful name and description for the role. Be sure to include what this role intends to accomplish. Click "Save". - -![Keycloak clients add jupyterhub role form](/img/how-tos/keycloak_jupyterhub_add_role.png) - -3. Now the role has been created, but it does nothing. Let's add some permissions to it by clicking on the "Attributes" tab - and adding scopes. The following sections will explain the `components` and `scopes` in more detail. - - ![Keycloak clients add jupyterhub role form](/img/how-tos/keycloak_add_role_attributes.png) - -## Adding Role to Group(s) / User(s) - -Creating a role in Keycloak has no effect on any user or group's permissions. To grant a set of permissions -to a user or group, we need to _attach_ the role to the user or group. To add a role to a user: - -1. Select users on the left sidebar and enter the username in the Lookup searchbar. - - ![Keycloak clients add jupyterhub role form](/img/how-tos/keycloak_select_user.png) - -2. Select that user and click on the "Role Mappings" tab. - -![Keycloak clients add jupyterhub role form](/img/how-tos/keycloak_user_role_mapping_tab.png) - -3. Select the Client associated with the Role being added. - -![Keycloak clients add jupyterhub role form](/img/how-tos/keycloak_user_role_mapping_roles.png) - -4. Select the role in the "Available Roles" and click on "Add Selected >>". - -![Keycloak clients add jupyterhub role form](/img/how-tos/keycloak_user_role_mapping_add_role.png) - -To attach a role to a group, follow the above steps by clicking on the groups tab and -selecting a group instead of selecting the user in the first step. - -In the above section, we learned how to create a role with some attributes and attach it to a user or a group. -Now we will learn how to create scopes to grant a particular set of permissions to the user. - -:::note -After the roles are assigned to a user or group in Keycloak, the user **must** logout and login back in to the service -for the roles to take in effect. For example let's say we add a set of roles for `conda-store` to the user named -"John Doe", now for the user "John Doe" to be able to avail newly granted/revoked roles, they need to login to -conda-store again (similarly for `jupyterhub` as well), after the roles are granted/revoked. -::: - -### Adding Users to a Group - -To add users to a group: - -1. **Navigate to Users**: Click on **Users** in the left-hand menu. -2. **Select a User**: Choose the user you want to add to a group. -3. **Assign to Group**: - - Click on the **Groups** tab within the user's details. - - Click the **Join** button. - - Select the group (and subgroup, if applicable) you wish to add the user to. - - Click **Join** to add the user to the group. - -Repeat this process for all users who should be part of the group. - -### Managing Subgroups - -Subgroups allow you to create a hierarchical structure of groups, representing organizational units, projects, or teams. Subgroups inherit roles and attributes from their parent groups unless explicitly overridden. - -To manage subgroups: - -- **Creating Subgroups**: Select the parent group, navigate to the **Sub Groups** tab, and create a new subgroup. -- **Assigning Roles to Subgroups**: Roles are not automatically inherited; assign roles to subgroups as needed. -- **Use Cases**: Organize groups hierarchically to reflect organizational structures. - -For more information, see the [Keycloak documentation on Group Hierarchies](https://www.keycloak.org/docs/latest/server_admin/#group-hierarchies). - -## Managing Group Directories in JupyterHub - -:::note Important -As of version `2024.9.1`, JupyterHub creates and mounts directories only for groups with the `allow-group-directory-creation-role`. By default, this includes the `admin`, `analyst`, and `developer` groups. Previously, directories were automatically created for all Keycloak groups. This change gives administrators more control over shared directories and overall access management without cluttering the file system. -::: - -### Understanding Group Directories - -A group directory in JupyterHub is a shared folder accessible to all members of a specific group. It provides a shared space within the file system for collaboration. - -An example directory structure: - -```yaml -/shared -├── admin -│ ├── file1.txt -│ └── file2.txt -├── analyst -│ ├── file1.txt -│ └── file2.txt -└── developer - ├── file1.txt - └── file2.txt -``` - -### Assigning the `allow-group-directory-creation-role` to a Group - -To enable directory creation and mounting for a group in JupyterHub, assign the `allow-group-directory-creation-role` to the group in Keycloak. - -#### Steps to Assign the Role: - -1. **Navigate to the Group**: Log in to Keycloak as an administrator and select the group. -2. **Go to Role Mappings**: Click on the **Role Mappings** tab. -3. **Assign the Role**: - - Under **Client Roles**, select the `jupyterhub` client. - - Select `allow-group-directory-creation-role` and click **Add selected**. - -Users in this group will now have access to the group's shared directory in JupyterHub. - -### Rolling Back the Change - -To remove the group's directory access: - -1. **Navigate to the Group's Role Mappings**: Select the group and go to the **Role Mappings** tab. -2. **Remove the Role**: - - Under **Assigned Roles**, select `allow-group-directory-creation-role`. - - Click **Remove selected**. - -**Data Preservation:** No data is deleted when you remove the role; the directory is simply unmounted from users' JupyterLab sessions. - -### Managing Subgroup Directories - -Subgroups can have their own directories in JupyterHub if assigned the `allow-group-directory-creation-role`. Assign the role to subgroups as needed to control access and collaboration. - -## In-Depth Look at Roles and Groups - -Understanding how roles and groups work together is essential for effectively managing access and permissions within Nebari. - -### Groups - -Groups represent collections of users who perform similar functions or belong to the same organizational unit. They simplify user management by allowing you to assign roles and permissions at the group level rather than individually to each user. - -By default, Nebari is deployed with the following groups: - -- **admin**: Users with administrative privileges who can manage the system, configurations, and users. -- **developer**: Users who need access to development tools and environments. -- **analyst**: Users who primarily analyze data and may have restricted access compared to developers. - -Groups can be organized hierarchically using subgroups, allowing for more granular control and reflecting organizational structures. - -:::info -**Shared Directories:** Users in a particular group will have access to that group's shared directory in JupyterHub if the group has the `allow-group-directory-creation-role` assigned. -::: - -### Roles - -Roles define a set of permissions that grant access to specific resources or capabilities within Nebari's services. They are assigned to groups or users and determine what actions they can perform. - -Roles can be of two types: - -- **Realm Roles**: Apply globally across all clients (applications) in Keycloak. -- **Client Roles**: Specific to a client (application), such as `jupyterhub`, `conda-store`, or `grafana`. - -Examples of roles include: - -- `admin`: Grants administrative privileges within a client. -- `developer`: Grants development-related permissions. -- `conda_store_admin`: Allows managing Conda environments in Conda-Store. - -### Interaction Between Roles and Groups - -Roles are assigned to groups, and users inherit those roles through their group memberships. This means that: - -- **Users in a Group Get the Group's Roles**: If a group has certain roles assigned, all users in that group will inherit those roles. -- **Multiple Roles Can Be Assigned**: A group can have multiple roles from different clients, providing access to various services. -- **Roles Are Not Inherited by Subgroups**: Roles need to be explicitly assigned to subgroups if you want them to have specific permissions. - -### Role Hierarchies and Stacking - -Roles can be designed to reflect hierarchical permissions. For example, an `admin` role may encompass all the permissions of a `developer` role. - -:::info -**Role Stacking:** If a user is a member of multiple groups with different roles, they effectively have the combined permissions of all assigned roles. For instance, if a user is in a group with the `conda_store_admin` role and another group with the `conda_store_viewer` role, the user will have administrative privileges in Conda-Store due to the higher permission level of `conda_store_admin`. -::: - -### Assigning Roles to Groups - -When creating or editing a group in Keycloak: - -1. **Select the Group**: Navigate to the group you wish to assign roles to. -2. **Go to Role Mappings**: Click on the **Role Mappings** tab. -3. **Assign Roles**: - - Under **Client Roles**, select the client application. - - Select the roles you wish to assign and click **Add selected**. - -## Access Levels and Permissions - -Roles on the other hand represent the type or category of user. This includes access and permissions that this category of user will need to perform their regular job duties. The differences between `groups` and `roles` are subtle. Particular roles (one or many), like `conda_store_admin`, are associated with a particular group, such as `admin` and any user in this group will then assume the role of `conda_store_admin`. - -:::info -These roles can be stacked. This means that if a user is in one group with role `conda_store_admin` and another group with role `conda_store_viewer`, this user ultimately has the role `conda_store_admin`. -::: - -Below is a summary of default groups, their access to Nebari resources, assigned roles, and permissions descriptions. - -| Group | Access to Nebari Resources | Roles | Permissions Description | -| ------------ | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `analyst` | | | | -| `developer` | | | | -| `admin` | | | | -| `superadmin` | | | | - -:::info -Check [Conda-store authorization model](https://conda-store.readthedocs.io/en/latest/contributing.html#authorization-model) for more details on conda-store authorization. -::: - -:::caution -The role `jupyterhub_admin` gives users elevated permissions to JupyterHub and should be applied judiciously. As mentioned in the table above, a JupyterHub admin is able to impersonate other users and view the contents of their home folder. For more details, read through the [JupyterHub documentation](https://z2jh.jupyter.org/en/stable/jupyterhub/customizing/user-management.html#admin-users). -::: - -To create new groups or modify (or delete) existing groups, log in as `root` and click **Groups** on the left-hand side. - -As an example, we create a new group named `conda-store-manager`. This group will have administrator access to the [Conda-Store service]. - -1. Click **New** in the upper-right hand corner under **Groups**. - -![Keycloak groups tab screenshot - user groups view](/img/how-tos/keycloak_groups.png) - -- Then, give the new group an appropriate name. - -![Keycloak add group form - name field set to conda-store-manager](/img/how-tos/keycloak_new_group1.png) - -2. Under **Role Mapping**, add the appropriate **Client Roles** as needed; there should be no need to update the **Realm Roles**. - -![Keycloak group conda-store-manager form - role mappings tab focused with expanded client roles dropdown](/img/how-tos/keycloak_new_group2.png) - -In this example, the new group only has one mapped role, `conda_store_admin`; however, it's possible to attach multiple **Client Roles** to a single group. - -![Keycloak group conda-store-manager form - role mappings tab focused ](/img/how-tos/keycloak_new_group3.png) - -Once complete, return to the **Users** section in the dashboard and add the relevant users to this newly created group. - -### Components Attribute - -We have seen in the above example the `component` attribute while creating a role. The value of this parameter -depends on the type of component in the service, we're creating a role for, currently we only have two components: - -- `jupyterhub`: to create `jupyterhub` native roles in the `jupyterhub` client. -- `conda-store`: to create `conda-store` roles in the `conda_store` client - -### JupyterHub Scopes - -The syntax for the `scopes` attribute for a `jupyterhub` role in Keycloak in Nebari follows the native RBAC scopes syntax -for JupyterHub itself. The documentation can be found [here](https://jupyterhub.readthedocs.io/en/stable/rbac/scopes.html#scope-conventions). - -As an example, scopes for allowing users to share apps in Nebari's `jhub-apps` launcher may look like this: - -> `shares!user,read:users:name,read:groups:name` - -The `scopes` defined above consists of three scopes: - -- `shares!user`: grants permissions to share user's server -- `read:users:name`: grants permissions to read other user's names -- `read:groups:name`: grants permissions to read other groups's names - -To be able to share a server to a group or a user you need to be able to read other user's or group's names and must have -permissions to be able to share your server, this is what this set of permissions implement. - -### Conda Store Scopes - -The scopes for roles for the `conda-store` Client are applied to the `namespace` level of `conda-store`. - -Below is example of granting a user specialized permissions to `conda-store`: - -> `admin!namespace=analyst,developer!namespace=nebari-git` - -The `scopes` defined above consists of two scopes: - -- `admin!namespace=analyst`: grants `admin` access to namespace `analyst` -- `developer!namespace=nebari-git`: grants `developer` access to namespace `nebari-git` - -When attached to a user or a group, the above-mentioned permissions will be granted to the user/group. diff --git a/docs/docs/how-tos/fine-grained-permissions.mdx b/docs/docs/how-tos/fine-grained-permissions.mdx new file mode 100644 index 000000000..260f7425c --- /dev/null +++ b/docs/docs/how-tos/fine-grained-permissions.mdx @@ -0,0 +1,641 @@ +--- +id: fine-grained-permissions +title: Creating and Managing Groups, Roles, and Directories +description: How to configure Keycloak's permissions, groups and roles, and manage group directories in JupyterHub. +--- + +## TL;DR + +- **Groups** organize users with similar permissions (e.g., `admin`, `developer`, `analyst`) +- **Roles** define permission bundles for specific services (e.g., `conda_store_admin`, `jupyterhub_developer`) +- **Scopes** are fine-grained permissions attached to roles (e.g., `read:users:name`) +- Create roles → Assign to groups → Add users to groups → Users must log out/in for changes to take effect +- Group directories in JupyterHub require the `allow-group-directory-creation-role` (since version 2024.9.1) + +## Overview + +Groups are a fundamental and vital part of the Nebari ecosystem. They are used to manage access to a wide range of services within Nebari, including JupyterHub instances, Keycloak realms, Conda environments, and computing resources. By grouping users based on roles, projects, or departments, Nebari simplifies the management of permissions and resource sharing. + +Beyond managing access, groups play a crucial role in organizing and sharing data across the JupyterHub ecosystem. Each group can have its own shared directory within JupyterHub, allowing users within the same group to collaborate seamlessly by sharing files and resources. This facilitates team collaboration and ensures that data is organized and accessible to those who need it. + +In this document, we will cover: + +- [Understanding Roles, Groups, and Scopes](#understanding-roles-groups-and-scopes) +- [Managing Groups in Keycloak](#managing-groups-in-keycloak) +- [Default Clients, Groups, and Permissions](#default-clients-groups-and-permissions) +- [Creating Custom Roles with Scopes](#creating-custom-roles-with-scopes) +- [Managing Users and Group Membership](#managing-users-and-group-membership) +- [Managing Group Directories in JupyterHub](#managing-group-directories-in-jupyterhub) +- [Permission Precedence and Troubleshooting](#permission-precedence-and-troubleshooting) + +## Understanding Roles, Groups, and Scopes + +Nebari uses [Keycloak](https://www.keycloak.org/) to manage access for core services like `jupyterhub` and `conda-store`. Access is expressed through three related concepts: + +- **Roles**: Named permission bundles for a specific client/service (e.g., `conda_store_admin`, `jupyterhub_developer`). Roles define what actions can be performed. +- **Groups**: Collections of users who should receive the same roles (e.g., `admin`, `developer`, `analyst`). Groups aggregate users with similar responsibilities. +- **Scopes**: Fine-grained permission strings attached to roles (e.g., `read:users:name` for JupyterHub, or `admin!namespace=analyst` for Conda-Store). Scopes are the actual permissions. +- **Components**: Specifies the service that a role's scopes apply to (e.g., `jupyterhub` or `conda-store`). + +**Permission Flow**: User → Group → Role → Scopes → Permissions + +Nebari ships with **default clients, groups, and roles** that cover most needs. When you need more granular control, create **custom roles with specific scopes** in Keycloak and assign them to a group (or directly to a user). + +:::note +Creating or editing a role does nothing until it is assigned via **Role Mappings** (to a group or user). Users must log out and back in for changes to take effect because Keycloak issues new access tokens on login. +::: + +:::tip Technical Note +Roles in Nebari can stack, meaning that if a user is in one group with role `conda_store_admin` and another group with role `conda_store_viewer`, this user ultimately has the role `conda_store_admin`. In case of different levels of access originating from different groups, precedence is given to the highest level of access. See [Permission Precedence](#permission-precedence-and-troubleshooting) for details. +::: + +## Default Clients, Groups, and Permissions + +A fresh Nebari deployment comes with several custom Keycloak clients (services) enabled. You can manage these from **Clients** in the Keycloak admin console: +```yaml +clients: + - jupyterhub + - conda_store + - grafana (if monitoring is enabled) + - argo-server-sso (if argo is enabled) + - forwardauth +``` + +To manage and configure these clients, you can navigate to the `Clients` tab within the Keycloak admin console, as illustrated in the image below. + +![Keycloak clients](/img/how-tos/fine_grainer_permissions_keycloak_clients.png) + +This can be accessed at `/auth/admin/master/console/#/realms/nebari/clients` + +### Default Groups and Their Roles + +Groups represent a collection of users that perform similar actions and therefore require similar permissions. By default, Nebari is deployed with the following groups: `admin`, `developer`, and `analyst` (in roughly descending order of permissions and scope). + +Roles on the other hand represent the type or category of user. This includes access and permissions that this category of user will need to perform their regular job duties. The differences between `groups` and `roles` are subtle. Particular roles (one or many), like `conda_store_admin`, are associated with a particular group, such as `admin` and any user in this group will then assume the role of `conda_store_admin`. + +:::info Shared Folders +Membership in one of the default groups also grants access to the corresponding shared folder (e.g., the `developer` group has access to `~/shared/developer`). Since `2024.9.1`, Nebari requires a special role to access the shared folders: `allow-group-directory-creation-role`. +::: + +### Default Roles and Permissions + +Below is a table that outlines the default roles, groups, and permissions that come with Nebari: + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +import { Table } from '@site/src/components/MarkdownTable'; + +
+ + + + + + + +
+ + + +
+ + + +
+ + + + +## Managing Groups in Keycloak + +Keycloak is the identity and access management service used in Nebari. It allows you to create and manage users, groups, roles, and permissions. Groups in Keycloak are collections of users, and they can be assigned specific roles that grant permissions to access various services within Nebari. + +For detailed information on managing groups in Keycloak, refer to the [Keycloak documentation on Group Management](https://www.keycloak.org/docs/latest/server_admin/#_group_management). + +Below we outline the steps specific to Nebari. + +### Creating a New Group + +To create a new group in Keycloak: + +1. **Log in to Keycloak** as an administrator (usually the `root` user). +2. **Navigate to Groups**: Click on **Groups** in the left-hand menu. +3. **Create the Group**: Click the **New** button, enter an appropriate name for your new group (e.g., `conda-store-manager`), and save. + +If you wish to organize your groups hierarchically, you can create subgroups by selecting a parent group and adding a subgroup. + +**Example Workflow**: Creating a `conda-store-manager` group + +1. Click **New** in the upper-right corner under **Groups**. + +![Keycloak groups tab screenshot - user groups view](/img/how-tos/keycloak_groups.png) + +2. Give the new group an appropriate name. + +![Keycloak add group form - name field set to conda-store-manager](/img/how-tos/keycloak_new_group1.png) + +3. Under **Role Mapping**, add the appropriate **Client Roles** as needed; there should be no need to update the **Realm Roles**. + +![Keycloak group conda-store-manager form - role mappings tab focused with expanded client roles dropdown](/img/how-tos/keycloak_new_group2.png) + +In this example, the new group only has one mapped role, `conda_store_admin`; however, it's possible to attach multiple **Client Roles** to a single group. + +![Keycloak group conda-store-manager form - role mappings tab focused](/img/how-tos/keycloak_new_group3.png) + +### Managing Subgroups + +Subgroups allow you to create a hierarchical structure of groups, representing organizational units, projects, or teams. + +To manage subgroups: + +- **Creating Subgroups**: Select the parent group, navigate to the **Sub Groups** tab, and create a new subgroup. +- **Assigning Roles to Subgroups**: Assign roles to subgroups following the same process as regular groups. +- **Use Cases**: Organize groups hierarchically to reflect organizational structures (e.g., `engineering` → `data-science`, `engineering` → `backend`). + +For more information, see the [Keycloak documentation on Group Hierarchies](https://www.keycloak.org/docs/latest/server_admin/#group-hierarchies). + +### Deleting a Group + +To delete a group: + +1. **Navigate to Groups**: Click on **Groups** in the left-hand menu. +2. **Select the Group**: Click on the group you want to delete. +3. **Delete**: Click the **Delete** button and confirm. + +**Important**: Deleting a group does not delete the users in that group. Users will simply lose the permissions associated with that group. Any shared directories associated with the group will remain in the file system but will no longer be mounted for users. + + +## Creating Custom Roles with Scopes + +Beyond Nebari's default roles, you can also create your own with highly specific permissions. + +### Understanding Components and Scopes + +In Nebari, **scopes** are closely tied to the service (client) you're working with—in Keycloak roles we identify them as "components." + +#### Components Attribute + +The value of the `component` attribute depends on the service we are creating a role for. Currently, Nebari supports: + +- `jupyterhub`: to create `jupyterhub` native roles in the `jupyterhub` client. +- `conda-store`: to create `conda-store` roles in the `conda_store` client. + +#### Scope Syntax by Service + +
+ +##### JupyterHub Scopes + +JupyterHub scopes syntax in Nebari follow [JupyterHub's built-in RBAC syntax](https://jupyterhub.readthedocs.io/en/stable/rbac/scopes.html). + +**Syntax**: `scope1,scope2,scope3` + +**Example**: +``` +shares!user,read:users:name,read:groups:name +``` + +- `shares!user`: Allows sharing servers with other users. +- `read:users:name`: Grants read access to other users' names. +- `read:groups:name`: Grants read access to other groups' names. + +This combination allows a user to share their server with others (via `shares!user`) and also read other users' and groups' names. + +**Common Use Case - App Sharing**: +``` +shares!user=alice,shares!group=data-science,read:users:name,read:groups:name +``` +Allows sharing apps with user `alice` and the `data-science` group. + +For a complete list of JupyterHub scopes, see the JupyterHub documentation: [https://jupyterhub.readthedocs.io/en/stable/rbac/scopes.html#available-scopes](https://jupyterhub.readthedocs.io/en/stable/rbac/scopes.html#available-scopes) + +Nebari extends these scopes for some integrations (e.g., `jhub-apps`) to support sharing apps with users/groups. + +--- + +##### Conda-Store Scopes + +Conda-Store scopes are defined at the namespace level. + +**Syntax**: `!namespace=` + +**Access Levels**: `admin`, `developer`, `viewer` + +**Example**: +``` +admin!namespace=analyst,developer!namespace=nebari-git +``` + +- `admin!namespace=analyst`: Grants an **admin** role in the `analyst` namespace. +- `developer!namespace=nebari-git`: Grants a **developer** role in the `nebari-git` namespace. + +**Common Use Case - Multi-namespace Access**: +``` +admin!namespace=data-science,viewer!namespace=production,viewer!namespace=shared-datasets +``` +Grants admin access to the team's namespace, but read-only to production and shared datasets. + +See: [Conda-Store role mappings](https://conda.store/conda-store/explanations/conda-store-concepts#role-mappings) + +
+ +:::note Conda-Store Defaults and Nebari's Internal Group Handling +Nebari grants some Conda-Store permissions **automatically**, regardless of how your Keycloak groups are configured. The authentication class applies these defaults at login so users can work even if the Keycloak groups don't define any `conda_store_*` client roles. + +**What Nebari applies by default** + +- **Personal namespace** → always **admin** + `"{username}/*" → {"admin"}` (cannot be downgraded by scopes). +- **Deployment default namespace** → **viewer** + `"{default_namespace}/*" → {"viewer"}`. +- **Nebari default groups** (`analyst`, `developer`, `admin`) → **handled internally** + If a user is in one of these groups, Nebari binds a **same-named namespace** to the user with that group's effective Conda-Store role (e.g., group `analyst` ⇒ `analyst/*`), **even when the Keycloak group itself has no client roles**. +- **SuperAdmin** (`conda_store_superadmin`) → full wildcard + `"*/*" → {"admin"}` (includes delete & service-token privileges). + +**How it reconciles with Keycloak scopes** + +- On each login, Nebari re-applies permissions from Keycloak role **scopes** (e.g., `admin!namespace=analyst`). +- If multiple grants target the same namespace, the **highest** permission wins. +- Order of precedence (highest → lowest): **SuperAdmin** → **explicit scopes** → **internal defaults**. +- Changes take effect after the user signs in again. +::: + +### Step-by-Step: Creating a Custom Role + +**Typical Workflow**: + +1. **Create** a role (and define its scopes) under the relevant **Client** (e.g., `jupyterhub`, `conda_store`). +2. **Assign** this role to a **group** (or directly to an individual user). +3. **Add users** to that group if you haven't already. +4. **Users log out and back in** for changes to take effect. + +#### Step 1: Create the Role + +In the following example, we create a new role for the `JupyterHub` client granting permissions to share apps with users and groups. + +1. Select the appropriate client and click on **Add Role**. + +![Keycloak client add jupyterhub role](/img/how-tos/keycloak_jupyterhub_client.png) + +2. On the **Add Role** form, write a meaningful name and description for the role. Click **Save**. + +![Keycloak clients add jupyterhub role form](/img/how-tos/keycloak_jupyterhub_add_role.png) + +3. Now the role has been created, but it does nothing. Add permissions by clicking on the **Attributes** tab and adding `components` and `scopes`. + +![Keycloak clients add jupyterhub role attributes](/img/how-tos/keycloak_add_role_attributes.png) + +#### Step 2: Assign Role to a Group + +Creating a role in Keycloak has no effect on any user or group's permissions until it is assigned. + +To add a role to a group: + +1. Select **Groups** on the left sidebar and search for the group. +2. Select that group and click on the **Role Mappings** tab. +3. Select the client associated with the role from the **Client Roles** dropdown. +4. Select the role in **Available Roles** and click **Add Selected >>**. + +Once complete, any users in this group will inherit the role's permissions after they log out and back in. + +#### Step 3: Assign Role to a User (Alternative) + +To add a role directly to a user: + +1. Select **Users** on the left sidebar and search for the user. + +![Keycloak select user](/img/how-tos/keycloak_select_user.png) + +2. Select that user and click on the **Role Mappings** tab. + +![Keycloak user role mapping tab](/img/how-tos/keycloak_user_role_mapping_tab.png) + +3. Select the client associated with the role. + +![Keycloak user role mapping roles](/img/how-tos/keycloak_user_role_mapping_roles.png) + +4. Select the role in **Available Roles** and click **Add Selected >>**. + +![Keycloak user role mapping add role](/img/how-tos/keycloak_user_role_mapping_add_role.png) + +### Example: Custom Role Scenarios + +
+ +**Scenario 1: Data Science Team Lead** +- **Goal**: Admin access to team namespace, read-only to production +- **Component**: `conda-store` +- **Scopes**: `admin!namespace=data-science,viewer!namespace=production` + +--- +**Scenario 2: App Collaborator** +- **Goal**: Share JupyterHub apps with specific users and groups +- **Component**: `jupyterhub` +- **Scopes**: `shares!user,shares!group,read:users:name,read:groups:name` + +--- +**Scenario 3: Cross-functional Analyst** +- **Goal**: Developer access to multiple namespaces +- **Component**: `conda-store` +- **Scopes**: `developer!namespace=marketing,developer!namespace=sales,viewer!namespace=finance` + +
+ +## Managing Users and Group Membership + +### Adding Users to a Group + +To add users to a group: + +1. **Navigate to Users**: Click on **Users** in the left-hand menu. +2. **Select a User**: Choose the user you want to add to a group. +3. **Assign to Group**: + - Click on the **Groups** tab within the user's details. + - Click the **Join** button. + - Select the group (and subgroup, if applicable) you wish to add the user to. + - Click **Join** to add the user to the group. + +Repeat this process for all users who should be part of the group. + +### Removing Users from a Group + +To remove users from a group: + +1. **Navigate to Users**: Click on **Users** in the left-hand menu. +2. **Select a User**: Choose the user you want to remove from a group. +3. **Remove from Group**: + - Click on the **Groups** tab within the user's details. + - Find the group in the **Group Membership** list. + - Click **Leave** next to the group name. + +The user will immediately lose access to group-level permissions, but changes won't take effect until their next login (when a new access token is issued). + +### Removing Roles from Users or Groups + +To remove a role from a user or group: + +1. Navigate to the user or group's **Role Mappings** tab. +2. Under **Assigned Roles**, find the role you want to remove. +3. Select the role and click **Remove selected**. + +Again, changes take effect after the user logs out and back in. + +## Managing Group Directories in JupyterHub + +:::note Important +As of version `2024.9.1`, JupyterHub creates and mounts directories only for groups with the `allow-group-directory-creation-role`. By default, this includes the `admin`, `analyst`, and `developer` groups. Previously, directories were automatically created for all Keycloak groups. This change gives administrators more control over shared directories and overall access management without cluttering the file system. +::: + +### Understanding Group Directories + +A group directory in JupyterHub is a shared folder accessible to all members of a specific group. It provides a shared space within the file system for collaboration. + +An example directory structure: +```yaml +/shared +├── admin +│ ├── file1.txt +│ └── file2.txt +├── analyst +│ ├── file1.txt +│ └── file2.txt +└── developer + ├── file1.txt + └── file2.txt +``` + +### Assigning the `allow-group-directory-creation-role` to a Group + +To enable directory creation and mounting for a group in JupyterHub, assign the `allow-group-directory-creation-role` to the group in Keycloak. + +1. **Navigate to the Group**: Log in to Keycloak as an administrator and select the group. +2. **Go to Role Mappings**: Click on the **Role Mappings** tab. +3. **Assign the Role**: + - Under **Client Roles**, select the `jupyterhub` client. + - Select `allow-group-directory-creation-role` and click **Add selected**. + +Users in this group will now have access to the group's shared directory in JupyterHub after they log out and back in. + +### Removing Group Directory Access + +To remove the group's directory access: + +1. **Navigate to the Group's Role Mappings**: Select the group and go to the **Role Mappings** tab. +2. **Remove the Role**: + - Under **Assigned Roles**, select `allow-group-directory-creation-role`. + - Click **Remove selected**. + +**Data Preservation**: No data is deleted when you remove the role; the directory is simply unmounted from users' JupyterLab sessions. + +### Managing Subgroup Directories + +Subgroups can have their own directories in JupyterHub if assigned the `allow-group-directory-creation-role`. Assign the role to subgroups as needed to control access and collaboration. + +**Example Hierarchy**: +```yaml +/shared +├── engineering +│ ├── data-science +│ │ └── models +│ └── backend +│ └── configs +└── marketing + └── campaigns +``` + +## Permission Precedence and Troubleshooting + +### How Permissions Stack + +When a user belongs to multiple groups or has multiple roles assigned, Nebari uses the following rules to determine effective permissions: + +1. **Highest Permission Wins**: If conflicting permissions exist, the most permissive one takes precedence. + - Example: User in both `viewer` group (read-only) and `admin` group (read-write) → user gets read-write access + +2. **Service-Specific Precedence**: + - **Conda-Store**: SuperAdmin > Explicit Scopes > Internal Defaults > No Access + - **JupyterHub**: Scopes are additive; all granted scopes apply + +3. **Personal Namespaces**: Always admin access in Conda-Store (cannot be downgraded) + +### Common Issues and Solutions + +
+ +#### Issue 1: Changes Not Taking Effect + +**Symptom**: User was added to a group or assigned a role, but still can't access resources. + +**Solution**: +- User must **log out completely** and **log back in** +- Keycloak issues new access tokens on login with updated permissions +- Refreshing the page is not sufficient + +--- + +#### Issue 2: User Has Too Many Permissions + +**Symptom**: User can access resources they shouldn't be able to. + +**Solution**: +1. Check all groups the user belongs to +2. Check direct role assignments to the user +3. Remember: highest permission wins, so remove the more permissive role/group +4. User must log out and back in + +--- + +#### Issue 3: Group Directory Not Appearing + +**Symptom**: Users in a group can't see the shared directory. + +**Solution**: +1. Verify the group has `allow-group-directory-creation-role` assigned +2. User must log out and back in to mount the directory +3. Check JupyterHub logs for errors +4. Verify the directory exists in the file system + +--- + +#### Issue 4: Custom Role Not Working + +**Symptom**: Created a custom role with scopes, but it has no effect. + +**Solution**: +1. Verify `components` attribute is set correctly (e.g., `jupyterhub` or `conda-store`) +2. Verify `scopes` attribute syntax matches the service requirements +3. Ensure role is assigned to a group or user via **Role Mappings** +4. User must log out and back in + +
+ +### Testing Permissions + +To verify a user's permissions: + +1. **JupyterHub**: Check the user's token scopes via the JupyterHub API: +```bash + curl -H "Authorization: token " \ + https:///hub/api/user +``` + +2. **Conda-Store**: Attempt to access namespaces through the UI or API and observe access levels + +3. **Keycloak**: Review the user's **Role Mappings** and **Group Membership** tabs + +## See Also + +- [Keycloak Official Documentation - Group Management](https://www.keycloak.org/docs/latest/server_admin/#_group_management) +- [Keycloak Official Documentation - Group Hierarchies](https://www.keycloak.org/docs/latest/server_admin/#group-hierarchies) +- [JupyterHub RBAC Documentation](https://jupyterhub.readthedocs.io/en/stable/rbac/scopes.html) +- [Conda-Store Role Mappings](https://conda.store/conda-store/explanations/conda-store-concepts#role-mappings) diff --git a/docs/package.json b/docs/package.json index c170eba3b..774fb6ccb 100644 --- a/docs/package.json +++ b/docs/package.json @@ -44,12 +44,14 @@ "@docusaurus/preset-classic": "3.5.0", "@mdx-js/react": "^3.0.0", "clsx": "^2.1.1", + "dedent": "^0.7.0", "docusaurus-lunr-search": "^3.3.0", "docusaurus-plugin-sass": "^0.2.5", "prism-react-renderer": "^2.1.0", "raw-loader": "^4.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-markdown": "^8.0.7", "sass": "^1.77.8" }, "devDependencies": { diff --git a/docs/src/components/MarkdownTable/index.tsx b/docs/src/components/MarkdownTable/index.tsx new file mode 100644 index 000000000..eb882aff8 --- /dev/null +++ b/docs/src/components/MarkdownTable/index.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import ReactMarkdown from 'react-markdown'; +import dedent from 'dedent'; + +/** + * Each row in the table is an array of cells. + * Each cell can be either: + * - A single Markdown string, OR + * - An array of Markdown lines (which we'll join with '\n'). + */ +export interface MarkdownTableProps { + headers: string[]; + rows: Array>; +} + +export function Table({ headers, rows }: MarkdownTableProps) { + return ( +
+ + + {headers.map((header, headerIdx) => ( + + ))} + + + + {rows.map((rowData, rowIdx) => ( + + {rowData.map((cellData, cellIdx) => { + // If the cellData is an array of strings, join them with "\n" + const cellContent = Array.isArray(cellData) + ? dedent(cellData.join('\n')) + : dedent(cellData); + + return ( + + ); + })} + + ))} + +
{header}
+ {cellContent} +
+ ); +} diff --git a/docs/src/theme/TOCItems/Tree.tsx b/docs/src/theme/TOCItems/Tree.tsx new file mode 100644 index 000000000..05f85ed6b --- /dev/null +++ b/docs/src/theme/TOCItems/Tree.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; +import type { Props } from '@theme/TOCItems/Tree'; + +function TOCItemTree({ + toc, + className, + linkClassName, + isChild, +}: Props): JSX.Element | null { + if (!toc.length) { + return null; + } + + return ( +
    + {toc.map((heading) => { + // Parse the heading ID to check if it has "provider::actualId" + let providerQuery = null; + let actualId = heading.id; + + if (heading.id.includes('::')) { + const [provider, ...rest] = heading.id.split('::'); + providerQuery = provider; + actualId = rest.join('::'); // In case there's more than one '::', join back the rest. + } + + // If a provider is found, build the URL with the query param + const linkHref = providerQuery + ? `?provider=${providerQuery}#${heading.id}` + : `#${heading.id}`; + + return ( +
  • + + +
  • + ); + })} +
+ ); +} + +export default React.memo(TOCItemTree); diff --git a/docs/src/theme/TOCItems/index.tsx b/docs/src/theme/TOCItems/index.tsx new file mode 100644 index 000000000..b61bcd1ef --- /dev/null +++ b/docs/src/theme/TOCItems/index.tsx @@ -0,0 +1,54 @@ +import React, {useMemo} from 'react'; +import {useThemeConfig} from '@docusaurus/theme-common'; +import { + useTOCHighlight, + useFilteredAndTreeifiedTOC, + type TOCHighlightConfig, +} from '@docusaurus/theme-common/internal'; +import TOCItemTree from '@theme/TOCItems/Tree'; +import type {Props} from '@theme/TOCItems'; + +export default function TOCItems({ + toc, + className = 'table-of-contents table-of-contents__left-border', + linkClassName = 'table-of-contents__link', + linkActiveClassName = undefined, + minHeadingLevel: minHeadingLevelOption, + maxHeadingLevel: maxHeadingLevelOption, + ...props +}: Props): JSX.Element | null { + const themeConfig = useThemeConfig(); + + const minHeadingLevel = + minHeadingLevelOption ?? themeConfig.tableOfContents.minHeadingLevel; + const maxHeadingLevel = + maxHeadingLevelOption ?? themeConfig.tableOfContents.maxHeadingLevel; + + const tocTree = useFilteredAndTreeifiedTOC({ + toc, + minHeadingLevel, + maxHeadingLevel, + }); + + const tocHighlightConfig: TOCHighlightConfig | undefined = useMemo(() => { + if (linkClassName && linkActiveClassName) { + return { + linkClassName, + linkActiveClassName, + minHeadingLevel, + maxHeadingLevel, + }; + } + return undefined; + }, [linkClassName, linkActiveClassName, minHeadingLevel, maxHeadingLevel]); + useTOCHighlight(tocHighlightConfig); + + return ( + + ); +}