Skip to content
Open
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
45 changes: 29 additions & 16 deletions libs/sdk-py/langgraph_sdk/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from __future__ import annotations

import inspect
Expand Down Expand Up @@ -392,7 +392,7 @@
def __call__(
self,
*,
resources: str | Sequence[str],
resources: str | Sequence[str] | None = None,
actions: str | Sequence[str] | None = None,
) -> Callable[
[_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]],
Expand All @@ -416,25 +416,38 @@
_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch],
]
):
if fn is not None:
_validate_handler(fn)
return typing.cast(
"_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]",
_register_handler(self.auth, self.resource, "*", fn),
)

def decorator(
def register(
handler: _ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch],
) -> _ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]:
_validate_handler(handler)
return typing.cast(
"_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]",
_register_handler(self.auth, self.resource, "*", handler),
)
if isinstance(resources, str):
resource_list = [resources]
else:
resource_list = (
list(resources) if resources is not None else [self.resource]
)
if resource_list != [self.resource]:
raise ValueError(
f"Resource-specific decorator for {self.resource!r} cannot "
f"register handlers for {resource_list!r}. Use @auth.on(...) "
"for multiple resources."
)
if isinstance(actions, str):
action_list = [actions]
else:
action_list = list(actions) if actions is not None else ["*"]
for action in action_list:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nothing validates the action strings here, so auth.on.threads(actions="raed") registers a ("threads", "raed") key that never gets looked up and the handler silently never runs. before this change a typo still landed on the "*" key so the handler at least fired; the attribute form (auth.on.threads.read) also fails loudly with AttributeError. worth checking action against the resource's known actions and raising, since the failure mode is a permissive one.

_register_handler(self.auth, self.resource, action, handler)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If action_list contains an action that already has a handler, earlier actions in the list have already been registered when _register_handler raises. That leaves auth state partially mutated even though decorator evaluation failed; can this validate all (resource, action) keys before registering any of them?

return handler

# Accept keyword-only parameters for future filtering behavior; referenced to satisfy linters.
_ = resources, actions
return decorator
if fn is not None:
return register(
typing.cast(
"_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]",
fn,
)
)
return register


class _AssistantsOn(
Expand Down
44 changes: 44 additions & 0 deletions libs/sdk-py/tests/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import pytest

from langgraph_sdk import Auth


def _handler():
async def handler(ctx, value):
return ctx is not None and value is not None

return handler


def test_resource_decorator_registers_specific_actions():
auth = Auth()

handler = auth.on.threads(actions=["read", "search"])(_handler())

assert auth._handlers == {
("threads", "read"): [handler],
("threads", "search"): [handler],
}


def test_resource_decorator_registers_single_action():
auth = Auth()

handler = auth.on.threads(actions="read")(_handler())

assert auth._handlers == {("threads", "read"): [handler]}


def test_resource_decorator_without_actions_registers_resource_wildcard():
auth = Auth()

handler = auth.on.threads(_handler())

assert auth._handlers == {("threads", "*"): [handler]}


def test_resource_decorator_rejects_mismatched_resources():
auth = Auth()

with pytest.raises(ValueError, match=r"Use @auth\.on"):
auth.on.threads(resources="assistants", actions="read")(_handler())
Loading