Skip to content

Commit

Permalink
Support async Authorizers (#1373)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zsailer authored Dec 5, 2023
1 parent 3e08300 commit 3bd347b
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 5 deletions.
4 changes: 2 additions & 2 deletions jupyter_server/auth/authorizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# Distributed under the terms of the Modified BSD License.
from __future__ import annotations

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Awaitable

from traitlets import Instance
from traitlets.config import LoggingConfigurable
Expand Down Expand Up @@ -44,7 +44,7 @@ class Authorizer(LoggingConfigurable):

def is_authorized(
self, handler: JupyterHandler, user: User, action: str, resource: str
) -> bool:
) -> Awaitable[bool] | bool:
"""A method to determine if ``user`` is authorized to perform ``action``
(read, write, or execute) on the ``resource`` type.
Expand Down
15 changes: 12 additions & 3 deletions jupyter_server/auth/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
"""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import asyncio
from functools import wraps
from typing import Any, Callable, Optional, TypeVar, Union, cast

from jupyter_core.utils import ensure_async
from tornado.log import app_log
from tornado.web import HTTPError

Expand Down Expand Up @@ -42,7 +44,7 @@ def authorized(

def wrapper(method):
@wraps(method)
def inner(self, *args, **kwargs):
async def inner(self, *args, **kwargs):
# default values for action, resource
nonlocal action
nonlocal resource
Expand All @@ -61,8 +63,15 @@ def inner(self, *args, **kwargs):
raise HTTPError(status_code=403, log_message=message)
# If the user is allowed to do this action,
# call the method.
if self.authorizer.is_authorized(self, user, action, resource):
return method(self, *args, **kwargs)
authorized = await ensure_async(
self.authorizer.is_authorized(self, user, action, resource)
)
if authorized:
out = method(self, *args, **kwargs)
# If the method is a coroutine, await it
if asyncio.iscoroutine(out):
return await out
return out
# else raise an exception.
else:
raise HTTPError(status_code=403, log_message=message)
Expand Down
48 changes: 48 additions & 0 deletions tests/auth/test_authorizer.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
"""Tests for authorization"""
import asyncio
import json
import os
from typing import Awaitable

import pytest
from jupyter_client.kernelspec import NATIVE_KERNEL_NAME
from nbformat import writes
from nbformat.v4 import new_notebook
from traitlets import Bool

from jupyter_server.auth.authorizer import Authorizer
from jupyter_server.auth.identity import User
from jupyter_server.base.handlers import JupyterHandler
from jupyter_server.services.security import csp_report_uri


Expand Down Expand Up @@ -217,3 +223,45 @@ async def test_authorized_requests(

code = await send_request(url, body=body, method=method)
assert code in expected_codes


class AsyncAuthorizerTest(Authorizer):
"""Test that an asynchronous authorizer would still work."""

called = Bool(False)

async def mock_async_fetch(self) -> True:
"""Mock an async fetch"""
# Mock a hang for a half a second.
await asyncio.sleep(0.5)
return True

async def is_authorized(
self, handler: JupyterHandler, user: User, action: str, resource: str
) -> Awaitable[bool]:
response = await self.mock_async_fetch()
self.called = True
return response


@pytest.mark.parametrize(
"jp_server_config,",
[
{
"ServerApp": {"authorizer_class": AsyncAuthorizerTest},
"jpserver_extensions": {"jupyter_server_terminals": True},
}
],
)
async def test_async_authorizer(
request,
io_loop,
send_request,
tmp_path,
jp_serverapp,
):
code = await send_request("/api/status", method="GET")
assert code == 200
# Ensure that the authorizor method finished its request.
assert hasattr(jp_serverapp.authorizer, "called")
assert jp_serverapp.authorizer.called is True

0 comments on commit 3bd347b

Please sign in to comment.