From 1b5f598c79929174d30cf585b1cea734e44c9e7f Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Sun, 16 Nov 2025 16:44:31 +0900 Subject: [PATCH] src: add permission support to config file --- doc/api/permissions.md | 28 +++++ doc/node-config-schema.json | 52 +++++++++ src/node_options.cc | 30 +++-- src/node_options.h | 3 +- .../fixtures/permission/child-process-test.js | 2 + .../permission/config-addons-wasi.json | 6 + .../permission/config-child-worker.json | 6 + .../permission/config-fs-read-write.json | 10 ++ .../permission/config-net-inspector.json | 6 + test/fixtures/permission/fs-read-test.js | 1 + test/fixtures/permission/fs-write-test.js | 6 + test/parallel/test-permission-config-file.mjs | 107 ++++++++++++++++++ 12 files changed, 248 insertions(+), 9 deletions(-) create mode 100644 test/fixtures/permission/child-process-test.js create mode 100644 test/fixtures/permission/config-addons-wasi.json create mode 100644 test/fixtures/permission/config-child-worker.json create mode 100644 test/fixtures/permission/config-fs-read-write.json create mode 100644 test/fixtures/permission/config-net-inspector.json create mode 100644 test/fixtures/permission/fs-read-test.js create mode 100644 test/fixtures/permission/fs-write-test.js create mode 100644 test/parallel/test-permission-config-file.mjs diff --git a/doc/api/permissions.md b/doc/api/permissions.md index 88f485df103021..dedb314f43e7eb 100644 --- a/doc/api/permissions.md +++ b/doc/api/permissions.md @@ -153,6 +153,34 @@ does not exist, the wildcard will not be added, and access will be limited to yet, make sure to explicitly include the wildcard: `/my-path/folder-do-not-exist/*`. +#### Configuration file support + +In addition to passing permission flags on the command line, they can also be +declared in a Node.js configuration file when using the experimental +\[`--experimental-config-file`]\[] flag. Permission options must be placed inside +the `permission` top-level object. + +Example `node.config.json`: + +```json +{ + "permission": { + "allow-fs-read": ["./foo"], + "allow-fs-write": ["./bar"], + "allow-child-process": true, + "allow-worker": true, + "allow-net": true, + "allow-addons": false + } +} +``` + +Run with the configuration file: + +```console +$ node --permission --experimental-default-config-file app.js +``` + #### Using the Permission Model with `npx` If you're using [`npx`][] to execute a Node.js script, you can enable the diff --git a/doc/node-config-schema.json b/doc/node-config-schema.json index 893f55d9251ca4..916bc1c4f62002 100644 --- a/doc/node-config-schema.json +++ b/doc/node-config-schema.json @@ -603,6 +603,58 @@ }, "type": "object" }, + "permission": { + "type": "object", + "additionalProperties": false, + "properties": { + "allow-addons": { + "type": "boolean" + }, + "allow-child-process": { + "type": "boolean" + }, + "allow-fs-read": { + "oneOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string", + "minItems": 1 + }, + "type": "array" + } + ] + }, + "allow-fs-write": { + "oneOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string", + "minItems": 1 + }, + "type": "array" + } + ] + }, + "allow-inspector": { + "type": "boolean" + }, + "allow-net": { + "type": "boolean" + }, + "allow-wasi": { + "type": "boolean" + }, + "allow-worker": { + "type": "boolean" + } + } + }, "testRunner": { "type": "object", "additionalProperties": false, diff --git a/src/node_options.cc b/src/node_options.cc index 6f5475c3d9bd14..982ebebc9ab464 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -603,35 +603,49 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { AddOption("--allow-fs-read", "allow permissions to read the filesystem", &EnvironmentOptions::allow_fs_read, - kAllowedInEnvvar); + kAllowedInEnvvar, + OptionNamespaces::kPermissionNamespace); AddOption("--allow-fs-write", "allow permissions to write in the filesystem", &EnvironmentOptions::allow_fs_write, - kAllowedInEnvvar); + kAllowedInEnvvar, + OptionNamespaces::kPermissionNamespace); AddOption("--allow-addons", "allow use of addons when any permissions are set", &EnvironmentOptions::allow_addons, - kAllowedInEnvvar); + kAllowedInEnvvar, + false, + OptionNamespaces::kPermissionNamespace); AddOption("--allow-child-process", "allow use of child process when any permissions are set", &EnvironmentOptions::allow_child_process, - kAllowedInEnvvar); + kAllowedInEnvvar, + false, + OptionNamespaces::kPermissionNamespace); AddOption("--allow-inspector", "allow use of inspector when any permissions are set", &EnvironmentOptions::allow_inspector, - kAllowedInEnvvar); + kAllowedInEnvvar, + false, + OptionNamespaces::kPermissionNamespace); AddOption("--allow-net", "allow use of network when any permissions are set", &EnvironmentOptions::allow_net, - kAllowedInEnvvar); + kAllowedInEnvvar, + false, + OptionNamespaces::kPermissionNamespace); AddOption("--allow-wasi", "allow wasi when any permissions are set", &EnvironmentOptions::allow_wasi, - kAllowedInEnvvar); + kAllowedInEnvvar, + false, + OptionNamespaces::kPermissionNamespace); AddOption("--allow-worker", "allow worker threads when any permissions are set", &EnvironmentOptions::allow_worker_threads, - kAllowedInEnvvar); + kAllowedInEnvvar, + false, + OptionNamespaces::kPermissionNamespace); AddOption("--experimental-repl-await", "experimental await keyword support in REPL", &EnvironmentOptions::experimental_repl_await, diff --git a/src/node_options.h b/src/node_options.h index 620ba0fc035a5b..9afdeb983d6b26 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -416,7 +416,8 @@ std::vector MapAvailableNamespaces(); #define OPTION_NAMESPACE_LIST(V) \ V(kNoNamespace, "") \ V(kTestRunnerNamespace, "testRunner") \ - V(kWatchNamespace, "watch") + V(kWatchNamespace, "watch") \ + V(kPermissionNamespace, "permission") enum class OptionNamespaces { #define V(name, _) name, diff --git a/test/fixtures/permission/child-process-test.js b/test/fixtures/permission/child-process-test.js new file mode 100644 index 00000000000000..364ba59f7cca3c --- /dev/null +++ b/test/fixtures/permission/child-process-test.js @@ -0,0 +1,2 @@ +const { spawnSync } = require('child_process'); +spawnSync(process.execPath, ['--version']); diff --git a/test/fixtures/permission/config-addons-wasi.json b/test/fixtures/permission/config-addons-wasi.json new file mode 100644 index 00000000000000..c0ee6d61656dd2 --- /dev/null +++ b/test/fixtures/permission/config-addons-wasi.json @@ -0,0 +1,6 @@ +{ + "permission": { + "allow-addons": true, + "allow-wasi": true + } +} \ No newline at end of file diff --git a/test/fixtures/permission/config-child-worker.json b/test/fixtures/permission/config-child-worker.json new file mode 100644 index 00000000000000..222cd1b7551f57 --- /dev/null +++ b/test/fixtures/permission/config-child-worker.json @@ -0,0 +1,6 @@ +{ + "permission": { + "allow-child-process": true, + "allow-worker": true + } +} \ No newline at end of file diff --git a/test/fixtures/permission/config-fs-read-write.json b/test/fixtures/permission/config-fs-read-write.json new file mode 100644 index 00000000000000..3e9ba75c3657d2 --- /dev/null +++ b/test/fixtures/permission/config-fs-read-write.json @@ -0,0 +1,10 @@ +{ + "permission": { + "allow-fs-read": [ + "*" + ], + "allow-fs-write": [ + "*" + ] + } +} \ No newline at end of file diff --git a/test/fixtures/permission/config-net-inspector.json b/test/fixtures/permission/config-net-inspector.json new file mode 100644 index 00000000000000..39743b8337301c --- /dev/null +++ b/test/fixtures/permission/config-net-inspector.json @@ -0,0 +1,6 @@ +{ + "permission": { + "allow-net": true, + "allow-inspector": true + } +} \ No newline at end of file diff --git a/test/fixtures/permission/fs-read-test.js b/test/fixtures/permission/fs-read-test.js new file mode 100644 index 00000000000000..dbe81615459f60 --- /dev/null +++ b/test/fixtures/permission/fs-read-test.js @@ -0,0 +1 @@ +require('fs').readFileSync(__filename); diff --git a/test/fixtures/permission/fs-write-test.js b/test/fixtures/permission/fs-write-test.js new file mode 100644 index 00000000000000..9c9c7664daa42a --- /dev/null +++ b/test/fixtures/permission/fs-write-test.js @@ -0,0 +1,6 @@ +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const tmpFile = path.join(os.tmpdir(), 'permission-test-' + Date.now() + '.txt'); +fs.writeFileSync(tmpFile, 'test'); diff --git a/test/parallel/test-permission-config-file.mjs b/test/parallel/test-permission-config-file.mjs new file mode 100644 index 00000000000000..9ca50284435707 --- /dev/null +++ b/test/parallel/test-permission-config-file.mjs @@ -0,0 +1,107 @@ +import { spawnPromisified } from '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; +import assert from 'node:assert'; +import { describe, it } from 'node:test'; + +describe('Permission model config file support', () => { + it('should load filesystem read/write permissions from config file', async () => { + const configPath = fixtures.path('permission/config-fs-read-write.json'); + const readTestPath = fixtures.path('permission/fs-read-test.js'); + const writeTestPath = fixtures.path('permission/fs-write-test.js'); + + { + const result = await spawnPromisified(process.execPath, [ + '--permission', + '--experimental-config-file', + configPath, + readTestPath, + ]); + assert.strictEqual(result.code, 0); + } + + { + const result = await spawnPromisified(process.execPath, [ + '--permission', + '--experimental-config-file', + configPath, + writeTestPath, + ]); + assert.strictEqual(result.code, 0); + } + }); + + it('should load child process and worker permissions from config file', async () => { + const configPath = fixtures.path('permission/config-child-worker.json'); + const childTestPath = fixtures.path('permission/child-process-test.js'); + + const result = await spawnPromisified(process.execPath, [ + '--permission', + '--experimental-config-file', + configPath, + '--allow-fs-read=*', + childTestPath, + ]); + assert.strictEqual(result.code, 0); + }); + + it('should load network and inspector permissions from config file', async () => { + const configPath = fixtures.path('permission/config-net-inspector.json'); + + const result = await spawnPromisified(process.execPath, [ + '--permission', + '--experimental-config-file', + configPath, + '--allow-fs-read=*', + '-p', + 'process.permission.has("net") && process.permission.has("inspector")', + ]); + assert.match(result.stdout, /true/); + assert.strictEqual(result.code, 0); + }); + + it('should load addons and wasi permissions from config file', async () => { + const configPath = fixtures.path('permission/config-addons-wasi.json'); + + const result = await spawnPromisified(process.execPath, [ + '--permission', + '--experimental-config-file', + configPath, + '--allow-fs-read=*', + '-p', + 'process.permission.has("addon") && process.permission.has("wasi")', + ]); + assert.match(result.stdout, /true/); + assert.strictEqual(result.code, 0); + }); + + it('should deny operations when permissions are not in config file', async () => { + const configPath = fixtures.path('permission/config-fs-read-write.json'); + + const result = await spawnPromisified(process.execPath, [ + '--permission', + '--experimental-config-file', + configPath, + '--allow-fs-read=*', + '-p', + 'process.permission.has("child")', + ]); + assert.match(result.stdout, /false/); + assert.strictEqual(result.code, 0); + }); + + it('should combine config file permissions with CLI flags', async () => { + const configPath = fixtures.path('permission/config-fs-read-write.json'); + + const result = await spawnPromisified(process.execPath, [ + '--permission', + '--experimental-config-file', + configPath, + '--allow-child-process', + '--allow-fs-read=*', + '-p', + 'process.permission.has("child") && process.permission.has("fs.read")', + ]); + assert.match(result.stdout, /true/); + assert.strictEqual(result.code, 0); + }); +});