Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion src/commandExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { Config, Command, Flags, Parser } from '@oclif/core';
import { Org, SfError } from '@salesforce/core';
import { Org, SfError, matchesJwtAccessToken, matchesOpaqueAccessToken } from '@salesforce/core';
import { AsyncCreatable } from '@salesforce/kit';
import { isNumber, JsonMap, Optional } from '@salesforce/ts-types';
import { parseVarArgs } from '@salesforce/sf-plugins-core';
Expand All @@ -35,6 +35,37 @@ type PluginInfo = {
version: Optional<string>;
};

export type AccessTokenType = 'jwt' | 'opaque' | 'unknown';

export const classifyAccessToken = (token: string | undefined): AccessTokenType | undefined => {
if (!token) return undefined;
if (matchesJwtAccessToken(token)) return 'jwt';
if (matchesOpaqueAccessToken(token)) return 'opaque';
return 'unknown';
};

/**
* Salesforce-owned OAuth client IDs we intentionally surface in telemetry, mapped
* to the stable label reported on the event. Anything not in this map (a customer's
* own connected app) is reported as `undefined` so a customer consumer key is never
* emitted. This mirrors core's log filter, which leaves `PlatformCLI` visible and
* redacts every other clientId.
*
* `'PlatformCLI'` and `'CodeBuilder'` are core's `DEFAULT_CONNECTED_APP_INFO.clientId`
* and `CODE_BUILDER_CONNECTED_APP_INFO.clientId`; those constants are not re-exported
* from `@salesforce/core`, so the (stable, public) literals are used here. Add the
* Global ECA framework client id once its value is confirmed.
*/
export type KnownClientId = 'PlatformCLI' | 'CodeBuilder';

const KNOWN_CLIENT_IDS: Record<string, KnownClientId> = {
PlatformCLI: 'PlatformCLI',
CodeBuilder: 'CodeBuilder',
};

export const classifyKnownClientId = (clientId: string | undefined): KnownClientId | undefined =>
clientId ? KNOWN_CLIENT_IDS[clientId] : undefined;

export class CommandExecution extends AsyncCreatable {
public status?: number;
private specifiedFlags: string[] = [];
Expand All @@ -50,6 +81,10 @@ export class CommandExecution extends AsyncCreatable {
private agentPseudoTypeUsed?: boolean;
private orgApiVersion?: string;
private devhubApiVersion?: string;
private orgAccessTokenType?: AccessTokenType;
private devhubAccessTokenType?: AccessTokenType;
private orgKnownClientId?: KnownClientId;
private devhubKnownClientId?: KnownClientId;
private argKeys: string[] = [];
private enableO11y?: boolean;
private o11yUploadEndpoint?: string;
Expand Down Expand Up @@ -120,6 +155,10 @@ export class CommandExecution extends AsyncCreatable {
devhubId: this.devhubId,
orgApiVersion: this.orgApiVersion,
devhubApiVersion: this.devhubApiVersion,
orgAccessTokenType: this.orgAccessTokenType,
devhubAccessTokenType: this.devhubAccessTokenType,
orgKnownClientId: this.orgKnownClientId,
devhubKnownClientId: this.devhubKnownClientId,
specifiedEnvs: envs.specifiedEnvs.join(' '),
uniqueEnvs: envs.uniqueEnvs.join(' '),
argKeys: this.argKeys.sort().join(' '),
Expand Down Expand Up @@ -186,6 +225,8 @@ export class CommandExecution extends AsyncCreatable {
this.devhubId = targetDevHub ? targetDevHub.getOrgId() : undefined;
this.orgApiVersion = targetOrg ? targetOrg.getConnection().getApiVersion() : undefined;
this.devhubApiVersion = targetDevHub ? targetDevHub.getConnection().getApiVersion() : undefined;
this.setAccessTokenTypes(targetOrg, targetDevHub);
this.setKnownClientIds(targetOrg, targetDevHub);
this.determineSpecifiedFlags(argv, flags, flagDefinitions);

// Read o11y configuration from the plugin's package.json (plugin that owns the command)
Expand All @@ -195,6 +236,21 @@ export class CommandExecution extends AsyncCreatable {
}
}

// Classify the in-memory access token format for the resolved target org and target dev hub.
private setAccessTokenTypes(targetOrg: Optional<Org>, targetDevHub: Optional<Org>): void {
this.orgAccessTokenType = classifyAccessToken(targetOrg?.getConnection().getConnectionOptions().accessToken);
this.devhubAccessTokenType = classifyAccessToken(targetDevHub?.getConnection().getConnectionOptions().accessToken);
}

// Report only Salesforce-owned OAuth client IDs (see KNOWN_CLIENT_IDS); a custom
// connected app resolves to undefined so no customer consumer key is emitted.
// Reads the already-in-memory auth fields (no extra disk read, no decrypt: clientId
// is stored in plaintext) off the same connection used above.
private setKnownClientIds(targetOrg: Optional<Org>, targetDevHub: Optional<Org>): void {
this.orgKnownClientId = classifyKnownClientId(targetOrg?.getConnection().getAuthInfoFields()?.clientId);
this.devhubKnownClientId = classifyKnownClientId(targetDevHub?.getConnection().getAuthInfoFields()?.clientId);
}

// Get and set the O11y configuration from the plugin's package.json
private async setO11yConfig(pluginRoot: string): Promise<void> {
try {
Expand Down
271 changes: 270 additions & 1 deletion test/commandExecution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ import fs from 'node:fs/promises';
import path from 'node:path';
import type { Command } from '@oclif/core';
import { Interfaces, Performance } from '@oclif/core';
import type { Connection, Org } from '@salesforce/core';
import { stubInterface, stubMethod } from '@salesforce/ts-sinon';
import { expect } from 'chai';
import sinon from 'sinon';
import { CommandExecution } from '../src/commandExecution.js';
import { classifyAccessToken, classifyKnownClientId, CommandExecution } from '../src/commandExecution.js';
import { MyCommand } from './helpers/myCommand.js';
import { MyArgCommand } from './helpers/myArgCommand.js';
import { MyOrgCommand, orgFlagState } from './helpers/myOrgCommand.js';

describe('toJson', () => {
const sandbox = sinon.createSandbox();
Expand Down Expand Up @@ -431,4 +433,271 @@ describe('toJson', () => {
expect(actual.productFeatureId).to.equal(undefined);
});
});

describe('classifyAccessToken', () => {
it('classifies a JWT-shaped access token as jwt', () => {
expect(
classifyAccessToken(
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ.signaturepart'
)
).to.equal('jwt');
});

it('classifies an opaque access token as opaque', () => {
expect(classifyAccessToken('00D5f000000abcd!AQEAQxyz.longtail')).to.equal('opaque');
});

it('classifies a non-matching string as unknown', () => {
expect(classifyAccessToken('this-is-not-a-real-token')).to.equal('unknown');
});

it('returns undefined for an empty string', () => {
expect(classifyAccessToken('')).to.equal(undefined);
});

it('returns undefined for undefined', () => {
expect(classifyAccessToken(undefined)).to.equal(undefined);
});
});

describe('orgAccessTokenType / devhubAccessTokenType', () => {
afterEach(() => {
orgFlagState.targetOrg = undefined;
orgFlagState.targetDevHub = undefined;
});

const fakeOrgWithToken = (accessToken: string): Org => {
const connection = stubInterface<Connection>(sandbox, {
getConnectionOptions: () => ({ accessToken }),
getApiVersion: () => '62.0',
});
return stubInterface<Org>(sandbox, {
getConnection: () => connection,
getOrgId: () => '00D000000000000EAA',
getUsername: () => 'me@example.com',
}) as unknown as Org;
};

it('sets orgAccessTokenType to jwt when the target-org token is JWT-shaped', async () => {
process.env.CI = 'true';
orgFlagState.targetOrg = fakeOrgWithToken(
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ.signaturepart'
);
const config = stubInterface<Interfaces.Config>(sandbox, {});
const execution = await CommandExecution.create({
argv: ['--target-org', 'myOrg'],
command: MyOrgCommand,
config,
});
const actual = execution.toJson();

expect(actual.orgAccessTokenType).to.equal('jwt');
});

it('sets orgAccessTokenType to opaque when the target-org token is opaque', async () => {
process.env.CI = 'true';
orgFlagState.targetOrg = fakeOrgWithToken('00D5f000000abcd!AQEAQxyz.longtail');
const config = stubInterface<Interfaces.Config>(sandbox, {});
const execution = await CommandExecution.create({
argv: ['--target-org', 'myOrg'],
command: MyOrgCommand,
config,
});
const actual = execution.toJson();

expect(actual.orgAccessTokenType).to.equal('opaque');
});

it('sets devhubAccessTokenType to jwt when the target-dev-hub token is JWT-shaped', async () => {
process.env.CI = 'true';
orgFlagState.targetDevHub = fakeOrgWithToken(
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ.signaturepart'
);
const config = stubInterface<Interfaces.Config>(sandbox, {});
const execution = await CommandExecution.create({
argv: ['--target-dev-hub', 'myHub'],
command: MyOrgCommand,
config,
});
const actual = execution.toJson();

expect(actual.devhubAccessTokenType).to.equal('jwt');
});

it('sets devhubAccessTokenType to opaque when the target-dev-hub token is opaque', async () => {
process.env.CI = 'true';
orgFlagState.targetDevHub = fakeOrgWithToken('00D5f000000abcd!AQEAQxyz.longtail');
const config = stubInterface<Interfaces.Config>(sandbox, {});
const execution = await CommandExecution.create({
argv: ['--target-dev-hub', 'myHub'],
command: MyOrgCommand,
config,
});
const actual = execution.toJson();

expect(actual.devhubAccessTokenType).to.equal('opaque');
});

it('leaves orgAccessTokenType and devhubAccessTokenType undefined when there is no resolved org/dev-hub', async () => {
process.env.CI = 'true';
const config = stubInterface<Interfaces.Config>(sandbox, {});
const execution = await CommandExecution.create({
argv: [],
command: MyOrgCommand,
config,
});
const actual = execution.toJson();

expect(actual.orgAccessTokenType).to.equal(undefined);
expect(actual.devhubAccessTokenType).to.equal(undefined);
});

it('leaves orgAccessTokenType undefined when the resolved org has no access token in memory', async () => {
process.env.CI = 'true';
const connection = stubInterface<Connection>(sandbox, {
getConnectionOptions: () => ({}),
getApiVersion: () => '62.0',
});
orgFlagState.targetOrg = stubInterface<Org>(sandbox, {
getConnection: () => connection,
getOrgId: () => '00D000000000000EAA',
getUsername: () => 'me@example.com',
}) as unknown as Org;
const config = stubInterface<Interfaces.Config>(sandbox, {});
const execution = await CommandExecution.create({
argv: ['--target-org', 'myOrg'],
command: MyOrgCommand,
config,
});
const actual = execution.toJson();

expect(actual.orgAccessTokenType).to.equal(undefined);
});
});

describe('classifyKnownClientId', () => {
it('maps the PlatformCLI client id to PlatformCLI', () => {
expect(classifyKnownClientId('PlatformCLI')).to.equal('PlatformCLI');
});

it('maps the CodeBuilder client id to CodeBuilder', () => {
expect(classifyKnownClientId('CodeBuilder')).to.equal('CodeBuilder');
});

it('returns undefined for a custom (non-allowlisted) client id', () => {
expect(classifyKnownClientId('3MVG9custom.connected.app.consumer.key')).to.equal(undefined);
});

it('returns undefined for an empty string', () => {
expect(classifyKnownClientId('')).to.equal(undefined);
});

it('returns undefined for undefined', () => {
expect(classifyKnownClientId(undefined)).to.equal(undefined);
});
});

describe('orgKnownClientId / devhubKnownClientId', () => {
afterEach(() => {
orgFlagState.targetOrg = undefined;
orgFlagState.targetDevHub = undefined;
});

const fakeOrgWithClientId = (clientId?: string): Org => {
const connection = stubInterface<Connection>(sandbox, {
getConnectionOptions: () => ({}),
getAuthInfoFields: () => (clientId ? { clientId } : {}),
getApiVersion: () => '62.0',
});
return stubInterface<Org>(sandbox, {
getConnection: () => connection,
getOrgId: () => '00D000000000000EAA',
getUsername: () => 'me@example.com',
}) as unknown as Org;
};

it('sets orgKnownClientId to PlatformCLI when the target-org uses the default CLI connected app', async () => {
process.env.CI = 'true';
orgFlagState.targetOrg = fakeOrgWithClientId('PlatformCLI');
const config = stubInterface<Interfaces.Config>(sandbox, {});
const execution = await CommandExecution.create({
argv: ['--target-org', 'myOrg'],
command: MyOrgCommand,
config,
});
const actual = execution.toJson();

expect(actual.orgKnownClientId).to.equal('PlatformCLI');
});

it('sets orgKnownClientId to CodeBuilder when the target-org uses the Code Builder connected app', async () => {
process.env.CI = 'true';
orgFlagState.targetOrg = fakeOrgWithClientId('CodeBuilder');
const config = stubInterface<Interfaces.Config>(sandbox, {});
const execution = await CommandExecution.create({
argv: ['--target-org', 'myOrg'],
command: MyOrgCommand,
config,
});
const actual = execution.toJson();

expect(actual.orgKnownClientId).to.equal('CodeBuilder');
});

it('leaves orgKnownClientId undefined when the target-org uses a custom connected app', async () => {
process.env.CI = 'true';
orgFlagState.targetOrg = fakeOrgWithClientId('3MVG9custom.connected.app.consumer.key');
const config = stubInterface<Interfaces.Config>(sandbox, {});
const execution = await CommandExecution.create({
argv: ['--target-org', 'myOrg'],
command: MyOrgCommand,
config,
});
const actual = execution.toJson();

expect(actual.orgKnownClientId).to.equal(undefined);
});

it('sets devhubKnownClientId to PlatformCLI when the target-dev-hub uses the default CLI connected app', async () => {
process.env.CI = 'true';
orgFlagState.targetDevHub = fakeOrgWithClientId('PlatformCLI');
const config = stubInterface<Interfaces.Config>(sandbox, {});
const execution = await CommandExecution.create({
argv: ['--target-dev-hub', 'myHub'],
command: MyOrgCommand,
config,
});
const actual = execution.toJson();

expect(actual.devhubKnownClientId).to.equal('PlatformCLI');
});

it('leaves orgKnownClientId and devhubKnownClientId undefined when there is no resolved org/dev-hub', async () => {
process.env.CI = 'true';
const config = stubInterface<Interfaces.Config>(sandbox, {});
const execution = await CommandExecution.create({
argv: [],
command: MyOrgCommand,
config,
});
const actual = execution.toJson();

expect(actual.orgKnownClientId).to.equal(undefined);
expect(actual.devhubKnownClientId).to.equal(undefined);
});

it('leaves orgKnownClientId undefined when the resolved org has no clientId in memory', async () => {
process.env.CI = 'true';
orgFlagState.targetOrg = fakeOrgWithClientId();
const config = stubInterface<Interfaces.Config>(sandbox, {});
const execution = await CommandExecution.create({
argv: ['--target-org', 'myOrg'],
command: MyOrgCommand,
config,
});
const actual = execution.toJson();

expect(actual.orgKnownClientId).to.equal(undefined);
});
});
});
Loading
Loading