Skip to content

Commit

Permalink
Update the UI to fix config parsing (#14)
Browse files Browse the repository at this point in the history
* Update the UI to fix config parsing

* Bump SDK

* Only show config if enabled

* hide and show config with disable/enable buttons

* lets go

* improve logs
  • Loading branch information
estephinson authored Aug 24, 2023
1 parent 815f72d commit 080c4c1
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 104 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"dependencies": {
"@netlify/plugin-csp-nonce": "^1.2.1",
"@netlify/sdk": "^0.7.9",
"@netlify/sdk": "^0.8.1",
"typescript": "^5.1.6"
}
}
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from "fs";

export const onPreBuild = async ({ inputs, netlifyConfig, utils }) => {
const config = JSON.stringify(inputs, null, 2);
export const onPreBuild = async ({ config, netlifyConfig, utils }) => {
const configString = JSON.stringify(config, null, 2);
const { build } = netlifyConfig;

// CSP_NONCE_DISTRIBUTION is a number from 0 to 1,
Expand All @@ -23,15 +23,15 @@ export const onPreBuild = async ({ inputs, netlifyConfig, utils }) => {
// make the directory in case it actually doesn't exist yet
await utils.run.command(`mkdir -p ${edgeFunctionsDir}`);

fs.writeFileSync(`${edgeFunctionsDir}/__csp-nonce-inputs.json`, config);
fs.writeFileSync(`${edgeFunctionsDir}/__csp-nonce-inputs.json`, configString);
console.log(` Writing nonce edge function to ${edgeFunctionsDir}...`);
const nonceSource =
".netlify/plugins/node_modules/@netlify/plugin-csp-nonce/src/__csp-nonce.ts";
const nonceDest = `${edgeFunctionsDir}/__csp-nonce.ts`;
fs.copyFileSync(nonceSource, nonceDest);

// if no reportUri in config input, deploy function on site's behalf
if (!inputs.reportUri) {
if (!config.reportUri) {
const functionsDir = build.functions || "./netlify/functions";
// make the directory in case it actually doesn't exist yet
await utils.run.command(`mkdir -p ${functionsDir}`);
Expand All @@ -41,7 +41,7 @@ export const onPreBuild = async ({ inputs, netlifyConfig, utils }) => {
const violationsDest = `${functionsDir}/__csp-violations.ts`;
fs.copyFileSync(violationsSource, violationsDest);
} else {
console.log(` Using ${inputs.reportUri} as report-uri directive...`);
console.log(` Using ${config.reportUri} as report-uri directive...`);
}

console.log(` Done.`);
Expand Down
148 changes: 97 additions & 51 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,97 @@
// Documentation: https://github.com/netlify/sdk
import { NetlifyIntegration } from "@netlify/sdk";
import { NetlifyIntegration, z } from "@netlify/sdk";
import { onPreBuild } from "./hooks";

type SiteConfig = {
buildHook?: {
url: string;
id: string;
};
cspConfig?: {
reportOnly: boolean;
reportUri: string;
unsafeEval: boolean;
path: string | string[];
excludedPath: string[];
};
};

type BuildContext = {
inputs?: SiteConfig["cspConfig"];
};

const integration = new NetlifyIntegration<SiteConfig, any, BuildContext>();

integration.addBuildHook("onPreBuild", ({ buildContext, ...opts }) => {
// We lean on this favoured order of precedence:
// 1. Incoming hook body
// 2. Build context
// 3. Plugin options - realistically won't ever be called in this context
let { inputs } = buildContext ?? opts ?? {};

if (process.env.INCOMING_HOOK_BODY) {
console.log("Using temporary config from test build.");
inputs = JSON.parse(process.env.INCOMING_HOOK_BODY);
} else {
console.log("Using stored CSP config.");
}
const siteConfigSchema = z.object({
buildHook: z
.object({
url: z.string(),
id: z.string(),
})
.optional(),
cspConfig: z
.object({
reportOnly: z.boolean(),
reportUri: z.string(),
unsafeEval: z.boolean(),
path: z.string().array(),
excludedPath: z.string().array(),
})
.optional(),
});

if (inputs) {
inputs.reportOnly = inputs.reportOnly === "true" ? true : false;
inputs.unsafeEval = inputs.unsafeEval === "true" ? true : false;
}
const buildConfigSchema = z.object({
reportOnly: z.boolean().optional(),
reportUri: z.string().optional(),
unsafeEval: z.boolean().optional(),
path: z.string().array().optional(),
excludedPath: z.string().array().optional(),
});

const newOpts = {
...opts,
inputs,
};
const buildContextSchema = z.object({
config: siteConfigSchema.shape.cspConfig,
});

return onPreBuild(newOpts);
const integration = new NetlifyIntegration({
siteConfigSchema,
buildConfigSchema,
buildContextSchema,
});

integration.addBuildContext(async ({ site_config }) => {
integration.addBuildEventHandler(
"onPreBuild",
({ buildContext, netlifyConfig, utils, ...opts }) => {
// We lean on this favoured order of precedence:
// 1. Incoming hook body
// 2. Build context
// 3. Plugin options - realistically won't ever be called in this context

let { config } = buildContext ?? opts ?? {};

if (process.env.INCOMING_HOOK_BODY) {
console.log("Using temporary config from test build.");
try {
const hookBody = JSON.parse(process.env.INCOMING_HOOK_BODY);
config = integration._buildConfigurationSchema.parse(hookBody);
} catch (e) {
console.warn("Failed to parse incoming hook body.");
console.log(e);
}
} else {
if (!config) {
console.log();
config = {
reportOnly: true,
reportUri: "",
unsafeEval: true,
path: ["/*"],
excludedPath: [],
};
console.log("Using default CSP config.");
} else {
console.log("Using stored CSP config.");
}
}

const newOpts = {
...opts,
netlifyConfig,
utils,
config,
};

return onPreBuild(newOpts);
}
);

integration.addBuildEventContext(async ({ site_config }) => {
if (site_config.cspConfig) {
return {
inputs: site_config.cspConfig,
config: site_config.cspConfig,
};
}
return {};

return undefined;
});

integration.addHandler("get-config", async (_, { client, siteId }) => {
Expand All @@ -75,13 +111,23 @@ integration.addHandler("get-config", async (_, { client, siteId }) => {
integration.addHandler("save-config", async ({ body }, { client, siteId }) => {
console.log(`Saving config for ${siteId}.`);

const config = JSON.parse(body) as SiteConfig["cspConfig"];
const result = integration._siteConfigSchema.shape.cspConfig.safeParse(
JSON.parse(body)
);

if (!result.success) {
return {
statusCode: 400,
body: JSON.stringify(result),
};
}
const { data } = result;

const existingConfig = await client.getSiteIntegration(siteId);

await client.updateSiteIntegration(siteId, {
...existingConfig.config,
cspConfig: config,
cspConfig: data,
});

return {
Expand Down Expand Up @@ -119,7 +165,7 @@ integration.addHandler(
async (_, { client, siteId, teamId }) => {
const { token } = await client.generateBuildToken(siteId);
await client.setBuildToken(teamId, siteId, token);
await client.enableBuildhook(siteId);
await client.enableBuildEventHandlers(siteId);

const { url, id } = await client.createBuildHook(siteId, {
title: "CSP Configuration Tests",
Expand Down Expand Up @@ -149,7 +195,7 @@ integration.addHandler(
},
} = await client.getSiteIntegration(siteId);

await client.disableBuildhook(siteId);
await client.disableBuildEventHandlers(siteId);
await client.removeBuildToken(teamId, siteId);

await client.deleteBuildHook(siteId, buildHookId);
Expand All @@ -171,7 +217,7 @@ integration.onDisable(async ({ queryStringParameters }, { client }) => {
const { id: buildHookId } = buildHook ?? {};

if (buildHookId) {
await client.disableBuildhook(siteId);
await client.disableBuildEventHandlers(siteId);
await client.removeBuildToken(teamId, siteId);

await client.deleteBuildHook(siteId, buildHookId);
Expand Down
Loading

0 comments on commit 080c4c1

Please sign in to comment.