---
title: Plugin ACLs
sidebar_label: Plugin ACLs
---

Plugins such as `deephaven.ui` and Deephaven Express deliver server objects (tables and other references) to the client. Because [Edge ACLs](./persistent-query-acls.md) apply ACLs at the next downstream operation, _where_ and _in whose context_ the operation is performed determines what a viewer sees. This makes an ACLed table especially flexible inside a plugin: an operation performed in a `@ui.component` runs in the viewer's context, so the ACL is applied for the viewer, while the same operation at the top level runs in the query owner's context.

The worked-examples script below demonstrates this end-to-end: an un-ACLed table, an Edge-ACLed table, and dashboards and components that display, sum, plot, read, and listen to ACLed tables - including how to mark a derived result exportable to the current user with `apply_full_access_for_current_user`. Run it as a Persistent Query owned by an admin/owner; the examples use a viewer named "alice".

> [!TIP]
> Download the worked-examples script: [plugin-acls.py](../../assets/sys-admin/permissions/plugin-acls.py).

## Setup

Import the plugins and create the sample data. These examples use the Deephaven Express "stocks" data set (columns `Timestamp`, `Sym`, `Exchange`, `Size`, `Price`, `Dollars`, `Side`; `Sym` values `CAT`, `DOG`, `FISH`, `BIRD`, `LIZARD`), and permit Alice to see only `DOG` and `FISH`.

```python
from deephaven.plot import express as dx
import deephaven.ui as ui
from deephaven_enterprise.edge_acl import EdgeAclProvider
from deephaven_enterprise import acl_generator

stocks = dx.data.stocks(ticking=True)
```

## Example 1 - raw_table: no ACL applied

`raw_table` carries no `EdgeAclProvider`. The query owner (or an admin) can open it directly. Alice cannot: because ACLs have been applied somewhere in this query (see `acl_table` in Example 2), `requireAclsToExport()` is true, and a table with no ACL marker is denied to viewers. When Alice tries to fetch `raw_table` directly, she gets a `TableAccessException,` and the table simply does not open for her.

```python
raw_table = stocks
```

In the panels menu, `raw_table` is not shown because `alice` does not have permission:

![img](../../assets/sys-admin/permissions/plugin-acls/alice-no-raw-table-listed.png)

Even though the table is not listed in the panels menu, it may still be in Alice's workspace (for example, if the query writer changed the permissions of the table). When the table is loaded, she is then presented with a not found error:
![img](../../assets/sys-admin/permissions/plugin-acls/alice-raw-table-not-found.png)

## Example 2 - acl_table: an Edge ACL permitting Alice to see DOG and FISH

`apply_to` returns a new table. When Alice opens the new table, the server applies the row ACL for Alice, so she sees only the rows where `Sym` is `DOG` or `FISH`. The owner/admin opening it sees everything (admins are not subject to edge ACLs).

ACLs are keyed by group. Here we grant the filter to a group named "alice":

```python
alice_acl = (
    EdgeAclProvider.builder()
    .row_acl("alice", acl_generator.where("Sym in `DOG`, `FISH`"))
    .build()
)
acl_table = alice_acl.apply_to(raw_table)
```

Alice can open `acl_table` and sees only rows for `DOG` and `FISH`:
![img](../../assets/sys-admin/permissions/plugin-acls/alice-acl-dog-and-fish.png)

## Example 3 - a dashboard showing raw_table and acl_table side by side

A `deephaven.ui` dashboard exports its tables to the client as plugin references. `deephaven.ui` declares `authorization_export_behavior="transform"`, so each table the dashboard exports is run through the authorization transform in the viewer's context - a dashboard table is subject to the same authorization as opening it directly:

- `acl_table`: a table with an ACL, so the transform applies Alice's row ACL, and she sees only `DOG` and `FISH`.
- `raw_table`: has no ACL marker, and ACLs are required to export (an ACL is applied in this query), so the transform denies it for Alice - exactly as when she opens it directly (Example 1). The transform raises a `TableAccessException`, so Alice's `raw_table` panel displays an error instead of any data.

The owner/admin is exempt from edge ACLs and sees both panels in full:

```python
example3_dashboard = ui.dashboard(
    ui.row(
        ui.panel(ui.table(raw_table), title="raw_table - denied to alice"),
        ui.panel(ui.table(acl_table), title="acl_table - filtered to DOG, FISH"),
    )
)
EdgeAclProvider.add_full_access(example3_dashboard)
```

Alice sees an error for the `raw_table` and the contents of `acl_table`:
![img](../../assets/sys-admin/permissions/plugin-acls/alice-example3.png)

The server's log contains two error messages:

```text
DheAuthorizationProvider.transform: Transformer TableTicketTransformer denied access with TableAccessException: User {alice} may not access this Table
Plugin reference is not authorized for {alice}: class=io.deephaven.engine.table.impl.QueryTable, index=0
```

## Example 4 - summing trade sizes in a component

We sum the `Size` column inside a `@ui.component` and show four result tables. The component body runs at render time in the viewer's context, so the `sum_by` occurs in Alice's context, and its row ACL is applied before the sum. `acl_total` is correctly the sum over Alice's `DOG` + `FISH` rows.

`raw_total` and `acl_total` are _derived_ tables: aggregating a table with ACLs yields a plain result that no longer carries an ACL marker. When `deephaven.ui` exports them through the transform, ACLs are required to export and neither is marked, so the transform denies both (raising a `TableAccessException`):

- `raw_total`: derived from an un-ACLed table; denied.
- `acl_total`: correctly filtered to `DOG` + `FISH`, yet still unmarked - so also denied, even though its data is exactly what Alice may see.

To show a derived result, re-mark it with an ACL the transform accepts. The last two panels re-mark each result for the current user with `apply_full_access_for_current_user()`. That grants full access to the viewing user only and does no filtering - it simply marks the result exportable to that user - so whatever you mark is shown. Both are legitimate, deliberate choices by the query writer:

- `shown_acl_total`: alice's own `DOG` + `FISH` total. Safe because `acl_total` was computed in her context and holds only her rows.
- `shown_raw_total`: the grand total over all Syms. A valid choice to share as an aggregate, even though Alice may not see the unsummarized rows.

> [!IMPORTANT]
> `apply_full_access_for_current_user()` asserts the data is safe to provide to the viewer; it adds no protection of its own. The query writer is responsible for ensuring each result is properly sanitized for the user before marking it - whether by filtering it to the user's rows or by otherwise transforming the data.

```python
@ui.component
def sum_sizes_component():
    raw_total = raw_table.view("Size").sum_by()
    acl_total = acl_table.view("Size").sum_by()
    # Mark each result exportable to the current viewer only. The writer decides each
    # is safe to share: acl_total holds only alice's rows, and raw_total is a grand
    # total that exposes no row-level data.
    shown_acl_total = EdgeAclProvider.apply_full_access_for_current_user(acl_total)
    shown_raw_total = EdgeAclProvider.apply_full_access_for_current_user(raw_total)
    return ui.row(
        ui.panel(ui.table(raw_total), title="raw total - denied (unmarked)"),
        ui.panel(ui.table(acl_total), title="acl total - DOG+FISH, denied (unmarked)"),
        ui.panel(ui.table(shown_acl_total), title="acl total - full access for alice"),
        ui.panel(
            ui.table(shown_raw_total),
            title="grand total - full access for alice (aggregate)",
        ),
    )


example4_dashboard = ui.dashboard(sum_sizes_component())
EdgeAclProvider.add_full_access(example4_dashboard)
```

Alice sees errors for the two unmarked tables, her own totals in the third column, and the grand totals in the fourth column:
![img](../../assets/sys-admin/permissions/plugin-acls/alice-example4.png)

In this example, we used `apply_full_access_for_current_user`, but `apply_full_access` has an identical effect. The plugin exports the result table to "alice", so although `apply_full_access` permits "allusers" to view the tables, other users are not provided a reference to the derived tables.

## Example 5 - an ACL that grants access only to Bob

`bob_acl` grants a row filter to the "bob" group and no one else. Alice is not in Bob's group. `bob_only_table` has ACLs, so when `deephaven.ui` exports it for Alice, the authorization transform looks up her row filters, finds none, and treats that as "no access" - raising a `TableAccessException`.

Unlike opening it directly - where the denial is caught and obfuscated (typically surfaced as a generic not-found, so it does not reveal the table) - the exception raised while exporting a `deephaven.ui` reference propagates out as an internal server error in Alice's UI. This is the same denial as Example 3's `raw_table`.

```python
bob_acl = (
    EdgeAclProvider.builder()
    .row_acl("bob", acl_generator.where("Sym in `CAT`"))
    .build()
)
bob_only_table = bob_acl.apply_to(stocks)


@ui.component
def bob_only_component():
    # For alice the transform denies this reference (no permitted rows) ->
    # TableAccessException -> Internal Server Error. For bob (or the owner/admin)
    # it renders normally.
    return ui.table(bob_only_table)


example5_dashboard = ui.dashboard(bob_only_component())
EdgeAclProvider.add_full_access(example5_dashboard)
```

Alice is permitted to see the dashboard, but the table inside the dashboard produces an error:

![img](../../assets/sys-admin/permissions/plugin-acls/alice-example5.png)

## Example 6 - a line plot built at the top level; object ACL does not re-filter

Applying an Edge ACL to a table does not automatically make it safe for use with widgets or other components. We build a line plot from `acl_table` with `by="Sym"`. The `by` partitions the table now, at instantiation time, which runs in the query owner's context. The owner is an admin, so they see the raw, unfiltered table; the plot is built over all symbols, and that result is baked into the figure.

We then add an object ACL to make the plot visible to Alice. Object ACLs only control _visibility_ of the object - they do not (and cannot) re-filter data that was already computed. So Alice sees a plot containing `CAT`, `DOG`, `FISH`, `BIRD`, and `LIZARD`, not just `DOG` and `FISH`.

Importantly, an object ACL is not a substitute for an edge ACL applied in the viewer's context. To give Alice a correctly filtered plot, build the plot inside a `@ui.component` (so the `by` runs in her context, like Example 4) rather than at the top level - see Example 7.

```python
line_plot = dx.line(acl_table, x="Timestamp", y="Price", by="Sym")
EdgeAclProvider.add_full_access(line_plot)
```

Alice sees the `line_plot` object, with all of the symbols:
![img](../../assets/sys-admin/permissions/plugin-acls/alice-line-plot.png)

## Example 7 - the same line plot, built inside a component

This is the mitigation for Example 6: wrapping the plot construction in a `@ui.component` makes the plot do the right thing for edge ACLs _without the plot needing to know anything about ACLs_.

The component body runs at render time in the viewer's context. So when `dx.line` applies `by="Sym"`, the downstream operation occurs under Alice's auth context, and the ACL is applied for her. Alice's figure contains only `DOG` and `FISH`; the owner/admin still sees every symbol.

Contrast this to Example 6, where the identical `dx.line` call at the top level operated in the owner's (admin) context and baked an unfiltered figure that no later object ACL could re-filter.

The broader point: many widgets (Deephaven Express plots among them) were never designed with ACLs in mind. By composing them with `deephaven.ui` so that they execute in the user's context, the widget inherits correct, per-viewer filtering for free when it accesses a table with ACLs. This lets you retrofit ACL-correct behavior onto widgets that have no ACL awareness of their own.

```python
@ui.component
def acl_line_plot_component():
    # dx.line's `by="Sym"` operates on acl_table here, in the viewer's context, so
    # the row ACL is applied before the figure is built. For alice this yields
    # a plot of DOG and FISH only.
    plot = dx.line(acl_table, x="Timestamp", y="Price", by="Sym")
    EdgeAclProvider.add_full_access(plot)
    return plot


example7_dashboard = ui.dashboard(acl_line_plot_component())
EdgeAclProvider.add_full_access(example7_dashboard)
```

Alice sees the `line_plot` object, with only `DOG` and `FISH`:
![img](../../assets/sys-admin/permissions/plugin-acls/alice-filtered-plot.png)

## Example 8a - reading row data from a derived top-level table: denied

`ui.use_row_data` (like the other data-reading hooks) applies the authorization transform to the table it is handed, the same as exporting a table directly. The data it returns is only as safe as the table passed into it.

Here, `acl_table.tail(1)` is evaluated at the top level, in the query owner's context. The result is a plain derived table: it carries no `EdgeAclProvider` (ACLs are not propagated through downstream operations). Because ACLs have been applied somewhere in this query, `requireAclsToExport()` is true, and `deephaven.ui` denies the unmarked table for Alice when `use_row_data` attempts to read it - she gets an error.

```python
@ui.component
def ui_table_row(table):
    row_data = ui.use_row_data(table)
    if row_data is None:
        return ui.heading("No data yet.")
    return ui.heading(f"Row data is {row_data}. Value of Sym is {row_data['Sym']}")


example8a_dashboard = ui.dashboard(ui_table_row(acl_table.tail(1)))
EdgeAclProvider.add_full_access(example8a_dashboard)
```

Alice sees an error because the derived table is unmarked and denied by the authorization transform:

![img](../../assets/sys-admin/permissions/plugin-acls/alice-example8a.png)

## Example 8b - reading row data with use_row_data; tail done inside the component

The table with ACLs is passed in, and the `.tail(1)` is done inside the component, at render time, in the viewer's context. Because the tail operation is performed under Alice's context, `.tail(1)` only sees her permitted rows. The result is then marked as exportable to the current user with `apply_full_access_for_current_user`, so that `use_row_data` can read it.

This demonstrates the same concept as Examples 4, 6, and 7, now for data-extraction hooks: perform downstream operations (here `.tail`) inside the per-user component, not at the top level, so the ACL is applied in the viewer's context before any data is read out.

```python
@ui.component
def ui_last_table_row(table):
    tailed_data = EdgeAclProvider.apply_full_access_for_current_user(table.tail(1))
    row_data = ui.use_row_data(tailed_data)
    if row_data is None:
        return ui.heading("No data yet.")
    return ui.heading(f"Last row data is {row_data}. Value of Sym is {row_data['Sym']}")


example8b_dashboard = ui.dashboard(ui_last_table_row(acl_table))
EdgeAclProvider.add_full_access(example8b_dashboard)
```

Alice sees "DOG" (her last permitted row):
![img](../../assets/sys-admin/permissions/plugin-acls/alice-example8b.png)

## Example 9 - listening to a table with use_table_listener

This component subscribes to its table argument with `ui.use_table_listener` and, on each update, toasts the added rows whose `Size` exceeds 5000, including the `Sym` and `Size` - that is, it exfiltrates row data, much like `use_row_data` in Example 8. (`Sym` is the column the row ACL filters on.)

The subscription is created inside the `@ui.component`, at render time, in the viewer's context, so the listener attaches to a table filtered for that viewer: Alice's listener only ever sees `DOG`/`FISH` rows, and the toast only shows data she is permitted to see.

```python
@ui.component
def toast_table(t):
    render_queue = ui.use_render_queue()

    def listener_function(update, is_replay):
        # added() rows have already passed through the per-viewer
        # filter, so this only reads rows the viewer is permitted to see.
        added = update.added()
        syms = added["Sym"]
        sizes = added["Size"]
        for i in range(len(sizes)):
            # Only toast notable trades; include the size in the message.
            if sizes[i] > 5000:
                render_queue(
                    lambda sym=syms[i], size=sizes[i]: ui.toast(
                        f"{sym} traded size {size}", timeout=5000
                    )
                )

    ui.use_table_listener(t, listener_function, [t])
    return t


example9_toast_table = toast_table(acl_table)
EdgeAclProvider.add_full_access(example9_toast_table)
```

Alice sees only rows for `DOG` and `FISH`; and the toast pop-ups are also limited to those values:
![img](../../assets/sys-admin/permissions/plugin-acls/alice-toasts.png)
