diff --git a/lib/start-proxy-action.js b/lib/start-proxy-action.js index 9ca4c44d7..79679c358 100644 --- a/lib/start-proxy-action.js +++ b/lib/start-proxy-action.js @@ -47749,12 +47749,12 @@ var require_concat_map = __commonJS({ var res = []; for (var i = 0; i < xs.length; i++) { var x = fn(xs[i], i); - if (isArray(x)) res.push.apply(res, x); + if (isArray2(x)) res.push.apply(res, x); else res.push(x); } return res; }; - var isArray = Array.isArray || function(xs) { + var isArray2 = Array.isArray || function(xs) { return Object.prototype.toString.call(xs) === "[object Array]"; }; } @@ -52097,8 +52097,8 @@ var require_object = __commonJS({ "node_modules/@typespec/ts-http-runtime/dist/commonjs/util/object.js"(exports2) { "use strict"; Object.defineProperty(exports2, "__esModule", { value: true }); - exports2.isObject = isObject2; - function isObject2(input) { + exports2.isObject = isObject3; + function isObject3(input) { return typeof input === "object" && input !== null && !Array.isArray(input) && !(input instanceof RegExp) && !(input instanceof Date); } } @@ -56584,7 +56584,7 @@ var require_commonjs4 = __commonJS({ exports2.computeSha256Hmac = computeSha256Hmac; exports2.getRandomIntegerInclusive = getRandomIntegerInclusive; exports2.isError = isError; - exports2.isObject = isObject2; + exports2.isObject = isObject3; exports2.randomUUID = randomUUID; exports2.uint8ArrayToString = uint8ArrayToString; exports2.stringToUint8Array = stringToUint8Array; @@ -56631,7 +56631,7 @@ var require_commonjs4 = __commonJS({ function isError(e) { return tspRuntime.isError(e); } - function isObject2(input) { + function isObject3(input) { return tspRuntime.isObject(input); } function randomUUID() { @@ -121350,6 +121350,23 @@ function isAuthToken(value, patterns = GITHUB_TOKEN_PATTERNS) { return void 0; } +// src/json/index.ts +function parseString(data) { + return JSON.parse(data); +} +function isObject2(value) { + return typeof value === "object"; +} +function isArray(value) { + return Array.isArray(value); +} +function isString(value) { + return typeof value === "string"; +} +function isStringOrUndefined(value) { + return value === void 0 || isString(value); +} + // src/languages.ts var KnownLanguage = /* @__PURE__ */ ((KnownLanguage2) => { KnownLanguage2["actions"] = "actions"; @@ -121373,10 +121390,10 @@ function isUsernamePassword(config) { return hasUsername(config) && "password" in config; } function isToken(config) { - return "token" in config; + return "token" in config && isStringOrUndefined(config.token); } function isAzureConfig(config) { - return "tenant_id" in config && "client_id" in config && isDefined2(config.tenant_id) && isDefined2(config.client_id); + return "tenant_id" in config && "client_id" in config && isDefined2(config.tenant_id) && isDefined2(config.client_id) && isString(config.tenant_id) && isString(config.client_id); } function isAWSConfig(config) { const requiredProperties = [ @@ -121387,14 +121404,23 @@ function isAWSConfig(config) { "domain_owner" ]; for (const property of requiredProperties) { - if (!(property in config) || !isDefined2(config[property])) { + if (!(property in config) || !isDefined2(config[property]) || !isString(config[property])) { return false; } } + if ("audience" in config && !isStringOrUndefined(config.audience)) { + return false; + } return true; } function isJFrogConfig(config) { - return "jfrog_oidc_provider_name" in config && isDefined2(config.jfrog_oidc_provider_name); + if ("audience" in config && !isStringOrUndefined(config.audience)) { + return false; + } + if ("identity_mapping_name" in config && !isStringOrUndefined(config.identity_mapping_name)) { + return false; + } + return "jfrog_oidc_provider_name" in config && isDefined2(config.jfrog_oidc_provider_name) && isString(config.jfrog_oidc_provider_name); } function credentialToStr(credential) { let result = `Type: ${credential.type};`; @@ -121829,12 +121855,12 @@ var NEW_LANGUAGE_TO_REGISTRY_TYPE = { go: ["goproxy_server", "git_source"] }; function getRegistryAddress(registry) { - if (isDefined2(registry.url)) { + if (isDefined2(registry.url) && isString(registry.url) && isStringOrUndefined(registry.host)) { return { url: registry.url, host: registry.host }; - } else if (isDefined2(registry.host)) { + } else if (isDefined2(registry.host) && isString(registry.host)) { return { url: void 0, host: registry.host @@ -121872,12 +121898,18 @@ function getAuthConfig(config) { } return { username: config.username, token: config.token }; } else { - if ("password" in config && isDefined2(config.password)) { + let username = void 0; + let password = void 0; + if ("password" in config && isString(config.password)) { core10.setSecret(config.password); + password = config.password; + } + if ("username" in config && isString(config.username)) { + username = config.username; } return { - username: "username" in config ? config.username : void 0, - password: "password" in config ? config.password : void 0 + username, + password }; } } @@ -121897,22 +121929,22 @@ function getCredentials(logger, registrySecrets, registriesCredentials, language } let parsed; try { - parsed = JSON.parse(credentialsStr); + parsed = parseString(credentialsStr); } catch { logger.error("Failed to parse the credentials data."); throw new ConfigurationError("Invalid credentials format."); } - if (!Array.isArray(parsed)) { + if (!isArray(parsed)) { throw new ConfigurationError( "Expected credentials data to be an array of configurations, but it is not." ); } const out = []; for (const e of parsed) { - if (e === null || typeof e !== "object") { + if (e === null || !isObject2(e)) { throw new ConfigurationError("Invalid credentials - must be an object"); } - if (!isDefined2(e.type)) { + if (!isDefined2(e.type) || !isString(e.type)) { throw new ConfigurationError("Invalid credentials - must have a type"); } const authConfig = getAuthConfig(e); diff --git a/src/start-proxy.ts b/src/start-proxy.ts index b6c02bfa3..6b55cbe45 100644 --- a/src/start-proxy.ts +++ b/src/start-proxy.ts @@ -17,11 +17,11 @@ import { Feature, FeatureEnablement, } from "./feature-flags"; +import * as json from "./json"; import { KnownLanguage } from "./languages"; import { Logger } from "./logging"; import { Address, - RawCredential, Registry, Credential, AuthConfig, @@ -36,6 +36,7 @@ import { JFrogConfig, isUsernamePassword, hasUsername, + RawCredential, } from "./start-proxy/types"; import { ActionName, @@ -267,13 +268,19 @@ const NEW_LANGUAGE_TO_REGISTRY_TYPE: Required = { * * @throws A `ConfigurationError` if the `Registry` value contains neither a `url` or `host` field. */ -function getRegistryAddress(registry: Partial): Address { - if (isDefined(registry.url)) { +function getRegistryAddress( + registry: json.UnvalidatedObject, +): Address { + if ( + isDefined(registry.url) && + json.isString(registry.url) && + json.isStringOrUndefined(registry.host) + ) { return { url: registry.url, host: registry.host, }; - } else if (isDefined(registry.host)) { + } else if (isDefined(registry.host) && json.isString(registry.host)) { return { url: undefined, host: registry.host, @@ -287,7 +294,9 @@ function getRegistryAddress(registry: Partial): Address { } /** Extracts an `AuthConfig` value from `config`. */ -export function getAuthConfig(config: Partial): AuthConfig { +export function getAuthConfig( + config: json.UnvalidatedObject, +): AuthConfig { // Start by checking for the OIDC configurations, since they have required properties // which we can use to identify them. if (isAzureConfig(config)) { @@ -311,25 +320,44 @@ export function getAuthConfig(config: Partial): AuthConfig { audience: config.audience, } satisfies JFrogConfig; } else if (isToken(config)) { - // For token-based authentication, both the token and username are optional. - // If the token is absent, then it doesn't matter if we end up treating it - // as a `UsernamePassword` object internally. + // There are three scenarios for non-OIDC authentication based on the registry type: + // + // 1. `username`+`token` + // 2. A `token` that combines the username and actual token, seperated by ':'. + // 3. `username`+`password` + // + // In all three cases, all fields are optional. If the `token` field is present, + // we accept the configuration as a `Token` typed configuration, with the `token` + // value and an optional `username`. Otherwise, we accept the configuration + // typed as `UsernamePassword` (in the `else` clause below) with optional + // username and password. I.e. a private registry type that uses 1. or 2., + // but has no `token` configured, will get accepted as `UsernamePassword` here. - // Mask token to reduce chance of accidental leakage in logs, if we have one. if (isDefined(config.token)) { + // Mask token to reduce chance of accidental leakage in logs, if we have one. core.setSecret(config.token); } return { username: config.username, token: config.token } satisfies Token; } else { - // Mask password to reduce chance of accidental leakage in logs, if we have one. - if ("password" in config && isDefined(config.password)) { + let username: string | undefined = undefined; + let password: string | undefined = undefined; + + // Both "username" and "password" are optional. If we have reached this point, we need + // to validate which of them are present and that they have the correct type if so. + if ("password" in config && json.isString(config.password)) { + // Mask password to reduce chance of accidental leakage in logs, if we have one. core.setSecret(config.password); + password = config.password; + } + if ("username" in config && json.isString(config.username)) { + username = config.username; } + // Return the `UsernamePassword` object. Both username and password may be undefined. return { - username: "username" in config ? config.username : undefined, - password: "password" in config ? config.password : undefined, + username, + password, } satisfies UsernamePassword; } } @@ -364,9 +392,9 @@ export function getCredentials( } // Parse and validate the credentials - let parsed: RawCredential[]; + let parsed: unknown; try { - parsed = JSON.parse(credentialsStr) as RawCredential[]; + parsed = json.parseString(credentialsStr); } catch { // Don't log the error since it might contain sensitive information. logger.error("Failed to parse the credentials data."); @@ -374,7 +402,7 @@ export function getCredentials( } // Check that the parsed data is indeed an array. - if (!Array.isArray(parsed)) { + if (!json.isArray(parsed)) { throw new ConfigurationError( "Expected credentials data to be an array of configurations, but it is not.", ); @@ -382,12 +410,12 @@ export function getCredentials( const out: Credential[] = []; for (const e of parsed) { - if (e === null || typeof e !== "object") { + if (e === null || !json.isObject(e)) { throw new ConfigurationError("Invalid credentials - must be an object"); } // The configuration must have a type. - if (!isDefined(e.type)) { + if (!isDefined(e.type) || !json.isString(e.type)) { throw new ConfigurationError("Invalid credentials - must have a type"); } diff --git a/src/start-proxy/types.ts b/src/start-proxy/types.ts index f60e0f64f..f3e4db364 100644 --- a/src/start-proxy/types.ts +++ b/src/start-proxy/types.ts @@ -1,3 +1,5 @@ +import type { UnvalidatedObject } from "../json"; +import * as json from "../json"; import { isDefined } from "../util"; /** @@ -5,7 +7,7 @@ import { isDefined } from "../util"; * present or not. This type is used to represent such values, which we expect to be * `Credential` values, but haven't validated yet. */ -export type RawCredential = Partial; +export type RawCredential = UnvalidatedObject; /** Usernames may be present for both authentication with tokens or passwords. */ export type Username = { @@ -14,7 +16,7 @@ export type Username = { }; /** Decides whether `config` has a username. */ -export function hasUsername(config: Partial): config is Username { +export function hasUsername(config: AuthConfig): config is Username { return "username" in config; } @@ -44,8 +46,10 @@ export type Token = { } & Username; /** Decides whether `config` is token-based. */ -export function isToken(config: Partial): config is Token { - return "token" in config; +export function isToken( + config: UnvalidatedObject, +): config is Token { + return "token" in config && json.isStringOrUndefined(config.token); } /** Configuration for Azure OIDC. */ @@ -53,13 +57,15 @@ export type AzureConfig = { tenant_id: string; client_id: string }; /** Decides whether `config` is an Azure OIDC configuration. */ export function isAzureConfig( - config: Partial, + config: UnvalidatedObject, ): config is AzureConfig { return ( "tenant_id" in config && "client_id" in config && isDefined(config.tenant_id) && - isDefined(config.client_id) + isDefined(config.client_id) && + json.isString(config.tenant_id) && + json.isString(config.client_id) ); } @@ -74,7 +80,9 @@ export type AWSConfig = { }; /** Decides whether `config` is an AWS OIDC configuration. */ -export function isAWSConfig(config: Partial): config is AWSConfig { +export function isAWSConfig( + config: UnvalidatedObject, +): config is AWSConfig { // All of these properties are required. const requiredProperties = [ "aws_region", @@ -85,10 +93,20 @@ export function isAWSConfig(config: Partial): config is AWSConfig { ]; for (const property of requiredProperties) { - if (!(property in config) || !isDefined(config[property])) { + if ( + !(property in config) || + !isDefined(config[property]) || + !json.isString(config[property]) + ) { return false; } } + + // The "audience" field is optional, but should be a string if present. + if ("audience" in config && !json.isStringOrUndefined(config.audience)) { + return false; + } + return true; } @@ -101,11 +119,23 @@ export type JFrogConfig = { /** Decides whether `config` is a JFrog OIDC configuration. */ export function isJFrogConfig( - config: Partial, + config: UnvalidatedObject, ): config is JFrogConfig { + // The "audience" and "identity_mapping_name" fields is optional, but should be strings if present. + if ("audience" in config && !json.isStringOrUndefined(config.audience)) { + return false; + } + if ( + "identity_mapping_name" in config && + !json.isStringOrUndefined(config.identity_mapping_name) + ) { + return false; + } + return ( "jfrog_oidc_provider_name" in config && - isDefined(config.jfrog_oidc_provider_name) + isDefined(config.jfrog_oidc_provider_name) && + json.isString(config.jfrog_oidc_provider_name) ); }