@backstage/plugin-scaffolder-backend 0.15.13 → 0.15.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs.js CHANGED
@@ -5,12 +5,12 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var errors = require('@backstage/errors');
6
6
  var catalogModel = require('@backstage/catalog-model');
7
7
  var fs = require('fs-extra');
8
- var path = require('path');
9
8
  var yaml = require('yaml');
10
9
  var backendCommon = require('@backstage/backend-common');
10
+ var path = require('path');
11
11
  var globby = require('globby');
12
- var nunjucks = require('nunjucks');
13
12
  var isbinaryfile = require('isbinaryfile');
13
+ var vm2 = require('vm2');
14
14
  var pluginScaffolderBackendModuleCookiecutter = require('@backstage/plugin-scaffolder-backend-module-cookiecutter');
15
15
  var child_process = require('child_process');
16
16
  var stream = require('stream');
@@ -27,6 +27,7 @@ var luxon = require('luxon');
27
27
  var Handlebars = require('handlebars');
28
28
  var winston = require('winston');
29
29
  var jsonschema = require('jsonschema');
30
+ var nunjucks = require('nunjucks');
30
31
  var express = require('express');
31
32
  var Router = require('express-promise-router');
32
33
  var os = require('os');
@@ -56,14 +57,13 @@ function _interopNamespace(e) {
56
57
  }
57
58
 
58
59
  var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
59
- var path__namespace = /*#__PURE__*/_interopNamespace(path);
60
- var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
61
60
  var yaml__namespace = /*#__PURE__*/_interopNamespace(yaml);
61
+ var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
62
62
  var globby__default = /*#__PURE__*/_interopDefaultLegacy(globby);
63
- var nunjucks__default = /*#__PURE__*/_interopDefaultLegacy(nunjucks);
64
63
  var fetch__default = /*#__PURE__*/_interopDefaultLegacy(fetch);
65
64
  var Handlebars__namespace = /*#__PURE__*/_interopNamespace(Handlebars);
66
65
  var winston__namespace = /*#__PURE__*/_interopNamespace(winston);
66
+ var nunjucks__default = /*#__PURE__*/_interopDefaultLegacy(nunjucks);
67
67
  var express__default = /*#__PURE__*/_interopDefaultLegacy(express);
68
68
  var Router__default = /*#__PURE__*/_interopDefaultLegacy(Router);
69
69
  var os__default = /*#__PURE__*/_interopDefaultLegacy(os);
@@ -182,7 +182,7 @@ function createCatalogWriteAction() {
182
182
  async handler(ctx) {
183
183
  ctx.logStream.write(`Writing catalog-info.yaml`);
184
184
  const {entity} = ctx.input;
185
- await fs__default['default'].writeFile(path.resolve(ctx.workspacePath, "catalog-info.yaml"), yaml__namespace.stringify(entity));
185
+ await fs__default['default'].writeFile(backendCommon.resolveSafeChildPath(ctx.workspacePath, "catalog-info.yaml"), yaml__namespace.stringify(entity));
186
186
  }
187
187
  });
188
188
  }
@@ -226,7 +226,7 @@ ${files.map((f) => ` - ${path.relative(ctx.workspacePath, f)}`).join("\n")}`);
226
226
  async function recursiveReadDir(dir) {
227
227
  const subdirs = await fs.readdir(dir);
228
228
  const files = await Promise.all(subdirs.map(async (subdir) => {
229
- const res = path.resolve(dir, subdir);
229
+ const res = path.join(dir, subdir);
230
230
  return (await fs.stat(res)).isDirectory() ? recursiveReadDir(res) : [res];
231
231
  }));
232
232
  return files.reduce((a, f) => a.concat(f), []);
@@ -250,7 +250,7 @@ async function fetchContents({
250
250
  }
251
251
  if (!fetchUrlIsAbsolute && (baseUrl == null ? void 0 : baseUrl.startsWith("file://"))) {
252
252
  const basePath = baseUrl.slice("file://".length);
253
- const srcDir = backendCommon.resolveSafeChildPath(path__namespace.dirname(basePath), fetchUrl);
253
+ const srcDir = backendCommon.resolveSafeChildPath(path__default['default'].dirname(basePath), fetchUrl);
254
254
  await fs__default['default'].copy(srcDir, outputPath);
255
255
  } else {
256
256
  let readUrl;
@@ -313,7 +313,100 @@ function createFetchPlainAction(options) {
313
313
  });
314
314
  }
315
315
 
316
- nunjucks__default['default'].installJinjaCompat();
316
+ const mkScript = (nunjucksSource) => `
317
+ const { render, renderCompat } = (() => {
318
+ const module = {};
319
+ const process = { env: {} };
320
+ const require = (pkg) => { if (pkg === 'events') { return function (){}; }};
321
+
322
+ ${nunjucksSource}
323
+
324
+ const env = module.exports.configure({
325
+ autoescape: false,
326
+ tags: {
327
+ variableStart: '\${{',
328
+ variableEnd: '}}',
329
+ },
330
+ });
331
+
332
+ const compatEnv = module.exports.configure({
333
+ autoescape: false,
334
+ tags: {
335
+ variableStart: '{{',
336
+ variableEnd: '}}',
337
+ },
338
+ });
339
+ compatEnv.addFilter('jsonify', compatEnv.getFilter('dump'));
340
+
341
+ if (typeof parseRepoUrl !== 'undefined') {
342
+ const safeHelperRef = parseRepoUrl;
343
+
344
+ env.addFilter('parseRepoUrl', repoUrl => {
345
+ return JSON.parse(safeHelperRef(repoUrl))
346
+ });
347
+ env.addFilter('projectSlug', repoUrl => {
348
+ const { owner, repo } = JSON.parse(safeHelperRef(repoUrl));
349
+ return owner + '/' + repo;
350
+ });
351
+ }
352
+
353
+ let uninstallCompat = undefined;
354
+
355
+ function render(str, values) {
356
+ try {
357
+ if (uninstallCompat) {
358
+ uninstallCompat();
359
+ uninstallCompat = undefined;
360
+ }
361
+ return env.renderString(str, JSON.parse(values));
362
+ } catch (error) {
363
+ // Make sure errors don't leak anything
364
+ throw new Error(String(error.message));
365
+ }
366
+ }
367
+
368
+ function renderCompat(str, values) {
369
+ try {
370
+ if (!uninstallCompat) {
371
+ uninstallCompat = module.exports.installJinjaCompat();
372
+ }
373
+ return compatEnv.renderString(str, JSON.parse(values));
374
+ } catch (error) {
375
+ // Make sure errors don't leak anything
376
+ throw new Error(String(error.message));
377
+ }
378
+ }
379
+
380
+ return { render, renderCompat };
381
+ })();
382
+ `;
383
+ class SecureTemplater {
384
+ static async loadRenderer(options = {}) {
385
+ const {parseRepoUrl, cookiecutterCompat} = options;
386
+ let sandbox = void 0;
387
+ if (parseRepoUrl) {
388
+ sandbox = {
389
+ parseRepoUrl: (url) => JSON.stringify(parseRepoUrl(url))
390
+ };
391
+ }
392
+ const vm = new vm2.VM({sandbox});
393
+ const nunjucksSource = await fs__default['default'].readFile(backendCommon.resolvePackagePath("@backstage/plugin-scaffolder-backend", "assets/nunjucks.js.txt"), "utf-8");
394
+ vm.run(mkScript(nunjucksSource));
395
+ const render = (template, values) => {
396
+ if (!vm) {
397
+ throw new Error("SecureTemplater has not been initialized");
398
+ }
399
+ vm.setGlobal("templateStr", template);
400
+ vm.setGlobal("templateValues", JSON.stringify(values));
401
+ if (cookiecutterCompat) {
402
+ return vm.run(`renderCompat(templateStr, templateValues)`);
403
+ }
404
+ return vm.run(`render(templateStr, templateValues)`);
405
+ };
406
+ return render;
407
+ }
408
+ }
409
+
317
410
  function createFetchTemplateAction(options) {
318
411
  const {reader, integrations} = options;
319
412
  return createTemplateAction({
@@ -364,7 +457,7 @@ function createFetchTemplateAction(options) {
364
457
  var _a;
365
458
  ctx.logger.info("Fetching template content from remote URL");
366
459
  const workDir = await ctx.createTemporaryDirectory();
367
- const templateDir = path.resolve(workDir, "template");
460
+ const templateDir = backendCommon.resolveSafeChildPath(workDir, "template");
368
461
  const targetPath = (_a = ctx.input.targetPath) != null ? _a : "./";
369
462
  const outputDir = backendCommon.resolveSafeChildPath(ctx.workspacePath, targetPath);
370
463
  if (ctx.input.copyWithoutRender && !Array.isArray(ctx.input.copyWithoutRender)) {
@@ -400,23 +493,14 @@ function createFetchTemplateAction(options) {
400
493
  onlyFiles: false,
401
494
  markDirectories: true
402
495
  })))).flat());
403
- const templater = nunjucks__default['default'].configure({
404
- ...ctx.input.cookiecutterCompat ? {} : {
405
- tags: {
406
- variableStart: "${{",
407
- variableEnd: "}}"
408
- }
409
- },
410
- autoescape: false
411
- });
412
- if (ctx.input.cookiecutterCompat) {
413
- templater.addFilter("jsonify", templater.getFilter("dump"));
414
- }
415
496
  const {cookiecutterCompat, values} = ctx.input;
416
497
  const context = {
417
498
  [cookiecutterCompat ? "cookiecutter" : "values"]: values
418
499
  };
419
500
  ctx.logger.info(`Processing ${allEntriesInTemplate.length} template files/directories with input values`, ctx.input.values);
501
+ const renderTemplate = await SecureTemplater.loadRenderer({
502
+ cookiecutterCompat: ctx.input.cookiecutterCompat
503
+ });
420
504
  for (const location of allEntriesInTemplate) {
421
505
  let renderFilename;
422
506
  let renderContents;
@@ -431,9 +515,9 @@ function createFetchTemplateAction(options) {
431
515
  renderFilename = renderContents = !nonTemplatedEntries.has(location);
432
516
  }
433
517
  if (renderFilename) {
434
- localOutputPath = templater.renderString(localOutputPath, context);
518
+ localOutputPath = renderTemplate(localOutputPath, context);
435
519
  }
436
- const outputPath = path.resolve(outputDir, localOutputPath);
520
+ const outputPath = backendCommon.resolveSafeChildPath(outputDir, localOutputPath);
437
521
  if (outputDir === outputPath) {
438
522
  continue;
439
523
  }
@@ -444,7 +528,7 @@ function createFetchTemplateAction(options) {
444
528
  ctx.logger.info(`Writing directory ${location} to template output path.`);
445
529
  await fs__default['default'].ensureDir(outputPath);
446
530
  } else {
447
- const inputFilePath = path.resolve(templateDir, location);
531
+ const inputFilePath = backendCommon.resolveSafeChildPath(templateDir, location);
448
532
  if (await isbinaryfile.isBinaryFile(inputFilePath)) {
449
533
  ctx.logger.info(`Copying binary file ${location} to template output path.`);
450
534
  await fs__default['default'].copy(inputFilePath, outputPath);
@@ -452,7 +536,7 @@ function createFetchTemplateAction(options) {
452
536
  const statsObj = await fs__default['default'].stat(inputFilePath);
453
537
  ctx.logger.info(`Writing file ${location} to template output path with mode ${statsObj.mode}.`);
454
538
  const inputFileContents = await fs__default['default'].readFile(inputFilePath, "utf-8");
455
- await fs__default['default'].outputFile(outputPath, renderContents ? templater.renderString(inputFileContents, context) : inputFileContents, {mode: statsObj.mode});
539
+ await fs__default['default'].outputFile(outputPath, renderContents ? renderTemplate(inputFileContents, context) : inputFileContents, {mode: statsObj.mode});
456
540
  }
457
541
  }
458
542
  }
@@ -1400,7 +1484,7 @@ const createPublishGithubPullRequestAction = ({
1400
1484
  dot: true
1401
1485
  });
1402
1486
  const fileContents = await Promise.all(localFilePaths.map((filePath) => {
1403
- const absPath = path__default['default'].resolve(fileRoot, filePath);
1487
+ const absPath = backendCommon.resolveSafeChildPath(fileRoot, filePath);
1404
1488
  const base64EncodedContent = fs__default['default'].readFileSync(absPath).toString("base64");
1405
1489
  const fileStat = fs__default['default'].statSync(absPath);
1406
1490
  const githubTreeItemMode = isExecutable(fileStat.mode) ? "100755" : "100644";
@@ -2270,35 +2354,27 @@ const createStepLogger = ({
2270
2354
  class NunjucksWorkflowRunner {
2271
2355
  constructor(options) {
2272
2356
  this.options = options;
2273
- this.nunjucksOptions = {
2357
+ }
2358
+ isSingleTemplateString(input) {
2359
+ var _a, _b;
2360
+ const {parser, nodes} = nunjucks__default['default'];
2361
+ const parsed = parser.parse(input, {}, {
2274
2362
  autoescape: false,
2275
2363
  tags: {
2276
2364
  variableStart: "${{",
2277
2365
  variableEnd: "}}"
2278
2366
  }
2279
- };
2280
- this.nunjucks = nunjucks__default['default'].configure(this.nunjucksOptions);
2281
- this.nunjucks.addFilter("parseRepoUrl", (repoUrl) => {
2282
- return parseRepoUrl(repoUrl, this.options.integrations);
2283
- });
2284
- this.nunjucks.addFilter("projectSlug", (repoUrl) => {
2285
- const {owner, repo} = parseRepoUrl(repoUrl, this.options.integrations);
2286
- return `${owner}/${repo}`;
2287
2367
  });
2368
+ return parsed.children.length === 1 && !(((_b = (_a = parsed.children[0]) == null ? void 0 : _a.children) == null ? void 0 : _b[0]) instanceof nodes.TemplateData);
2288
2369
  }
2289
- isSingleTemplateString(input) {
2290
- const {parser, nodes} = require("nunjucks");
2291
- const parsed = parser.parse(input, {}, this.nunjucksOptions);
2292
- return parsed.children.length === 1 && !(parsed.children[0] instanceof nodes.TemplateData);
2293
- }
2294
- render(input, context) {
2370
+ render(input, context, renderTemplate) {
2295
2371
  return JSON.parse(JSON.stringify(input), (_key, value) => {
2296
2372
  try {
2297
2373
  if (typeof value === "string") {
2298
2374
  try {
2299
2375
  if (this.isSingleTemplateString(value)) {
2300
2376
  const wrappedDumped = value.replace(/\${{(.+)}}/g, "${{ ( $1 ) | dump }}");
2301
- const templated2 = this.nunjucks.renderString(wrappedDumped, context);
2377
+ const templated2 = renderTemplate(wrappedDumped, context);
2302
2378
  if (templated2 === "") {
2303
2379
  return void 0;
2304
2380
  }
@@ -2307,7 +2383,7 @@ class NunjucksWorkflowRunner {
2307
2383
  } catch (ex) {
2308
2384
  this.options.logger.error(`Failed to parse template string: ${value} with error ${ex.message}`);
2309
2385
  }
2310
- const templated = this.nunjucks.renderString(value, context);
2386
+ const templated = renderTemplate(value, context);
2311
2387
  if (templated === "") {
2312
2388
  return void 0;
2313
2389
  }
@@ -2325,6 +2401,12 @@ class NunjucksWorkflowRunner {
2325
2401
  throw new errors.InputError("Wrong template version executed with the workflow engine");
2326
2402
  }
2327
2403
  const workspacePath = path__default['default'].join(this.options.workingDirectory, await task.getWorkspaceName());
2404
+ const {integrations} = this.options;
2405
+ const renderTemplate = await SecureTemplater.loadRenderer({
2406
+ parseRepoUrl(url) {
2407
+ return parseRepoUrl(url, integrations);
2408
+ }
2409
+ });
2328
2410
  try {
2329
2411
  await fs__default['default'].ensureDir(workspacePath);
2330
2412
  await task.emitLog(`Starting up task with ${task.spec.steps.length} steps`);
@@ -2335,7 +2417,7 @@ class NunjucksWorkflowRunner {
2335
2417
  for (const step of task.spec.steps) {
2336
2418
  try {
2337
2419
  if (step.if) {
2338
- const ifResult = await this.render(step.if, context);
2420
+ const ifResult = await this.render(step.if, context, renderTemplate);
2339
2421
  if (!isTruthy(ifResult)) {
2340
2422
  await task.emitLog(`Skipping step ${step.id} because it's if condition was false`, {stepId: step.id, status: "skipped"});
2341
2423
  continue;
@@ -2347,7 +2429,7 @@ class NunjucksWorkflowRunner {
2347
2429
  });
2348
2430
  const action = this.options.actionRegistry.get(step.action);
2349
2431
  const {taskLogger, streamLogger} = createStepLogger({task, step});
2350
- const input = (_a = step.input && this.render(step.input, context)) != null ? _a : {};
2432
+ const input = (_a = step.input && this.render(step.input, context, renderTemplate)) != null ? _a : {};
2351
2433
  if ((_b = action.schema) == null ? void 0 : _b.input) {
2352
2434
  const validateResult = jsonschema.validate(input, action.schema.input);
2353
2435
  if (!validateResult.valid) {
@@ -2392,7 +2474,7 @@ class NunjucksWorkflowRunner {
2392
2474
  throw err;
2393
2475
  }
2394
2476
  }
2395
- const output = this.render(task.spec.output, context);
2477
+ const output = this.render(task.spec.output, context, renderTemplate);
2396
2478
  return {output};
2397
2479
  } finally {
2398
2480
  if (workspacePath) {