diff --git a/.github/workflows/cli-oidc-test.yml b/.github/workflows/cli-oidc-test.yml index 7232015cb..de5effd7a 100644 --- a/.github/workflows/cli-oidc-test.yml +++ b/.github/workflows/cli-oidc-test.yml @@ -36,9 +36,15 @@ jobs: JF_URL: ${{ secrets.JFROG_PLATFORM_URL }} with: oidc-provider-name: setup-jfrog-cli-test - # This can be removed after the default CLI version will be updated - version: 2.75.0 - name: Test JFrog CLI run: | - jf rt ping \ No newline at end of file + jf rt ping + + - name: Test User Output + shell: bash + run: test -n "${{ steps.setup-jfrog-cli.outputs.oidc-user }}" + + - name: Test Token Output + shell: bash + run: test -n "${{ steps.setup-jfrog-cli.outputs.oidc-token }}" \ No newline at end of file diff --git a/.github/workflows/manual-oidc-test.yml b/.github/workflows/manual-oidc-test.yml index 62f19d8d4..5e24b0cae 100644 --- a/.github/workflows/manual-oidc-test.yml +++ b/.github/workflows/manual-oidc-test.yml @@ -72,8 +72,6 @@ jobs: JF_URL: ${{ secrets.JFROG_PLATFORM_URL }} with: oidc-provider-name: ${{ env.OIDC_PROVIDER_NAME }} - # The last version which outputs OIDC params as step outputs - version: '2.74.1' - name: Test JFrog CLI run: | diff --git a/action.yml b/action.yml index 86b83a28f..e205306d5 100644 --- a/action.yml +++ b/action.yml @@ -4,7 +4,7 @@ author: "JFrog" inputs: version: description: "JFrog CLI Version" - default: "2.74.1" + default: "2.75.0" required: false download-repository: description: "Remote repository in Artifactory pointing to 'https://releases.jfrog.io/artifactory/jfrog-cli'. Use this parameter in case you don't have an Internet access." diff --git a/lib/utils.js b/lib/utils.js index 8fe3e4193..7a4014136 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -33,6 +33,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Utils = void 0; +exports.parseInput = parseInput; const core = __importStar(require("@actions/core")); const exec_1 = require("@actions/exec"); const http_client_1 = require("@actions/http-client"); @@ -291,42 +292,49 @@ class Utils { * @param jfrogCredentials existing JFrog credentials - url, access token, username + password */ static getSeparateEnvConfigArgs(jfrogCredentials) { - /** - * @name url - JFrog Platform URL - * @name user - JFrog Platform basic authentication - * @name password - JFrog Platform basic authentication - * @name accessToken - Jfrog Platform access token - * @name oidcProviderName - OpenID Connect provider name defined in the JFrog Platform - * @name oidcAudience - JFrog Platform OpenID Connect audience - */ - let url = jfrogCredentials.jfrogUrl; - let user = jfrogCredentials.username; - let password = jfrogCredentials.password; - let accessToken = jfrogCredentials.accessToken; - let oidcProviderName = jfrogCredentials.oidcProviderName; - let oidcTokenId = jfrogCredentials.oidcTokenId; - let oidcAudience = jfrogCredentials.oidcAudience; - // Url is mandatory for JFrog CLI configuration - if (!url) { - return; - } - const configCmd = [Utils.getServerIdForConfig(), '--url', url, '--interactive=false', '--overwrite=true']; - // OIDC auth - if (this.isCLIVersionOidcSupported() && !!oidcProviderName) { - configCmd.push(`--oidc-provider-name=${oidcProviderName}`); - configCmd.push('--oidc-provider-type=Github'); - configCmd.push(`--oidc-token-id=${oidcTokenId}`); - configCmd.push(`--oidc-audience=${oidcAudience}`); + return __awaiter(this, void 0, void 0, function* () { + /** + * @name url - JFrog Platform URL + * @name user - JFrog Platform basic authentication + * @name password - JFrog Platform basic authentication + * @name accessToken - Jfrog Platform access token + * @name oidcProviderName - OpenID Connect provider name defined in the JFrog Platform + * @name oidcAudience - JFrog Platform OpenID Connect audience + */ + let url = jfrogCredentials.jfrogUrl; + let user = jfrogCredentials.username; + let password = jfrogCredentials.password; + let accessToken = jfrogCredentials.accessToken; + let oidcProviderName = jfrogCredentials.oidcProviderName; + let oidcTokenId = jfrogCredentials.oidcTokenId; + let oidcAudience = jfrogCredentials.oidcAudience; + // Url is mandatory for JFrog CLI configuration + if (!url) { + return; + } + // OIDC + if (!!oidcProviderName && !!oidcTokenId) { + core.info('calling EOT ! '); + let output = yield (0, exec_1.getExecOutput)('jf', ['eot', oidcProviderName, oidcTokenId, '--url', url, '--oidc-audience', oidcAudience], { silent: true, ignoreReturnCode: true }); + const { accessToken, username } = parseInput(output.stdout); + // Sets the OIDC token as access token to be used in config. + core.info('setting as secret'); + core.setSecret('oidc-token'); + core.setOutput('oidc-token', username); + core.setSecret('oidc-user'); + core.setOutput('oidc-user', accessToken); + } + const configCmd = [Utils.getServerIdForConfig(), '--url', url, '--interactive=false', '--overwrite=true']; // Access Token - } - else if (!!accessToken) { - configCmd.push('--access-token', accessToken); - // Basic Auth - } - else if (!!user && !!password) { - configCmd.push('--user', user, '--password', password); - } - return configCmd; + if (!!accessToken) { + configCmd.push('--access-token', accessToken); + // Basic Auth + } + else if (!!user && !!password) { + configCmd.push('--user', user, '--password', password); + } + return configCmd; + }); } /** * Get server ID for JFrog CLI configuration. Save the server ID in the servers env var if it doesn't already exist. @@ -413,7 +421,7 @@ class Utils { core.setSecret(configToken); yield Utils.runCli(cliConfigCmd.concat('import', configToken)); } - let configArgs = Utils.getSeparateEnvConfigArgs(jfrogCredentials); + let configArgs = yield Utils.getSeparateEnvConfigArgs(jfrogCredentials); if (configArgs) { yield Utils.runCli(cliConfigCmd.concat('add', ...configArgs)); } @@ -1023,3 +1031,28 @@ Utils.METRIC_PARAM_KEY = 'm'; Utils.METRIC_PARAM_VALUE = '1'; Utils.MIN_CLI_OIDC_VERSION = '2.75.0'; Utils.DEFAULT_OIDC_AUDIENCE = 'jfrog-github'; +function parseInput(input) { + try { + // Attempt to parse as JSON + const parsed = JSON.parse(input); + if (parsed.AccessToken && parsed.Username) { + return { + accessToken: parsed.AccessToken, + username: parsed.Username, + }; + } + throw new Error('JSON does not contain required fields.'); + } + catch (_a) { + // Fallback to regex extraction + const regex = /AccessToken:\s*(\S+)\s*Username:\s*(\S+)/; + const match = regex.exec(input); + if (!match) { + throw new Error('Failed to extract values. Input format is invalid.'); + } + return { + accessToken: match[1], + username: match[2], + }; + } +} diff --git a/src/utils.ts b/src/utils.ts index 7c440a2bd..ba7ded4ef 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -359,7 +359,7 @@ export class Utils { * Get separate env config for the URL and connection details and return args to add to the config add command * @param jfrogCredentials existing JFrog credentials - url, access token, username + password */ - public static getSeparateEnvConfigArgs(jfrogCredentials: JfrogCredentials): string[] | undefined { + public static async getSeparateEnvConfigArgs(jfrogCredentials: JfrogCredentials): Promise { /** * @name url - JFrog Platform URL * @name user - JFrog Platform basic authentication @@ -381,15 +381,26 @@ export class Utils { return; } + // OIDC + if (!!oidcProviderName && !!oidcTokenId) { + core.info('calling EOT ! '); + let output: ExecOutput = await getExecOutput( + 'jf', + ['eot', oidcProviderName, oidcTokenId, '--url', url, '--oidc-audience', oidcAudience], + { silent: true, ignoreReturnCode: true }, + ); + const { accessToken, username }: { accessToken: string; username: string } = parseInput(output.stdout); + // Sets the OIDC token as access token to be used in config. + core.info('setting as secret'); + core.setSecret('oidc-token'); + core.setOutput('oidc-token', username); + core.setSecret('oidc-user'); + core.setOutput('oidc-user', accessToken); + } + const configCmd: string[] = [Utils.getServerIdForConfig(), '--url', url, '--interactive=false', '--overwrite=true']; - // OIDC auth - if (this.isCLIVersionOidcSupported() && !!oidcProviderName) { - configCmd.push(`--oidc-provider-name=${oidcProviderName}`); - configCmd.push('--oidc-provider-type=Github'); - configCmd.push(`--oidc-token-id=${oidcTokenId}`); - configCmd.push(`--oidc-audience=${oidcAudience}`); - // Access Token - } else if (!!accessToken) { + // Access Token + if (!!accessToken) { configCmd.push('--access-token', accessToken); // Basic Auth } else if (!!user && !!password) { @@ -500,7 +511,7 @@ export class Utils { await Utils.runCli(cliConfigCmd.concat('import', configToken)); } - let configArgs: string[] | undefined = Utils.getSeparateEnvConfigArgs(jfrogCredentials); + let configArgs: string[] | undefined = await Utils.getSeparateEnvConfigArgs(jfrogCredentials); if (configArgs) { await Utils.runCli(cliConfigCmd.concat('add', ...configArgs)); } @@ -1040,6 +1051,33 @@ export class Utils { } } +export function parseInput(input: string): { accessToken: string; username: string } { + try { + // Attempt to parse as JSON + const parsed: { AccessToken?: string; Username?: string } = JSON.parse(input); + if (parsed.AccessToken && parsed.Username) { + return { + accessToken: parsed.AccessToken, + username: parsed.Username, + }; + } + throw new Error('JSON does not contain required fields.'); + } catch { + // Fallback to regex extraction + const regex: RegExp = /AccessToken:\s*(\S+)\s*Username:\s*(\S+)/; + const match: RegExpMatchArray | null = regex.exec(input); + + if (!match) { + throw new Error('Failed to extract values. Input format is invalid.'); + } + + return { + accessToken: match[1], + username: match[2], + }; + } +} + export interface DownloadDetails { artifactoryUrl: string; repository: string; diff --git a/test/main.spec.ts b/test/main.spec.ts index 99efc67c7..f714d7b3f 100644 --- a/test/main.spec.ts +++ b/test/main.spec.ts @@ -1,7 +1,7 @@ import * as os from 'os'; import * as core from '@actions/core'; -import { DownloadDetails, JfrogCredentials, JWTTokenData, Utils } from '../src/utils'; +import { DownloadDetails, JfrogCredentials, JWTTokenData, parseInput, Utils } from '../src/utils'; import * as jsYaml from 'js-yaml'; import * as fs from 'fs'; import * as path from 'path'; @@ -122,22 +122,22 @@ describe('Collect JFrog Credentials from env vars exceptions', () => { }); }); -function testConfigCommand(expectedServerId: string) { +async function testConfigCommand(expectedServerId: string) { // No url - let configCommand: string[] | undefined = Utils.getSeparateEnvConfigArgs({} as JfrogCredentials); + let configCommand: string[] | undefined = await Utils.getSeparateEnvConfigArgs({} as JfrogCredentials); expect(configCommand).toBe(undefined); let jfrogCredentials: JfrogCredentials = {} as JfrogCredentials; jfrogCredentials.jfrogUrl = DEFAULT_CLI_URL; // No credentials - configCommand = Utils.getSeparateEnvConfigArgs(jfrogCredentials); + configCommand = await Utils.getSeparateEnvConfigArgs(jfrogCredentials); expect(configCommand).toStrictEqual([expectedServerId, '--url', DEFAULT_CLI_URL, '--interactive=false', '--overwrite=true']); // Basic authentication jfrogCredentials.username = 'user'; jfrogCredentials.password = 'password'; - configCommand = Utils.getSeparateEnvConfigArgs(jfrogCredentials); + configCommand = await Utils.getSeparateEnvConfigArgs(jfrogCredentials); expect(configCommand).toStrictEqual([ expectedServerId, '--url', @@ -154,7 +154,7 @@ function testConfigCommand(expectedServerId: string) { jfrogCredentials.username = ''; jfrogCredentials.password = ''; jfrogCredentials.accessToken = 'accessToken'; - configCommand = Utils.getSeparateEnvConfigArgs(jfrogCredentials); + configCommand = await Utils.getSeparateEnvConfigArgs(jfrogCredentials); expect(configCommand).toStrictEqual([ expectedServerId, '--url', @@ -178,7 +178,7 @@ describe('JFrog CLI Configuration', () => { }); // Before setting a custom server ID, expect the default server ID to be used. - testConfigCommand(Utils.getRunDefaultServerId()); + await testConfigCommand(Utils.getRunDefaultServerId()); // Expect the custom server ID to be used. let customServerId: string = 'custom-server-id'; @@ -188,7 +188,7 @@ describe('JFrog CLI Configuration', () => { } return ''; // Default return value for other arguments }); - testConfigCommand(customServerId); + await testConfigCommand(customServerId); // Expect the servers env var to include both servers. const servers: string[] = Utils.getConfiguredJFrogServers(); @@ -582,48 +582,48 @@ describe('getSeparateEnvConfigArgs', () => { jest.restoreAllMocks(); }); - it('should return undefined if URL is not set', () => { + it('should return undefined if URL is not set', async () => { const creds: JfrogCredentials = {} as JfrogCredentials; - expect(Utils.getSeparateEnvConfigArgs(creds)).toBeUndefined(); + expect(await Utils.getSeparateEnvConfigArgs(creds)).toBeUndefined(); }); - it('should use access token if provided', () => { + it('should use access token if provided', async () => { const creds: JfrogCredentials = { jfrogUrl: 'https://example.jfrog.io', accessToken: 'abc', } as JfrogCredentials; - const args: string[] | undefined = Utils.getSeparateEnvConfigArgs(creds); + const args: string[] | undefined = await Utils.getSeparateEnvConfigArgs(creds); expect(args).toContain('--access-token'); expect(args).toContain('abc'); }); - it('should use username and password if provided and access token is not', () => { + it('should use username and password if provided and access token is not', async () => { const creds: JfrogCredentials = { jfrogUrl: 'https://example.jfrog.io', username: 'admin', password: '1234', } as JfrogCredentials; - const args: string[] | undefined = Utils.getSeparateEnvConfigArgs(creds); + const args: string[] | undefined = await Utils.getSeparateEnvConfigArgs(creds); expect(args).toContain('--user'); expect(args).toContain('admin'); expect(args).toContain('--password'); expect(args).toContain('1234'); }); - it('should use OIDC provider if specified', () => { + it('should use OIDC provider if specified', async () => { const creds: JfrogCredentials = { jfrogUrl: 'https://example.jfrog.io', oidcProviderName: 'setup-jfrog-cli', oidcTokenId: 'abc-123', oidcAudience: 'jfrog-github', } as JfrogCredentials; - const args: string[] | undefined = Utils.getSeparateEnvConfigArgs(creds); + const args: string[] | undefined = await Utils.getSeparateEnvConfigArgs(creds); expect(args).toContain('--oidc-provider-name=setup-jfrog-cli'); expect(args).toContain('--oidc-provider-type=Github'); expect(args).toContain('--oidc-token-id=abc-123'); expect(args).toContain('--oidc-audience=jfrog-github'); }); - it('should not include conflicting or duplicate arguments in the config command', () => { + it('should not include conflicting or duplicate arguments in the config command', async () => { const jfrogCredentials: JfrogCredentials = { jfrogUrl: 'https://example.jfrog.io', username: 'test-user', @@ -634,7 +634,7 @@ describe('getSeparateEnvConfigArgs', () => { oidcTokenId: '', }; - const configArgs: string[] | undefined = Utils.getSeparateEnvConfigArgs(jfrogCredentials); + const configArgs: string[] | undefined = await Utils.getSeparateEnvConfigArgs(jfrogCredentials); // Ensure the command does not include conflicting or duplicate arguments const configString: string = configArgs?.join(' ') || ''; @@ -645,7 +645,7 @@ describe('getSeparateEnvConfigArgs', () => { expect(configString).toContain('--oidc-audience=jfrog-github'); expect(configString).not.toContain('--access-token test-access-token --username test-user'); // Ensure no conflicting auth methods }); - it('Access Token Auth should be prioritized over basic auth', () => { + it('Access Token Auth should be prioritized over basic auth', async () => { const jfrogCredentials: JfrogCredentials = { jfrogUrl: 'https://example.jfrog.io', username: 'test-user', @@ -656,7 +656,7 @@ describe('getSeparateEnvConfigArgs', () => { oidcTokenId: '', }; - const configArgs: string[] | undefined = Utils.getSeparateEnvConfigArgs(jfrogCredentials); + const configArgs: string[] | undefined = await Utils.getSeparateEnvConfigArgs(jfrogCredentials); // Ensure the command does not include conflicting or duplicate arguments const configString: string = configArgs?.join(' ') || ''; @@ -739,7 +739,7 @@ describe('handleOidcAuth', () => { const result: JfrogCredentials = await (Utils as any).handleOidcAuth(credentials); expect(result.accessToken).toBe('forced-manual-token'); }); - it('should include OIDC flags only for supported versions', () => { + it('should include OIDC flags only for supported versions', async () => { const creds: JfrogCredentials = { jfrogUrl: 'https://example.jfrog.io', oidcProviderName: 'setup-jfrog-cli', @@ -752,14 +752,14 @@ describe('handleOidcAuth', () => { return ''; }); - const args: string[] | undefined = Utils.getSeparateEnvConfigArgs(creds); + const args: string[] | undefined = await Utils.getSeparateEnvConfigArgs(creds); expect(args).toContain('--oidc-provider-name=setup-jfrog-cli'); expect(args).toContain('--oidc-provider-type=Github'); expect(args).toContain('--oidc-token-id=abc-123'); expect(args).toContain('--oidc-audience=jfrog-github'); }); - it('should not include OIDC flags for unsupported versions', () => { + it('should not include OIDC flags for unsupported versions', async () => { const creds: JfrogCredentials = { jfrogUrl: 'https://example.jfrog.io', oidcProviderName: 'setup-jfrog-cli', @@ -772,10 +772,34 @@ describe('handleOidcAuth', () => { return ''; }); - const args: string[] | undefined = Utils.getSeparateEnvConfigArgs(creds); + const args: string[] | undefined = await Utils.getSeparateEnvConfigArgs(creds); expect(args).not.toContain('--oidc-provider-name=setup-jfrog-cli'); expect(args).not.toContain('--oidc-provider-type=Github'); expect(args).not.toContain('--oidc-token-id=abc-123'); expect(args).not.toContain('--oidc-audience=jfrog-github'); }); }); + +describe('parseInput', () => { + it('should parse valid JSON input', () => { + const input: string = '{"AccessToken": "abc123", "Username": "user456"}'; + const result: { accessToken: string; username: string } = parseInput(input); + expect(result).toEqual({ accessToken: 'abc123', username: 'user456' }); + }); + + it('should fallback to regex for non-JSON input', () => { + const input: string = '{ AccessToken: abc123 Username: user456 }'; + const result: { accessToken: string; username: string } = parseInput(input); + expect(result).toEqual({ accessToken: 'abc123', username: 'user456' }); + }); + + it('should throw an error for invalid input format', () => { + const input: string = 'Invalid input'; + expect(() => parseInput(input)).toThrow('Failed to extract values. Input format is invalid.'); + }); + + it('should throw an error for JSON without required fields', () => { + const input: string = '{"key": "value"}'; + expect(() => parseInput(input)).toThrow('Failed to extract values. Input format is invalid.'); + }); +});