Skip to content
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
2 changes: 1 addition & 1 deletion ATTRIBUTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6684,7 +6684,7 @@ multidict implementation

```

## nicegui (3.9.0) - UNKNOWN
## nicegui (3.10.0) - UNKNOWN

Create web-based user interfaces with Python. The nice way.

Expand Down
79 changes: 54 additions & 25 deletions src/aignostics_foundry_core/gui/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def dashboard(user: dict) -> None:
import contextlib
import inspect
import time
from collections.abc import Awaitable, Callable
from collections.abc import Awaitable, Callable, Generator
from dataclasses import dataclass
from enum import StrEnum
from typing import Any
Expand All @@ -51,6 +51,7 @@ def dashboard(user: dict) -> None:
from loguru import logger

from aignostics_foundry_core.api.auth import AUTH0_ROLE_ADMIN, AuthSettings, get_auth_client
from aignostics_foundry_core.foundry import get_context
from aignostics_foundry_core.sentry import set_sentry_user
from aignostics_foundry_core.settings import load_settings

Expand Down Expand Up @@ -81,7 +82,7 @@ class _PageEntry:

access: AccessLevel
path: str
title: str
title: str | None # None → resolved at request time from get_context().name.title()
func: Callable[..., Any]


Expand Down Expand Up @@ -218,9 +219,22 @@ async def require_gui_user(request: Request, return_to: str | None = None) -> di
# ---------------------------------------------------------------------------


@contextlib.contextmanager
def _frame_context(
frame_func: FrameFunc,
title: str,
user: dict[str, Any] | None,
) -> Generator[None, None, None]:
if frame_func is not None:
with frame_func(title, user=user):
yield
else:
yield


def _actualize_public(
path: str,
title: str = "",
title: str | None = None,
frame_func: FrameFunc = None,
) -> Callable[..., Callable[[Request], Awaitable[None]]]:
"""Register a public NiceGUI page immediately with the given frame_func.
Expand All @@ -235,8 +249,9 @@ def decorator(
) -> Callable[[Request], Awaitable[None]]:
@ui.page(path, response_timeout=RESPONSE_TIMEOUT)
async def wrapper(request: Request) -> None:
resolved_title = title if title is not None else get_context().name.title()
user = await get_gui_user(request)
with frame_func(title, user=user) if frame_func is not None else contextlib.nullcontext():
with _frame_context(frame_func, resolved_title, user):
await _invoke_page_func(func, user)

wrapper.__name__ = func.__name__
Expand All @@ -250,7 +265,7 @@ async def wrapper(request: Request) -> None:

def _actualize_authenticated(
path: str,
title: str = "",
title: str | None = None,
frame_func: FrameFunc = None,
) -> Callable[..., Callable[[Request], Awaitable[None]]]:
"""Register an authenticated NiceGUI page immediately with the given frame_func.
Expand All @@ -265,10 +280,11 @@ def decorator(
) -> Callable[[Request], Awaitable[None]]:
@ui.page(path, response_timeout=RESPONSE_TIMEOUT)
async def wrapper(request: Request) -> None:
resolved_title = title if title is not None else get_context().name.title()
user = await require_gui_user(request)
if not user:
return
with frame_func(title, user=user) if frame_func is not None else contextlib.nullcontext():
with _frame_context(frame_func, resolved_title, user):
await _invoke_page_func(func, user)

wrapper.__name__ = func.__name__
Expand All @@ -282,7 +298,7 @@ async def wrapper(request: Request) -> None:

def _actualize_admin(
path: str,
title: str = "",
title: str | None = None,
frame_func: FrameFunc = None,
) -> Callable[..., Callable[[Request], Awaitable[None]]]:
"""Register an admin-only NiceGUI page immediately with the given frame_func.
Expand All @@ -297,18 +313,19 @@ def decorator(
) -> Callable[[Request], Awaitable[None]]:
@ui.page(path, response_timeout=RESPONSE_TIMEOUT)
async def wrapper(request: Request) -> None:
resolved_title = title if title is not None else get_context().name.title()
user = await require_gui_user(request)
if not user:
return

auth_settings = load_settings(AuthSettings)
role = user.get(auth_settings.auth0_role_claim)
if role != AUTH0_ROLE_ADMIN:
with frame_func(title, user=user) if frame_func is not None else contextlib.nullcontext():
with _frame_context(frame_func, resolved_title, user):
ui.label(f"{MSG_403_FORBIDDEN} - Admin access required").classes(CLASS_FORBIDDEN_ERROR)
return

with frame_func(title, user=user) if frame_func is not None else contextlib.nullcontext():
with _frame_context(frame_func, resolved_title, user):
await _invoke_page_func(func, user)

wrapper.__name__ = func.__name__
Expand All @@ -322,7 +339,7 @@ async def wrapper(request: Request) -> None:

def _actualize_internal(
path: str,
title: str = "",
title: str | None = None,
frame_func: FrameFunc = None,
) -> Callable[..., Callable[[Request], Awaitable[None]]]:
"""Register an internal-org-only NiceGUI page immediately with the given frame_func.
Expand All @@ -337,18 +354,19 @@ def decorator(
) -> Callable[[Request], Awaitable[None]]:
@ui.page(path, response_timeout=RESPONSE_TIMEOUT)
async def wrapper(request: Request) -> None:
resolved_title = title if title is not None else get_context().name.title()
user = await require_gui_user(request)
if not user:
return

auth_settings = load_settings(AuthSettings)
org_id = user.get("org_id")
if org_id != auth_settings.internal_org_id:
with frame_func(title, user=user) if frame_func is not None else contextlib.nullcontext():
with _frame_context(frame_func, resolved_title, user):
ui.label(f"{MSG_403_FORBIDDEN} - Internal access required").classes(CLASS_FORBIDDEN_ERROR)
return

with frame_func(title, user=user) if frame_func is not None else contextlib.nullcontext():
with _frame_context(frame_func, resolved_title, user):
await _invoke_page_func(func, user)

wrapper.__name__ = func.__name__
Expand All @@ -362,7 +380,7 @@ async def wrapper(request: Request) -> None:

def _actualize_internal_admin(
path: str,
title: str = "",
title: str | None = None,
frame_func: FrameFunc = None,
) -> Callable[..., Callable[[Request], Awaitable[None]]]:
"""Register an internal-org admin-only NiceGUI page immediately with the given frame_func.
Expand All @@ -377,6 +395,7 @@ def decorator(
) -> Callable[[Request], Awaitable[None]]:
@ui.page(path, response_timeout=RESPONSE_TIMEOUT)
async def wrapper(request: Request) -> None:
resolved_title = title if title is not None else get_context().name.title()
user = await require_gui_user(request)
if not user:
return
Expand All @@ -386,11 +405,11 @@ async def wrapper(request: Request) -> None:
role = user.get(auth_settings.auth0_role_claim)

if org_id != auth_settings.internal_org_id or role != AUTH0_ROLE_ADMIN:
with frame_func(title, user=user) if frame_func is not None else contextlib.nullcontext():
with _frame_context(frame_func, resolved_title, user):
ui.label(f"{MSG_403_FORBIDDEN} - Internal admin access required").classes(CLASS_FORBIDDEN_ERROR)
return

with frame_func(title, user=user) if frame_func is not None else contextlib.nullcontext():
with _frame_context(frame_func, resolved_title, user):
await _invoke_page_func(func, user)

wrapper.__name__ = func.__name__
Expand All @@ -408,7 +427,7 @@ async def wrapper(request: Request) -> None:
# ---------------------------------------------------------------------------


def page_public(path: str, title: str = "") -> Callable[..., Any]:
def page_public(path: str, title: str | None = None) -> Callable[..., Any]:
"""Decorator that registers a public page in the global registry.

The route is NOT registered with NiceGUI immediately. Call
Expand All @@ -418,6 +437,7 @@ def page_public(path: str, title: str = "") -> Callable[..., Any]:
Args:
path: The URL path for the page.
title: The title passed to the frame function when the page is actualized.
Defaults to the project name from ``get_context()`` when omitted.

Returns:
A decorator that records the page function in the registry and returns
Expand All @@ -437,7 +457,7 @@ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
return decorator


def page_authenticated(path: str, title: str = "") -> Callable[..., Any]:
def page_authenticated(path: str, title: str | None = None) -> Callable[..., Any]:
"""Decorator that registers an authenticated page in the global registry.

The route is NOT registered with NiceGUI immediately. Call
Expand All @@ -447,6 +467,7 @@ def page_authenticated(path: str, title: str = "") -> Callable[..., Any]:
Args:
path: The URL path for the page.
title: The title passed to the frame function when the page is actualized.
Defaults to the project name from ``get_context()`` when omitted.

Returns:
A decorator that records the page function in the registry and returns
Expand All @@ -466,7 +487,7 @@ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
return decorator


def page_admin(path: str, title: str = "") -> Callable[..., Any]:
def page_admin(path: str, title: str | None = None) -> Callable[..., Any]:
"""Decorator that registers an admin-only page in the global registry.

The route is NOT registered with NiceGUI immediately. Call
Expand All @@ -476,6 +497,7 @@ def page_admin(path: str, title: str = "") -> Callable[..., Any]:
Args:
path: The URL path for the page.
title: The title passed to the frame function when the page is actualized.
Defaults to the project name from ``get_context()`` when omitted.

Returns:
A decorator that records the page function in the registry and returns
Expand All @@ -495,7 +517,7 @@ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
return decorator


def page_internal(path: str, title: str = "") -> Callable[..., Any]:
def page_internal(path: str, title: str | None = None) -> Callable[..., Any]:
"""Decorator that registers an internal-org-only page in the global registry.

The route is NOT registered with NiceGUI immediately. Call
Expand All @@ -505,6 +527,7 @@ def page_internal(path: str, title: str = "") -> Callable[..., Any]:
Args:
path: The URL path for the page.
title: The title passed to the frame function when the page is actualized.
Defaults to the project name from ``get_context()`` when omitted.

Returns:
A decorator that records the page function in the registry and returns
Expand All @@ -524,7 +547,7 @@ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
return decorator


def page_internal_admin(path: str, title: str = "") -> Callable[..., Any]:
def page_internal_admin(path: str, title: str | None = None) -> Callable[..., Any]:
"""Decorator that registers an internal-org admin-only page in the global registry.

The route is NOT registered with NiceGUI immediately. Call
Expand All @@ -534,6 +557,7 @@ def page_internal_admin(path: str, title: str = "") -> Callable[..., Any]:
Args:
path: The URL path for the page.
title: The title passed to the frame function when the page is actualized.
Defaults to the project name from ``get_context()`` when omitted.

Returns:
A decorator that records the page function in the registry and returns
Expand Down Expand Up @@ -596,7 +620,7 @@ def __init__(self, frame_func: FrameFunc = None) -> None:
def public(
self,
path: str,
title: str = "",
title: str | None = None,
) -> Callable[..., Callable[[Request], Awaitable[None]]]:
"""Decorator for public NiceGUI pages.

Expand All @@ -606,6 +630,7 @@ def public(
Args:
path: The URL path for the page.
title: The title passed to the frame function (if configured).
Defaults to the project name from ``get_context()`` when omitted.

Returns:
A decorator that wraps the page function.
Expand All @@ -615,7 +640,7 @@ def public(
def authenticated(
self,
path: str,
title: str = "",
title: str | None = None,
) -> Callable[..., Callable[[Request], Awaitable[None]]]:
"""Decorator for authenticated NiceGUI pages.

Expand All @@ -625,6 +650,7 @@ def authenticated(
Args:
path: The URL path for the page.
title: The title passed to the frame function (if configured).
Defaults to the project name from ``get_context()`` when omitted.

Returns:
A decorator that wraps the page function.
Expand All @@ -634,7 +660,7 @@ def authenticated(
def admin(
self,
path: str,
title: str = "",
title: str | None = None,
) -> Callable[..., Callable[[Request], Awaitable[None]]]:
"""Decorator for admin-only NiceGUI pages.

Expand All @@ -644,6 +670,7 @@ def admin(
Args:
path: The URL path for the page.
title: The title passed to the frame function (if configured).
Defaults to the project name from ``get_context()`` when omitted.

Returns:
A decorator that wraps the page function.
Expand All @@ -653,7 +680,7 @@ def admin(
def internal(
self,
path: str,
title: str = "",
title: str | None = None,
) -> Callable[..., Callable[[Request], Awaitable[None]]]:
"""Decorator for internal-org-only NiceGUI pages.

Expand All @@ -663,6 +690,7 @@ def internal(
Args:
path: The URL path for the page.
title: The title passed to the frame function (if configured).
Defaults to the project name from ``get_context()`` when omitted.

Returns:
A decorator that wraps the page function.
Expand All @@ -672,7 +700,7 @@ def internal(
def internal_admin(
self,
path: str,
title: str = "",
title: str | None = None,
) -> Callable[..., Callable[[Request], Awaitable[None]]]:
"""Decorator for internal-org admin-only NiceGUI pages.

Expand All @@ -682,6 +710,7 @@ def internal_admin(
Args:
path: The URL path for the page.
title: The title passed to the frame function (if configured).
Defaults to the project name from ``get_context()`` when omitted.

Returns:
A decorator that wraps the page function.
Expand Down
4 changes: 0 additions & 4 deletions tests/aignostics_foundry_core/api/auth_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@
require_internal,
require_internal_admin,
)
from aignostics_foundry_core.foundry import reset_context, set_context
from tests.aignostics_foundry_core.api import AUTH0_ROLE_CLAIM_VAR_NAME, INTERNAL_ORG_ID_VAR_NAME
from tests.conftest import make_context

_INTERNAL_ORG_ID = "org_internal_123"
_OTHER_ORG_ID = "org_other_456"
Expand All @@ -38,13 +36,11 @@ def _auth_context() -> Generator[None, None, None]: # pyright: ignore[reportUnu
Yields:
None
"""
set_context(make_context())
os.environ[INTERNAL_ORG_ID_VAR_NAME] = _INTERNAL_ORG_ID
os.environ[AUTH0_ROLE_CLAIM_VAR_NAME] = _TEST_ROLE_CLAIM
yield
os.environ.pop(INTERNAL_ORG_ID_VAR_NAME, None)
os.environ.pop(AUTH0_ROLE_CLAIM_VAR_NAME, None)
reset_context()


@pytest.mark.unit
Expand Down
Loading
Loading