split the docs in docs/source/rbac folder

This commit is contained in:
IvanaH8
2021-02-15 16:19:13 +01:00
parent be34146d29
commit 7d1b6a2021
10 changed files with 240 additions and 60 deletions

View File

@@ -4,7 +4,7 @@ alabaster_jupyterhub
# Temporary fix of #3021. Revert back to released autodoc-traits when
# 0.1.0 released.
https://github.com/jupyterhub/autodoc-traits/archive/75885ee24636efbfebfceed1043459715049cd84.zip
myst_parser
myst-parser
pydata-sphinx-theme
pytablewriter>=0.56
recommonmark>=0.6

View File

@@ -53,11 +53,6 @@ todo_include_todos = False
# Set the default role so we can use `foo` instead of ``foo``
default_role = 'literal'
# -- Source -------------------------------------------------------------
import recommonmark
from recommonmark.transform import AutoStructify
# -- Config -------------------------------------------------------------
from jupyterhub.app import JupyterHub
from docutils import nodes
@@ -112,9 +107,7 @@ class HelpAllDirective(SphinxDirective):
def setup(app):
# app.add_config_value('recommonmark_config', {'enable_eval_rst': True}, True)
app.add_css_file('custom.css')
# app.add_transform(AutoStructify)
app.add_directive('jupyterhub-generate-config', ConfigDirective)
app.add_directive('jupyterhub-help-all', HelpAllDirective)

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -114,7 +114,7 @@ RBAC Reference
.. toctree::
:maxdepth: 2
rbac
rbac/index
Contributing
------------

View File

@@ -1,51 +0,0 @@
# JupyterHub RBAC
Role Based Access Control (RBAC) in JupyterHub serves to provide finer grained access to perform actions by users or services.
## Motivation
The JupyterHub API requires authentication before allowing changes to the administration system. For instance, currently the default behaviour is that creating or deleting users requires *admin rights*. This ensures that an arbitrary user, or even an unauthenticated third party, cannot disrupt the status of the Hub.
This system is functional, but lacks flexibility. If your Hub serves a number of users in different departments, you might want to delegate permissions to other users or automate certain processes. With this framework, appointing a 'group-only admin', or a bot that culls idle servers, requires granting full rights to all actions. This can be error-prone and violates the [principle of least privilige](https://en.wikipedia.org/wiki/Principle_of_least_privilege).
To remedy situations like this, we implement an RBAC system. By equipping users, groups and services with *roles* that supply them with a collection of permissions (*scopes*), administrators are able to fine-tune which parties are able to access which resources.
### Available scopes
[](./reference/rest-api.rst) documentation details all available scopes and which of these are required for what particular API request.
The roles can then be defined as follows:
```python
c.JupyterHub.load_groups = {
'class-A': ['johan', 'student1', 'student2'],
'class-B': ['johan', 'student3', 'student4']
}
c.JupyterHub.load_roles = [
{
'name': 'class-A-student',
'description': 'Grants access to information about the group',
'scopes': ['read:groups!group=class-A'],
'groups': ['class-A']
},
{
'name': 'class-B-student',
'description': 'Grants access to information about the group',
'scopes': ['read:groups!group=class-B'],
'groups': ['class-B']
},
{
'name': 'teacher',
'description': 'Allows for accessing information about teacher group members and starting/stopping their servers',
'scopes': ['read:users!group=class-A', 'read:users!group=class-B', 'users:servers!group=class-A', 'users:servers!group=class-B'],
'users': ['johan']
}
]
```
In the above example, `johan` has privileges inherited from class-A and class-B roles and the `teacher` role on top of those. Note the filters (`!group=`) limiting the priviliges only to the class-A and class-B group members.
## Technical Implementation
```{admonition} Here's my title
:class: warning
Here's my admonition content
```

29
docs/source/rbac/index.md Normal file
View File

@@ -0,0 +1,29 @@
# JupyterHub RBAC
Role Based Access Control (RBAC) in JupyterHub serves to provide finer grained access to perform actions by users or services.
## Motivation
The JupyterHub API requires authentication before allowing changes to the administration system. For instance, currently the default behaviour is that creating or deleting users requires *admin rights*. This ensures that an arbitrary user, or even an unauthenticated third party, cannot disrupt the status of the Hub.
This system is functional, but lacks flexibility. If your Hub serves a number of users in different departments, you might want to delegate permissions to other users or automate certain processes. With this framework, appointing a 'group-only admin', or a bot that culls idle servers, requires granting full rights to all actions. This can be error-prone and violates the [principle of least privilige](https://en.wikipedia.org/wiki/Principle_of_least_privilege).
To remedy situations like this, we implement an RBAC system. By equipping users, groups and services with *roles* that supply them with a collection of permissions (*scopes*), administrators are able to fine-tune which parties are able to access which resources.
## Definitions
__Scopes__ are specific permissions used to evaluate API requests. For example: the API endpoint `users/servers`, which enables starting or stopping user servers, is guarded by the scope `users:servers`.
Scopes are not directly assigned to requesters. Rather, when a client performs an API call, their access will be evaluated based on their assigned roles.
__Roles__ are collections of scopes that specify the level of what a client is allowed to do. For example, a group administrator may be granted permission to control the servers of group members, but not to create, modify or delete group members themselves.
Within the RBAC framework, this is achieved by assigning a role to the administrator that covers exactly those privileges.
## Technical Overview
```{toctree}
:maxdepth: 2
roles
scopes
tech-implementation
use-cases
```

61
docs/source/rbac/roles.md Normal file
View File

@@ -0,0 +1,61 @@
# Roles
JupyterHub provides three **default roles** which are automatically loaded to the database at the startup:
```{admonition} **Default roles**
- **_user_** role carries single `all` scope that grants _least user access_ to perform only default user actions.
- **_admin_** role contains all available scopes and grants full rights to all actions similarly to the current admin status.
- **_server_** role allows for posting activity only through the single `users:activity` scope.
```
Roles can also be customly defined and assigned to users, services, groups and tokens.
**_Users_** and **_services_** are assigned a default role if no custom role is requested based on their admin status.
**_Tokens_** roles cannot grant the token higher permissions than their owners roles. If no specific role is requested, tokens are assigned the default _user_ role.
**_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`
Roles can be defined or modified in the configuration file as a list of dictionaries. An example:
```python
c.JupyterHub.load_roles = [
{
'name': 'Server rights',
'description': 'Allows parties to start and stop user servers',
'scopes': ['users:servers', 'read:users:servers'],
'users': ['alice', 'bob'],
'services': ['idle-culler'],
'groups': ['admin-group'],
'tokens': ['foo-6f6e65','bar-74776f']
}
]
```
The role `server-rights` now allows the starting and stopping of servers by users `alice` and `bob` and the service `idle-culler`, and any member of the `admin-group` or requests using the tokens `foo-6f6e65`/`bar-74776f` (providing the tokens owner has at least the same permissions).
Another example:
```python
c.JupyterHub.load_roles.append({
'description': 'Read-only user models',
'name': 'reader',
'scopes': ['read:users'],
'services': ['external'],
'users': ['maria', 'joe']
}
)
```
The role `reader` allows users `maria` and `joe` and service `external` to read (but not modify) any users model.
```{admonition} Requirements
:class: warning
In a role definition, the `name` field is required, while all other fields are optional.
Moreover, `users`, `services`, `groups` and `tokens` only accept objects that already exist or are defined previously in the file.
It is not possible to implicitly add a new user to the database by defining a new role.
```
In case the role with a certain name already exists in the database, its definition and scopes will be overwritten. Any previously defined role bearers will remain unchanged, but newly defined requesters (in this case `maria` and `joe` and `external`) will be assigned the new role definition.

View File

@@ -0,0 +1,40 @@
# Scopes
A scope has a syntax-based design that reveals which resources it provides access to:
- The base`resource` scopes, such as `users` or `groups`, provides non-elevated 'default' read or write permissions to the resource itself and all sub-resources. For instance, the scope`users:servers` is included within the scope`users`.
- The elevated `admin:resource` scopes extend permissions beyond default. For instance, where the scope `users` provides read and write access to user information, the scope `admin:users` allows creating and deleting users.
- The scope structure `resource:subresource` vertical filtering: it provides access to a subset of the information granted by the `resource` scope. For example, the scope `users:names` provides access to user names only.
- The scope structure `read:resource` (or `read:resource:subresource`) limits permissions to read-only operations on `resource` (or `subresource`).
- The scope structure `resource!user=charlie` allows for horizontal filtering: it limits access to only return resources of user `charlie`. Only one filter per scope is allowed, but filters for the same scope have an additive effect; a larger filter can be used by supplying the scope multiple times with different filters.
- A resource can be filtered based on `user`, `server`, `group` or `service` name.
By adding a scope to an existing role, all role bearers will gain the associated permissions.
## Available scopes
[](../reference/rest-api.rst) documentation details all available scopes and which of these are required for what particular API request.
## Standard user scope
The standard user scope `all` provides access to the user's own resources and subresources, including the user's model, activity, servers and tokens. For example, the scope for a user `'gerard'` covers:
- `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.
Requested resources are filtered based on the filter of the corresponding scope. For instance, if a service requests a user list (guarded with scope `read:users`) with a role that only contains scopes `read:users!user=hannah` and `read:users!user=ivan`, the returned list of user models will be an intersection of all users and the collection `{hannah, ivan}`. In case this intersection is empty, the API call returns an HTTP 404 error, regardless if any users exist outside of the clients scope filter collection.
In case a user resource is being accessed, any scopes with *group* filters will be expanded to filters for each *user* in those groups.
## Vertical filtering
Vertical filtering, also called *attribute filtering*, is the concept of reducing the payload of an API call to cover only the *attributes* of the resources that the scopes of the client provides them access to. This occurs when the client scopes are subscopes of the API endpoint that is called.
For instance, if a client requests a user list with the only scope being `read:user:groups`, the returned list of user models will contain only a list of groups per user.
In case the client has multiple subscopes, the call returns the union of the data the client has access to.
The payload of an API call can be filtered both horizontally and vertically simultaneously. For instance, performing an API call to the endpoint `/users/` with the scope `users:names!user=juliette` returns a payload of `[{name: 'juliette'}]` (provided that this name is present in the database).

View File

@@ -0,0 +1,33 @@
# 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.
## Resolving roles and scopes
Roles and scopes are resolved on several occasions as shown in the {ref}`diagram below <checkpoint-fig>`.
```{figure} ../images/role-scope-resolution.png
:align: center
:name: checkpoint-fig
Figure 1. Checkpoints for resolving scopes in JupyterHub
```
### 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.
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.
### 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.
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 more filtering details refer to the {ref}`filtering<filtering-target>` section.

View File

@@ -0,0 +1,75 @@
# Use Cases
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
Below, different use cases are presented on how to use the RBAC framework.
## User scripting their own access
A regular user should be able to view and manage all of their own resources. This can be achieved using the scope `all` (or assigning the default _user_ role). If the user's access is to be restricted from modifying any of their resources (e.g., during marking), their role should be changed to read-only access, in this case scope `read:all`.
## 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.
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)
## API launcher
A service capable of creating/removing users and launching multiple servers should have access to:
1. POST and DELETE /users
2. POST and DELETE /users/{name}/server
3. Creating/deleting servers
From the above, the scopes required for the role are
1. `admin:users`
2. `users:servers`
3. `admin:users:servers`
If needed, the scopes can be modified to limit the associated permissions to e.g. a particular group members with `!group=groupname` filter.
## Users as group admins/Group admin roles
Roles can be used to specify different group member privileges.
For example, a group of students `class-A` may have a role allowing all group members to access information about their group. Teacher `johan`, who is a member of `class-A` and a member of another group of students `class-B`, can have additional role permitting him access information about his group members as well as start/stop their servers.
The roles can then be defined as follows:
```python
c.JupyterHub.load_groups = {
'class-A': ['johan', 'student1', 'student2'],
'class-B': ['johan', 'student3', 'student4']
}
c.JupyterHub.load_roles = [
{
'name': 'class-A-student',
'description': 'Grants access to information about the group',
'scopes': ['read:groups!group=class-A'],
'groups': ['class-A']
},
{
'name': 'class-B-student',
'description': 'Grants access to information about the group',
'scopes': ['read:groups!group=class-B'],
'groups': ['class-B']
},
{
'name': 'teacher',
'description': 'Allows for accessing information about teacher group members and starting/stopping their servers',
'scopes': ['read:users!group=class-A', 'read:users!group=class-B', 'users:servers!group=class-A', 'users:servers!group=class-B'],
'users': ['johan']
}
]
```
In the above example, `johan` has privileges inherited from class-A and class-B roles and the `teacher` role on top of those.
**Note the filters (`!group=`) limiting the priviliges only to the `class-A` and `class-B` group members.**