Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add doctor diagnostic tests for v2 crypto #939

Merged
merged 10 commits into from
Apr 5, 2024
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
11 changes: 11 additions & 0 deletions messages/diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# sfCryptoV2Support

Your current installation of Salesforce CLI, including all the plugins you've linked and installed, doesn't yet support v2 crypto. All plugins and libraries must use at least version 6.7.0 of `@salesforce/core` to support v2 crypto. You're generally still able to successfully authenticate with your current CLI installation, but not if you generate a v2 crypto key.

# sfCryptoV2Unstable

Your current installation of Salesforce CLI, including all the plugins you've linked and installed, is using v2 crypto without proper library support, which can cause authentication failures. We recommend that you switch back to v1 crypto.

# sfCryptoV2Desired

SF_CRYPTO_V2=true is set in your environment, but you're actually using v1 crypto. Your Salesforce CLI installation supports using v2 crypto. If you desire this behavior, follow the instructions in the documentation (provide link).
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@inquirer/select": "^1.3.3",
"@oclif/core": "^3.25.2",
"@salesforce/core": "^6.7.3",
"@salesforce/plugin-info": "^3.0.28",
"@salesforce/kit": "^3.1.0",
"@salesforce/sf-plugins-core": "^8.0.1",
"@salesforce/ts-types": "^2.0.9",
Expand Down Expand Up @@ -89,6 +90,9 @@
}
}
},
"hooks": {
"sf-doctor-@salesforce/plugin-auth": "./lib/hooks/diagnostics"
},
"flexibleTaxonomy": true,
"topicSeparator": " "
},
Expand Down
197 changes: 197 additions & 0 deletions src/hooks/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
* Copyright (c) 2022, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import childProcess from 'node:child_process';
import { ExecOptions, PromiseWithChild } from 'node:child_process';
import util from 'node:util';
import { join } from 'node:path';
import fs from 'node:fs';
import { Global, Lifecycle, Logger, Messages } from '@salesforce/core';
import { SfDoctor, SfDoctorDiagnosis } from '@salesforce/plugin-info';
import { asString, isString } from '@salesforce/ts-types';
import { parseJsonMap } from '@salesforce/kit';

type HookFunction = (options: { doctor: SfDoctor }) => Promise<[void]>;
type PromisifiedExec = (command: string, options?: ExecOptions) => PromiseWithChild<{ stdout: string; stderr: string }>;

let logger: Logger;
const getLogger = (): Logger => {
if (!logger) {
logger = Logger.childFromRoot('plugin-auth-diagnostics');
}
return logger;
};

const pluginName = '@salesforce/plugin-auth';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages(pluginName, 'diagnostics');

let exec: PromisifiedExec;

export const hook: HookFunction = async (options) => {
getLogger().debug(`Running SfDoctor diagnostics for ${pluginName}`);
exec = util.promisify(childProcess.exec);
try {
await exec('npm -v');
return await Promise.all([cryptoVersionTest(options.doctor)]);
} catch (e: unknown) {
const errMsg = e instanceof Error ? e.message : isString(e) ? e : 'unknown';
getLogger().warn(`Unable to run SfDoctor diagnostics for ${pluginName} due to: ${errMsg}`);
return Promise.resolve([undefined]);
}
};

type NpmExplanationDeps = {
type: string;
name: string;
spec: string;
from: NpmExplanation;
};
type NpmExplanation = {
name: string;
version: string;
location: string;
isWorkspace: string;
dependents: NpmExplanationDeps[];
};

// ============================
// *** DIAGNOSTIC TESTS ***
// ============================

// Detects if the auth key used is crypto v1 or v2
// Detects if the SF_CRYPTO_V2 env var is set and if it matches the key crypto version
const cryptoVersionTest = async (doctor: SfDoctor): Promise<void> => {
getLogger().debug('Running Crypto Version tests');

const sfCryptoV2Support = await supportsCliV2Crypto(doctor);
let cryptoVersion: 'unknown' | 'v1' | 'v2' = 'unknown';

const sfCryptoV2 = process.env.SF_CRYPTO_V2;

const isUsingGenericKeychain =
process.platform === 'win32' ||
process.env.SF_USE_GENERIC_UNIX_KEYCHAIN?.toLowerCase() === 'true' ||
process.env.USE_GENERIC_UNIX_KEYCHAIN?.toLowerCase() === 'true';

// If the CLI is using key.json, we can read the file and get the key length
// to discover the crypto version being used. If not, then we can't detect it.
if (isUsingGenericKeychain) {
try {
const keyFile = join(Global.DIR, 'key.json');
const key = asString(parseJsonMap(fs.readFileSync(keyFile, 'utf8'))?.key);
cryptoVersion = key?.length === 64 ? 'v2' : key?.length === 32 ? 'v1' : 'unknown';
} catch (e: unknown) {
const errMsg = e instanceof Error ? e.message : isString(e) ? e : 'unknown';
getLogger().debug(`Could not detect key size due to:\n${errMsg}`);
}
}

doctor.addPluginData(pluginName, {
sfCryptoV2,
isUsingGenericKeychain,
sfCryptoV2Support,
cryptoVersion,
});

const testName1 = `[${pluginName}] CLI supports v2 crypto`;
let status1 = 'pass';
if (!sfCryptoV2Support) {
status1 = 'warn';
doctor.addSuggestion(messages.getMessage('sfCryptoV2Support'));
}
void Lifecycle.getInstance().emit('Doctor:diagnostic', { testName: testName1, status: status1 });

// Only do this test if we know they are using v2 crypto
if (cryptoVersion === 'v2') {
const testName2 = `[${pluginName}] CLI using stable v2 crypto`;
let status2 = 'pass';
if (!sfCryptoV2Support) {
status2 = 'fail';
doctor.addSuggestion(messages.getMessage('sfCryptoV2Unstable'));
}
void Lifecycle.getInstance().emit('Doctor:diagnostic', { testName: testName2, status: status2 });
}

// Only do this test if we know they are using v1 crypto
if (cryptoVersion === 'v1') {
const testName3 = `[${pluginName}] CLI using stable v1 crypto`;
let status3 = 'pass';
if (sfCryptoV2?.toLowerCase() === 'true') {
// They have SF_CRYPTO_V2=true but are using v1 crypto. They might not know this
// or know how to generate a v2 key.
if (sfCryptoV2Support) {
status3 = 'warn';
doctor.addSuggestion(messages.getMessage('sfCryptoV2Desired'));
}
}
void Lifecycle.getInstance().emit('Doctor:diagnostic', { testName: testName3, status: status3 });
}
};

// Inspect CLI install and plugins to ensure all versions of `@salesforce/core` can support v2 crypto.
// This uses `npm explain @salesforce/core` to ensure all versions are greater than 6.6.0.
const supportsCliV2Crypto = async (doctor: SfDoctor): Promise<boolean> => {
const diagnosis: SfDoctorDiagnosis = doctor.getDiagnosis();
let coreSupportsV2 = false;
let pluginsSupportV2 = false;
let linksSupportsV2 = false;

const { root, dataDir } = diagnosis.cliConfig;
// check core CLI
if (root?.length) {
try {
const { stdout } = await exec('npm explain @salesforce/core --json', { cwd: root });
const coreExplanation = JSON.parse(stdout) as NpmExplanation[];
coreSupportsV2 = coreExplanation.every((exp) => exp?.version > '6.6.0');
} catch (e: unknown) {
const errMsg = e instanceof Error ? e.message : isString(e) ? e : 'unknown';
getLogger().debug(`Cannot determine CLI v2 crypto core support due to: ${errMsg}`);
}
}
// check installed plugins
if (dataDir?.length) {
try {
const { stdout } = await exec('npm explain @salesforce/core --json', { cwd: dataDir });
const pluginsExplanation = JSON.parse(stdout) as NpmExplanation[];
pluginsSupportV2 = pluginsExplanation?.length ? pluginsExplanation.every((exp) => exp?.version > '6.6.0') : true;
} catch (e: unknown) {
const errMsg = e instanceof Error ? e.message : isString(e) ? e : 'unknown';
if (errMsg.includes('No dependencies found matching @salesforce/core')) {
pluginsSupportV2 = true;
} else {
getLogger().debug(`Cannot determine CLI v2 crypto installed plugins support due to: ${errMsg}`);
}
}
}
// check linked plugins
const pluginVersions = diagnosis?.versionDetail?.pluginVersions;
const linkedPlugins = pluginVersions.filter((pv) => pv.includes('(link)'));
if (linkedPlugins?.length) {
try {
const coreVersionChecks = await Promise.all(
linkedPlugins.map(async (pluginEntry) => {
// last entry is the path. E.g., "auth 3.3.17 (link) /Users/me/dev/plugin-auth",
const pluginPath = pluginEntry.split(' ')?.pop();
if (pluginPath?.length) {
const { stdout } = await exec('npm explain @salesforce/core --json', { cwd: pluginPath });
const linksExplanation = JSON.parse(stdout) as NpmExplanation[];
return linksExplanation?.every((exp) => exp?.version > '6.6.0');
}
return true;
})
);
linksSupportsV2 = !coreVersionChecks.includes(false);
} catch (e: unknown) {
const errMsg = e instanceof Error ? e.message : isString(e) ? e : 'unknown';
getLogger().debug(`Cannot determine CLI v2 crypto linked plugins support due to: ${errMsg}`);
}
} else {
linksSupportsV2 = true;
}

return coreSupportsV2 && pluginsSupportV2 && linksSupportsV2;
};
Loading
Loading