Skip to content

Changes for jwt validation and sfap url #71

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

Merged
merged 8 commits into from
Nov 18, 2024
Merged
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
"@salesforce/kit": "^3.2.2",
"@salesforce/sf-plugins-core": "^11.3.12",
"form-data": "^4.0.1",
"got": "^14.4.4",
"tough-cookie": "^4.1.4"
"got": "^14.4.4"
},
"devDependencies": {
"@oclif/plugin-command-snapshot": "^5.2.22",
Expand Down
43 changes: 34 additions & 9 deletions src/commands/data-seeding/generate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { Messages, PollingClient, SfError, StatusResult } from '@salesforce/core';
import { Duration } from '@salesforce/kit';
import { initiateDataSeed, pollSeedStatus, PollSeedResponse } from '../../../utils/api.js';
import { initiateDataSeed, pollSeedStatus, PollSeedResponse, initiateJWTMint } from '../../../utils/api.js';
import { getSeedGenerateMso, getSeedGenerateStage as getStage } from '../../../utils/mso.js';
import { DataSeedingGenerateResult } from '../../../utils/types.js';
import { GenerateRequestCache } from '../../../utils/cache.js';
Expand All @@ -23,12 +23,12 @@ export default class DataSeedingGenerate extends SfCommand<DataSeedingGenerateRe

public static readonly flags = {
// TODO: The org flags will need to use Flags.requiredOrg() once auth is finalized
'target-org': Flags.string({
'target-org': Flags.requiredOrg({
summary: messages.getMessage('flags.target-org.summary'),
char: 'o',
required: true,
}),
'source-org': Flags.string({
'source-org': Flags.requiredOrg({
summary: messages.getMessage('flags.source-org.summary'),
char: 's',
required: true,
Expand All @@ -55,10 +55,28 @@ export default class DataSeedingGenerate extends SfCommand<DataSeedingGenerateRe

public async run(): Promise<DataSeedingGenerateResult> {
const { flags } = await this.parse(DataSeedingGenerate);
const { async, 'config-file': configFile, 'source-org': sourceOrg, 'target-org': targetOrg, wait } = flags;

const { request_id: jobId } = await initiateDataSeed(configFile, 'data-generation');

const { async, 'config-file': configFile, 'source-org': srcOrgObj, 'target-org': tgtOrgObj, wait } = flags;

const sourceOrg = srcOrgObj.getOrgId();
const srcAccessToken = srcOrgObj.getConnection().accessToken as string;
const srcOrgInstUrl = srcOrgObj.getConnection().instanceUrl;

const targetOrg = tgtOrgObj.getOrgId();
const tgtAccessToken = tgtOrgObj.getConnection().accessToken as string;
const tgtOrgInstUrl = tgtOrgObj.getConnection().instanceUrl;

// Fetch Valid JWT with Data Seed Org Perm
const { jwt: jwtValue } = await initiateJWTMint(srcOrgInstUrl, srcAccessToken, tgtOrgInstUrl, tgtAccessToken);
const { request_id: jobId } = await initiateDataSeed(
configFile,
'data-generation',
jwtValue,
srcOrgInstUrl,
srcAccessToken,
tgtOrgInstUrl,
tgtAccessToken,
sourceOrg
);
const reportMessage = messages.getMessage('report.suggestion', [jobId]);

if (!jobId) throw new Error('Failed to receive job id');
Expand All @@ -83,7 +101,13 @@ export default class DataSeedingGenerate extends SfCommand<DataSeedingGenerateRe

const options: PollingClient.Options = {
poll: async (): Promise<StatusResult> => {
const response = await pollSeedStatus(jobId);
const { jwt: jwtValueNew } = await initiateJWTMint(
srcOrgInstUrl,
srcAccessToken,
tgtOrgInstUrl,
tgtAccessToken
);
const response = await pollSeedStatus(jobId, jwtValueNew);

mso.goto(getStage(response.step), {
startTime: response.execution_start_time,
Expand Down Expand Up @@ -134,7 +158,8 @@ export default class DataSeedingGenerate extends SfCommand<DataSeedingGenerateRe
throw err;
}
} else {
const response = await pollSeedStatus(jobId);
const { jwt: jwtValueNew } = await initiateJWTMint(srcOrgInstUrl, srcAccessToken, tgtOrgInstUrl, tgtAccessToken);
const response = await pollSeedStatus(jobId, jwtValueNew);

const mso = getSeedGenerateMso({
jsonEnabled: this.jsonEnabled(),
Expand Down
2 changes: 1 addition & 1 deletion src/commands/data-seeding/generate/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default class DataSeedingGenerateReport extends SfCommand<DataSeedingRepo

if (!jobId) throw new SfError('No job ID provided or found in cache');

const response = await pollSeedStatus(jobId);
const response = await pollSeedStatus(jobId, '');

const data = {
jobId,
Expand Down
42 changes: 34 additions & 8 deletions src/commands/data-seeding/migrate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { Duration } from '@salesforce/kit';
import { Messages, PollingClient, StatusResult, SfError } from '@salesforce/core';
import { initiateDataSeed, PollSeedResponse, pollSeedStatus } from '../../../utils/api.js';
import { initiateDataSeed, PollSeedResponse, pollSeedStatus, initiateJWTMint } from '../../../utils/api.js';
import { DataSeedingMigrateResult } from '../../../utils/types.js';
import { getSeedMigrateMso, getSeedMigrateStage as getStage } from '../../../utils/mso.js';
import { MigrateRequestCache } from '../../../utils/cache.js';
Expand All @@ -23,12 +23,12 @@ export default class DataSeedingMigrate extends SfCommand<DataSeedingMigrateResu

public static readonly flags = {
// TODO: The org flags will need to use Flags.requiredOrg() once auth is finalized
'target-org': Flags.string({
'target-org': Flags.requiredOrg({
summary: messages.getMessage('flags.target-org.summary'),
char: 'o',
required: true,
}),
'source-org': Flags.string({
'source-org': Flags.requiredOrg({
summary: messages.getMessage('flags.source-org.summary'),
char: 's',
required: true,
Expand All @@ -55,9 +55,28 @@ export default class DataSeedingMigrate extends SfCommand<DataSeedingMigrateResu

public async run(): Promise<DataSeedingMigrateResult> {
const { flags } = await this.parse(DataSeedingMigrate);
const { async, 'config-file': configFile, 'source-org': sourceOrg, 'target-org': targetOrg, wait } = flags;

const { request_id: jobId } = await initiateDataSeed(configFile, 'data-copy');
const { async, 'config-file': configFile, 'source-org': sourceOrgObj, 'target-org': targetOrgObj, wait } = flags;

const sourceOrg = sourceOrgObj.getOrgId();
const srcAccessToken = sourceOrgObj.getConnection().accessToken as string;
const srcOrgInstUrl = sourceOrgObj.getConnection().instanceUrl;

const targetOrg = targetOrgObj.getOrgId();
const tgtAccessToken = targetOrgObj.getConnection().accessToken as string;
const tgtOrgInstUrl = targetOrgObj.getConnection().instanceUrl;

// Fetch Valid JWT with Data Seed Org Perm
const { jwt: jwtValue } = await initiateJWTMint(srcOrgInstUrl, srcAccessToken, tgtOrgInstUrl, tgtAccessToken);
const { request_id: jobId } = await initiateDataSeed(
configFile,
'data-copy',
jwtValue,
srcOrgInstUrl,
srcAccessToken,
tgtOrgInstUrl,
tgtAccessToken,
sourceOrg
);

if (!jobId) throw new Error('Failed to receive job id');

Expand All @@ -83,7 +102,13 @@ export default class DataSeedingMigrate extends SfCommand<DataSeedingMigrateResu

const options: PollingClient.Options = {
poll: async (): Promise<StatusResult> => {
const response = await pollSeedStatus(jobId);
const { jwt: jwtValueNew } = await initiateJWTMint(
srcOrgInstUrl,
srcAccessToken,
tgtOrgInstUrl,
tgtAccessToken
);
const response = await pollSeedStatus(jobId, jwtValueNew);

mso.goto(getStage(response.step), {
startTime: response.execution_start_time,
Expand Down Expand Up @@ -136,7 +161,8 @@ export default class DataSeedingMigrate extends SfCommand<DataSeedingMigrateResu
throw err;
}
} else {
const response = await pollSeedStatus(jobId);
const { jwt: jwtValueNew } = await initiateJWTMint(srcOrgInstUrl, srcAccessToken, tgtOrgInstUrl, tgtAccessToken);
const response = await pollSeedStatus(jobId, jwtValueNew);

const mso = getSeedMigrateMso({
jsonEnabled: this.jsonEnabled(),
Expand Down
6 changes: 3 additions & 3 deletions src/commands/data-seeding/migrate/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { getSeedMigrateMso, getSeedMigrateStage as getStage } from '../../../uti
import { DataSeedingReportResult } from '../../../utils/types.js';
import { MigrateRequestCache } from '../../../utils/cache.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url)
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-data-seeding', 'data-seeding.migrate.report');

export default class DataSeedingMigrateReport extends SfCommand<DataSeedingReportResult> {
Expand Down Expand Up @@ -40,7 +40,7 @@ export default class DataSeedingMigrateReport extends SfCommand<DataSeedingRepor

if (!jobId) throw new SfError('No job ID provided or found in cache');

const response = await pollSeedStatus(jobId);
const response = await pollSeedStatus(jobId, '');

const data = {
jobId,
Expand Down Expand Up @@ -77,4 +77,4 @@ export default class DataSeedingMigrateReport extends SfCommand<DataSeedingRepor
...data,
};
}
}
}
99 changes: 72 additions & 27 deletions src/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@

import fs from 'node:fs';
import got from 'got';
import { CookieJar } from 'tough-cookie';
import FormData from 'form-data';
import { SfError, Logger } from '@salesforce/core';

export type SeedResponse = {
request_id: string;
};
export type ServletResponse = {
jwt: string;
};
export type AuthServletResponse = {
statusCode: string;
body: string;
};

export type PollSeedResponse = {
execution_end_time: string;
Expand All @@ -26,41 +32,36 @@ export type PollSeedResponse = {

export type DataSeedingOperation = 'data-generation' | 'data-copy';

const baseUrl = process.env.SF_DATA_SEEDING_URL ?? 'https://data-seed-scratchpad5.sfdc-3vx9f4.svc.sfdcfc.net';
const csrfUrl = `${baseUrl}/get-csrf-token`;
const baseUrl = 'https://api.salesforce.com/platform/data-seed/v1';
const seedUrl = `${baseUrl}/data-seed`;
const pollUrl = `${baseUrl}/status`;

export const getCookieJar = async (): Promise<CookieJar> => {
const cookieJar = new CookieJar();
await got(csrfUrl, { cookieJar });
return cookieJar;
};

export const getCsrfToken = (cookieJar: CookieJar): string => {
const csrfToken = cookieJar.getCookiesSync(csrfUrl).find((cookie) => cookie.key === 'csrf_token')?.value;
if (!csrfToken) throw new SfError('Failed to obtain CSRF token');

return csrfToken;
};

export const initiateDataSeed = async (config: string, operation: DataSeedingOperation): Promise<SeedResponse> => {
const cookieJar = await getCookieJar();
const csrf = getCsrfToken(cookieJar);

const sfRegion = 'us-east-1'
export const initiateDataSeed = async (
config: string,
operation: DataSeedingOperation,
jwt: string,
srcOrgUrl: string,
srcAccessToken: string,
tgtOrgUrl: string,
tgtAccessToken: string,
srcOrgId: string
): Promise<SeedResponse> => {
const form = new FormData();
form.append('config_file', fs.createReadStream(config));
form.append('credentials_file', fs.createReadStream('ignore/credentials.txt'));
form.append('operation', operation);

form.append('source_access_token', srcAccessToken);
form.append('source_instance_url', srcOrgUrl);
form.append('target_access_token', tgtAccessToken);
form.append('target_instance_url', tgtOrgUrl);
form.append('source_org_id',srcOrgId);
// TODO: Update to use .json() instead of JSON.parse once the Error response is changed to be JSON
// Update the return type as well
const response = await got.post(seedUrl, {
throwHttpErrors: false,
cookieJar,
headers: {
...form.getHeaders(),
'X-CSRFToken': csrf,
Authorization: `Bearer ${jwt}`,
'x-salesforce-region':sfRegion,
},
body: form,
});
Expand All @@ -72,12 +73,56 @@ export const initiateDataSeed = async (config: string, operation: DataSeedingOpe
return JSON.parse(response.body) as SeedResponse;
};

export const pollSeedStatus = async (jobId: string): Promise<PollSeedResponse> => {
export const initiateJWTMint = async (
srcOrgUrl: string,
srcAccessToken: string,
tgtOrgUrl: string,
tgtAccessToken: string
): Promise<ServletResponse> => {
const srcServletUrl = `${srcOrgUrl}/dataseed/auth`;
const tgtServletUrl = `${tgtOrgUrl}/dataseed/auth`;

const [responseSrc, responseTgt] = await Promise.all([
callAuthServlet(srcServletUrl, srcAccessToken),
callAuthServlet(tgtServletUrl, tgtAccessToken),
]);

if (responseSrc.statusCode === '200') {
return JSON.parse(responseSrc.body) as ServletResponse;
}

if (responseTgt.statusCode === '200') {
return JSON.parse(responseTgt.body) as ServletResponse;
}

throw new SfError(
`Org permission for data seed not found in either the source or target org.\nSource Response: Error Code : ${responseSrc.statusCode} - ${responseSrc.body}. \nTarget Response: Error Code : ${responseTgt.statusCode} - ${responseTgt.body}`
);
};

const callAuthServlet = async (url: string, accessToken: string): Promise<AuthServletResponse> => {
const response = await got.post(url, {
throwHttpErrors: false,
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return {
statusCode: response.statusCode.toString(), // Convert to string
body: response.body,
};
};

export const pollSeedStatus = async (jobId: string, jwt: string): Promise<PollSeedResponse> => {
const logger = await Logger.child('PollSeedStatus');

// TODO: Update to use .json() instead of JSON.parse once the Error response is changed to be JSON
// Update the return type as well
const response = await got.get(`${pollUrl}/${jobId}`, { throwHttpErrors: false });
const headers = {
Authorization: `Bearer ${jwt}`,
'x-salesforce-region': sfRegion,
};
const response = await got.get(`${pollUrl}/${jobId}`, { throwHttpErrors: false, headers });

if (response.statusCode !== 200) {
// TODO: Print error body once the Error response is changed to be JSON
Expand Down
1 change: 1 addition & 0 deletions src/utils/mso.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type MsoGet = string | undefined;
// - They have been converted to lowercase for later comparison
// The values in this Map are used as the stage names in mso
const seedGenerateStagesMap = new Map<string, string>([
['init','Initializing'],
['querying source org', 'Querying Source Org'],
['data generation', 'Data Generation'],
['populating target org', 'Populating Target Org'],
Expand Down
10 changes: 0 additions & 10 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7396,16 +7396,6 @@ tough-cookie@*:
universalify "^0.2.0"
url-parse "^1.5.3"

tough-cookie@^4.1.4:
version "4.1.4"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36"
integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==
dependencies:
psl "^1.1.33"
punycode "^2.1.1"
universalify "^0.2.0"
url-parse "^1.5.3"

tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
Expand Down