Skip to content
Draft
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
58 changes: 58 additions & 0 deletions plugins/ui/src/deephaven/ui/hooks/_transform.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from __future__ import annotations

from typing import Any, Optional, TypeVar, overload

from deephaven.execution_context import get_exec_ctx
from deephaven.plugin_authorization import transform as _plugin_transform # type: ignore[import-not-found]

T = TypeVar("T")


def _current_user_context() -> str:
"""
Return a string describing the current user's authorization context, for use in error messages.

Returns:
A description of the current authorization context, or "unknown" if it cannot be determined.
"""
try:
return str(get_exec_ctx().j_exec_ctx.getAuthContext())
except Exception:
return "unknown"


@overload
def transform(obj: None) -> None:
...


@overload
def transform(obj: T) -> T:
...


def transform(obj: Optional[Any]) -> Optional[Any]:
"""
Apply the server's authorization transform to ``obj`` for the current user's context.

Unlike :func:`deephaven.plugin_authorization.transform`, this raises a clear error if the current user is not
permitted to access a non-``None`` object, rather than silently returning ``None`` (which would otherwise surface
later as a confusing ``AttributeError`` when the ``None`` result is used).

Args:
obj: The object to transform (typically a table). ``None`` is returned unchanged.

Returns:
The transformed object.

Raises:
PermissionError: if the current user is not permitted to access ``obj``.
"""
if obj is None:
return None
result = _plugin_transform(obj)
if result is None:
raise PermissionError(
f"The current user ({_current_user_context()}) is not permitted to access the requested object."
)
return result
9 changes: 6 additions & 3 deletions plugins/ui/src/deephaven/ui/hooks/use_cell_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@

from deephaven.table import Table

from ._transform import transform
from .use_memo import use_memo
from .use_table_data import first_column_table, use_table_data
from .use_table_data import first_column_table, _use_table_data_without_ticket_transform
from ..types import Sentinel


Expand Down Expand Up @@ -42,7 +43,9 @@ def use_cell_data(table: Table | None, sentinel: Sentinel = None) -> Any | Senti
Any: The top left cell of the table.
"""
filtered_table = use_memo(
lambda: None if table is None else first_column_table(table).head(1),
lambda: None if table is None else first_column_table(transform(table)).head(1),
[table],
)
return use_table_data(filtered_table, sentinel, _cell_data)
return _use_table_data_without_ticket_transform(
filtered_table, sentinel, _cell_data
)
9 changes: 6 additions & 3 deletions plugins/ui/src/deephaven/ui/hooks/use_column_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

from deephaven.table import Table

from ._transform import transform
from .use_memo import use_memo
from .use_table_data import first_column_table, use_table_data
from .use_table_data import first_column_table, _use_table_data_without_ticket_transform
from ..types import Sentinel, ColumnData


Expand Down Expand Up @@ -43,7 +44,9 @@ def use_column_data(
The first column of the table as a list or the sentinel value.
"""
filtered_table = use_memo(
lambda: None if table is None else first_column_table(table),
lambda: None if table is None else first_column_table(transform(table)),
[table],
)
return use_table_data(filtered_table, sentinel, _column_data)
return _use_table_data_without_ticket_transform(
filtered_table, sentinel, _column_data
)
9 changes: 6 additions & 3 deletions plugins/ui/src/deephaven/ui/hooks/use_row_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

from deephaven.table import Table

from ._transform import transform
from .use_memo import use_memo
from .use_table_data import use_table_data
from .use_table_data import _use_table_data_without_ticket_transform
from ..types import Sentinel, RowData


Expand Down Expand Up @@ -42,5 +43,7 @@ def use_row_data(
Returns:
The first row of the table as a dictionary or the sentinel value.
"""
filtered_table = use_memo(lambda: None if table is None else table.head(1), [table])
return use_table_data(filtered_table, sentinel, _row_data)
filtered_table = use_memo(
lambda: None if table is None else transform(table).head(1), [table]
)
return _use_table_data_without_ticket_transform(filtered_table, sentinel, _row_data)
9 changes: 6 additions & 3 deletions plugins/ui/src/deephaven/ui/hooks/use_row_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@

from deephaven.table import Table

from ._transform import transform
from .use_memo import use_memo
from .use_table_data import use_table_data
from .use_table_data import _use_table_data_without_ticket_transform
from ..types import Sentinel


Expand Down Expand Up @@ -43,5 +44,7 @@ def use_row_list(
Returns:
The first row of the table as a list or the sentinel value.
"""
filtered_table = use_memo(lambda: None if table is None else table.head(1), [table])
return use_table_data(filtered_table, sentinel, _row_list)
filtered_table = use_memo(
lambda: None if table is None else transform(table).head(1), [table]
)
return _use_table_data_without_ticket_transform(filtered_table, sentinel, _row_list)
33 changes: 31 additions & 2 deletions plugins/ui/src/deephaven/ui/hooks/use_table_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
from deephaven.server.executors import submit_task
from deephaven.update_graph import has_exclusive_lock

from ._transform import transform as apply_ticket_transform
from .use_callback import use_callback
from .use_effect import use_effect
from .use_state import use_state
from .use_table_listener import use_table_listener
from .use_table_listener import _use_table_listener_without_ticket_transform

from ..types import Sentinel, TableData, TransformedData

Expand Down Expand Up @@ -143,6 +144,34 @@ def use_table_data(
Returns a dictionary with the contents of the table. Component will redraw if the table
changes, resulting in an updated frame.

Args:
table: The table to listen to. If None, None will be returned, not the sentinel value.
sentinel: The sentinel value to return if the table is ticking but empty. Defaults to None.
transform: A function to transform the table data and is_sentinel values. Defaults to None, which will
return the data as TableData.

Returns:
The table data or the sentinel value.
"""
table = apply_ticket_transform(table)
return _use_table_data_without_ticket_transform(table, sentinel, transform)


def _use_table_data_without_ticket_transform(
table: Table | None,
sentinel: Sentinel = None,
transform: (
Callable[[pd.DataFrame | Sentinel | None, bool], TransformedData | Sentinel]
| None
) = None,
) -> TableData | Sentinel | TransformedData:
"""
Returns a dictionary with the contents of the table. Component will redraw if the table
changes, resulting in an updated frame.

Note that plugin transformations are not applied, so this function is intended only for use by other hooks that
have already applied a plugin transformation.

Args:
table: The table to listen to. If None, None will be returned, not the sentinel value.
sentinel: The sentinel value to return if the table is ticking but empty. Defaults to None.
Expand Down Expand Up @@ -181,6 +210,6 @@ def use_table_data(
)

# call table_updated every time the table updates
use_table_listener(table, listener, [])
_use_table_listener_without_ticket_transform(table, listener, [])

return transform(data, is_sentinel)
34 changes: 34 additions & 0 deletions plugins/ui/src/deephaven/ui/hooks/use_table_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from deephaven.table_listener import listen, TableUpdate, TableListener
from deephaven.execution_context import get_exec_ctx, ExecutionContext

from ._transform import transform
from .use_effect import use_effect
from ..types import LockType, Dependencies

Expand Down Expand Up @@ -79,6 +80,39 @@ def use_table_listener(
If any dependencies change, the listener will be recreated.
In this case, updates may be missed if the table updates while the listener is being recreated.

Args:
table: The table to listen to.
listener: Either a function or a TableListener with an on_update function.
The function must take a TableUpdate and is_replay bool.
dependencies: Dependencies of the listener function, so the hook knows when to recreate the listener
description: An optional description for the UpdatePerformanceTracker to append to the listener’s
entry description, default is None.
do_replay: Whether to replay the initial snapshot of the table, default is False.

Returns:
None
"""
transformed = transform(table)
return _use_table_listener_without_ticket_transform(
transformed, listener, dependencies, description, do_replay
)


def _use_table_listener_without_ticket_transform(
table: Table | None,
listener: Callable[[TableUpdate, bool], None] | TableListener,
dependencies: Dependencies,
description: str | None = None,
do_replay: bool = False,
) -> None:
"""
Listen to a table and call a listener when the table updates.
If any dependencies change, the listener will be recreated.
In this case, updates may be missed if the table updates while the listener is being recreated.

Note that plugin transformations are not applied, so this function is intended only for use by other hooks that
have already applied a plugin transformation.

Args:
table: The table to listen to.
listener: Either a function or a TableListener with an on_update function.
Expand Down
32 changes: 31 additions & 1 deletion plugins/ui/src/deephaven/ui/object_types/ElementType.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
from typing import Any
from typing import Any, Literal

from deephaven.plugin.object_type import BidirectionalObjectType, MessageStream
from ..elements import Element
from .ElementMessageStream import ElementMessageStream

# Escape-hatch configuration property. When set to true, deephaven.ui declares no authorization export behavior
# ("unset"), deferring to the server's default policy instead of enforcing the transform.
_DISABLE_AUTHORIZATION_EXPORT_TRANSFORM_PROPERTY = (
"deephaven.ui.disableAuthorizationExportTransform"
)


class ElementType(BidirectionalObjectType):
"""
Expand All @@ -14,6 +20,30 @@ class ElementType(BidirectionalObjectType):
def name(self) -> str:
return "deephaven.ui.Element"

@property
Comment thread
mofojed marked this conversation as resolved.
def authorization_export_behavior(self) -> Literal["transform", "unset"]:
"""Declares that deephaven.ui must export its references through the authorization transform.

Server objects (tables, etc.) handed to the client via a deephaven.ui component are run through the
authorization transform in the viewer's context, so they carry the viewer's ACLs rather than the query
owner's. Setting the configuration property ``deephaven.ui.disableAuthorizationExportTransform`` to true
reverts to the server's default ("unset") behavior. If the configuration cannot be read, the transform is
enforced (fail secure).
"""
try:
# deephaven.configuration only exists in the 42.x server package; the import is conditional to
# maintain compatibility with 41.x, where ImportError is caught and the transform is enforced.
from deephaven.configuration import get_configuration # type: ignore[import-untyped,import-not-found]

if get_configuration().get_bool(
_DISABLE_AUTHORIZATION_EXPORT_TRANSFORM_PROPERTY, False
):
return "unset"
except Exception:
# Fail secure: if the escape hatch cannot be evaluated, enforce the transform.
pass
return "transform"

def is_type(self, obj: Any) -> bool:
return isinstance(obj, Element)

Expand Down
Loading