-
Notifications
You must be signed in to change notification settings - Fork 312
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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]>
- Loading branch information
1 parent
0ab0e1e
commit 49dfe2e
Showing
53 changed files
with
1,355 additions
and
98 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
# Authorization in a simple Jupyter Notebook Server | ||
|
||
This folder contains the following examples: | ||
|
||
1. a "read-only" Jupyter Notebook Server | ||
2. a read/write Server without the ability to execute code on kernels. | ||
3. a "temporary notebook server", i.e. read and execute notebooks but cannot save/write files. | ||
|
||
## How does it work? | ||
|
||
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). | ||
|
||
The examples below demonstrate some basic implementations of an `Authorizer`. | ||
|
||
```python | ||
from jupyter_server.auth import Authorizer | ||
|
||
|
||
class MyCustomAuthorizer(Authorizer): | ||
"""Custom authorization manager.""" | ||
|
||
# Define my own method here for handling authorization. | ||
# The argument signature must have `self`, `handler`, `user`, `action`, and `resource`. | ||
def is_authorized(self, handler, user, action, resource): | ||
"""My override for handling authorization in Jupyter services.""" | ||
|
||
# Add logic here to check if user is allowed. | ||
# For example, here is an example of a read-only server | ||
if action != "read": | ||
return False | ||
|
||
return True | ||
|
||
# Pass this custom class to Jupyter Server | ||
c.ServerApp.authorizer_class = MyCustomAuthorizer | ||
``` | ||
|
||
In the `jupyter_nbclassic_readonly_config.py` | ||
|
||
## Try it out! | ||
|
||
### Read-only example | ||
|
||
1. Install nbclassic using `pip`. | ||
|
||
pip install nbclassic | ||
|
||
2. Navigate to the jupyter_authorized_server `examples/` folder. | ||
|
||
3. Launch nbclassic and load `jupyter_nbclassic_readonly_config.py`: | ||
|
||
jupyter nbclassic --config=jupyter_nbclassic_readonly_config.py | ||
|
||
4. Try creating a notebook, running a notebook in a cell, etc. You should see a `403: Forbidden` error. | ||
|
||
### Read+Write example | ||
|
||
1. Install nbclassic using `pip`. | ||
|
||
pip install nbclassic | ||
|
||
2. Navigate to the jupyter_authorized_server `examples/` folder. | ||
|
||
3. Launch nbclassic and load `jupyter_nbclassic_rw_config.py`: | ||
|
||
jupyter nbclassic --config=jupyter_nbclassic_rw_config.py | ||
|
||
4. Try running a cell in a notebook. You should see a `403: Forbidden` error. | ||
|
||
### Temporary notebook server example | ||
|
||
This configuration allows everything except saving files. | ||
|
||
1. Install nbclassic using `pip`. | ||
|
||
pip install nbclassic | ||
|
||
2. Navigate to the jupyter_authorized_server `examples/` folder. | ||
|
||
3. Launch nbclassic and load `jupyter_temporary_config.py`: | ||
|
||
jupyter nbclassic --config=jupyter_temporary_config.py | ||
|
||
4. Edit a notebook, run a cell, etc. Everything works fine. Then try to save your changes... you should see a `403: Forbidden` error. |
14 changes: 14 additions & 0 deletions
14
examples/authorization/jupyter_nbclassic_readonly_config.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
from jupyter_server.auth import Authorizer | ||
|
||
|
||
class ReadOnly(Authorizer): | ||
"""Authorizer that makes Jupyter Server a read-only server.""" | ||
|
||
def is_authorized(self, handler, user, action, resource): | ||
"""Only allows `read` operations.""" | ||
if action != "read": | ||
return False | ||
return True | ||
|
||
|
||
c.ServerApp.authorizer_class = ReadOnly |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
from jupyter_server.auth import Authorizer | ||
|
||
|
||
class ReadWriteOnly(Authorizer): | ||
"""Authorizer class that makes Jupyter Server a read/write-only server.""" | ||
|
||
def is_authorized(self, handler, user, action, resource): | ||
"""Only allows `read` and `write` operations.""" | ||
if action not in {"read", "write"}: | ||
return False | ||
return True | ||
|
||
|
||
c.ServerApp.authorizer_class = ReadWriteOnly |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
from jupyter_server.auth import Authorizer | ||
|
||
|
||
class TemporaryServerPersonality(Authorizer): | ||
"""Authorizer that prevents modifying files via the contents service""" | ||
|
||
def is_authorized(self, handler, user, action, resource): | ||
"""Allow everything but write on contents""" | ||
if action == "write" and resource == "contents": | ||
return False | ||
return True | ||
|
||
|
||
c.ServerApp.authorizer_class = TemporaryServerPersonality |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
from .authorizer import * # noqa | ||
from .decorator import authorized # noqa | ||
from .security import passwd # noqa |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
"""An Authorizer for use in the Jupyter server. | ||
The default authorizer (AllowAllAuthorizer) | ||
allows all authenticated requests | ||
.. versionadded:: 2.0 | ||
""" | ||
# Copyright (c) Jupyter Development Team. | ||
# Distributed under the terms of the Modified BSD License. | ||
from traitlets.config import LoggingConfigurable | ||
|
||
from jupyter_server.base.handlers import JupyterHandler | ||
|
||
|
||
class Authorizer(LoggingConfigurable): | ||
"""Base class for authorizing access to resources | ||
in the Jupyter Server. | ||
All authorizers used in Jupyter Server | ||
should inherit from this base class and, at the very minimum, | ||
implement an `is_authorized` method with the | ||
same signature as in this base class. | ||
The `is_authorized` method is called by the `@authorized` decorator | ||
in JupyterHandler. If it returns True, the incoming request | ||
to the server is accepted; if it returns False, the server | ||
returns a 403 (Forbidden) error code. | ||
The authorization check will only be applied to requests | ||
that have already been authenticated. | ||
.. versionadded:: 2.0 | ||
""" | ||
|
||
def is_authorized(self, handler: JupyterHandler, user: str, action: str, resource: str) -> bool: | ||
"""A method to determine if `user` is authorized to perform `action` | ||
(read, write, or execute) on the `resource` type. | ||
Parameters | ||
---------- | ||
user : usually a dict or string | ||
A truthy model representing the authenticated user. | ||
A username string by default, | ||
but usually a dict when integrating with an auth provider. | ||
action : str | ||
the category of action for the current request: read, write, or execute. | ||
resource : str | ||
the type of resource (i.e. contents, kernels, files, etc.) the user is requesting. | ||
Returns True if user authorized to make request; otherwise, returns False. | ||
""" | ||
raise NotImplementedError() | ||
|
||
|
||
class AllowAllAuthorizer(Authorizer): | ||
"""A no-op implementation of the Authorizer | ||
This authorizer allows all authenticated requests. | ||
.. versionadded:: 2.0 | ||
""" | ||
|
||
def is_authorized(self, handler: JupyterHandler, user: str, action: str, resource: str) -> bool: | ||
"""This method always returns True. | ||
All authenticated users are allowed to do anything in the Jupyter Server. | ||
""" | ||
return True |
Oops, something went wrong.