Skip to content

Commit 49dfe2e

Browse files
Zsailerdavidbrochartblink1073minrk
authored
Add authorization layer to server request handlers (#165)
* add authorization layer to request handlers * update authorized wrapper with resource * Add tests * Add documentation * Add AuthorizationManager class * Update examples/authorization/README.md * authorization: address review - "contents" applies to /view - "terminals" is plural - "server" is scope for shutdown - failed authorization is 403, not 401 - calling it Authorizer instead of AuthorizationManager - 'user' term is more broadly understood than 'subject'. Plus, it always comes from `self.current_user`. - default authorizer that allows all users is AllowAllAuthorizer * allow `@authorized` to be used with no arguments - use auth_resource on handler - use http method name for action * Structure authorization resources as a table * Move Authorizer to existing jupyter_server.auth since it's a public API packages should import, let's not nest it deep in services.auth.authorizer Co-authored-by: David Brochart <[email protected]> Co-authored-by: Steven Silvester <[email protected]> Co-authored-by: Min RK <[email protected]>
1 parent 0ab0e1e commit 49dfe2e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1355
-98
lines changed

docs/source/operators/security.rst

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,162 @@ but this is **NOT RECOMMENDED**, unless authentication or access restrictions ar
7777
c.ServerApp.token = ''
7878
c.ServerApp.password = ''
7979

80+
Authorization
81+
-------------
82+
83+
.. versionadded:: 2.0
84+
85+
Authorization in Jupyter Server serves to provide finer grained control of access to its
86+
API resources. With authentication, requests are accepted if the current user is known by
87+
the server. Thus it can restrain access to specific users, but there is no way to give allowed
88+
users more or less permissions. Jupyter Server provides a thin and extensible authorization layer
89+
which checks if the current user is authorized to make a specific request.
90+
91+
This is done by calling a ``is_authorized(handler, user, action, resource)`` method before each
92+
request handler. Each request is labeled as either a "read", "write", or "execute" ``action``:
93+
94+
- "read" wraps all ``GET`` and ``HEAD`` requests.
95+
In general, read permissions grants access to read but not modify anything about the given resource.
96+
- "write" wraps all ``POST``, ``PUT``, ``PATCH``, and ``DELETE`` requests.
97+
In general, write permissions grants access to modify the given resource.
98+
- "execute" wraps all requests to ZMQ/Websocket channels (terminals and kernels).
99+
Execute is a special permission that usually corresponds to arbitrary execution,
100+
such as via a kernel or terminal.
101+
These permissions should generally be considered sufficient to perform actions equivalent
102+
to ~all other permissions via other means.
103+
104+
The ``resource`` being accessed refers to the resource name in the Jupyter Server's API endpoints.
105+
In most cases, this is matches the field after `/api/`.
106+
For instance, values for ``resource`` in the endpoints provided by the base jupyter server package,
107+
and the corresponding permissions:
108+
109+
.. list-table::
110+
:header-rows: 1
111+
112+
* - resource
113+
- read
114+
- write
115+
- execute
116+
- endpoints
117+
118+
* - *resource name*
119+
- *what can you do with read permissions?*
120+
- *what can you do with write permissions?*
121+
- *what can you do with execute permissions, if anything?*
122+
- ``/api/...`` *what endpoints are governed by this resource?*
123+
124+
* - api
125+
- read server status (last activity, number of kernels, etc.), OpenAPI specification
126+
-
127+
-
128+
- ``/api/status``, ``/api/spec.yaml``
129+
* - csp
130+
-
131+
- report content-security-policy violations
132+
-
133+
- ``/api/security/csp-report``
134+
* - config
135+
- read frontend configuration, such as for notebook extensions
136+
- modify frontend configuration
137+
-
138+
- ``/api/config``
139+
* - contents
140+
- read files
141+
- modify files (create, modify, delete)
142+
-
143+
- ``/api/contents``, ``/view``, ``/files``
144+
* - kernels
145+
- list kernels, get status of kernels
146+
- start, stop, and restart kernels
147+
- Connect to kernel websockets, send/recv kernel messages.
148+
**This generally means arbitrary code execution,
149+
and should usually be considered equivalent to having all other permissions.**
150+
- ``/api/kernels``
151+
* - kernelspecs
152+
- read, list information about available kernels
153+
-
154+
-
155+
- ``/api/kernelspecs``
156+
* - nbconvert
157+
- render notebooks to other formats via nbconvert.
158+
**Note: depending on server-side configuration,
159+
this *could* involve execution.**
160+
-
161+
-
162+
- ``/api/nbconvert``
163+
* - server
164+
-
165+
- Shutdown the server
166+
-
167+
- ``/api/shutdown``
168+
* - sessions
169+
- list current sessions (association of documents to kernels)
170+
- create, modify, and delete existing sessions,
171+
which includes starting, stopping, and deleting kernels.
172+
-
173+
- ``/api/sessions``
174+
* - terminals
175+
- list running terminals and their last activity
176+
- start new terminals, stop running terminals
177+
- Connect to terminal websockets, execute code in a shell.
178+
**This generally means arbitrary code execution,
179+
and should usually be considered equivalent to having all other permissions.**
180+
- ``/api/terminals``
181+
182+
183+
Extensions may define their own resources.
184+
Extension resources should start with ``extension_name:`` to avoid namespace conflicts.
185+
186+
If ``is_authorized(...)`` returns ``True``, the request is made; otherwise, a
187+
``HTTPError(403)`` (403 means "Forbidden") error is raised, and the request is blocked.
188+
189+
By default, authorization is turned off—i.e. ``is_authorized()`` always returns ``True`` and
190+
all authenticated users are allowed to make all types of requests. To turn-on authorization, pass
191+
a class that inherits from ``Authorizer`` to the ``ServerApp.authorizer_class``
192+
parameter, implementing a ``is_authorized()`` method with your desired authorization logic, as
193+
follows:
194+
195+
.. sourcecode:: python
196+
197+
from jupyter_server.auth import Authorizer
198+
199+
class MyAuthorizationManager(Authorizer):
200+
"""Class for authorizing access to resources in the Jupyter Server.
201+
202+
All authorizers used in Jupyter Server should inherit from
203+
AuthorizationManager and, at the very minimum, override and implement
204+
an `is_authorized` method with the following signature.
205+
206+
The `is_authorized` method is called by the `@authorized` decorator in
207+
JupyterHandler. If it returns True, the incoming request to the server
208+
is accepted; if it returns False, the server returns a 403 (Forbidden) error code.
209+
"""
210+
211+
def is_authorized(self, handler: JupyterHandler, user: Any, action: str, resource: str) -> bool:
212+
"""A method to determine if `user` is authorized to perform `action`
213+
(read, write, or execute) on the `resource` type.
214+
215+
Parameters
216+
------------
217+
user : usually a dict or string
218+
A truthy model representing the authenticated user.
219+
A username string by default,
220+
but usually a dict when integrating with an auth provider.
221+
222+
action : str
223+
the category of action for the current request: read, write, or execute.
224+
225+
resource : str
226+
the type of resource (i.e. contents, kernels, files, etc.) the user is requesting.
227+
228+
Returns True if user authorized to make request; otherwise, returns False.
229+
"""
230+
return True # implement your authorization logic here
231+
232+
The ``is_authorized()`` method will automatically be called whenever a handler is decorated with
233+
``@authorized`` (from ``jupyter_server.auth``), similarly to the
234+
``@authenticated`` decorator for authorization (from ``tornado.web``).
235+
80236
Security in notebook documents
81237
==============================
82238

examples/authorization/README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Authorization in a simple Jupyter Notebook Server
2+
3+
This folder contains the following examples:
4+
5+
1. a "read-only" Jupyter Notebook Server
6+
2. a read/write Server without the ability to execute code on kernels.
7+
3. a "temporary notebook server", i.e. read and execute notebooks but cannot save/write files.
8+
9+
## How does it work?
10+
11+
To add a custom authorization system to the Jupyter Server, you will need to write your own `Authorizer` subclass and pass it to Jupyter's configuration system (i.e. by file or CLI).
12+
13+
The examples below demonstrate some basic implementations of an `Authorizer`.
14+
15+
```python
16+
from jupyter_server.auth import Authorizer
17+
18+
19+
class MyCustomAuthorizer(Authorizer):
20+
"""Custom authorization manager."""
21+
22+
# Define my own method here for handling authorization.
23+
# The argument signature must have `self`, `handler`, `user`, `action`, and `resource`.
24+
def is_authorized(self, handler, user, action, resource):
25+
"""My override for handling authorization in Jupyter services."""
26+
27+
# Add logic here to check if user is allowed.
28+
# For example, here is an example of a read-only server
29+
if action != "read":
30+
return False
31+
32+
return True
33+
34+
# Pass this custom class to Jupyter Server
35+
c.ServerApp.authorizer_class = MyCustomAuthorizer
36+
```
37+
38+
In the `jupyter_nbclassic_readonly_config.py`
39+
40+
## Try it out!
41+
42+
### Read-only example
43+
44+
1. Install nbclassic using `pip`.
45+
46+
pip install nbclassic
47+
48+
2. Navigate to the jupyter_authorized_server `examples/` folder.
49+
50+
3. Launch nbclassic and load `jupyter_nbclassic_readonly_config.py`:
51+
52+
jupyter nbclassic --config=jupyter_nbclassic_readonly_config.py
53+
54+
4. Try creating a notebook, running a notebook in a cell, etc. You should see a `403: Forbidden` error.
55+
56+
### Read+Write example
57+
58+
1. Install nbclassic using `pip`.
59+
60+
pip install nbclassic
61+
62+
2. Navigate to the jupyter_authorized_server `examples/` folder.
63+
64+
3. Launch nbclassic and load `jupyter_nbclassic_rw_config.py`:
65+
66+
jupyter nbclassic --config=jupyter_nbclassic_rw_config.py
67+
68+
4. Try running a cell in a notebook. You should see a `403: Forbidden` error.
69+
70+
### Temporary notebook server example
71+
72+
This configuration allows everything except saving files.
73+
74+
1. Install nbclassic using `pip`.
75+
76+
pip install nbclassic
77+
78+
2. Navigate to the jupyter_authorized_server `examples/` folder.
79+
80+
3. Launch nbclassic and load `jupyter_temporary_config.py`:
81+
82+
jupyter nbclassic --config=jupyter_temporary_config.py
83+
84+
4. Edit a notebook, run a cell, etc. Everything works fine. Then try to save your changes... you should see a `403: Forbidden` error.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from jupyter_server.auth import Authorizer
2+
3+
4+
class ReadOnly(Authorizer):
5+
"""Authorizer that makes Jupyter Server a read-only server."""
6+
7+
def is_authorized(self, handler, user, action, resource):
8+
"""Only allows `read` operations."""
9+
if action != "read":
10+
return False
11+
return True
12+
13+
14+
c.ServerApp.authorizer_class = ReadOnly
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from jupyter_server.auth import Authorizer
2+
3+
4+
class ReadWriteOnly(Authorizer):
5+
"""Authorizer class that makes Jupyter Server a read/write-only server."""
6+
7+
def is_authorized(self, handler, user, action, resource):
8+
"""Only allows `read` and `write` operations."""
9+
if action not in {"read", "write"}:
10+
return False
11+
return True
12+
13+
14+
c.ServerApp.authorizer_class = ReadWriteOnly
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from jupyter_server.auth import Authorizer
2+
3+
4+
class TemporaryServerPersonality(Authorizer):
5+
"""Authorizer that prevents modifying files via the contents service"""
6+
7+
def is_authorized(self, handler, user, action, resource):
8+
"""Allow everything but write on contents"""
9+
if action == "write" and resource == "contents":
10+
return False
11+
return True
12+
13+
14+
c.ServerApp.authorizer_class = TemporaryServerPersonality

jupyter_server/auth/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
from .authorizer import * # noqa
2+
from .decorator import authorized # noqa
13
from .security import passwd # noqa

jupyter_server/auth/authorizer.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""An Authorizer for use in the Jupyter server.
2+
3+
The default authorizer (AllowAllAuthorizer)
4+
allows all authenticated requests
5+
6+
.. versionadded:: 2.0
7+
"""
8+
# Copyright (c) Jupyter Development Team.
9+
# Distributed under the terms of the Modified BSD License.
10+
from traitlets.config import LoggingConfigurable
11+
12+
from jupyter_server.base.handlers import JupyterHandler
13+
14+
15+
class Authorizer(LoggingConfigurable):
16+
"""Base class for authorizing access to resources
17+
in the Jupyter Server.
18+
19+
All authorizers used in Jupyter Server
20+
should inherit from this base class and, at the very minimum,
21+
implement an `is_authorized` method with the
22+
same signature as in this base class.
23+
24+
The `is_authorized` method is called by the `@authorized` decorator
25+
in JupyterHandler. If it returns True, the incoming request
26+
to the server is accepted; if it returns False, the server
27+
returns a 403 (Forbidden) error code.
28+
29+
The authorization check will only be applied to requests
30+
that have already been authenticated.
31+
32+
.. versionadded:: 2.0
33+
"""
34+
35+
def is_authorized(self, handler: JupyterHandler, user: str, action: str, resource: str) -> bool:
36+
"""A method to determine if `user` is authorized to perform `action`
37+
(read, write, or execute) on the `resource` type.
38+
39+
Parameters
40+
----------
41+
user : usually a dict or string
42+
A truthy model representing the authenticated user.
43+
A username string by default,
44+
but usually a dict when integrating with an auth provider.
45+
action : str
46+
the category of action for the current request: read, write, or execute.
47+
48+
resource : str
49+
the type of resource (i.e. contents, kernels, files, etc.) the user is requesting.
50+
51+
Returns True if user authorized to make request; otherwise, returns False.
52+
"""
53+
raise NotImplementedError()
54+
55+
56+
class AllowAllAuthorizer(Authorizer):
57+
"""A no-op implementation of the Authorizer
58+
59+
This authorizer allows all authenticated requests.
60+
61+
.. versionadded:: 2.0
62+
"""
63+
64+
def is_authorized(self, handler: JupyterHandler, user: str, action: str, resource: str) -> bool:
65+
"""This method always returns True.
66+
67+
All authenticated users are allowed to do anything in the Jupyter Server.
68+
"""
69+
return True

0 commit comments

Comments
 (0)