From 74d76f3c6a652029b03290f51d19d32d12f31eda Mon Sep 17 00:00:00 2001 From: John Eke <63303283+johneke-auth0@users.noreply.github.com> Date: Fri, 26 Jan 2024 12:07:20 -0500 Subject: [PATCH] chore: add template schema validation github action (#4) --- .github/workflows/validators.yml | 82 ++++++++++ package-lock.json | 73 +++++++++ package.json | 25 ++++ scripts/templates/validate-templates.js | 140 ++++++++++++++++++ .../code.js | 36 +++++ .../manifest.yaml | 15 ++ 6 files changed, 371 insertions(+) create mode 100644 .github/workflows/validators.yml create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/templates/validate-templates.js create mode 100644 templates/active-directory-groups-PASSWORD_RESET_POST_CHALLENGE/code.js create mode 100644 templates/active-directory-groups-PASSWORD_RESET_POST_CHALLENGE/manifest.yaml diff --git a/.github/workflows/validators.yml b/.github/workflows/validators.yml new file mode 100644 index 0000000..cfe71f5 --- /dev/null +++ b/.github/workflows/validators.yml @@ -0,0 +1,82 @@ +name: Templates Validation + +on: + pull_request: + types: + - opened + - synchronize + +jobs: + install-cache: + runs-on: ubuntu-latest + name: Install & Cache modules + steps: + - uses: actions/checkout@v3 + - name: Cache node modules + uses: actions/cache@v3 + env: + cache-name: cache-node-modules + with: + path: | + node_modules + key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-npm- + - uses: actions/setup-node@v3 + with: + node-version: '18.x' + always-auth: true + - name: Install dependencies + if: steps.cache-dependencies.outputs.cache-hit != true + run: npm ci + + lint: + needs: install-cache + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Use node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + - name: Restore Cached Dependencies + uses: actions/cache@v3 + id: cache-dependencies + with: + path: | + node_modules + key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-npm- + - name: Lint Repo + run: npm run pretty:check:templates + + validate: + needs: lint + name: Validate Schema + runs-on: ubuntu-latest + + steps: + - name: Check Out Repository + uses: actions/checkout@v4 + + - name: Set Up Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Restore Cached Dependencies + uses: actions/cache@v3 + id: cache-dependencies + with: + path: | + node_modules + key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-npm- + + - name: Validate Templates + run: npm run validate:templates diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..bc649af --- /dev/null +++ b/package-lock.json @@ -0,0 +1,73 @@ +{ + "name": "opensource-marketplace", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "opensource-marketplace", + "version": "0.0.1", + "license": "Apache-2.0", + "devDependencies": { + "js-yaml": "^4.1.0", + "prettier": "^3.0.3", + "zod": "^3.22.4", + "zod-validation-error": "^2.1.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/prettier": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", + "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-2.1.0.tgz", + "integrity": "sha512-VJh93e2wb4c3tWtGgTa0OF/dTt/zoPCPzXq4V11ZjxmEAFaPi/Zss1xIZdEB5RD8GD00U0/iVXgqkF77RV7pdQ==", + "dev": true, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.18.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b6ac5b9 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "opensource-marketplace", + "version": "0.0.1", + "description": "A collection 3rd-party code that's used to manage the open source marketplace contributions.", + "scripts": { + "pretty:check:templates": "prettier --check ./templates", + "validate:templates": "node ./scripts/templates/validate-templates.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/auth0/opensource-marketplace.git" + }, + "author": "", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/auth0/opensource-marketplace/issues" + }, + "homepage": "https://github.com/auth0/opensource-marketplace#readme", + "devDependencies": { + "js-yaml": "^4.1.0", + "prettier": "^3.0.3", + "zod": "^3.22.4", + "zod-validation-error": "^2.1.0" + } +} diff --git a/scripts/templates/validate-templates.js b/scripts/templates/validate-templates.js new file mode 100644 index 0000000..214c28b --- /dev/null +++ b/scripts/templates/validate-templates.js @@ -0,0 +1,140 @@ +const yamlParser = require('js-yaml'); +const fs = require('fs'); +const path = require('path'); +const { z } = require('zod'); +const { fromZodError } = require('zod-validation-error'); + +// Changes here must be reflected here: +// https://github.com/auth0/managed-marketplace/blob/main/prisma/schema.prisma#L198 +const IntegrationTrigger = [ + 'POST_LOGIN', + 'CREDENTIALS_EXCHANGE', + 'PRE_USER_REGISTRATION', + 'POST_USER_REGISTRATION', + 'POST_CHANGE_PASSWORD', + 'SEND_PHONE_MESSAGE', + 'IGA_APPROVAL', + 'IGA_CERTIFICATION', + 'IGA_FULFILLMENT_ASSIGNMENT', + 'IGA_FULFILLMENT_EXECUTION', + 'PASSWORD_RESET_POST_CHALLENGE', +]; + +const UseCase = [ + 'MULTIFACTOR', + 'ACTION_FEATURE', + 'ENRICH_PROFILE', + 'ACCESS_CONTROL', +]; + +const configValue = z.object({ + label: z.string().min(2), + defaultValue: z.string().min(2), +}); +const moduleValue = z.object({ + name: z.string().min(2), + version: z.string().min(2), +}); + +const TemplateSchema = z + .object({ + id: z.string().uuid(), + name: z.string().min(3), + triggers: z.array(z.enum(IntegrationTrigger)), + useCases: z.array(z.enum(UseCase)), + public: z.boolean().optional(), + published: z.boolean().optional(), + deleted: z.boolean().optional(), + description: z.string().min(3), + version: z.string().optional(), + runtime: z.string().optional(), + secrets: z.array(configValue).optional(), + config: z.array(configValue).optional(), + sourceUrl: z.string().url(), + code: z.string().min(3), + modules: z.array(moduleValue).optional(), + notes: z.string().optional(), + }) + .strict(); + +function templateDirs() { + const dir = path.normalize(`${__dirname}/../../templates`); + + // Read the contents of the directory + const items = fs.readdirSync(dir); + + // Filter out only directories (folders) + return items + .map((item) => `${dir}/${item}`) + .filter((item) => { + return fs.statSync(item).isDirectory(); + }); +} + +const templateToJSON = async (templateDir) => { + const yaml = yamlParser.load( + fs.readFileSync(`${templateDir}/manifest.yaml`, 'utf8') + ); + + const code = fs.readFileSync(`${templateDir}/code.js`, 'utf8'); + + return { + ...yaml, + code, + }; +}; + +const validateTemplates = async () => { + console.log('\nšŸ—„ļø Validating schema for all templates.\n'); + try { + // GET ALL TEMPLATES + let templates; + try { + templates = templateDirs(); + } catch (e) { + throw { + detail: 'failed to load templates', + err: e, + }; + } + + // BUNDLE INTO JSON + for (const templatePath of templates) { + console.log('šŸ—„ļø Validating template:', templatePath); + + let template; + try { + template = await templateToJSON(templatePath); + } catch (e) { + throw { + detail: `failed to load template: ${templatePath}`, + err: e, + }; + } + + // VALIDATE JSON + try { + TemplateSchema.parse(template); + } catch (e) { + throw { + detail: `template validation failed, template: ${templatePath}`, + err: fromZodError(e), + }; + } + } + } catch (e) { + console.error( + 'ā›”ļø - There was an error validatong templates:', + e.detail, + '\n\n', + e.err + ); + process.exit(1); + } + + console.log('\nāœ… Validation complete.\n'); +}; + +(async function () { + await validateTemplates(); +})(); diff --git a/templates/active-directory-groups-PASSWORD_RESET_POST_CHALLENGE/code.js b/templates/active-directory-groups-PASSWORD_RESET_POST_CHALLENGE/code.js new file mode 100644 index 0000000..2a0ddb3 --- /dev/null +++ b/templates/active-directory-groups-PASSWORD_RESET_POST_CHALLENGE/code.js @@ -0,0 +1,36 @@ +/** + * Handler that will be called during the execution of a Password Reset / Post Challenge Flow. + * + * --- AUTH0 ACTIONS TEMPLATE https://github.com/auth0/os-marketplace/blob/main/templates/active-directory-groups-PASSWORD_RESET_POST_CHALLENGE --- + * + * @param {Event} event - Details about the post challenge request. + * @param {PasswordResetPostChallengeAPI} api - Interface whose methods can be used to change the behavior of the post challenge flow. + */ +exports.onExecutePostChallenge = async (event, api) => { + // ensure that the allowed group is configured + const groupAllowed = event.secrets.ALLOWED_GROUP; + if (!groupAllowed) { + return api.access.deny('Invalid configuration'); + } + + // get the users groups + let groups = event.user.groups || []; + if (!Array.isArray(groups)) { + groups = [groups]; + } + + // if the allowed group is not one of the users, deny access + if (!groups.includes(groupAllowed)) { + return api.access.deny('Access denied'); + } +}; + +/** + * Handler that will be invoked when this action is resuming after an external redirect. If your + * onExecutePostChallenge function does not perform a redirect, this function can be safely ignored. + * + * @param {Event} event - Details about the user and the context in which they are logging in. + * @param {PasswordResetPostChallengeAPI} api - Interface whose methods can be used to change the behavior of the post challenge flow. + */ +// exports.onContinuePostChallenge = async (event, api) => { +// }; diff --git a/templates/active-directory-groups-PASSWORD_RESET_POST_CHALLENGE/manifest.yaml b/templates/active-directory-groups-PASSWORD_RESET_POST_CHALLENGE/manifest.yaml new file mode 100644 index 0000000..e5508d0 --- /dev/null +++ b/templates/active-directory-groups-PASSWORD_RESET_POST_CHALLENGE/manifest.yaml @@ -0,0 +1,15 @@ +id: 'c9fa1544-b8bd-4211-950e-3c6ba2a946f4' +name: 'Check if a user belongs to an active directory group.' +description: 'Check if a user belongs to an AD group and if not, deny access.' +public: true +triggers: + - 'PASSWORD_RESET_POST_CHALLENGE' +runtime: 'node18' +modules: [] +sourceUrl: 'https://github.com/auth0/os-marketplace/blob/main/templates/active-directory-groups-PASSWORD_RESET_POST_CHALLENGE' +notes: | + **Secrets** + + * `ALLOWED_GROUP` - the name of the allowed group. +useCases: + - 'ACCESS_CONTROL'