From c7232125d5a80bc81b2a1881037cf7b2e9c840f4 Mon Sep 17 00:00:00 2001 From: James Norton Date: Fri, 4 Oct 2024 15:15:09 -0400 Subject: [PATCH 01/80] HARMONY-1897: Add support for adding/deleting labels to/from jobs --- services/harmony/app/frontends/labels.ts | 58 +++++++++++ services/harmony/app/middleware/label.ts | 1 + services/harmony/app/models/label.ts | 125 ++++++++++++++++++++--- services/harmony/app/routers/router.ts | 8 ++ 4 files changed, 176 insertions(+), 16 deletions(-) create mode 100644 services/harmony/app/frontends/labels.ts diff --git a/services/harmony/app/frontends/labels.ts b/services/harmony/app/frontends/labels.ts new file mode 100644 index 000000000..14c12206e --- /dev/null +++ b/services/harmony/app/frontends/labels.ts @@ -0,0 +1,58 @@ +// functions to support labels routes + +import { Response, NextFunction } from 'express'; +import HarmonyRequest from '../models/harmony-request'; +import { addLabelsToJob, deleteLabelsFromJob } from '../models/label'; +import db from '../util/db'; + +/** + * Express.js handler that adds one or more labels to a job `(POST /labels/{jobID}/add)`. + * Currently only the job owner can add labels (no admin access). + * + * @param req - The request sent by the client + * @param res - The response to send to the client + * @param next - The next function in the call chain + * @returns Resolves when the request is complete + */ +export async function addJobLabels( + req: HarmonyRequest, res: Response, next: NextFunction, +): Promise { + try { + req.context.logger.info(`Adding label(s) ${JSON.stringify(req.body.label)} to job ${req.params.jobID} for user ${req.user}`); + await db.transaction(async (trx) => { + await addLabelsToJob(trx, req.params.jobID, req.user, req.body.label); + }); + + res.status(200); + res.send('OK'); + } catch (e) { + req.context.logger.error(e); + next(e); + } +} + +/** + * Express.js handler that removes one or more labels from a job `(POST /labels/{jobID}/delete)`. + * Currently only the job owner can add labels (no admin access). + * + * @param req - The request sent by the client + * @param res - The response to send to the client + * @param next - The next function in the call chain + * @returns Resolves when the request is complete + */ +export async function deleteJobLabels( + req: HarmonyRequest, res: Response, next: NextFunction, +): Promise { + try { + req.context.logger.info(`Adding label ${req.body.label} to job ${req.params.jobID} for user ${req.user}`); + await db.transaction(async (trx) => { + await deleteLabelsFromJob(trx, req.params.jobID, req.user, req.body.label); + }); + + res.status(200); + res.send('OK'); + } catch (e) { + req.context.logger.error(e); + next(e); + } +} \ No newline at end of file diff --git a/services/harmony/app/middleware/label.ts b/services/harmony/app/middleware/label.ts index 656ccc80a..194545e38 100644 --- a/services/harmony/app/middleware/label.ts +++ b/services/harmony/app/middleware/label.ts @@ -25,6 +25,7 @@ export default async function handleLabelParameter( if (lbl === '') { res.status(400); res.send('Labels must contain at least one non-whitespace character'); + return; } } req.body.label = label; diff --git a/services/harmony/app/models/label.ts b/services/harmony/app/models/label.ts index 1ade50180..c4bad3b80 100644 --- a/services/harmony/app/models/label.ts +++ b/services/harmony/app/models/label.ts @@ -1,4 +1,7 @@ import { Transaction } from '../util/db'; +import { Job } from './job'; +import { ForbiddenError, NotFoundError, RequestValidationError } from '../util/errors'; +import isUUID from '../util/uuid'; export const LABELS_TABLE = 'labels'; export const JOBS_LABELS_TABLE = 'jobs_labels'; @@ -27,6 +30,58 @@ export function normalizeLabel(label: string): string { return label.trim().toLowerCase(); } +/** + * Verify that the user can change the labels on a give job. Currently only job owners can + * change the labels for a job. + * @param trx - the transaction to use for querying + * @param jobID - the UUID associated with the job + * @throws `ForbiddenError` if the user does not own the job. + */ +export async function verifyUserAccessToUpdateLabels( + trx: Transaction, + jobID: string, + username: string): Promise { + if (!isUUID(jobID)) { + throw new RequestValidationError(`jobId ${jobID} is in invalid format.`); + } + const jobOwner = await trx(Job.table).select('username').where('jobID', '=', jobID).first(); + console.log(`JOB OWNER: ${JSON.stringify(jobOwner, null, 2)}`); + if (!jobOwner) { + throw new NotFoundError('Job does not exist'); + } + console.log(`USER NAME: ${username} JOB OWNER: ${JSON.stringify(jobOwner, null, 2)}`); + if (username !== jobOwner.username) { + throw new ForbiddenError('You do not have permission to update labels on this job'); + } +} + +/** + * + * @param trx - the transaction to use for querying + * @param labels - the string values for the labels + * @param username - the user adding the labels + * @returns A list of the ids of the saved labels + */ +async function saveLabels( + trx: Transaction, + labels: string[], + timeStamp: Date, + username: string): Promise { + const labelRows = labels.map((label) => { + return { username, value: label, createdAt: timeStamp, updatedAt: timeStamp }; + }); + + // this will upsert the labels - if a label already exists for a given user + // it will just update the `updatedAt` timestamp + const insertedRows = await trx(LABELS_TABLE) + .insert(labelRows) + .returning('id') + .onConflict(['username', 'value']) + .merge(['updatedAt']); + + return insertedRows.map((row) => row.id); +} + /** * Returns the labels for a given job * @param trx - the transaction to use for querying @@ -38,11 +93,11 @@ export async function getLabelsForJob( trx: Transaction, jobID: string, ): Promise { - const query = trx('jobs_labels') + const query = trx(JOBS_LABELS_TABLE) .where({ job_id: jobID }) - .orderBy(['jobs_labels.id']) - .innerJoin('labels', 'jobs_labels.label_id', '=', 'labels.id') - .select(['labels.value']); + .orderBy([`${JOBS_LABELS_TABLE}.id`]) + .innerJoin(LABELS_TABLE, `${JOBS_LABELS_TABLE}.label_id`, '=', `${LABELS_TABLE}.id`) + .select([`${LABELS_TABLE}.value`]); const rows = await query; @@ -68,29 +123,67 @@ export async function setLabelsForJob( if (!labels) return; // delete any labels that already exist for the job - await trx('jobs_labels') + await trx(JOBS_LABELS_TABLE) .where({ job_id: jobID }) .delete(); if (labels.length > 0) { const now = new Date(); - const labelRows = labels.map((label) => { - return { username, value: label, createdAt: now, updatedAt: now }; + const ids = await saveLabels(trx, labels, now, username); + const jobsLabelRows = ids.map((id) => { + return { job_id: jobID, label_id: id, createdAt: now, updatedAt: now }; }); - // this will insert the labels - if a label already exists for a given user - // it will just update the `updatedAt` timestamp - const insertedRows = await trx('labels') - .insert(labelRows) - .returning('id') - .onConflict(['username', 'value']) - .merge(['updatedAt']); + await trx(JOBS_LABELS_TABLE).insert(jobsLabelRows); + } +} - const ids = insertedRows.map((row) => row.id); +/** + * Add labels to a given job for the given user. Any labels that already exist for the given + * job will not be re-added or replaced. + * @param trx - the transaction to use for querying + * @param jobID - the UUID associated with the job + * @param username - the username the labels belong to + * @param labels - the array of strings representing the labels. + */ +export async function addLabelsToJob( + trx: Transaction, + jobID: string, + username: string, + labels: string[], +): Promise { + await verifyUserAccessToUpdateLabels(trx, jobID, username); + const now = new Date(); + const existingLabels = await getLabelsForJob(trx, jobID); + const labelsToAdd = labels.filter(label => !existingLabels.includes(label)); + if (labelsToAdd.length > 0) { + const ids = await saveLabels(trx, labelsToAdd, now, username); const jobsLabelRows = ids.map((id) => { return { job_id: jobID, label_id: id, createdAt: now, updatedAt: now }; }); - await trx('jobs_labels').insert(jobsLabelRows); + await trx(JOBS_LABELS_TABLE).insert(jobsLabelRows); } +} + +/** + * Delete labels from a given job for the given user. + * @param trx - the transaction to use for querying + * @param jobID - the UUID associated with the job + * @param username - the username the labels belong to + * @param labels - the array of strings representing the labels. + */ +export async function deleteLabelsFromJob( + trx: Transaction, + jobID: string, + username: string, + labels: string[], +): Promise { + await verifyUserAccessToUpdateLabels(trx, jobID, username); + + await trx(JOBS_LABELS_TABLE) + .where(`${JOBS_LABELS_TABLE}.job_id`, '=', jobID) + .join(LABELS_TABLE, `${JOBS_LABELS_TABLE}.label_id`, '=', `${LABELS_TABLE}.id`) + .where(`${LABELS_TABLE}.value`, 'in', labels) + .del(); } \ No newline at end of file diff --git a/services/harmony/app/routers/router.ts b/services/harmony/app/routers/router.ts index fb20c7bd2..93b3e4674 100644 --- a/services/harmony/app/routers/router.ts +++ b/services/harmony/app/routers/router.ts @@ -42,6 +42,7 @@ import { getCollectionCapabilitiesJson } from '../frontends/capabilities'; import extendDefault from '../middleware/extend'; import { getAdminHealth, getHealth } from '../frontends/health'; import handleLabelParameter from '../middleware/label'; +import { addJobLabels, deleteJobLabels } from '../frontends/labels'; export interface RouterConfig { PORT?: string | number; // The port to run the frontend server on BACKEND_PORT?: string | number; // The port to run the backend server on @@ -142,6 +143,7 @@ const authorizedRoutes = [ '/service-image*', '/service-deployment*', '/ogc-api-edr/.*/collections/*', + '/labels/*', ]; /** @@ -260,6 +262,12 @@ export default function router({ skipEarthdataLogin = 'false' }: RouterConfig): result.post('/jobs/skip-preview', jsonParser, asyncHandler(skipJobsPreview)); result.post('/jobs/pause', jsonParser, asyncHandler(pauseJobs)); + // job labels + result.get('/labels/:jobID/add', asyncHandler(addJobLabels)); + result.post('/labels/:jobID/add', jsonParser, asyncHandler(addJobLabels)); + result.get('/labels/:jobID/delete', asyncHandler(deleteJobLabels)); + result.post('/labels/:jobID/delete', jsonParser, asyncHandler(deleteJobLabels)); + result.get('/admin/request-metrics', asyncHandler(getRequestMetrics)); result.get('/workflow-ui', asyncHandler(getJobs)); From 79ba02353739b2a46261caed98611e0c3df4c66f Mon Sep 17 00:00:00 2001 From: James Norton Date: Tue, 8 Oct 2024 14:48:13 -0400 Subject: [PATCH 02/80] HARMONY-1897: Change labels routes to use HTTP actions --- services/harmony/app/frontends/labels.ts | 12 ++++++++---- services/harmony/app/routers/router.ts | 8 +++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/services/harmony/app/frontends/labels.ts b/services/harmony/app/frontends/labels.ts index 14c12206e..aef5f3f3a 100644 --- a/services/harmony/app/frontends/labels.ts +++ b/services/harmony/app/frontends/labels.ts @@ -18,10 +18,14 @@ export async function addJobLabels( req: HarmonyRequest, res: Response, next: NextFunction, ): Promise { try { - req.context.logger.info(`Adding label(s) ${JSON.stringify(req.body.label)} to job ${req.params.jobID} for user ${req.user}`); - await db.transaction(async (trx) => { - await addLabelsToJob(trx, req.params.jobID, req.user, req.body.label); - }); + req.context.logger.info('BODY:'); + req.context.logger.info(`${JSON.stringify(req.body, null, 2)}`); + for (const jobId of req.body.job) { + req.context.logger.info(`Adding label(s) ${JSON.stringify(req.body.label)} to job ${jobId} for user ${req.user}`); + await db.transaction(async (trx) => { + await addLabelsToJob(trx, jobId, req.user, req.body.label); + }); + } res.status(200); res.send('OK'); diff --git a/services/harmony/app/routers/router.ts b/services/harmony/app/routers/router.ts index 93b3e4674..c52b511cc 100644 --- a/services/harmony/app/routers/router.ts +++ b/services/harmony/app/routers/router.ts @@ -143,7 +143,7 @@ const authorizedRoutes = [ '/service-image*', '/service-deployment*', '/ogc-api-edr/.*/collections/*', - '/labels/*', + '/labels*', ]; /** @@ -263,10 +263,8 @@ export default function router({ skipEarthdataLogin = 'false' }: RouterConfig): result.post('/jobs/pause', jsonParser, asyncHandler(pauseJobs)); // job labels - result.get('/labels/:jobID/add', asyncHandler(addJobLabels)); - result.post('/labels/:jobID/add', jsonParser, asyncHandler(addJobLabels)); - result.get('/labels/:jobID/delete', asyncHandler(deleteJobLabels)); - result.post('/labels/:jobID/delete', jsonParser, asyncHandler(deleteJobLabels)); + result.put('/labels', jsonParser, asyncHandler(addJobLabels)); // add label(s) + result.delete('/labels', jsonParser, asyncHandler(deleteJobLabels)); // delete label(s) result.get('/admin/request-metrics', asyncHandler(getRequestMetrics)); From d60b2cdd5886bf3feca65f672913b0d82d356acc Mon Sep 17 00:00:00 2001 From: James Norton Date: Wed, 9 Oct 2024 12:22:51 -0400 Subject: [PATCH 03/80] HARMONY-1897: Add support for adding or deleting labels from a job --- db/db.sql | 1 + ...uniquene_constraint_for_job_id_label_id.js | 21 +++++ services/harmony/app/frontends/labels.ts | 33 ++++--- .../harmony/app/middleware/error-handler.ts | 2 +- services/harmony/app/models/label.ts | 87 +++++++++++-------- services/harmony/app/routers/router.ts | 6 +- 6 files changed, 98 insertions(+), 52 deletions(-) create mode 100644 db/migrations/20241009133047_add_uniquene_constraint_for_job_id_label_id.js diff --git a/db/db.sql b/db/db.sql index 681993497..9b530c7e7 100644 --- a/db/db.sql +++ b/db/db.sql @@ -63,6 +63,7 @@ CREATE TABLE `jobs_labels` ( `updatedAt` datetime not null, FOREIGN KEY(job_id) REFERENCES jobs(jobID) FOREIGN KEY(label_id) REFERENCES labels(id) + UNIQUE(job_id, label_id) ); CREATE TABLE `work_items` ( diff --git a/db/migrations/20241009133047_add_uniquene_constraint_for_job_id_label_id.js b/db/migrations/20241009133047_add_uniquene_constraint_for_job_id_label_id.js new file mode 100644 index 000000000..27249ac44 --- /dev/null +++ b/db/migrations/20241009133047_add_uniquene_constraint_for_job_id_label_id.js @@ -0,0 +1,21 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function(knex) { + return knex.schema.alterTable('jobs_labels', function (table) { + // Adding a composite unique constraint on job_id and label_id + table.unique(['job_id', 'label_id']); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function(knex) { + return knex.schema.alterTable('jobs_labels', function (table) { + // Dropping the unique constraint if rolled back + table.dropUnique(['job_id', 'label_id']); + }); +}; diff --git a/services/harmony/app/frontends/labels.ts b/services/harmony/app/frontends/labels.ts index aef5f3f3a..0542fb943 100644 --- a/services/harmony/app/frontends/labels.ts +++ b/services/harmony/app/frontends/labels.ts @@ -2,8 +2,9 @@ import { Response, NextFunction } from 'express'; import HarmonyRequest from '../models/harmony-request'; -import { addLabelsToJob, deleteLabelsFromJob } from '../models/label'; +import { addLabelsToJobs, deleteLabelsFromJobs } from '../models/label'; import db from '../util/db'; +import { isAdminUser } from '../util/edl-api'; /** * Express.js handler that adds one or more labels to a job `(POST /labels/{jobID}/add)`. @@ -17,17 +18,20 @@ import db from '../util/db'; export async function addJobLabels( req: HarmonyRequest, res: Response, next: NextFunction, ): Promise { + const isAdmin = await isAdminUser(req); + try { - req.context.logger.info('BODY:'); - req.context.logger.info(`${JSON.stringify(req.body, null, 2)}`); - for (const jobId of req.body.job) { - req.context.logger.info(`Adding label(s) ${JSON.stringify(req.body.label)} to job ${jobId} for user ${req.user}`); - await db.transaction(async (trx) => { - await addLabelsToJob(trx, jobId, req.user, req.body.label); - }); - } + // for (const jobId of req.body.job) { + // req.context.logger.info(`Adding label(s) ${JSON.stringify(req.body.label)} to job ${jobId} for user ${req.user}`); + // await db.transaction(async (trx) => { + // await addLabelsToJob(trx, jobId, req.user, req.body.label); + // }); + // } + await db.transaction(async (trx) => { + await addLabelsToJobs(trx, req.body.job, req.user, req.body.label, isAdmin); + }); - res.status(200); + res.status(201); res.send('OK'); } catch (e) { req.context.logger.error(e); @@ -47,14 +51,15 @@ export async function addJobLabels( export async function deleteJobLabels( req: HarmonyRequest, res: Response, next: NextFunction, ): Promise { + const isAdmin = await isAdminUser(req); + try { - req.context.logger.info(`Adding label ${req.body.label} to job ${req.params.jobID} for user ${req.user}`); await db.transaction(async (trx) => { - await deleteLabelsFromJob(trx, req.params.jobID, req.user, req.body.label); + await deleteLabelsFromJobs(trx, req.body.job, req.user, req.body.label, isAdmin); }); - res.status(200); - res.send('OK'); + res.status(204); + res.send(); } catch (e) { req.context.logger.error(e); next(e); diff --git a/services/harmony/app/middleware/error-handler.ts b/services/harmony/app/middleware/error-handler.ts index 93b6affef..9715e1c93 100644 --- a/services/harmony/app/middleware/error-handler.ts +++ b/services/harmony/app/middleware/error-handler.ts @@ -9,7 +9,7 @@ import { import HarmonyRequest from '../models/harmony-request'; const errorTemplate = fs.readFileSync(path.join(__dirname, '../views/server-error.mustache.html'), { encoding: 'utf8' }); -const jsonErrorRoutesRegex = /jobs|capabilities|ogc-api-coverages|ogc-api-edr|service-deployment(?:s-state)?|service-image-tag|stac|metrics|health|configuration|workflow-ui\/.*\/(?:links|logs|retry)/; +const jsonErrorRoutesRegex = /jobs|labels|capabilities|ogc-api-coverages|ogc-api-edr|service-deployment(?:s-state)?|service-image-tag|stac|metrics|health|configuration|workflow-ui\/.*\/(?:links|logs|retry)/; /** * Returns true if the provided error should be returned as JSON. diff --git a/services/harmony/app/models/label.ts b/services/harmony/app/models/label.ts index c4bad3b80..16391870f 100644 --- a/services/harmony/app/models/label.ts +++ b/services/harmony/app/models/label.ts @@ -1,6 +1,6 @@ import { Transaction } from '../util/db'; import { Job } from './job'; -import { ForbiddenError, NotFoundError, RequestValidationError } from '../util/errors'; +import { NotFoundError, RequestValidationError } from '../util/errors'; import isUUID from '../util/uuid'; export const LABELS_TABLE = 'labels'; @@ -34,28 +34,41 @@ export function normalizeLabel(label: string): string { * Verify that the user can change the labels on a give job. Currently only job owners can * change the labels for a job. * @param trx - the transaction to use for querying - * @param jobID - the UUID associated with the job + * @param jobIds - the UUIDs associated with the jobs * @throws `ForbiddenError` if the user does not own the job. */ export async function verifyUserAccessToUpdateLabels( trx: Transaction, - jobID: string, - username: string): Promise { - if (!isUUID(jobID)) { - throw new RequestValidationError(`jobId ${jobID} is in invalid format.`); + jobIds: string[], + username: string, + isAdmin: boolean = false): Promise { + for (const jobId of jobIds) { + if (!isUUID(jobId)) { + throw new RequestValidationError(`jobId ${jobId} is in invalid format.`); + } } - const jobOwner = await trx(Job.table).select('username').where('jobID', '=', jobID).first(); - console.log(`JOB OWNER: ${JSON.stringify(jobOwner, null, 2)}`); - if (!jobOwner) { - throw new NotFoundError('Job does not exist'); + const rows = await trx(Job.table).select('jobID', 'username') + .where('jobID', 'in', jobIds); + const foundJobs = []; + for (const row of rows) { + const jobId = row.jobID; + const jobOwner = row.username; + if (jobOwner != username && !isAdmin) { + //throw new ForbiddenError(`You do not have permission to update labels on job ${jobId}`); + throw new NotFoundError(); + } + foundJobs.push(jobId); } - console.log(`USER NAME: ${username} JOB OWNER: ${JSON.stringify(jobOwner, null, 2)}`); - if (username !== jobOwner.username) { - throw new ForbiddenError('You do not have permission to update labels on this job'); + + for (const jobId of jobIds) { + if (!foundJobs.includes(jobId)) { + throw new NotFoundError(`Unable to find job ${jobId}`); + } } } /** + * Save labels for a user to the labels table * * @param trx - the transaction to use for querying * @param labels - the string values for the labels @@ -139,51 +152,57 @@ export async function setLabelsForJob( } /** - * Add labels to a given job for the given user. Any labels that already exist for the given + * Add labels to the given jobs for the given user. Any labels that already exist for the given * job will not be re-added or replaced. * @param trx - the transaction to use for querying - * @param jobID - the UUID associated with the job + * @param jobIDs - the UUIDs associated with the jobs * @param username - the username the labels belong to * @param labels - the array of strings representing the labels. */ -export async function addLabelsToJob( +export async function addLabelsToJobs( trx: Transaction, - jobID: string, + jobIDs: string[], username: string, labels: string[], + isAdmin: boolean = false, ): Promise { - await verifyUserAccessToUpdateLabels(trx, jobID, username); + await verifyUserAccessToUpdateLabels(trx, jobIDs, username, isAdmin); const now = new Date(); - const existingLabels = await getLabelsForJob(trx, jobID); - const labelsToAdd = labels.filter(label => !existingLabels.includes(label)); - if (labelsToAdd.length > 0) { - const ids = await saveLabels(trx, labelsToAdd, now, username); - const jobsLabelRows = ids.map((id) => { - return { job_id: jobID, label_id: id, createdAt: now, updatedAt: now }; - }); - - await trx(JOBS_LABELS_TABLE).insert(jobsLabelRows); + const labelIds = await saveLabels(trx, labels, now, username); + const rowsToAdd = []; + for (const jobID of jobIDs) { + for (const labelId of labelIds) { + rowsToAdd.push({ job_id: jobID, label_id: labelId, createdAt: now, updatedAt: now }); + } + } + if (rowsToAdd.length > 0) { + await trx(JOBS_LABELS_TABLE).insert(rowsToAdd) + .onConflict(['job_id', 'label_id']) + .merge(['updatedAt']); } } + /** - * Delete labels from a given job for the given user. + * Delete one or more labels from the given jobs for the given user. * @param trx - the transaction to use for querying - * @param jobID - the UUID associated with the job + * @param jobIDs - the UUIDs associated with the jobs * @param username - the username the labels belong to * @param labels - the array of strings representing the labels. + * @param isAdmin - true if the user is an admin user */ -export async function deleteLabelsFromJob( +export async function deleteLabelsFromJobs( trx: Transaction, - jobID: string, + jobIDs: string[], username: string, labels: string[], + isAdmin: boolean = false, ): Promise { - await verifyUserAccessToUpdateLabels(trx, jobID, username); + await verifyUserAccessToUpdateLabels(trx, jobIDs, username, isAdmin); await trx(JOBS_LABELS_TABLE) - .where(`${JOBS_LABELS_TABLE}.job_id`, '=', jobID) + .where(`${JOBS_LABELS_TABLE}.job_id`, 'in', jobIDs) .join(LABELS_TABLE, `${JOBS_LABELS_TABLE}.label_id`, '=', `${LABELS_TABLE}.id`) .where(`${LABELS_TABLE}.value`, 'in', labels) .del(); -} \ No newline at end of file +} diff --git a/services/harmony/app/routers/router.ts b/services/harmony/app/routers/router.ts index c52b511cc..53f9b91cd 100644 --- a/services/harmony/app/routers/router.ts +++ b/services/harmony/app/routers/router.ts @@ -143,7 +143,7 @@ const authorizedRoutes = [ '/service-image*', '/service-deployment*', '/ogc-api-edr/.*/collections/*', - '/labels*', + '/labels', ]; /** @@ -263,8 +263,8 @@ export default function router({ skipEarthdataLogin = 'false' }: RouterConfig): result.post('/jobs/pause', jsonParser, asyncHandler(pauseJobs)); // job labels - result.put('/labels', jsonParser, asyncHandler(addJobLabels)); // add label(s) - result.delete('/labels', jsonParser, asyncHandler(deleteJobLabels)); // delete label(s) + result.put('/labels', jsonParser, asyncHandler(addJobLabels)); + result.delete('/labels', jsonParser, asyncHandler(deleteJobLabels)); result.get('/admin/request-metrics', asyncHandler(getRequestMetrics)); From 751a258f8d7e98232d3261270b390126df67b99e Mon Sep 17 00:00:00 2001 From: vinny Date: Wed, 9 Oct 2024 13:22:46 -0400 Subject: [PATCH 04/80] HARMONY-1892: Show all job checkboxes on non-admin workflow ui to enable labeling --- services/harmony/app/frontends/workflow-ui.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/services/harmony/app/frontends/workflow-ui.ts b/services/harmony/app/frontends/workflow-ui.ts index 6cc2050f9..17fff1e8e 100644 --- a/services/harmony/app/frontends/workflow-ui.ts +++ b/services/harmony/app/frontends/workflow-ui.ts @@ -159,10 +159,11 @@ function getPaginationDisplay(pagination: ILengthAwarePagination): { from: strin * a row of the jobs table. * @param logger - the logger to use * @param requestQuery - the query parameters from the request - * @param checked - whether the job should be selected + * @param isAdminRoute - whether the current endpoint being accessed is /admin/* + * @param jobIDs - list of IDs for selected jobs * @returns an object with rendering functions */ -function jobRenderingFunctions(logger: Logger, requestQuery: Record, jobIDs: string[] = []): object { +function jobRenderingFunctions(logger: Logger, requestQuery: Record, isAdminRoute: boolean, jobIDs: string[] = []): object { return { jobBadge(): string { return statusClass[this.status]; @@ -205,7 +206,7 @@ function jobRenderingFunctions(logger: Logger, requestQuery: Record }, jobSelectBox(): string { const checked = jobIDs.indexOf(this.jobID) > -1 ? 'checked' : ''; - if (this.hasTerminalStatus()) { + if (this.hasTerminalStatus() && isAdminRoute) { return ''; } return ``; @@ -323,7 +324,7 @@ export async function getJobs( const previousPage = pageLinks.find((l) => l.rel === 'prev'); const currentPage = pageLinks.find((l) => l.rel === 'self'); const paginationDisplay = getPaginationDisplay(pagination); - const selectAllBox = jobs.some((j) => !j.hasTerminalStatus()) ? + const selectAllBox = !isAdminRoute || jobs.some((j) => !j.hasTerminalStatus()) ? '' : ''; res.render('workflow-ui/jobs/index', { version, @@ -355,7 +356,7 @@ export async function getJobs( { ...nextPage, linkTitle: 'next' }, { ...lastPage, linkTitle: 'last' }, ], - ...jobRenderingFunctions(req.context.logger, requestQuery), + ...jobRenderingFunctions(req.context.logger, requestQuery, isAdminRoute), }); } catch (e) { req.context.logger.error(e); @@ -626,24 +627,25 @@ export async function getJobsTable( req: HarmonyRequest, res: Response, next: NextFunction, ): Promise { try { + const isAdminRoute = req.context.isAdminAccess; const { jobIDs } = req.body; const { isAdmin } = await getEdlGroupInformation( req.user, req.context.logger, ); const requestQuery = keysToLowerCase(req.query); - const { tableQuery } = parseQuery(requestQuery, JobStatus, req.context.isAdminAccess); + const { tableQuery } = parseQuery(requestQuery, JobStatus, isAdminRoute); const jobQuery = tableQueryToJobQuery(tableQuery, isAdmin, req.user); const { page, limit } = getPagingParams(req, env.defaultJobListPageSize, 1, true, true); const jobsRes = await Job.queryAll(db, jobQuery, page, limit); const jobs = jobsRes.data; const { pagination } = jobsRes; - const selectAllChecked = jobs.every((j) => j.hasTerminalStatus() || (jobIDs.indexOf(j.jobID) > -1)) ? 'checked' : ''; - const selectAllBox = jobs.some((j) => !j.hasTerminalStatus()) ? + const selectAllChecked = jobs.every((j) => (j.hasTerminalStatus() && isAdminRoute) || (jobIDs.indexOf(j.jobID) > -1)) ? 'checked' : ''; + const selectAllBox = !isAdminRoute || jobs.some((j) => !j.hasTerminalStatus()) ? `` : ''; const tableContext = { jobs, selectAllBox, - ...jobRenderingFunctions(req.context.logger, requestQuery, jobIDs), + ...jobRenderingFunctions(req.context.logger, requestQuery, isAdminRoute, jobIDs), isAdminRoute: req.context.isAdminAccess, }; const tableHtml = await new Promise((resolve, reject) => req.app.render( From e13febebc1144143f9d3dd76a0a4518e9e17dc4b Mon Sep 17 00:00:00 2001 From: vinny Date: Wed, 9 Oct 2024 14:17:55 -0400 Subject: [PATCH 05/80] HARMONY-1892:Ensure jobs length gt 1 --- services/harmony/app/frontends/workflow-ui.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/harmony/app/frontends/workflow-ui.ts b/services/harmony/app/frontends/workflow-ui.ts index 17fff1e8e..7878e5187 100644 --- a/services/harmony/app/frontends/workflow-ui.ts +++ b/services/harmony/app/frontends/workflow-ui.ts @@ -324,7 +324,7 @@ export async function getJobs( const previousPage = pageLinks.find((l) => l.rel === 'prev'); const currentPage = pageLinks.find((l) => l.rel === 'self'); const paginationDisplay = getPaginationDisplay(pagination); - const selectAllBox = !isAdminRoute || jobs.some((j) => !j.hasTerminalStatus()) ? + const selectAllBox = jobs.length > 0 && (!isAdminRoute || jobs.some((j) => !j.hasTerminalStatus())) ? '' : ''; res.render('workflow-ui/jobs/index', { version, @@ -640,7 +640,7 @@ export async function getJobsTable( const jobs = jobsRes.data; const { pagination } = jobsRes; const selectAllChecked = jobs.every((j) => (j.hasTerminalStatus() && isAdminRoute) || (jobIDs.indexOf(j.jobID) > -1)) ? 'checked' : ''; - const selectAllBox = !isAdminRoute || jobs.some((j) => !j.hasTerminalStatus()) ? + const selectAllBox = jobs.length > 0 && (!isAdminRoute || jobs.some((j) => !j.hasTerminalStatus())) ? `` : ''; const tableContext = { jobs, From a318a80249bb98e87c9cca18b5f6c305fe28c0de Mon Sep 17 00:00:00 2001 From: James Norton Date: Wed, 9 Oct 2024 15:54:01 -0400 Subject: [PATCH 06/80] HARMONY-1897: Add tests for labels --- services/harmony/app/frontends/labels.ts | 8 +-- .../172849780337476801 | 26 +++++++ services/harmony/test/helpers/labels.ts | 24 +++++++ services/harmony/test/labels/label_crud.ts | 68 +++++++++++++++++++ 4 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 services/harmony/fixtures/uat.urs.earthdata.nasa.gov-443/172849780337476801 create mode 100644 services/harmony/test/helpers/labels.ts create mode 100644 services/harmony/test/labels/label_crud.ts diff --git a/services/harmony/app/frontends/labels.ts b/services/harmony/app/frontends/labels.ts index 0542fb943..dc56f5cda 100644 --- a/services/harmony/app/frontends/labels.ts +++ b/services/harmony/app/frontends/labels.ts @@ -21,14 +21,8 @@ export async function addJobLabels( const isAdmin = await isAdminUser(req); try { - // for (const jobId of req.body.job) { - // req.context.logger.info(`Adding label(s) ${JSON.stringify(req.body.label)} to job ${jobId} for user ${req.user}`); - // await db.transaction(async (trx) => { - // await addLabelsToJob(trx, jobId, req.user, req.body.label); - // }); - // } await db.transaction(async (trx) => { - await addLabelsToJobs(trx, req.body.job, req.user, req.body.label, isAdmin); + await addLabelsToJobs(trx, req.body.jobID, req.user, req.body.label, isAdmin); }); res.status(201); diff --git a/services/harmony/fixtures/uat.urs.earthdata.nasa.gov-443/172849780337476801 b/services/harmony/fixtures/uat.urs.earthdata.nasa.gov-443/172849780337476801 new file mode 100644 index 000000000..03be02973 --- /dev/null +++ b/services/harmony/fixtures/uat.urs.earthdata.nasa.gov-443/172849780337476801 @@ -0,0 +1,26 @@ +GET /api/user_groups/groups_for_user/bob +accept: application/json, text/plain, */* +authorization: Bearer fake_access +accept-encoding: gzip, compress, deflate, br + +HTTP/1.1 401 Unauthorized +server: nginx/1.22.1 +date: Wed, 09 Oct 2024 18:16:43 GMT +content-type: application/json; charset=utf-8 +transfer-encoding: chunked +connection: keep-alive +x-frame-options: SAMEORIGIN +x-xss-protection: 1; mode=block +x-content-type-options: nosniff +x-download-options: noopen +x-permitted-cross-domain-policies: none +referrer-policy: strict-origin-when-cross-origin +cache-control: no-store +pragma: no-cache +expires: Fri, 01 Jan 1990 00:00:00 GMT +www-authenticate: Bearer realm="Earthdata Login",error="invalid_token" +x-request-id: 3c72fe06-5bf9-4a32-a148-8f444a27906e +x-runtime: 0.002180 +strict-transport-security: max-age=31536000 + +{"error":"invalid_token"} \ No newline at end of file diff --git a/services/harmony/test/helpers/labels.ts b/services/harmony/test/helpers/labels.ts new file mode 100644 index 000000000..74b2284b5 --- /dev/null +++ b/services/harmony/test/helpers/labels.ts @@ -0,0 +1,24 @@ +import request, { Test } from 'supertest'; +import { auth } from './auth'; + +/** + * Submits an add labels request + * + * @param app - The express application (typically this.frontend) + * @param jobIDs - The job ids + * @param labels - the labels to add to the jobs + */ +export function addJobsLabels(app: Express.Application, jobIds: string[], labels: string[], username: string): Test { + return request(app).put('/labels').use(auth({ username })).send({ jobID: jobIds, label: labels }); +} + +/** + * Submits a delete labels request + * + * @param app - The express application (typically this.frontend) + * @param jobIDs - The job ids + * @param labels - the labels to set delete from the jobs + */ +export function deleteJobsLabels(app: Express.Application, jobIds: string[], labels: string[], username: string): Test { + return request(app).delete('/labels').use(auth({ username })).send({ jobID: jobIds, label: labels }); +} \ No newline at end of file diff --git a/services/harmony/test/labels/label_crud.ts b/services/harmony/test/labels/label_crud.ts new file mode 100644 index 000000000..ff4e0e228 --- /dev/null +++ b/services/harmony/test/labels/label_crud.ts @@ -0,0 +1,68 @@ +import { expect } from 'chai'; +import { hookTransaction } from '../helpers/db'; +import { buildJob, getFirstJob } from '../helpers/jobs'; +import { addJobsLabels } from '../helpers/labels'; +import hookServersStartStop from '../helpers/servers'; +import db from '../../app/util/db'; + +describe('Job label CRUD', function () { + hookServersStartStop({ skipEarthdataLogin: false }); + hookTransaction(); + const joeJob1 = buildJob({ username: 'joe' }); + before(async function () { + await joeJob1.save(this.trx); + this.trx.commit(); + this.trx = null; + }); + + const { jobID } = joeJob1; + const notFoundJobID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'; + + describe('Set job labels', function () { + describe('When the user created the job', function () { + describe('When the jobs and labels are valid', function () { + it('sets the normalized labels on the jobs', async function () { + const response = await addJobsLabels(this.frontend, [jobID], ['foo', ' Bar '], 'joe'); + expect(response.status).to.equal(201); + const savedJob = await getFirstJob(db, { where: { jobID } }); + expect(savedJob.labels).deep.equal(['foo', 'bar']); + }); + }); + + describe('When a job ID is not valid', function () { + it('Returns an error for the job ID', async function () { + const response = await addJobsLabels(this.frontend, ['bad-id'], ['foo', ' Bar '], 'joe'); + expect(response.status).to.equal(400); + expect(JSON.parse(response.text).description).to.equal('Error: jobId bad-id is in invalid format.'); + }); + }); + + describe('When a job does not exit', function () { + it('Returns a not-found error error', async function () { + const response = await addJobsLabels(this.frontend, [jobID, notFoundJobID], ['foo'], 'joe'); + expect(response.status).to.equal(404); + expect(JSON.parse(response.text).description).to.equal('Error: Unable to find job aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'); + }); + }); + + }); + + describe('When the user is an admin user that did not create the job', function () { + describe('When the jobs and labels are valid', function () { + it('sets the labels on the jobs', async function () { + const response = await addJobsLabels(this.frontend, [jobID], ['foo', ' Bar '], 'adam'); + expect(response.status).to.equal(201); + const savedJob = await getFirstJob(db, { where: { jobID } }); + expect(savedJob.labels).deep.equal(['foo', 'bar']); + }); + }); + }); + + describe('When the user is not and admin user and did not create the job', function () { + it('Pretends the job does not exist', async function () { + const response = await addJobsLabels(this.frontend, [jobID], ['foo'], 'bob'); + expect(response.status).to.equal(404); + }); + }); + }); +}); \ No newline at end of file From c1cf631017a59fd46d57ca4271d2cda731130896 Mon Sep 17 00:00:00 2001 From: James Norton Date: Thu, 10 Oct 2024 16:19:54 -0400 Subject: [PATCH 07/80] HARMONY-1897: Refactor labels tables to make it easier to handle mulitple users editing a job's labels --- db/db.sql | 18 +++-- ...uniquene_constraint_for_job_id_label_id.js | 21 ------ .../20241009200617_create_raw_labels_table.js | 28 ++++++++ ...1009200628_create_jobs_raw_labels_table.js | 42 +++++++++++ ...009200648_create_users_raw_labels_table.js | 64 +++++++++++++++++ services/harmony/app/frontends/labels.ts | 2 +- services/harmony/app/models/label.ts | 72 +++++++++++-------- services/harmony/test/labels/label_crud.ts | 71 ++++++++++++++++-- services/harmony/test/parameters/label.ts | 12 ++++ 9 files changed, 269 insertions(+), 61 deletions(-) delete mode 100644 db/migrations/20241009133047_add_uniquene_constraint_for_job_id_label_id.js create mode 100644 db/migrations/20241009200617_create_raw_labels_table.js create mode 100644 db/migrations/20241009200628_create_jobs_raw_labels_table.js create mode 100644 db/migrations/20241009200648_create_users_raw_labels_table.js diff --git a/db/db.sql b/db/db.sql index 9b530c7e7..720916952 100644 --- a/db/db.sql +++ b/db/db.sql @@ -46,26 +46,34 @@ CREATE TABLE `job_errors` ( FOREIGN KEY(jobID) REFERENCES jobs(jobID) ); -CREATE TABLE `labels` ( +CREATE TABLE `raw_labels` ( `id` integer not null primary key autoincrement, - `username` varchar(255) not null, `value` varchar(255) not null, `createdAt` datetime not null, `updatedAt` datetime not null, - UNIQUE(username, value) + UNIQUE(value) ); -CREATE TABLE `jobs_labels` ( +CREATE TABLE `jobs_raw_labels` ( `id` integer not null primary key autoincrement, `job_id` char(36) not null, `label_id` integer not null, `createdAt` datetime not null, `updatedAt` datetime not null, FOREIGN KEY(job_id) REFERENCES jobs(jobID) - FOREIGN KEY(label_id) REFERENCES labels(id) + FOREIGN KEY(label_id) REFERENCES raw_labels(id) UNIQUE(job_id, label_id) ); +CREATE TABLE `users_labels` ( + `id` integer not null primary key autoincrement, + `username` varchar(255) not null, + `value` varchar(255) not null, + `createdAt` datetime not null, + `updatedAt` datetime not null, + UNIQUE(username, value) +); + CREATE TABLE `work_items` ( `id` integer not null primary key autoincrement, `jobID` char(36) not null, diff --git a/db/migrations/20241009133047_add_uniquene_constraint_for_job_id_label_id.js b/db/migrations/20241009133047_add_uniquene_constraint_for_job_id_label_id.js deleted file mode 100644 index 27249ac44..000000000 --- a/db/migrations/20241009133047_add_uniquene_constraint_for_job_id_label_id.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @param { import("knex").Knex } knex - * @returns { Promise } - */ -exports.up = function(knex) { - return knex.schema.alterTable('jobs_labels', function (table) { - // Adding a composite unique constraint on job_id and label_id - table.unique(['job_id', 'label_id']); - }); -}; - -/** - * @param { import("knex").Knex } knex - * @returns { Promise } - */ -exports.down = function(knex) { - return knex.schema.alterTable('jobs_labels', function (table) { - // Dropping the unique constraint if rolled back - table.dropUnique(['job_id', 'label_id']); - }); -}; diff --git a/db/migrations/20241009200617_create_raw_labels_table.js b/db/migrations/20241009200617_create_raw_labels_table.js new file mode 100644 index 000000000..ebca11f0a --- /dev/null +++ b/db/migrations/20241009200617_create_raw_labels_table.js @@ -0,0 +1,28 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema + .createTable('raw_labels', (t) => { + t.increments('id') + .primary(); + t.string('value', 255).notNullable(); + t.timestamp('createdAt').notNullable(); + t.timestamp('updatedAt').notNullable(); + t.unique(['value']); + t.index(['value']); + }).raw(` + ALTER TABLE "raw_labels" + ADD CONSTRAINT "lower_case_value" + CHECK (value = lower(value)) + `); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function(knex) { + return knex.schema.dropTable('raw_labels'); +}; diff --git a/db/migrations/20241009200628_create_jobs_raw_labels_table.js b/db/migrations/20241009200628_create_jobs_raw_labels_table.js new file mode 100644 index 000000000..12fc5d2b1 --- /dev/null +++ b/db/migrations/20241009200628_create_jobs_raw_labels_table.js @@ -0,0 +1,42 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function(knex) { + return knex.schema + .createTable('jobs_raw_labels', (t) => { + t.increments('id') + .primary(); + + t.uuid('job_id') + .notNullable() + .references('jobID') + .inTable('jobs') + .onDelete('CASCADE'); + + t.integer('label_id') + .notNullable() + .references('id') + .inTable('raw_labels') + .onDelete('CASCADE'); + + t.timestamp('createdAt') + .notNullable(); + + t.timestamp('updatedAt') + .notNullable(); + + t.unique(['job_id', 'label_id']); + t.index(['job_id']); + t.index(['label_id']); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema.dropTable('jobs_raw_labels') + +}; diff --git a/db/migrations/20241009200648_create_users_raw_labels_table.js b/db/migrations/20241009200648_create_users_raw_labels_table.js new file mode 100644 index 000000000..9eec4c642 --- /dev/null +++ b/db/migrations/20241009200648_create_users_raw_labels_table.js @@ -0,0 +1,64 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema + .createTable('users_labels', (t) => { + t.increments('id') + .primary(); + + t.string('username').notNullable(); + + t.string('value', 255).notNullable(); + + t.timestamp('createdAt') + .notNullable(); + + t.timestamp('updatedAt') + .notNullable(); + + t.unique(['username', 'value']); + t.index(['username']); + t.index(['value']); + }) + .then(async () => { + // Populate the raw_labels, jobs_raw_labels, and users_labels tables + const now = new Date(); + const rows = await knex.select(['username', 'job_id', 'value']).from('jobs_labels').innerJoin('labels', 'jobs_labels.label_id', '=', 'labels.id'); + const uniqueRawLabels = Array.from(new Set(rows.map((row) => row.value))); + const rawLabelRows = uniqueRawLabels.map((value) => { return { value, createdAt: now, updatedAt: now, }; }); + const labelIdValues = await knex('raw_labels').insert(rawLabelRows).returning(['id', 'value']); + // make a map of values to row ids + const labelValueIds = labelIdValues.reduce((acc, idValue) => { + const { id, value } = idValue; + acc[value] = id; + return acc; + }, {}); + + let jobsRawLabelRows = []; + let usersLabelsRows = []; + + rows.forEach((row) => { + const jobID = row.job_id; + const { username, value } = row; + const labelId = labelValueIds[value]; + + jobsRawLabelRows.push({ job_id: jobID, label_id: labelId, createdAt: now, updatedAt: now }); + usersLabelsRows.push({ username, value, createdAt: now, updatedAt: now }); + }); + // remove duplicates + jobsRawLabelRows = Array.from(new Set(jobsRawLabelRows.map(JSON.stringify))).map(JSON.parse); + usersLabelsRows = Array.from(new Set(usersLabelsRows.map(JSON.stringify))).map(JSON.parse); + await knex('jobs_raw_labels').insert(jobsRawLabelRows); + await knex('users_labels').insert(usersLabelsRows); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema.dropTable('users_labels'); +}; diff --git a/services/harmony/app/frontends/labels.ts b/services/harmony/app/frontends/labels.ts index dc56f5cda..b8ac3dc46 100644 --- a/services/harmony/app/frontends/labels.ts +++ b/services/harmony/app/frontends/labels.ts @@ -49,7 +49,7 @@ export async function deleteJobLabels( try { await db.transaction(async (trx) => { - await deleteLabelsFromJobs(trx, req.body.job, req.user, req.body.label, isAdmin); + await deleteLabelsFromJobs(trx, req.body.jobID, req.user, req.body.label, isAdmin); }); res.status(204); diff --git a/services/harmony/app/models/label.ts b/services/harmony/app/models/label.ts index 16391870f..c173e3dc9 100644 --- a/services/harmony/app/models/label.ts +++ b/services/harmony/app/models/label.ts @@ -3,8 +3,9 @@ import { Job } from './job'; import { NotFoundError, RequestValidationError } from '../util/errors'; import isUUID from '../util/uuid'; -export const LABELS_TABLE = 'labels'; -export const JOBS_LABELS_TABLE = 'jobs_labels'; +export const LABELS_TABLE = 'raw_labels'; +export const JOBS_LABELS_TABLE = 'jobs_raw_labels'; +export const USERS_LABELS_TABLE = 'users_labels'; /** * Returns an error message if a label exceeds 255 characters in length @@ -54,7 +55,6 @@ export async function verifyUserAccessToUpdateLabels( const jobId = row.jobID; const jobOwner = row.username; if (jobOwner != username && !isAdmin) { - //throw new ForbiddenError(`You do not have permission to update labels on job ${jobId}`); throw new NotFoundError(); } foundJobs.push(jobId); @@ -68,7 +68,7 @@ export async function verifyUserAccessToUpdateLabels( } /** - * Save labels for a user to the labels table + * Save labels for a user to the raw_labels table and to the users_labels table * * @param trx - the transaction to use for querying * @param labels - the string values for the labels @@ -80,15 +80,26 @@ async function saveLabels( labels: string[], timeStamp: Date, username: string): Promise { - const labelRows = labels.map((label) => { - return { username, value: label, createdAt: timeStamp, updatedAt: timeStamp }; + const uniqueLabels = Array.from(new Set(labels)); + const labelRows = uniqueLabels.map((label) => { + return { value: label, createdAt: timeStamp, updatedAt: timeStamp }; }); - // this will upsert the labels - if a label already exists for a given user + // this will 'upsert' the labels - if a label already exists // it will just update the `updatedAt` timestamp const insertedRows = await trx(LABELS_TABLE) .insert(labelRows) - .returning('id') + .returning(['id', 'value']) + .onConflict(['value']) + .merge(['updatedAt']); + + const usersRawLabelsRows = []; + for (const row of insertedRows) { + usersRawLabelsRows.push({ username, value: row.value, createdAt: timeStamp, updatedAt: timeStamp }); + } + + await trx(USERS_LABELS_TABLE) + .insert(usersRawLabelsRows) .onConflict(['username', 'value']) .merge(['updatedAt']); @@ -98,16 +109,16 @@ async function saveLabels( /** * Returns the labels for a given job * @param trx - the transaction to use for querying - * @param jobID - the UUID associated with the job + * @param jobId - the UUID associated with the job * * @returns A promise that resolves to an array of strings, one for each label */ export async function getLabelsForJob( trx: Transaction, - jobID: string, + jobId: string, ): Promise { const query = trx(JOBS_LABELS_TABLE) - .where({ job_id: jobID }) + .where({ job_id: jobId }) .orderBy([`${JOBS_LABELS_TABLE}.id`]) .innerJoin(LABELS_TABLE, `${JOBS_LABELS_TABLE}.label_id`, '=', `${LABELS_TABLE}.id`) .select([`${LABELS_TABLE}.value`]); @@ -121,14 +132,14 @@ export async function getLabelsForJob( * Set the labels for a given job/user. This is atomic - all the labels are set at once. Any * existing labels are replaced. * @param trx - the transaction to use for querying - * @param jobID - the UUID associated with the job + * @param jobId - the UUID associated with the job * @param username - the username the labels belong to * @param labels - the array of strings representing the labels. These will be forced to lower-case. * If this is an empty array then any existing labels for the job will be cleared. */ export async function setLabelsForJob( trx: Transaction, - jobID: string, + jobId: string, username: string, labels: string[], ): Promise { @@ -137,14 +148,14 @@ export async function setLabelsForJob( // delete any labels that already exist for the job await trx(JOBS_LABELS_TABLE) - .where({ job_id: jobID }) + .where({ job_id: jobId }) .delete(); if (labels.length > 0) { const now = new Date(); const ids = await saveLabels(trx, labels, now, username); const jobsLabelRows = ids.map((id) => { - return { job_id: jobID, label_id: id, createdAt: now, updatedAt: now }; + return { job_id: jobId, label_id: id, createdAt: now, updatedAt: now }; }); await trx(JOBS_LABELS_TABLE).insert(jobsLabelRows); @@ -155,24 +166,24 @@ export async function setLabelsForJob( * Add labels to the given jobs for the given user. Any labels that already exist for the given * job will not be re-added or replaced. * @param trx - the transaction to use for querying - * @param jobIDs - the UUIDs associated with the jobs + * @param jobIds - the UUIDs associated with the jobs * @param username - the username the labels belong to * @param labels - the array of strings representing the labels. */ export async function addLabelsToJobs( trx: Transaction, - jobIDs: string[], + jobIds: string[], username: string, labels: string[], isAdmin: boolean = false, ): Promise { - await verifyUserAccessToUpdateLabels(trx, jobIDs, username, isAdmin); + await verifyUserAccessToUpdateLabels(trx, jobIds, username, isAdmin); const now = new Date(); const labelIds = await saveLabels(trx, labels, now, username); const rowsToAdd = []; - for (const jobID of jobIDs) { + for (const jobId of jobIds) { for (const labelId of labelIds) { - rowsToAdd.push({ job_id: jobID, label_id: labelId, createdAt: now, updatedAt: now }); + rowsToAdd.push({ job_id: jobId, label_id: labelId, createdAt: now, updatedAt: now }); } } if (rowsToAdd.length > 0) { @@ -182,27 +193,30 @@ export async function addLabelsToJobs( } } - /** * Delete one or more labels from the given jobs for the given user. * @param trx - the transaction to use for querying - * @param jobIDs - the UUIDs associated with the jobs + * @param jobIds - the UUIDs associated with the jobs * @param username - the username the labels belong to * @param labels - the array of strings representing the labels. * @param isAdmin - true if the user is an admin user */ export async function deleteLabelsFromJobs( trx: Transaction, - jobIDs: string[], + jobIds: string[], username: string, labels: string[], isAdmin: boolean = false, ): Promise { - await verifyUserAccessToUpdateLabels(trx, jobIDs, username, isAdmin); - - await trx(JOBS_LABELS_TABLE) - .where(`${JOBS_LABELS_TABLE}.job_id`, 'in', jobIDs) - .join(LABELS_TABLE, `${JOBS_LABELS_TABLE}.label_id`, '=', `${LABELS_TABLE}.id`) - .where(`${LABELS_TABLE}.value`, 'in', labels) + await verifyUserAccessToUpdateLabels(trx, jobIds, username, isAdmin); + + // unfortunately sqlite doesn't seem to like deletes with joins, so we have to do this in two + // queries + const labelIds = await trx(`${LABELS_TABLE}`) + .select('id') + .where('value', 'in', labels); + await trx(`${JOBS_LABELS_TABLE}`) + .where('job_id', 'in', jobIds) + .andWhere('label_id', 'in', labelIds.map(row => row.id)) .del(); } diff --git a/services/harmony/test/labels/label_crud.ts b/services/harmony/test/labels/label_crud.ts index ff4e0e228..e6c2c7c54 100644 --- a/services/harmony/test/labels/label_crud.ts +++ b/services/harmony/test/labels/label_crud.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { hookTransaction } from '../helpers/db'; import { buildJob, getFirstJob } from '../helpers/jobs'; -import { addJobsLabels } from '../helpers/labels'; +import { addJobsLabels, deleteJobsLabels } from '../helpers/labels'; import hookServersStartStop from '../helpers/servers'; import db from '../../app/util/db'; @@ -37,8 +37,8 @@ describe('Job label CRUD', function () { }); }); - describe('When a job does not exit', function () { - it('Returns a not-found error error', async function () { + describe('When a job does not exist', function () { + it('Returns a not-found error', async function () { const response = await addJobsLabels(this.frontend, [jobID, notFoundJobID], ['foo'], 'joe'); expect(response.status).to.equal(404); expect(JSON.parse(response.text).description).to.equal('Error: Unable to find job aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'); @@ -50,10 +50,10 @@ describe('Job label CRUD', function () { describe('When the user is an admin user that did not create the job', function () { describe('When the jobs and labels are valid', function () { it('sets the labels on the jobs', async function () { - const response = await addJobsLabels(this.frontend, [jobID], ['foo', ' Bar '], 'adam'); + const response = await addJobsLabels(this.frontend, [jobID], ['foo', ' Buzz '], 'adam'); expect(response.status).to.equal(201); const savedJob = await getFirstJob(db, { where: { jobID } }); - expect(savedJob.labels).deep.equal(['foo', 'bar']); + expect(savedJob.labels).deep.equal(['foo', 'bar', 'buzz']); }); }); }); @@ -65,4 +65,65 @@ describe('Job label CRUD', function () { }); }); }); + + describe('Delete job labels', function () { + beforeEach(async function () { + await addJobsLabels(this.frontend, [jobID], ['label1', 'label2'], 'joe'); + }); + + describe('When the user created the jobs', function () { + describe('When the jobs and labels are valid', function () { + it('deletes the labels from the jobs', async function () { + const response = await deleteJobsLabels(this.frontend, [jobID], ['label1'], 'joe'); + expect(response.status).to.equal(204); + const savedJob = await getFirstJob(db, { where: { jobID } }); + expect(savedJob.labels).deep.equal(['foo', 'bar', 'buzz', 'label2']); + }); + }); + + describe('When some of the labels are not on the jobs', function () { + it('ignores the labels that are not on the jobs', async function () { + const response = await deleteJobsLabels(this.frontend, [jobID], ['label1', 'missing-label'], 'joe'); + expect(response.status).to.equal(204); + const savedJob = await getFirstJob(db, { where: { jobID } }); + expect(savedJob.labels).deep.equal(['foo', 'bar', 'buzz', 'label2']); + }); + }); + + describe('When a job ID is not valid', function () { + it('Returns an error for the job ID', async function () { + const response = await deleteJobsLabels(this.frontend, ['bad-id'], ['label1'], 'joe'); + expect(response.status).to.equal(400); + expect(JSON.parse(response.text).description).to.equal('Error: jobId bad-id is in invalid format.'); + }); + }); + + describe('When a job does not exist', function () { + it('Returns a not-found error', async function () { + const response = await deleteJobsLabels(this.frontend, [jobID, notFoundJobID], ['foo'], 'joe'); + expect(response.status).to.equal(404); + expect(JSON.parse(response.text).description).to.equal('Error: Unable to find job aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'); + }); + }); + + }); + + describe('When the user is an admin user that did not create the job', function () { + describe('When the jobs and labels are valid', function () { + it('deletes the labels from the jobs', async function () { + const response = await deleteJobsLabels(this.frontend, [jobID], ['label2', 'buzz'], 'adam'); + expect(response.status).to.equal(204); + const savedJob = await getFirstJob(db, { where: { jobID } }); + expect(savedJob.labels).deep.equal(['foo', 'bar', 'label1']); + }); + }); + }); + + describe('When the user is not and admin user and did not create the job', function () { + it('Pretends the job does not exist', async function () { + const response = await deleteJobsLabels(this.frontend, [jobID], ['label1'], 'bob'); + expect(response.status).to.equal(404); + }); + }); + }); }); \ No newline at end of file diff --git a/services/harmony/test/parameters/label.ts b/services/harmony/test/parameters/label.ts index c50dc8807..4562e68e5 100644 --- a/services/harmony/test/parameters/label.ts +++ b/services/harmony/test/parameters/label.ts @@ -123,6 +123,18 @@ describe('labels', function () { }); }); + describe('when passing in repeated labels with the request', function () { + + hookPartials[apiType](['bar', 'buzz', 'bar', ' buzz ']); + hookRedirect('joe'); + + it('it deduplicates the labels', async function () { + const jobStatus = JSON.parse(this.res.text); + const job = await Job.byJobID(db, jobStatus.jobID, false, true, false); + expect(job.job.labels).deep.equal(['bar', 'buzz']); + }); + }); + describe('when passing in labels with just whitespace with the request', function () { hookPartials[apiType](['foo', ' \t \t ']); From 2b38ca1b8c47b3274562ab5ee802d0c17c06f288 Mon Sep 17 00:00:00 2001 From: James Norton Date: Fri, 11 Oct 2024 11:52:40 -0400 Subject: [PATCH 08/80] HARMONY-1897: Add check to migration to prevent error on deployments that didn't already have labels --- ...009200648_create_users_raw_labels_table.js | 48 ++++++++++--------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/db/migrations/20241009200648_create_users_raw_labels_table.js b/db/migrations/20241009200648_create_users_raw_labels_table.js index 9eec4c642..57aa83850 100644 --- a/db/migrations/20241009200648_create_users_raw_labels_table.js +++ b/db/migrations/20241009200648_create_users_raw_labels_table.js @@ -26,32 +26,34 @@ exports.up = function (knex) { // Populate the raw_labels, jobs_raw_labels, and users_labels tables const now = new Date(); const rows = await knex.select(['username', 'job_id', 'value']).from('jobs_labels').innerJoin('labels', 'jobs_labels.label_id', '=', 'labels.id'); - const uniqueRawLabels = Array.from(new Set(rows.map((row) => row.value))); - const rawLabelRows = uniqueRawLabels.map((value) => { return { value, createdAt: now, updatedAt: now, }; }); - const labelIdValues = await knex('raw_labels').insert(rawLabelRows).returning(['id', 'value']); - // make a map of values to row ids - const labelValueIds = labelIdValues.reduce((acc, idValue) => { - const { id, value } = idValue; - acc[value] = id; - return acc; - }, {}); + if (rows.length > 0) { + const uniqueRawLabels = Array.from(new Set(rows.map((row) => row.value))); + const rawLabelRows = uniqueRawLabels.map((value) => { return { value, createdAt: now, updatedAt: now, }; }); + const labelIdValues = await knex('raw_labels').insert(rawLabelRows).returning(['id', 'value']); + // make a map of values to row ids + const labelValueIds = labelIdValues.reduce((acc, idValue) => { + const { id, value } = idValue; + acc[value] = id; + return acc; + }, {}); - let jobsRawLabelRows = []; - let usersLabelsRows = []; + let jobsRawLabelRows = []; + let usersLabelsRows = []; - rows.forEach((row) => { - const jobID = row.job_id; - const { username, value } = row; - const labelId = labelValueIds[value]; + rows.forEach((row) => { + const jobID = row.job_id; + const { username, value } = row; + const labelId = labelValueIds[value]; - jobsRawLabelRows.push({ job_id: jobID, label_id: labelId, createdAt: now, updatedAt: now }); - usersLabelsRows.push({ username, value, createdAt: now, updatedAt: now }); - }); - // remove duplicates - jobsRawLabelRows = Array.from(new Set(jobsRawLabelRows.map(JSON.stringify))).map(JSON.parse); - usersLabelsRows = Array.from(new Set(usersLabelsRows.map(JSON.stringify))).map(JSON.parse); - await knex('jobs_raw_labels').insert(jobsRawLabelRows); - await knex('users_labels').insert(usersLabelsRows); + jobsRawLabelRows.push({ job_id: jobID, label_id: labelId, createdAt: now, updatedAt: now }); + usersLabelsRows.push({ username, value, createdAt: now, updatedAt: now }); + }); + // remove duplicates + jobsRawLabelRows = Array.from(new Set(jobsRawLabelRows.map(JSON.stringify))).map(JSON.parse); + usersLabelsRows = Array.from(new Set(usersLabelsRows.map(JSON.stringify))).map(JSON.parse); + await knex('jobs_raw_labels').insert(jobsRawLabelRows); + await knex('users_labels').insert(usersLabelsRows); + } }); }; From 6adff1f252931c51a9d943dc5be9497caf799f7b Mon Sep 17 00:00:00 2001 From: James Norton Date: Fri, 11 Oct 2024 14:30:40 -0400 Subject: [PATCH 09/80] HARMONY-1897: Change PUSH to PUT in docs to undo stupidity --- services/harmony/app/markdown/labels.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/harmony/app/markdown/labels.md b/services/harmony/app/markdown/labels.md index f477376c2..ee1d7d9cd 100644 --- a/services/harmony/app/markdown/labels.md +++ b/services/harmony/app/markdown/labels.md @@ -4,10 +4,10 @@ Labels can be applied to a job when a Harmony request is made using the `label` in the [Services API section](#using-the-service-apis). After a request is made the labels can be viewed in the job status page and in the workflow-ui. -Labels can be added to existing jobs by the job owner, or anyone with admin permissions, using an HTTP PUSH request and specifying the job IDs and labels in the body of the PUSH. An EDL bearer token must be provided and a `Content-Type: application/json` header. A `curl` example that adds two labels to two different jobs follows: +Labels can be added to existing jobs by the job owner, or anyone with admin permissions, using an HTTP PUT request and specifying the job IDs and labels in the body of the PUT. An EDL bearer token must be provided and a `Content-Type: application/json` header. A `curl` example that adds two labels to two different jobs follows: ``` -curl -bj {{root}} -XPUSH -d '{"jobID": ["", ""], "label": ["foo", "bar"]}' -H "Content-Type: application/json" -H "Authorization: bearer " +curl -bj {{root}} -XPUT -d '{"jobID": ["", ""], "label": ["foo", "bar"]}' -H "Content-Type: application/json" -H "Authorization: bearer " ``` Similarly, labels can be removed from one or more jobs using an HTTP DELETE: From ec1032fef96db2611e94678b1a53696fc49bb4a9 Mon Sep 17 00:00:00 2001 From: James Norton Date: Tue, 15 Oct 2024 11:00:26 -0400 Subject: [PATCH 10/80] HARMONY-1897: Fix typos --- services/harmony/app/markdown/labels.md | 4 ++-- services/harmony/app/models/label.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/harmony/app/markdown/labels.md b/services/harmony/app/markdown/labels.md index ee1d7d9cd..f328b1817 100644 --- a/services/harmony/app/markdown/labels.md +++ b/services/harmony/app/markdown/labels.md @@ -7,12 +7,12 @@ job status page and in the workflow-ui. Labels can be added to existing jobs by the job owner, or anyone with admin permissions, using an HTTP PUT request and specifying the job IDs and labels in the body of the PUT. An EDL bearer token must be provided and a `Content-Type: application/json` header. A `curl` example that adds two labels to two different jobs follows: ``` -curl -bj {{root}} -XPUT -d '{"jobID": ["", ""], "label": ["foo", "bar"]}' -H "Content-Type: application/json" -H "Authorization: bearer " +curl -bj {{root}}/labels -XPUT -d '{"jobID": ["", ""], "label": ["foo", "bar"]}' -H "Content-Type: application/json" -H "Authorization: bearer " ``` Similarly, labels can be removed from one or more jobs using an HTTP DELETE: ``` -curl -bj {{root}} -XDELETE -d '{"jobID": ["", ""], "label": ["foo"]}' -H "Content-Type: application/json" -H "Authorization: bearer " +curl -bj {{root}}/labels -XDELETE -d '{"jobID": ["", ""], "label": ["foo"]}' -H "Content-Type: application/json" -H "Authorization: bearer " ``` diff --git a/services/harmony/app/models/label.ts b/services/harmony/app/models/label.ts index c0fbc2c0e..e8ca6f437 100644 --- a/services/harmony/app/models/label.ts +++ b/services/harmony/app/models/label.ts @@ -32,7 +32,7 @@ export function normalizeLabel(label: string): string { } /** - * Verify that the user can change the labels on a give job. Currently only job owners can + * Verify that the user can change the labels on a given job. Currently only job owners and admin can * change the labels for a job. * @param trx - the transaction to use for querying * @param jobIds - the UUIDs associated with the jobs From f40c9cb248560ca8a90d5f550d7309073fac9aee Mon Sep 17 00:00:00 2001 From: vinny Date: Tue, 15 Oct 2024 12:58:04 -0400 Subject: [PATCH 11/80] HARMONY-1892: Pass new isAdminRoute parameter to job rendering functions --- services/harmony/app/frontends/workflow-ui.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/harmony/app/frontends/workflow-ui.ts b/services/harmony/app/frontends/workflow-ui.ts index 04ee23850..579523d40 100644 --- a/services/harmony/app/frontends/workflow-ui.ts +++ b/services/harmony/app/frontends/workflow-ui.ts @@ -532,6 +532,7 @@ export async function getWorkItemsTable( const { jobID } = req.params; const { checkJobStatus } = req.query; try { + const isAdminRoute = req.context.isAdminAccess; const { isAdmin, isLogViewer } = await getEdlGroupInformation( req.user, req.context.logger, ); @@ -575,7 +576,7 @@ export async function getWorkItemsTable( .replace('/work-items', '') .replace(/(&|\?)checkJobStatus=(true|false)/, '') : ''); }, - ...jobRenderingFunctions(req.context.logger, requestQuery), + ...jobRenderingFunctions(req.context.logger, requestQuery, isAdminRoute), }); } catch (e) { req.context.logger.error(e); From a8931e922ad943f32681585c0d0f99aa02dcc0fe Mon Sep 17 00:00:00 2001 From: James Norton Date: Tue, 15 Oct 2024 13:38:44 -0400 Subject: [PATCH 12/80] HARMONY-1897: Fix vulnerable dependency --- services/harmony/package-lock.json | 140 ++++++++++++++++------ services/harmony/package.json | 3 +- services/service-runner/package-lock.json | 81 +++++++++++-- services/service-runner/package.json | 3 +- services/work-scheduler/package-lock.json | 81 +++++++++++-- services/work-scheduler/package.json | 3 +- 6 files changed, 256 insertions(+), 55 deletions(-) diff --git a/services/harmony/package-lock.json b/services/harmony/package-lock.json index d07bb1571..1f4bb5b90 100644 --- a/services/harmony/package-lock.json +++ b/services/harmony/package-lock.json @@ -2357,6 +2357,30 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.2.1.tgz", + "integrity": "sha512-gaHqbubTi29aZpVbBlECRpmdia+L5/lh2BwtIJTmtxdbecEyyX/ejAOg7eQDGNvGOUmPY7Z2Yxdy9ioyH/VJeA==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.3.tgz", + "integrity": "sha512-XfZgry4DwEZvSFtS/6Y+R48D7qJYJK6R9/yJFyUFHCIUMEEHuJ4X95TDgJp5QkmzfLYvapMPzskV5HpIDrREug==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, "node_modules/@kubernetes/client-node": { "version": "0.18.1", "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.18.1.tgz", @@ -8551,11 +8575,12 @@ } }, "node_modules/cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", "dependencies": { - "cookie": "0.4.1", + "cookie": "0.7.2", "cookie-signature": "1.0.6" }, "engines": { @@ -8563,9 +8588,10 @@ } }, "node_modules/cookie-parser/node_modules/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -10088,9 +10114,9 @@ "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -10098,7 +10124,7 @@ "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -10241,9 +10267,10 @@ } }, "node_modules/express/node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -13833,6 +13860,15 @@ "node": ">=12.0.0" } }, + "node_modules/jsep": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.9.tgz", + "integrity": "sha512-i1rBX5N7VPl0eYb6+mHNp52sEuaS2Wi8CDYx1X5sn9naevL78+265XJqy1qENEk7mRKwS06NHpUqiBwR7qeodw==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -13957,11 +13993,21 @@ ] }, "node_modules/jsonpath-plus": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz", - "integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.0.0.tgz", + "integrity": "sha512-v7j76HGp/ibKlXYeZ7UrfCLSNDaBWuJMA0GaMjA4sZJtCtY89qgPyToDDcl2zdeHh4B5q/B3g2pQdW76fOg/dA==", + "license": "MIT", + "dependencies": { + "@jsep-plugin/assignment": "^1.2.1", + "@jsep-plugin/regex": "^1.0.3", + "jsep": "^1.3.9" + }, + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, "engines": { - "node": ">=12.0.0" + "node": ">=18.0.0" } }, "node_modules/jsonschema": { @@ -24250,6 +24296,18 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@jsep-plugin/assignment": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.2.1.tgz", + "integrity": "sha512-gaHqbubTi29aZpVbBlECRpmdia+L5/lh2BwtIJTmtxdbecEyyX/ejAOg7eQDGNvGOUmPY7Z2Yxdy9ioyH/VJeA==", + "requires": {} + }, + "@jsep-plugin/regex": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.3.tgz", + "integrity": "sha512-XfZgry4DwEZvSFtS/6Y+R48D7qJYJK6R9/yJFyUFHCIUMEEHuJ4X95TDgJp5QkmzfLYvapMPzskV5HpIDrREug==", + "requires": {} + }, "@kubernetes/client-node": { "version": "0.18.1", "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.18.1.tgz", @@ -24262,7 +24320,7 @@ "byline": "^5.0.0", "isomorphic-ws": "^5.0.0", "js-yaml": "^4.1.0", - "jsonpath-plus": "^7.2.0", + "jsonpath-plus": "^10.0.0", "openid-client": "^5.3.0", "request": "^2.88.0", "rfc4648": "^1.3.0", @@ -28882,18 +28940,18 @@ "dev": true }, "cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", "requires": { - "cookie": "0.4.1", + "cookie": "0.7.2", "cookie-signature": "1.0.6" }, "dependencies": { "cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" }, "cookie-signature": { "version": "1.0.6", @@ -30016,16 +30074,16 @@ "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" }, "express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -30054,9 +30112,9 @@ }, "dependencies": { "cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" }, "cookie-signature": { "version": "1.0.6", @@ -32712,6 +32770,11 @@ "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", "dev": true }, + "jsep": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.9.tgz", + "integrity": "sha512-i1rBX5N7VPl0eYb6+mHNp52sEuaS2Wi8CDYx1X5sn9naevL78+265XJqy1qENEk7mRKwS06NHpUqiBwR7qeodw==" + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -32806,9 +32869,14 @@ "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==" }, "jsonpath-plus": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz", - "integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==" + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.0.0.tgz", + "integrity": "sha512-v7j76HGp/ibKlXYeZ7UrfCLSNDaBWuJMA0GaMjA4sZJtCtY89qgPyToDDcl2zdeHh4B5q/B3g2pQdW76fOg/dA==", + "requires": { + "@jsep-plugin/assignment": "^1.2.1", + "@jsep-plugin/regex": "^1.0.3", + "jsep": "^1.3.9" + } }, "jsonschema": { "version": "1.4.1", diff --git a/services/harmony/package.json b/services/harmony/package.json index d5765922e..585260e9f 100644 --- a/services/harmony/package.json +++ b/services/harmony/package.json @@ -211,6 +211,7 @@ "fast-json-patch": "3.1.1", "semver": "^7.6.2", "braces": "^3.0.3", - "fast-xml-parser": "4.4.1" + "fast-xml-parser": "4.4.1", + "jsonpath-plus": "^10.0.0" } } diff --git a/services/service-runner/package-lock.json b/services/service-runner/package-lock.json index b5b35d349..9462ad6ec 100644 --- a/services/service-runner/package-lock.json +++ b/services/service-runner/package-lock.json @@ -1938,6 +1938,30 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.2.1.tgz", + "integrity": "sha512-gaHqbubTi29aZpVbBlECRpmdia+L5/lh2BwtIJTmtxdbecEyyX/ejAOg7eQDGNvGOUmPY7Z2Yxdy9ioyH/VJeA==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.3.tgz", + "integrity": "sha512-XfZgry4DwEZvSFtS/6Y+R48D7qJYJK6R9/yJFyUFHCIUMEEHuJ4X95TDgJp5QkmzfLYvapMPzskV5HpIDrREug==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, "node_modules/@kubernetes/client-node": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.17.1.tgz", @@ -5678,6 +5702,15 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, + "node_modules/jsep": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.9.tgz", + "integrity": "sha512-i1rBX5N7VPl0eYb6+mHNp52sEuaS2Wi8CDYx1X5sn9naevL78+265XJqy1qENEk7mRKwS06NHpUqiBwR7qeodw==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -5730,11 +5763,21 @@ } }, "node_modules/jsonpath-plus": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.19.0.tgz", - "integrity": "sha512-GSVwsrzW9LsA5lzsqe4CkuZ9wp+kxBb2GwNniaWzI2YFn5Ig42rSW8ZxVpWXaAfakXNrx5pgY5AbQq7kzX29kg==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.0.0.tgz", + "integrity": "sha512-v7j76HGp/ibKlXYeZ7UrfCLSNDaBWuJMA0GaMjA4sZJtCtY89qgPyToDDcl2zdeHh4B5q/B3g2pQdW76fOg/dA==", + "license": "MIT", + "dependencies": { + "@jsep-plugin/assignment": "^1.2.1", + "@jsep-plugin/regex": "^1.0.3", + "jsep": "^1.3.9" + }, + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, "engines": { - "node": ">=6.0" + "node": ">=18.0.0" } }, "node_modules/jsprim": { @@ -10237,6 +10280,18 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@jsep-plugin/assignment": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.2.1.tgz", + "integrity": "sha512-gaHqbubTi29aZpVbBlECRpmdia+L5/lh2BwtIJTmtxdbecEyyX/ejAOg7eQDGNvGOUmPY7Z2Yxdy9ioyH/VJeA==", + "requires": {} + }, + "@jsep-plugin/regex": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.3.tgz", + "integrity": "sha512-XfZgry4DwEZvSFtS/6Y+R48D7qJYJK6R9/yJFyUFHCIUMEEHuJ4X95TDgJp5QkmzfLYvapMPzskV5HpIDrREug==", + "requires": {} + }, "@kubernetes/client-node": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.17.1.tgz", @@ -10246,7 +10301,7 @@ "execa": "5.0.0", "isomorphic-ws": "^4.0.1", "js-yaml": "^4.1.0", - "jsonpath-plus": "^0.19.0", + "jsonpath-plus": "^10.0.0", "openid-client": "^5.1.6", "request": "^2.88.0", "rfc4648": "^1.3.0", @@ -13061,6 +13116,11 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, + "jsep": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.9.tgz", + "integrity": "sha512-i1rBX5N7VPl0eYb6+mHNp52sEuaS2Wi8CDYx1X5sn9naevL78+265XJqy1qENEk7mRKwS06NHpUqiBwR7qeodw==" + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -13101,9 +13161,14 @@ "dev": true }, "jsonpath-plus": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.19.0.tgz", - "integrity": "sha512-GSVwsrzW9LsA5lzsqe4CkuZ9wp+kxBb2GwNniaWzI2YFn5Ig42rSW8ZxVpWXaAfakXNrx5pgY5AbQq7kzX29kg==" + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.0.0.tgz", + "integrity": "sha512-v7j76HGp/ibKlXYeZ7UrfCLSNDaBWuJMA0GaMjA4sZJtCtY89qgPyToDDcl2zdeHh4B5q/B3g2pQdW76fOg/dA==", + "requires": { + "@jsep-plugin/assignment": "^1.2.1", + "@jsep-plugin/regex": "^1.0.3", + "jsep": "^1.3.9" + } }, "jsprim": { "version": "1.4.2", diff --git a/services/service-runner/package.json b/services/service-runner/package.json index fa1cdbd5f..517aa9646 100644 --- a/services/service-runner/package.json +++ b/services/service-runner/package.json @@ -77,6 +77,7 @@ }, "overrides": { "braces": "^3.0.3", - "fast-xml-parser": "4.4.1" + "fast-xml-parser": "4.4.1", + "jsonpath-plus": "^10.0.0" } } diff --git a/services/work-scheduler/package-lock.json b/services/work-scheduler/package-lock.json index 1d32e4f2c..b06066f08 100644 --- a/services/work-scheduler/package-lock.json +++ b/services/work-scheduler/package-lock.json @@ -1922,6 +1922,30 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.2.1.tgz", + "integrity": "sha512-gaHqbubTi29aZpVbBlECRpmdia+L5/lh2BwtIJTmtxdbecEyyX/ejAOg7eQDGNvGOUmPY7Z2Yxdy9ioyH/VJeA==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.3.tgz", + "integrity": "sha512-XfZgry4DwEZvSFtS/6Y+R48D7qJYJK6R9/yJFyUFHCIUMEEHuJ4X95TDgJp5QkmzfLYvapMPzskV5HpIDrREug==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, "node_modules/@kubernetes/client-node": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.17.1.tgz", @@ -5400,6 +5424,15 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, + "node_modules/jsep": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.9.tgz", + "integrity": "sha512-i1rBX5N7VPl0eYb6+mHNp52sEuaS2Wi8CDYx1X5sn9naevL78+265XJqy1qENEk7mRKwS06NHpUqiBwR7qeodw==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -5452,11 +5485,21 @@ } }, "node_modules/jsonpath-plus": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.19.0.tgz", - "integrity": "sha512-GSVwsrzW9LsA5lzsqe4CkuZ9wp+kxBb2GwNniaWzI2YFn5Ig42rSW8ZxVpWXaAfakXNrx5pgY5AbQq7kzX29kg==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.0.0.tgz", + "integrity": "sha512-v7j76HGp/ibKlXYeZ7UrfCLSNDaBWuJMA0GaMjA4sZJtCtY89qgPyToDDcl2zdeHh4B5q/B3g2pQdW76fOg/dA==", + "license": "MIT", + "dependencies": { + "@jsep-plugin/assignment": "^1.2.1", + "@jsep-plugin/regex": "^1.0.3", + "jsep": "^1.3.9" + }, + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, "engines": { - "node": ">=6.0" + "node": ">=18.0.0" } }, "node_modules/jsprim": { @@ -9816,6 +9859,18 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@jsep-plugin/assignment": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.2.1.tgz", + "integrity": "sha512-gaHqbubTi29aZpVbBlECRpmdia+L5/lh2BwtIJTmtxdbecEyyX/ejAOg7eQDGNvGOUmPY7Z2Yxdy9ioyH/VJeA==", + "requires": {} + }, + "@jsep-plugin/regex": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.3.tgz", + "integrity": "sha512-XfZgry4DwEZvSFtS/6Y+R48D7qJYJK6R9/yJFyUFHCIUMEEHuJ4X95TDgJp5QkmzfLYvapMPzskV5HpIDrREug==", + "requires": {} + }, "@kubernetes/client-node": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.17.1.tgz", @@ -9825,7 +9880,7 @@ "execa": "5.0.0", "isomorphic-ws": "^4.0.1", "js-yaml": "^4.1.0", - "jsonpath-plus": "^0.19.0", + "jsonpath-plus": "^10.0.0", "openid-client": "^5.1.6", "request": "^2.88.0", "rfc4648": "^1.3.0", @@ -12476,6 +12531,11 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, + "jsep": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.9.tgz", + "integrity": "sha512-i1rBX5N7VPl0eYb6+mHNp52sEuaS2Wi8CDYx1X5sn9naevL78+265XJqy1qENEk7mRKwS06NHpUqiBwR7qeodw==" + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -12516,9 +12576,14 @@ "dev": true }, "jsonpath-plus": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.19.0.tgz", - "integrity": "sha512-GSVwsrzW9LsA5lzsqe4CkuZ9wp+kxBb2GwNniaWzI2YFn5Ig42rSW8ZxVpWXaAfakXNrx5pgY5AbQq7kzX29kg==" + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.0.0.tgz", + "integrity": "sha512-v7j76HGp/ibKlXYeZ7UrfCLSNDaBWuJMA0GaMjA4sZJtCtY89qgPyToDDcl2zdeHh4B5q/B3g2pQdW76fOg/dA==", + "requires": { + "@jsep-plugin/assignment": "^1.2.1", + "@jsep-plugin/regex": "^1.0.3", + "jsep": "^1.3.9" + } }, "jsprim": { "version": "1.4.2", diff --git a/services/work-scheduler/package.json b/services/work-scheduler/package.json index dfd7db1b4..9c08465ed 100644 --- a/services/work-scheduler/package.json +++ b/services/work-scheduler/package.json @@ -83,6 +83,7 @@ "overrides": { "semver": "^7.6.2", "braces": "^3.0.3", - "fast-xml-parser": "4.4.1" + "fast-xml-parser": "4.4.1", + "jsonpath-plus": "^10.0.0" } } From 71839dbce5ad4a170b5c1717ddcdd58f7ab5527c Mon Sep 17 00:00:00 2001 From: vinny Date: Tue, 15 Oct 2024 13:51:37 -0400 Subject: [PATCH 13/80] HARMONY-1892: Add a dummy label link to the nav --- .../app/views/workflow-ui/job/index.mustache.html | 4 ++-- .../app/views/workflow-ui/jobs/index.mustache.html | 9 +++++++-- .../harmony/public/js/workflow-ui/status-change-links.js | 9 +++------ 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/services/harmony/app/views/workflow-ui/job/index.mustache.html b/services/harmony/app/views/workflow-ui/job/index.mustache.html index 83e261f71..6537f7f1b 100644 --- a/services/harmony/app/views/workflow-ui/job/index.mustache.html +++ b/services/harmony/app/views/workflow-ui/job/index.mustache.html @@ -48,9 +48,9 @@ {{/isAdminRoute}} - +
diff --git a/services/harmony/app/views/workflow-ui/jobs/index.mustache.html b/services/harmony/app/views/workflow-ui/jobs/index.mustache.html index edfc704e6..0aa570c2f 100644 --- a/services/harmony/app/views/workflow-ui/jobs/index.mustache.html +++ b/services/harmony/app/views/workflow-ui/jobs/index.mustache.html @@ -42,9 +42,14 @@ - +
  • + + label + +
  • +
    diff --git a/services/harmony/public/js/workflow-ui/status-change-links.js b/services/harmony/public/js/workflow-ui/status-change-links.js index 5833ea3d6..a72c40bcf 100644 --- a/services/harmony/public/js/workflow-ui/status-change-links.js +++ b/services/harmony/public/js/workflow-ui/status-change-links.js @@ -33,11 +33,7 @@ class StatusChangeLinks { ${link.href.split('/').pop()} `; - return ` - - `; + return `${links.map(linkToLi).join('')}`; } /** @@ -57,7 +53,8 @@ class StatusChangeLinks { */ insertLinksHtml(links, linksContainerId) { const html = this.buildLinksHtml(links); - document.getElementById(linksContainerId).innerHTML = html; + document.getElementById(linksContainerId).innerHTML = html + + document.getElementById(linksContainerId).innerHTML; document.querySelectorAll('.state-change-link').forEach((link) => { link.addEventListener('click', (event) => { this.handleClick(event); From 49b7569d7aecc40e4450e0505701d65850ed878d Mon Sep 17 00:00:00 2001 From: vinny Date: Tue, 15 Oct 2024 14:36:09 -0400 Subject: [PATCH 14/80] HARMONY-1892: Make the labels link a dropdown --- .../app/views/workflow-ui/jobs/index.mustache.html | 11 +++++++---- services/harmony/public/css/workflow-ui/default.css | 4 ++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/services/harmony/app/views/workflow-ui/jobs/index.mustache.html b/services/harmony/app/views/workflow-ui/jobs/index.mustache.html index 0aa570c2f..f8e442fc6 100644 --- a/services/harmony/app/views/workflow-ui/jobs/index.mustache.html +++ b/services/harmony/app/views/workflow-ui/jobs/index.mustache.html @@ -44,10 +44,13 @@
    diff --git a/services/harmony/public/css/workflow-ui/default.css b/services/harmony/public/css/workflow-ui/default.css index bb087380f..d2a0a4cb0 100644 --- a/services/harmony/public/css/workflow-ui/default.css +++ b/services/harmony/public/css/workflow-ui/default.css @@ -54,4 +54,8 @@ .job-alert { margin-bottom: 0; +} + +#label-nav-item > * { + z-index: 1021; } \ No newline at end of file From e24156cf0a3e34c19b0afed3df57ba67ec1641f4 Mon Sep 17 00:00:00 2001 From: vinny Date: Tue, 15 Oct 2024 15:02:00 -0400 Subject: [PATCH 15/80] HARMONY-1892: Add submit button and search input to label dropdown --- .../harmony/app/views/workflow-ui/jobs/index.mustache.html | 7 ++++++- services/harmony/public/css/workflow-ui/default.css | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/services/harmony/app/views/workflow-ui/jobs/index.mustache.html b/services/harmony/app/views/workflow-ui/jobs/index.mustache.html index f8e442fc6..d8a4b33d7 100644 --- a/services/harmony/app/views/workflow-ui/jobs/index.mustache.html +++ b/services/harmony/app/views/workflow-ui/jobs/index.mustache.html @@ -47,9 +47,14 @@ diff --git a/services/harmony/public/css/workflow-ui/default.css b/services/harmony/public/css/workflow-ui/default.css index d2a0a4cb0..fd1ece243 100644 --- a/services/harmony/public/css/workflow-ui/default.css +++ b/services/harmony/public/css/workflow-ui/default.css @@ -58,4 +58,8 @@ #label-nav-item > * { z-index: 1021; +} + +.dropdown-divider { + border-top: 1px solid #6b7c8d; } \ No newline at end of file From 69357a4b94b65c21c7eba1ff2a8c0e7e2e5f49ad Mon Sep 17 00:00:00 2001 From: vinny Date: Tue, 15 Oct 2024 15:06:21 -0400 Subject: [PATCH 16/80] HARMONY-1892: Add a label icon --- services/harmony/app/views/workflow-ui/jobs/index.mustache.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/harmony/app/views/workflow-ui/jobs/index.mustache.html b/services/harmony/app/views/workflow-ui/jobs/index.mustache.html index d8a4b33d7..fe370999d 100644 --- a/services/harmony/app/views/workflow-ui/jobs/index.mustache.html +++ b/services/harmony/app/views/workflow-ui/jobs/index.mustache.html @@ -54,7 +54,7 @@
  • yellow-label-yellow
  • purple
  • -
  • apply
  • +
  •  apply
  • From 769dea3cce785b40b6fb26d73f9df58954e9fd51 Mon Sep 17 00:00:00 2001 From: vinny Date: Tue, 15 Oct 2024 15:08:32 -0400 Subject: [PATCH 17/80] HARMONY-1892: Increase margin for search box --- services/harmony/app/views/workflow-ui/jobs/index.mustache.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/harmony/app/views/workflow-ui/jobs/index.mustache.html b/services/harmony/app/views/workflow-ui/jobs/index.mustache.html index fe370999d..a471a40d7 100644 --- a/services/harmony/app/views/workflow-ui/jobs/index.mustache.html +++ b/services/harmony/app/views/workflow-ui/jobs/index.mustache.html @@ -47,7 +47,7 @@
  • diff --git a/services/harmony/public/css/workflow-ui/default.css b/services/harmony/public/css/workflow-ui/default.css index d8b243815..62fc1508b 100644 --- a/services/harmony/public/css/workflow-ui/default.css +++ b/services/harmony/public/css/workflow-ui/default.css @@ -70,4 +70,8 @@ max-height: calc(100vh - 300px); list-style: none; padding: 5px 0; +} + +.label-li { + max-width: 200px; } \ No newline at end of file From 7711d23240e42b24182c8274787fdd3d5d8f6625 Mon Sep 17 00:00:00 2001 From: vinny Date: Wed, 13 Nov 2024 11:47:10 -0500 Subject: [PATCH 65/80] HARMONY-1892: toggleLabelNavVisibility based on number of jobs selected --- .../workflow-ui/jobs/index.mustache.html | 2 +- .../harmony/public/js/workflow-ui/labels.js | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/services/harmony/app/views/workflow-ui/jobs/index.mustache.html b/services/harmony/app/views/workflow-ui/jobs/index.mustache.html index 27f9ef3a7..c0dae40bb 100644 --- a/services/harmony/app/views/workflow-ui/jobs/index.mustache.html +++ b/services/harmony/app/views/workflow-ui/jobs/index.mustache.html @@ -42,7 +42,7 @@ {{^isAdminRoute}} {{#jobs.length}} {{#labels.length}} -