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
11 changes: 11 additions & 0 deletions messages/diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# sfCryptoV2Support

This CLI does not yet support v2 crypto. All plugins and libraries must use at least version 6.7.0 of `@salesforce/core` to support v2 crypto. This will not cause a problem with CLI authentication unless a v2 crypto key has been generated.
cristiand391 marked this conversation as resolved.
Show resolved Hide resolved

# sfCryptoV2Unstable

This CLI is using v2 crypto without proper library support. This can cause authentication failures. Switching back to v1 crypto is recommended.
cristiand391 marked this conversation as resolved.
Show resolved Hide resolved

# sfCryptoV2Desired

SF_CRYPTO_V2=true is set but v1 crypto is actually in use. This CLI supports using v2 crypto. If this is desired please follow the instructions in the docs (provide link).
cristiand391 marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@oclif/core": "^3.20.0",
"@salesforce/core": "^6.7.0",
"@salesforce/kit": "^3.0.15",
"@salesforce/plugin-info": "^3.0.28",
"@salesforce/sf-plugins-core": "^7.1.14",
"@salesforce/ts-types": "^2.0.9",
"chalk": "^5.3.0",
Expand Down Expand Up @@ -89,6 +90,9 @@
}
}
},
"hooks": {
"sf-doctor-@salesforce/plugin-auth": "./lib/hooks/diagnostics"
},
"flexibleTaxonomy": true,
"topicSeparator": " "
},
Expand Down
187 changes: 187 additions & 0 deletions src/hooks/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* 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 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]>;

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');

export const hook: HookFunction = async (options) => {
getLogger().debug(`Running SfDoctor diagnostics for ${pluginName}`);
return Promise.all([cryptoVersionTest(options.doctor)]);
};

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 exec = util.promisify(childProcess.exec);

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