Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support async Authorizers #1373

Merged
merged 2 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading