From 04e1e5b90a860ec7f63ce19037c2cf63e0169cd4 Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Thu, 9 Dec 2021 20:18:42 +0000 Subject: [PATCH] feat: add support for yaml.unmarshal builtin This uses the "yaml" npm package to provide support for unmarshalling YAML strings within rego via the yaml.unmarshal() function. Test functionality has been included to verify parsing and handling of syntax, metadata, reference and warnings to match the current behavior of the opa tool. --- package-lock.json | 16 +++- package.json | 3 +- src/builtins/index.js | 2 + src/builtins/yaml.js | 28 +++++++ test/fixtures/yaml-support/.gitignore | 2 + .../yaml-support/yaml-support-policy.rego | 50 ++++++++++++ test/opa-yaml-support.test.js | 78 +++++++++++++++++++ 7 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 src/builtins/yaml.js create mode 100644 test/fixtures/yaml-support/.gitignore create mode 100644 test/fixtures/yaml-support/yaml-support-policy.rego create mode 100644 test/opa-yaml-support.test.js diff --git a/package-lock.json b/package-lock.json index fc12d24b..30a869f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "1.4.0", "license": "Apache-2.0", "dependencies": { - "sprintf-js": "^1.1.2" + "sprintf-js": "^1.1.2", + "yaml": "^1.10.2" }, "devDependencies": { "jest": "^27.2.4", @@ -4115,6 +4116,14 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -7314,6 +7323,11 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + }, "yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/package.json b/package.json index 822a0aa8..9186d8ea 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "typescript": "^4.4.3" }, "dependencies": { - "sprintf-js": "^1.1.2" + "sprintf-js": "^1.1.2", + "yaml": "^1.10.2" } } diff --git a/src/builtins/index.js b/src/builtins/index.js index e4405d54..b7b33d08 100644 --- a/src/builtins/index.js +++ b/src/builtins/index.js @@ -1,7 +1,9 @@ const strings = require("./strings"); const regex = require("./regex"); +const yaml = require("./yaml"); module.exports = { ...strings, ...regex, + ...yaml, }; diff --git a/src/builtins/yaml.js b/src/builtins/yaml.js new file mode 100644 index 00000000..27c70e0c --- /dev/null +++ b/src/builtins/yaml.js @@ -0,0 +1,28 @@ +const yaml = require("yaml"); + +// see: https://eemeli.org/yaml/v1/#errors +const errors = new Set([ + "YAMLReferenceError", + "YAMLSemanticError", + "YAMLSyntaxError", + "YAMLWarning", +]); + +const unmarshal = function (str) { + const YAML_SILENCE_WARNINGS_CACHED = global.YAML_SILENCE_WARNINGS; + try { + // see: https://eemeli.org/yaml/v1/#silencing-warnings + global.YAML_SILENCE_WARNINGS = true; + return yaml.parse(str); + } catch (err) { + // Ignore parser errors. + if (err && errors.has(err.name)) { + return false; + } + throw err; + } finally { + global.YAML_SILENCE_WARNINGS = YAML_SILENCE_WARNINGS_CACHED; + } +}; + +module.exports = { "yaml.unmarshal": unmarshal }; diff --git a/test/fixtures/yaml-support/.gitignore b/test/fixtures/yaml-support/.gitignore new file mode 100644 index 00000000..159e24b8 --- /dev/null +++ b/test/fixtures/yaml-support/.gitignore @@ -0,0 +1,2 @@ +bundle.tar.gz +policy.wasm \ No newline at end of file diff --git a/test/fixtures/yaml-support/yaml-support-policy.rego b/test/fixtures/yaml-support/yaml-support-policy.rego new file mode 100644 index 00000000..1aa07c5a --- /dev/null +++ b/test/fixtures/yaml-support/yaml-support-policy.rego @@ -0,0 +1,50 @@ +package yaml.support + +fixture := ` +--- +openapi: "3.0.1" +info: + title: test +paths: + /path1: + get: + x-amazon-apigateway-integration: + type: "mock" + httpMethod: "GET" +x-amazon-apigateway-policy: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + AWS: "*" + Action: + - 'execute-api:Invoke' + Resource: '*' +` + +default canParseYaml = false + +canParseYAML { + resource := yaml.unmarshal(fixture) + resource.info.title == "test" +} + +hasSemanticError { + # see: https://github.com/eemeli/yaml/blob/395f892ec9a26b9038c8db388b675c3281ab8cd3/tests/doc/errors.js#L22 + yaml.unmarshal("a:\n\t1\nb:\n\t2\n") +} + +hasSyntaxError { + # see: https://github.com/eemeli/yaml/blob/395f892ec9a26b9038c8db388b675c3281ab8cd3/tests/doc/errors.js#L49 + yaml.unmarshal("{ , }\n---\n{ 123,,, }\n") +} + +hasReferenceError { + # see: https://github.com/eemeli/yaml/blob/395f892ec9a26b9038c8db388b675c3281ab8cd3/tests/doc/errors.js#L245 + yaml.unmarshal("{ , }\n---\n{ 123,,, }\n") +} + +hasYAMLWarning { + # see: https://github.com/eemeli/yaml/blob/395f892ec9a26b9038c8db388b675c3281ab8cd3/tests/doc/errors.js#L224 + yaml.unmarshal("%FOO\n---bar\n") +} diff --git a/test/opa-yaml-support.test.js b/test/opa-yaml-support.test.js new file mode 100644 index 00000000..b5384ffb --- /dev/null +++ b/test/opa-yaml-support.test.js @@ -0,0 +1,78 @@ +const { readFileSync } = require("fs"); +const { execFileSync } = require("child_process"); +const { loadPolicy } = require("../src/opa.js"); + +describe("yaml.unmarshal() support", () => { + const fixturesFolder = "test/fixtures/yaml-support"; + + let policy; + + beforeAll(async () => { + const bundlePath = `${fixturesFolder}/bundle.tar.gz`; + + execFileSync("opa", [ + "build", + fixturesFolder, + "-o", + bundlePath, + "-t", + "wasm", + "-e", + "yaml/support/canParseYAML", + "-e", + "yaml/support/hasSyntaxError", + "-e", + "yaml/support/hasSemanticError", + "-e", + "yaml/support/hasReferenceError", + "-e", + "yaml/support/hasYAMLWarning", + ]); + + execFileSync("tar", [ + "-xzf", + bundlePath, + "-C", + `${fixturesFolder}/`, + "/policy.wasm", + ]); + + const policyWasm = readFileSync(`${fixturesFolder}/policy.wasm`); + const opts = { initial: 5, maximum: 10 }; + policy = await loadPolicy(policyWasm, opts); + }); + + it("should unmarshall YAML strings", () => { + const result = policy.evaluate({}, "yaml/support/canParseYAML"); + expect(result.length).not.toBe(0); + expect(result[0]).toMatchObject({ result: true }); + }); + + it("should ignore YAML syntax errors", () => { + expect(() => policy.evaluate({}, "yaml/support/hasSyntaxError")).not + .toThrow(); + const result = policy.evaluate({}, "yaml/support/hasSyntaxError"); + expect(result.length).toBe(0); + }); + + it("should ignore YAML semantic errors", () => { + expect(() => policy.evaluate({}, "yaml/support/hasSemanticError")).not + .toThrow(); + const result = policy.evaluate({}, "yaml/support/hasSemanticError"); + expect(result.length).toBe(0); + }); + + it("should ignore YAML reference errors", () => { + expect(() => policy.evaluate({}, "yaml/support/hasReferenceError")).not + .toThrow(); + const result = policy.evaluate({}, "yaml/support/hasReferenceError"); + expect(result.length).toBe(0); + }); + + it("should ignore YAML warnings", () => { + expect(() => policy.evaluate({}, "yaml/support/hasYAMLWarning")).not + .toThrow(); + const result = policy.evaluate({}, "yaml/support/hasYAMLWarning"); + expect(result.length).toBe(0); + }); +});