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
31 changes: 31 additions & 0 deletions crates/kernel/tests/permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,37 @@ fn permission_glob_double_star_still_matches_nested_paths() {
assert!(permission_glob_matches("tcp://**", "tcp://127.0.0.1:43111"));
}

#[test]
fn network_permission_patterns_match_the_resource_uri_not_a_bare_host() {
// Network resources are formatted as URIs (`format_tcp_resource` /
// `format_dns_resource`), so a pattern is matched against the whole URI.
assert!(permission_glob_matches(
"tcp://api.example.com:443",
"tcp://api.example.com:443",
));
assert!(permission_glob_matches(
"tcp://api.example.com:*",
"tcp://api.example.com:443",
));
assert!(permission_glob_matches(
"dns://api.example.com",
"dns://api.example.com",
));

// A bare host or `host:port` never matches, and a single `*` cannot reach
// past the `//`, so neither form can be used to allow or deny a host.
assert!(!permission_glob_matches(
"api.example.com",
"tcp://api.example.com:443",
));
assert!(!permission_glob_matches(
"api.example.com:443",
"tcp://api.example.com:443",
));
assert!(!permission_glob_matches("*", "tcp://api.example.com:443"));
assert!(!permission_glob_matches("*.example.com", "dns://api.example.com"));
}

#[test]
fn kernel_vm_config_defaults_to_deny_all_permissions() {
let mut kernel = KernelVm::new(MemoryFileSystem::new(), KernelVmConfig::new("vm-defaults"));
Expand Down
4 changes: 3 additions & 1 deletion docs/content/docs/permissions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ To invert it, flip `default` to `"deny"` and allow just one subtree:

## Allow only specific network hosts

Every non-`fs` scope matches by `patterns` instead of `paths`. For `network`, a pattern is a host (or `host:port`), and the operations are `fetch`, `http`, `dns`, and `listen`.
Every non-`fs` scope matches by `patterns` instead of `paths`. The operations for `network` are `fetch`, `http`, `dns`, and `listen`.

A `network` pattern is matched against the URI form of the connection, not a bare host: `tcp://<host>:<port>` for `fetch`, `http`, and `listen`, and `dns://<hostname>` for `dns`. Patterns are globs in which a single `*` does not cross `/`, so use `tcp://api.example.com:*` to allow every port on a host, and `tcp://**` to allow every host. A bare `api.example.com` matches nothing.

<CodeSnippet file="examples/permissions/server.ts" region="allow-one-host" />

Expand Down
11 changes: 9 additions & 2 deletions examples/permissions/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,18 @@ const denyVault = {
} satisfies Permissions;

// docs:start allow-one-host
// Deny the network by default, allow only api.example.com.
// Deny the network by default, allow only api.example.com (any port).
// Patterns match the URI form of the connection, so a bare host never matches.
const allowOneHost = {
network: {
default: "deny",
rules: [{ mode: "allow", operations: ["*"], patterns: ["api.example.com"] }],
rules: [
{
mode: "allow",
operations: ["*"],
patterns: ["tcp://api.example.com:*", "dns://api.example.com"],
},
],
},
} satisfies Permissions;
// docs:end allow-one-host
Expand Down