Skip to content
Merged
18 changes: 18 additions & 0 deletions apps/skit/src/permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ impl Permissions {
"containers::*".to_string(),
// Transport: allow MoQ, deny HTTP fetcher by default (SSRF risk)
"transport::moq::*".to_string(),
// Transport HTTP: allow the sink/IO nodes that serve or receive over the
// client's own request (no arbitrary-URL fetch), but keep
// `transport::http::fetcher` denied (SSRF risk). This lets least-privilege
// gateways serve live casts (mse) and run oneshots without admin.
"transport::http::mse".to_string(),
"streamkit::http_input".to_string(),
"streamkit::http_output".to_string(),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

📝 Info: http_input/http_output are implicitly allowed for oneshot regardless of allowlist

Adding streamkit::http_input and streamkit::http_output to the user role's allowed_nodes is harmless but redundant for the oneshot path: apps/skit/src/server/oneshot.rs:529-536 explicitly skips these marker kinds, treating them as implicitly allowed whenever oneshot execution is permitted. The allowlist entries only have an effect at the other is_node_allowed call sites (sessions/websocket/mcp/validation), where these oneshot-only marker nodes would not normally appear. Net: the entries are safe and the documented gateway example is still correct, just over-specified for oneshot use.

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks — agreed, info-only and correct. The oneshot path (server/oneshot.rs) skips these marker kinds so they're implicitly allowed there; the allowlist entries only matter at the session/websocket/mcp/validation is_node_allowed call sites. I'm keeping them for consistency with the documented gateway role example (and the new samples/skit.toml role) so the same allowlist works whether or not a node is gated — over-specified but not incorrect.

// Core: explicitly allow safe-ish nodes; deny core::file_writer by default (arbitrary write risk)
"core::passthrough".to_string(),
"core::file_reader".to_string(),
Expand Down Expand Up @@ -511,6 +518,17 @@ mod tests {
assert!(user.is_node_allowed("plugin::wasm::gain_filter_rust"));
}

#[test]
fn test_default_user_allows_http_sink_and_io_nodes() {
let user = Permissions::user();
// Sink/IO HTTP nodes are safe (serve/receive over the client's own request).
assert!(user.is_node_allowed("transport::http::mse"));
assert!(user.is_node_allowed("streamkit::http_input"));
assert!(user.is_node_allowed("streamkit::http_output"));
// The HTTP fetcher remains denied (arbitrary-URL fetch / SSRF risk).
assert!(!user.is_node_allowed("transport::http::fetcher"));
}

#[test]
fn test_global_session_limits() {
let config = PermissionsConfig { max_concurrent_sessions: Some(10), ..Default::default() };
Expand Down
29 changes: 29 additions & 0 deletions docs/src/content/docs/guides/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,35 @@ allowed_assets = ["*"]
> [!NOTE]
> Role permissions are deny-by-default. If you define a custom role in `skit.toml`, any permission you omit defaults to `false`.

> [!NOTE]
> The built-in `user` role allows the safe HTTP sink/IO nodes — `transport::http::mse` (live-cast playback), `streamkit::http_input` and `streamkit::http_output` (oneshot request body/response) — but **not** `transport::http::fetcher`, which can fetch arbitrary URLs (SSRF risk). A trusted gateway that only serves or receives over the caller's own request therefore does not need `admin`.

## Example: Least-privilege gateway role

Trusted intermediaries (e.g. the `web-capture` or `speech-gateway` examples) build a small set of fixed pipelines and should run with a scoped token instead of `admin`. This role can create/destroy sessions and use the HTTP-transport sink/IO nodes plus a specific plugin, but cannot load/delete plugins or touch other users' sessions:

```toml
[permissions.roles.gateway]
create_sessions = true
destroy_sessions = true
modify_sessions = true
tune_nodes = true
list_sessions = true
list_nodes = true
access_all_sessions = false # Only its own sessions
load_plugins = false
delete_plugins = false
upload_assets = false
delete_assets = false
allowed_nodes = [
"transport::http::mse", # serve live casts to the browser (MSE)
"streamkit::http_input", # oneshot request body
"streamkit::http_output", # oneshot response
"core::*",
]
allowed_plugins = ["plugin::native::servo"] # only what the gateway needs
```

## Permission reference

| Permission | Description |
Expand Down
Loading