diff --git a/package.json b/package.json index 7998160..40dba21 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/commands/data-seeding/generate/index.ts b/src/commands/data-seeding/generate/index.ts index 4f308c8..dd104ef 100644 --- a/src/commands/data-seeding/generate/index.ts +++ b/src/commands/data-seeding/generate/index.ts @@ -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'; @@ -23,12 +23,12 @@ export default class DataSeedingGenerate extends SfCommand { 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'); @@ -83,7 +101,13 @@ export default class DataSeedingGenerate extends SfCommand => { - 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, @@ -134,7 +158,8 @@ export default class DataSeedingGenerate extends SfCommand { 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'); @@ -83,7 +102,13 @@ export default class DataSeedingMigrate extends SfCommand => { - 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, @@ -136,7 +161,8 @@ export default class DataSeedingMigrate extends SfCommand { @@ -40,7 +40,7 @@ export default class DataSeedingMigrateReport extends SfCommand => { - 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 => { - 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 => { 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, }); @@ -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 => { +export const initiateJWTMint = async ( + srcOrgUrl: string, + srcAccessToken: string, + tgtOrgUrl: string, + tgtAccessToken: string +): Promise => { + 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 => { + 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 => { 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 diff --git a/src/utils/mso.ts b/src/utils/mso.ts index 82a04fd..ccc206c 100644 --- a/src/utils/mso.ts +++ b/src/utils/mso.ts @@ -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([ + ['init','Initializing'], ['querying source org', 'Querying Source Org'], ['data generation', 'Data Generation'], ['populating target org', 'Populating Target Org'], diff --git a/yarn.lock b/yarn.lock index e2eee5d..fc577ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"