From 0fd8048baacc0b4327c2e370f59495adae5aaad7 Mon Sep 17 00:00:00 2001 From: Shaun Cooley Date: Tue, 19 Sep 2023 13:49:57 -0700 Subject: [PATCH] New CONTRACT_ALLOWLIST option to limit device types/contracts Resolves: #1433 Change-type: minor --- config/confd/conf.d/env.toml | 1 + config/confd/templates/env.tmpl | 1 + src/features/contracts/contracts-directory.ts | 24 ++++++++++++++++++- .../device-types/device-types-list.ts | 16 ++++++++++++- src/lib/config.ts | 17 +++++++++++++ 5 files changed, 57 insertions(+), 2 deletions(-) diff --git a/config/confd/conf.d/env.toml b/config/confd/conf.d/env.toml index 44d08be61..e69f93b54 100644 --- a/config/confd/conf.d/env.toml +++ b/config/confd/conf.d/env.toml @@ -29,6 +29,7 @@ keys = [ "IMAGE_STORAGE_FORCE_PATH_STYLE", "IMAGE_STORAGE_PREFIX", "IMAGE_STORAGE_SECRET_KEY", + "CONTRACT_ALLOWLIST", "JSON_WEB_TOKEN_EXPIRY_MINUTES", "JSON_WEB_TOKEN_SECRET", "MIXPANEL_TOKEN", diff --git a/config/confd/templates/env.tmpl b/config/confd/templates/env.tmpl index 123bbdbdb..55d261d5f 100644 --- a/config/confd/templates/env.tmpl +++ b/config/confd/templates/env.tmpl @@ -8,6 +8,7 @@ COOKIE_SESSION_SECRET={{getenv "COOKIE_SESSION_SECRET"}} {{if getenv "CONTRACTS_PRIVATE_REPO_NAME"}}CONTRACTS_PRIVATE_REPO_NAME={{getenv "CONTRACTS_PRIVATE_REPO_NAME"}}{{end}} {{if getenv "CONTRACTS_PRIVATE_REPO_BRANCH"}}CONTRACTS_PRIVATE_REPO_BRANCH"CONTRACTS_PRIVATE_REPO_BRANCH"}}{{end}} {{if getenv "CONTRACTS_PRIVATE_REPO_TOKEN"}}CONTRACTS_PRIVATE_REPO_TOKEN={{getenv "CONTRACTS_PRIVATE_REPO_TOKEN"}}{{end}} +{{if getenv "CONTRACT_ALLOWLIST"}}CONTRACT_ALLOWLIST={{getenv "CONTRACT_ALLOWLIST"}}{{end}} DATABASE_URL=postgres://{{getenv "DB_USER"}}:{{getenv "DB_PASSWORD"}}@{{getenv "DB_HOST"}}:{{getenv "DB_PORT"}}/{{getenv "DB_NAME" "resin"}} DB_HOST={{getenv "DB_HOST"}} DB_PASSWORD={{getenv "DB_PASSWORD"}} diff --git a/src/features/contracts/contracts-directory.ts b/src/features/contracts/contracts-directory.ts index 02c8fa8e1..2d40e7154 100644 --- a/src/features/contracts/contracts-directory.ts +++ b/src/features/contracts/contracts-directory.ts @@ -11,6 +11,7 @@ import validator from 'validator'; import type { RepositoryInfo, Contract } from './index'; import { getBase64DataUri } from '../../lib/utils'; import { captureException } from '../../infra/error-handling'; +import { CONTRACT_ALLOWLIST } from '../../lib/config'; const pipeline = util.promisify(stream.pipeline); const exists = util.promisify(fs.exists); @@ -177,13 +178,34 @@ export const getContracts = async (type: string): Promise => { return []; } - const contractFiles = await glob( + let contractFiles = await glob( `${CONTRACTS_BASE_DIR}/**/contracts/${type}/**/*.json`, ); if (!contractFiles.length) { return []; } + // If there are explicit includes, then everything else is excluded so we need to + // filter the contractFiles list to include only contracts that are in the CONTRACT_ALLOWLIST map + if (CONTRACT_ALLOWLIST.size > 0) { + const slugRegex = new RegExp(`/contracts/(${_.escapeRegExp(type)}/[^/]+)/`); + const before = contractFiles.length; + contractFiles = contractFiles.filter((file) => { + // Get the contract slug from the file path + const deviceTypeSlug = file.match(slugRegex)?.[1]; + if (!deviceTypeSlug) { + return false; + } + + // Check if this slug is included in the map + return CONTRACT_ALLOWLIST.has(deviceTypeSlug); + }); + + console.log( + `CONTRACT_ALLOWLIST reduced ${type} contract slugs from ${before} to ${contractFiles.length}`, + ); + } + const contracts = await Promise.all( contractFiles.map(async (file) => { let contract; diff --git a/src/features/device-types/device-types-list.ts b/src/features/device-types/device-types-list.ts index 9197c5337..32ef61e59 100644 --- a/src/features/device-types/device-types-list.ts +++ b/src/features/device-types/device-types-list.ts @@ -14,6 +14,7 @@ import { multiCacheMemoizee } from '../../infra/cache'; import { DEVICE_TYPES_CACHE_LOCAL_TIMEOUT, DEVICE_TYPES_CACHE_TIMEOUT, + CONTRACT_ALLOWLIST, } from '../../lib/config'; export interface DeviceTypeInfo { @@ -58,7 +59,20 @@ const getFirstValidBuild = async ( export const getDeviceTypes = multiCacheMemoizee( async (): Promise> => { const result: Dictionary = {}; - const slugs = await listFolders(IMAGE_STORAGE_PREFIX); + let slugs = await listFolders(IMAGE_STORAGE_PREFIX); + + // If there are explicit includes, then everything else is excluded so we need to + // filter the slugs list to include only contracts that are in the CONTRACT_ALLOWLIST map + if (CONTRACT_ALLOWLIST.size > 0) { + const before = slugs.length; + slugs = slugs.filter((slug) => + CONTRACT_ALLOWLIST.has(`hw.device-type/${slug}`), + ); + console.log( + `CONTRACT_ALLOWLIST reduced device type slugs from ${before} to ${slugs.length}`, + ); + } + await Promise.all( slugs.map(async (slug) => { try { diff --git a/src/lib/config.ts b/src/lib/config.ts index 25ce84ea2..abbe92972 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -82,6 +82,23 @@ export const AUTH_RESINOS_REGISTRY_CODE = optionalVar( 'AUTH_RESINOS_REGISTRY_CODE', ); export const COOKIE_SESSION_SECRET = requiredVar('COOKIE_SESSION_SECRET'); + +/** + * null: include all device type and device contract slugs + * "x;y;z": include only the specified device type and contract slugs - note that you MUST list + * all dependent slugs as well so for hw.device-type/asus-tinker-board-s you would need: + * `arch.sw/armv7hf;hw.device-manufacturer/asus;hw.device-family/tinkerboard;hw.device-type/asus-tinker-board-s` + * For something like hw.device-type/iot-gate-imx8 you would need: + * `arch.sw/aarch64;hw.device-type/iot-gate-imx8` + * (the order of the slugs in this variable does not matter) + */ +export const CONTRACT_ALLOWLIST = new Set( + optionalVar('CONTRACT_ALLOWLIST', '') + .split(';') + .map((slug) => slug.trim()) + .filter((slug) => slug.length > 0), +); + export const CONTRACTS_PUBLIC_REPO_OWNER = optionalVar( 'CONTRACTS_PUBLIC_REPO_OWNER', 'balena-io',