mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-08 02:24:08 +00:00
updated tech implementation section
This commit is contained in:
@@ -477,7 +477,9 @@ paths:
|
||||
'400':
|
||||
description: Body must be a JSON dict or empty
|
||||
'403':
|
||||
description: Requested role does not exist
|
||||
description: Requested roles cannot have higher permissions than the token owner
|
||||
'404':
|
||||
description: Requested roles not found
|
||||
/users/{name}/tokens/{token_id}:
|
||||
parameters:
|
||||
- name: name
|
||||
|
BIN
docs/source/images/rbac-api-request-chart.png
Normal file
BIN
docs/source/images/rbac-api-request-chart.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 232 KiB |
BIN
docs/source/images/rbac-api-token-request-chart.png
Normal file
BIN
docs/source/images/rbac-api-token-request-chart.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 209 KiB |
Binary file not shown.
Before Width: | Height: | Size: 48 KiB |
@@ -24,6 +24,6 @@ Within the RBAC framework, this is achieved by assigning a role to the administr
|
||||
|
||||
roles
|
||||
scopes
|
||||
tech-implementation
|
||||
use-cases
|
||||
tech-implementation
|
||||
```
|
||||
|
@@ -16,7 +16,6 @@ Roles can also be customly defined and assigned to users, services, groups and t
|
||||
**_Groups_** do not require any role and are not assigned any roles by default. Once group roles are defined and assigned, the privileges of each group member are extended with the group roles in the background during the API request permission check. This is useful for requesting the same permissions for several users.
|
||||
|
||||
(define_role_target)=
|
||||
|
||||
## Defining Roles
|
||||
|
||||
### In `jupyterhub_config.py`
|
||||
|
@@ -19,10 +19,7 @@ The standard user scope `all` provides access to the user's own resources and su
|
||||
- `users!user=gerard` where the `users` scope includes access to the full user model, activity and starting/stopping servers. The filter restricts this access to the user's own resources
|
||||
- `users:tokens!user=gerard` allows the user to access, request and delete his own tokens only.
|
||||
|
||||
*Note: I'm hoping that horizontal and vertical filtering are somehow intuitive concepts, but maybe I am making up words for things that are already pretty well defined?*
|
||||
|
||||
(filtering-target)=
|
||||
|
||||
## Horizontal filtering
|
||||
|
||||
Horizontal filtering, also called *resource filtering*, is the concept of reducing the payload of an API call to cover only the subset of the *resources* that the scopes of the client provides them access to.
|
||||
|
@@ -1,33 +1,49 @@
|
||||
# Technical Implementation
|
||||
|
||||
Roles are stored in the database similarly as users, services, etc., and can be added or modified as explained in {ref}`define_role_target`. Objects can gain, change and lose roles. For example, one can change a token's role, and as such its permissions, without the need to initiate new token (currently through `add_obj` and `remove_obj` functions in `roles.py`, this will be eventually available through APIs). Roles' and scopes' utilities can be found in `roles.py` and `scopes.py` modules.
|
||||
Roles are stored in the database similarly as users, services, etc., and can be added or modified as explained in {ref}`define_role_target`. Objects can gain, change and lose roles. For example, one can change a token's role, and as such its permissions, without the need to initiate new token (currently through `update_roles` and `remove_obj` functions in `roles.py`, this will be eventually available through APIs). Roles' and scopes' utilities can be found in `roles.py` and `scopes.py` modules.
|
||||
|
||||
## Resolving roles and scopes
|
||||
Roles and scopes are resolved on several occasions as shown in the {ref}`diagram below <checkpoint-fig>`.
|
||||
Roles and scopes are resolved on several occasions, for example when requesting an API token with specific roles or making an API request. The following sections provide more details.
|
||||
|
||||
```{figure} ../images/role-scope-resolution.png
|
||||
### Requesting API token with specific roles
|
||||
API tokens grant access to JupyterHub's APIs. The RBAC framework allows for requesting tokens with specific existing roles. To date, it is only possible to add roles to a token in two ways:
|
||||
1. through the `config.py` file as described in the {ref}`define_role_target` section
|
||||
2. through the POST /users/{name}/tokens API where the roles can be specified in the token parameters body (see [](../reference/rest-api.rst)).
|
||||
|
||||
The RBAC framework adds several steps into the token issue flow.
|
||||
|
||||
If no roles are requested, the token is issued with a default role (providing the requester is allowed to create the token).
|
||||
|
||||
If the token is requested with a specific role or multiple roles, the permissions of the would-be token's owner are checked against the requested permissions to ensure the token will not grant its owner additional privileges. In practice, this corresponds to resolving all the token owner's roles (including the roles associated with their groups) and the token's requested roles into a set of scopes. The two sets are compared and if the token's scopes are a subset of the token owner's scopes, the token is issued with the requested roles.
|
||||
|
||||
{ref}`Figure 1 <api-token-request-chart>` below illustrates this process. The orange rectangles highlight where in the process the roles and scopes are resolved.
|
||||
|
||||
```{figure} ../images/rbac-api-token-request-chart.png
|
||||
:align: center
|
||||
:name: checkpoint-fig
|
||||
:name: api-token-request-chart
|
||||
|
||||
Figure 1. Checkpoints for resolving scopes in JupyterHub
|
||||
Figure 1. Resolving roles and scopes during API token request
|
||||
```
|
||||
|
||||
### Checkpoint 1: Requesting a token with specific roles
|
||||
When a token is requested with a specific role or multiple roles, the permissions of the token's owner (client in {ref}`Figure 1 <checkpoint-fig>`) are checked against the requested permissions to ensure the token will not grant its owner additional privileges. In practice, this corresponds to resolving all the client's roles (including the roles associated with their groups) and the token's requested roles into a set of scopes. The two sets are compared and if the token's scopes (s5 in {ref}`Figure 1 <checkpoint-fig>`) are a subset of the client's scopes, the token is issued with the requested roles (role D in {ref}`Figure 1 <checkpoint-fig>`).
|
||||
|
||||
```{note}
|
||||
The above check is also performed when roles are requested for existing tokens, e.g., when adding tokens to {ref}`role definitions through the config.py <define_role_target>`.
|
||||
```
|
||||
### Checkpoint 2: Making an API request
|
||||
Each authenticated API request is guarded by a scope decorator that specifies which scopes are required to gain the access to the API (scopes s1, s5 and s6 in {ref}`Figure 1 <checkpoint-fig>`).
|
||||
|
||||
When an API request is performed, the token's roles are again resolved into a set of scopes and compared to the required scopes in the same way as in checkpoint 1. The access to the API is then either allowed or denied.
|
||||
### Making API request
|
||||
With the RBAC framework each authenticated JupyterHub API request is guarded by a scope decorator that specifies which scopes are required to gain the access to the API.
|
||||
|
||||
For instance, a token with a role with `groups!group=class-C` scope will be allowed to access the _GET /groups_ API but not allowed to make the _POST /groups/{name}_ API request.
|
||||
When an API request is performed, the passed API token's roles are again resolved into a set of scopes and compared to the scopes required to access the API as follows:
|
||||
- if the API scopes are present within the set of token's scopes, the access is granted and the API returns its "full" response
|
||||
- if that is not the case, another check is utilized to determine if sub-scopes of the required API scopes can be found in the token's scope set:
|
||||
- if the subscopes are present, the RBAC framework employs the {ref}`filtering <filtering-target>` procedures to refine the API response to provide access to only resource attributes specified by the token provided scopes
|
||||
- for example, providing a scope `read:users:activity!group=class-C` for the _GET /users_ API will return a list of user models from group `class-C` containing only the `last_activity` attribute for each user model
|
||||
- if the subscopes are not present, the access to API is denied
|
||||
|
||||
### Checkpoint 3: API request response
|
||||
The third checkpoint takes place at the API response level. The scopes provided for the request (s5 in {ref}`Figure 1 <checkpoint-fig>`) are used to filter through the API response to provide access to only resource attributes specified by the scopes.
|
||||
{ref}`Figure 2 <api-request-chart>` illustrates this process highlighting the steps where the role and scope resolutions as well as filtering occur in orange.
|
||||
|
||||
For example, providing a scope `read:users:activity!group=class-C` for the _GET /users_ API will return a list of user models from group `class-C` containing only the last_activity attribute.
|
||||
```{figure} ../images/rbac-api-request-chart.png
|
||||
:align: center
|
||||
:name: api-request-chart
|
||||
|
||||
For more filtering details refer to the {ref}`filtering<filtering-target>` section.
|
||||
Figure 2. Resolving roles and scopes when an API request is made
|
||||
```
|
||||
|
@@ -4,7 +4,8 @@ To determine which scopes a role should have it is best to follow these steps:
|
||||
1. Determine what actions the role holder should have/have not access to
|
||||
2. Match the actions against the JupyterHub's REST APIs
|
||||
3. Check which scopes are required to access the APIs
|
||||
4. Define the role with required scopes and assign to users/services/groups/tokens
|
||||
4. Customize the scopes with filters if needed
|
||||
5. Define the role with required scopes and assign to users/services/groups/tokens
|
||||
|
||||
Below, different use cases are presented on how to use the RBAC framework.
|
||||
|
||||
@@ -15,14 +16,35 @@ A regular user should be able to view and manage all of their own resources. Thi
|
||||
## Service to cull idle servers
|
||||
|
||||
Finding and shutting down idle servers can save a lot of computational resources.
|
||||
Below follows a short tutorial on how one can add a cull-idle service to JupyterHub.
|
||||
We can make use of [jupyterhub-idle-culler](https://github.com/jupyterhub/jupyterhub-idle-culler) to manage this for us.
|
||||
Below follows a short tutorial on how to add a cull-idle service in the RBAC system.
|
||||
|
||||
1. Request an API token
|
||||
2. Define the service (`idle-culler`)
|
||||
3. Define the role (scopes `users:servers`, `admin:users:servers`)
|
||||
4. Install cull-idle servers (`pip install jupyterhub-idle-culler`)
|
||||
5. Add the service to `jupyterhub_config.py`
|
||||
6. (Restart JupyterHub)
|
||||
1. Install the cull-idle server script with `pip install jupyterhub-idle-culler`.
|
||||
2. Define a new service `idle-culler` and a new role for this service:
|
||||
```python
|
||||
c.JupyterHub.roles.append(
|
||||
{
|
||||
"name": "idle-culler",
|
||||
"description": "Culls idle servers",
|
||||
"scopes": ["read:users:servers", "admin:users:servers"],
|
||||
"services": ["idle-culler"],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
c.JupyterHub.services.append(
|
||||
{
|
||||
"name": "idle-culler",
|
||||
"command": [
|
||||
sys.executable, "-m",
|
||||
"jupyterhub_idle_culler",
|
||||
"--timeout=3600"
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
```
|
||||
3. Restart JupyterHub to complete the process.
|
||||
|
||||
|
||||
## API launcher
|
||||
|
@@ -371,7 +371,7 @@ class UserTokenListAPIHandler(APIHandler):
|
||||
except ValueError:
|
||||
raise web.HTTPError(
|
||||
403,
|
||||
"Requested token roles %r have higher permissions than the token owner"
|
||||
"Requested roles %r cannot have higher permissions than the token owner"
|
||||
% token_roles,
|
||||
)
|
||||
if requester is not user:
|
||||
|
@@ -223,7 +223,7 @@ def update_roles(db, obj, kind, roles=None):
|
||||
% rolename
|
||||
)
|
||||
else:
|
||||
raise NameError('Role %r does not exist' % rolename)
|
||||
raise NameError('Requested role %r does not exist' % rolename)
|
||||
else:
|
||||
add_obj(db, objname=obj.name, kind=kind, rolename=rolename)
|
||||
else:
|
||||
|
Reference in New Issue
Block a user