Initial Commit
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
Session scoping for experimental task identifiers.
|
||||
|
||||
Task IDs generated by `run_task()` embed an opaque, per-session marker (the
|
||||
"session scope") so that the default task handlers can tell which session
|
||||
created a task. The default handlers for tasks/get, tasks/result, tasks/list,
|
||||
and tasks/cancel only operate on tasks created by the requesting session.
|
||||
|
||||
Task IDs without a session scope (explicitly provided IDs, IDs created
|
||||
directly through a TaskStore, or IDs created in stateless mode) have no known
|
||||
creator. They can be used with tasks/get, tasks/result, and tasks/cancel from
|
||||
any session - possession of the ID is what grants access - but they are never
|
||||
included in tasks/list responses.
|
||||
|
||||
WARNING: These APIs are experimental and may change without notice.
|
||||
"""
|
||||
|
||||
import re
|
||||
from uuid import uuid4
|
||||
|
||||
__all__ = [
|
||||
"new_session_scope",
|
||||
"scoped_task_id",
|
||||
"session_scope_of",
|
||||
"task_in_session_scope",
|
||||
"task_listable_in_session_scope",
|
||||
]
|
||||
|
||||
# A scoped task ID has the form "<32 hex chars>:<uuid4>". Both halves must
|
||||
# match exactly so that explicitly chosen task IDs are never mistaken for
|
||||
# scoped ones. \Z rather than $ so a trailing newline cannot match.
|
||||
_SCOPED_TASK_ID = re.compile(
|
||||
r"\A(?P<scope>[0-9a-f]{32}):"
|
||||
r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\Z"
|
||||
)
|
||||
|
||||
|
||||
def new_session_scope() -> str:
|
||||
"""Create a new opaque session scope token."""
|
||||
return uuid4().hex
|
||||
|
||||
|
||||
def scoped_task_id(session_scope: str) -> str:
|
||||
"""Generate a task ID associated with the given session scope."""
|
||||
return f"{session_scope}:{uuid4()}"
|
||||
|
||||
|
||||
def session_scope_of(task_id: str) -> str | None:
|
||||
"""Return the session scope embedded in a task ID, or None if it has none."""
|
||||
match = _SCOPED_TASK_ID.match(task_id)
|
||||
return match.group("scope") if match else None
|
||||
|
||||
|
||||
def task_in_session_scope(task_id: str, session_scope: str | None) -> bool:
|
||||
"""Whether a task may be used by a requestor with the given session scope.
|
||||
|
||||
Used by tasks/get, tasks/result, and tasks/cancel. A task whose ID carries
|
||||
no session scope has no known creator, so possession of the ID is what
|
||||
grants access to it: it can be used from any session.
|
||||
"""
|
||||
embedded = session_scope_of(task_id)
|
||||
return embedded is None or embedded == session_scope
|
||||
|
||||
|
||||
def task_listable_in_session_scope(task_id: str, session_scope: str | None) -> bool:
|
||||
"""Whether a task may be included in a tasks/list response for the given session scope.
|
||||
|
||||
Used by tasks/list. Listing is stricter than access by ID: a task is only
|
||||
listed to the session that created it. Tasks with no session scope are
|
||||
never listed because they have no known creator, and requestors with no
|
||||
session scope are never shown any tasks because the server cannot tell
|
||||
them apart.
|
||||
"""
|
||||
embedded = session_scope_of(task_id)
|
||||
return embedded is not None and embedded == session_scope
|
||||
Reference in New Issue
Block a user