From 6ff80ebba3a79351229b5a050351f7fe234d4cd7 Mon Sep 17 00:00:00 2001 From: Rui Gao Date: Mon, 20 Oct 2025 17:27:41 +0800 Subject: [PATCH 01/11] add history_vclist in user info to save the vc list which the user used to belong to --- .../src/utils/manager/user/crudK8sSecret.js | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/rest-server/src/utils/manager/user/crudK8sSecret.js b/src/rest-server/src/utils/manager/user/crudK8sSecret.js index 19454c15..20d30d88 100644 --- a/src/rest-server/src/utils/manager/user/crudK8sSecret.js +++ b/src/rest-server/src/utils/manager/user/crudK8sSecret.js @@ -17,6 +17,7 @@ const User = require('./user'); const logger = require('@pai/config/logger'); +const groupModel = require('@pai/models/v2/group'); const k8sModel = require('@pai/models/kubernetes/kubernetes'); const { Mutex } = require('async-mutex'); @@ -196,6 +197,20 @@ async function create(key, value) { extension: value.extension, }); await User.encryptUserPassword(userInstance); + + // retrieve VC from group list + const vcSet = new Set(); + const vcPromises = value.grouplist.map(async group => { + try { + return await groupModel.getGroupVCs(group); + } catch (error) { + console.error(`Failed to fetch VCs for group ${group}:`, error); + return []; // Return an empty array on failure + } + }); + const vcResults = await Promise.all(vcPromises); + vcResults.forEach(vcs => vcs.forEach(vc => vcSet.add(vc))); + const userData = { metadata: { name: hexKey }, type: 'Opaque', @@ -209,6 +224,7 @@ async function create(key, value) { extension: Buffer.from(JSON.stringify(userInstance.extension)).toString( 'base64', ), + history_vclist: Buffer.from(JSON.stringify(Array.from(vcSet))).toString('base64'), }, }; const logId = Math.floor(Math.random() * 100000); @@ -252,10 +268,32 @@ async function update(key, value, updatePassword = false) { grouplist: value.grouplist, email: value.email, extension: value.extension, + history_vclist: value.history_vclist || [], }); if (updatePassword) { await User.encryptUserPassword(userInstance); } + + // retrieve VC from group list + const vcSet = new Set(); + const vcPromises = value.grouplist.map(async group => { + try { + return await groupModel.getGroupVCs(group); + } catch (error) { + console.error(`Failed to fetch VCs for group ${group}:`, error); + return []; // Return an empty array on failure + } + }); + const vcResults = await Promise.all(vcPromises); + vcResults.forEach(vcs => vcs.forEach(vc => vcSet.add(vc))); + + // Merge vcResults into userInstance.history_vclist and remove duplicates + const mergedVCList = new Set([ + ...userInstance.history_vclist, + ...Array.from(vcSet), + ]); + userInstance.history_vclist = Array.from(mergedVCList); + const userData = { metadata: { name: hexKey }, data: { @@ -268,6 +306,7 @@ async function update(key, value, updatePassword = false) { extension: Buffer.from(JSON.stringify(userInstance.extension)).toString( 'base64', ), + history_vclist: Buffer.from(JSON.stringify(userInstance.history_vclist)).toString('base64'), }, }; const logId = Math.floor(Math.random() * 100000); From c2181e81d0b4768f22361d9cfc543369c6a007f3 Mon Sep 17 00:00:00 2001 From: Rui Gao Date: Tue, 21 Oct 2025 12:09:00 +0800 Subject: [PATCH 02/11] use hostory vc list to retrieve user's job --- src/rest-server/src/controllers/v2/job.js | 10 +++++++++- src/rest-server/src/controllers/v2/user.js | 6 ++++++ .../src/utils/manager/user/crudK8sSecret.js | 6 ++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/rest-server/src/controllers/v2/job.js b/src/rest-server/src/controllers/v2/job.js index 6f59d3d3..60142e39 100644 --- a/src/rest-server/src/controllers/v2/job.js +++ b/src/rest-server/src/controllers/v2/job.js @@ -50,7 +50,15 @@ const list = asyncHandler(async (req, res) => { } const username = req[userProperty].username; - myvcs = await userController.getUserVCs(username); + currentvcs = await userController.getUserVCs(username); + let myvcs = await userController.getUserHistoryVCs(username); + if (!myvcs || myvcs.length === 0) { + myvcs = currentvcs; + } + else { + myvcs = Array.from(new Set([...myvcs, ...currentvcs])); + } + if (!filters.virtualCluster || filters.virtualCluster.length === 0) { filters.virtualCluster = myvcs; } else { diff --git a/src/rest-server/src/controllers/v2/user.js b/src/rest-server/src/controllers/v2/user.js index 4d887e7c..ec3580f7 100644 --- a/src/rest-server/src/controllers/v2/user.js +++ b/src/rest-server/src/controllers/v2/user.js @@ -35,6 +35,11 @@ const getUserVCs = async (username) => { return [...virtualClusters]; }; +const getUserHistoryVCs = async (username) => { + const userInfo = await userModel.getUser(username); + return userInfo.history_vclist || []; +}; + const getUser = async (req, res, next) => { try { const username = req.params.username; @@ -810,4 +815,5 @@ module.exports = { updateUserPassword, createUser, getUserVCs, + getUserHistoryVCs, }; diff --git a/src/rest-server/src/utils/manager/user/crudK8sSecret.js b/src/rest-server/src/utils/manager/user/crudK8sSecret.js index 20d30d88..ff5f3528 100644 --- a/src/rest-server/src/utils/manager/user/crudK8sSecret.js +++ b/src/rest-server/src/utils/manager/user/crudK8sSecret.js @@ -87,6 +87,9 @@ async function read(key) { extension: JSON.parse( Buffer.from(userData.data.extension, 'base64').toString(), ), + history_vclist: JSON.parse( + Buffer.from(userData.data.history_vclist, 'base64').toString(), + ), }); cache.set(key, userInstance); @@ -154,6 +157,9 @@ async function readAll() { extension: JSON.parse( Buffer.from(item.data.extension, 'base64').toString(), ), + history_vclist: JSON.parse( + Buffer.from(item.data.history_vclist, 'base64').toString(), + ), }); allUserInstance.push(userInstance); } catch (error) { From 2a4dca7a0dabb05f9e6bf6fd3add612ff7872551 Mon Sep 17 00:00:00 2001 From: Rui Gao Date: Thu, 23 Oct 2025 01:39:31 +0000 Subject: [PATCH 03/11] add history_vclist into validation schema --- src/rest-server/src/controllers/v2/user.js | 1 + .../src/utils/manager/user/crudK8sSecret.js | 12 ++++++------ src/rest-server/src/utils/manager/user/user.js | 1 + 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/rest-server/src/controllers/v2/user.js b/src/rest-server/src/controllers/v2/user.js index ec3580f7..c4316a4f 100644 --- a/src/rest-server/src/controllers/v2/user.js +++ b/src/rest-server/src/controllers/v2/user.js @@ -139,6 +139,7 @@ const createUserIfUserNotExist = async (req, res, next) => { password: userData.oid, grouplist: grouplist, extension: {}, + history_vclist: [], }; const existUser = await userModel.getUser(username).catch(() => null); diff --git a/src/rest-server/src/utils/manager/user/crudK8sSecret.js b/src/rest-server/src/utils/manager/user/crudK8sSecret.js index ff5f3528..cc4d07e0 100644 --- a/src/rest-server/src/utils/manager/user/crudK8sSecret.js +++ b/src/rest-server/src/utils/manager/user/crudK8sSecret.js @@ -87,9 +87,9 @@ async function read(key) { extension: JSON.parse( Buffer.from(userData.data.extension, 'base64').toString(), ), - history_vclist: JSON.parse( - Buffer.from(userData.data.history_vclist, 'base64').toString(), - ), + history_vclist: userData.data.history_vclist + ? JSON.parse(Buffer.from(userData.data.history_vclist, 'base64').toString()) + : [], }); cache.set(key, userInstance); @@ -157,9 +157,9 @@ async function readAll() { extension: JSON.parse( Buffer.from(item.data.extension, 'base64').toString(), ), - history_vclist: JSON.parse( - Buffer.from(item.data.history_vclist, 'base64').toString(), - ), + history_vclist: item.data.history_vclist + ? JSON.parse(Buffer.from(item.data.history_vclist, 'base64').toString()) + : [], }); allUserInstance.push(userInstance); } catch (error) { diff --git a/src/rest-server/src/utils/manager/user/user.js b/src/rest-server/src/utils/manager/user/user.js index 86c6c609..7fee7953 100644 --- a/src/rest-server/src/utils/manager/user/user.js +++ b/src/rest-server/src/utils/manager/user/user.js @@ -27,6 +27,7 @@ const userSchema = Joi.object() grouplist: Joi.array().items(Joi.string()).required(), password: Joi.string().empty('').default(''), extension: Joi.object().pattern(/\w+/, Joi.required()).required(), + history_vclist: Joi.array().items(Joi.string()), }) .required(); From 72e5dac72e3122908a74e34b527eaef062579b84 Mon Sep 17 00:00:00 2001 From: Rui Gao Date: Tue, 28 Oct 2025 12:50:16 +0800 Subject: [PATCH 04/11] retrieve the vc list and update user info when create/update user --- src/rest-server/src/controllers/v2/job.js | 8 ++++++++ .../src/utils/manager/user/crudK8sSecret.js | 20 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/rest-server/src/controllers/v2/job.js b/src/rest-server/src/controllers/v2/job.js index 60142e39..5389dc35 100644 --- a/src/rest-server/src/controllers/v2/job.js +++ b/src/rest-server/src/controllers/v2/job.js @@ -26,6 +26,13 @@ const { Op } = require('sequelize'); const { userProperty } = require('@pai/config/token'); const userController = require('@pai/controllers/v2/user'); +const getUserHistoryVCs = async (username) => { + const jobs = await job.list(['virtualCluster'], { userName: username }); + const vcs = Array.from(new Set(jobs.map((j) => j.virtualCluster))); + logger.info(`User ${username} has accessed historical virtual clusters: ${vcs}`); + return vcs; +}; + const list = asyncHandler(async (req, res) => { // ?keyword=&username=,&vc=, // &state=,&offset=&limit=&withTotalCount=true @@ -385,6 +392,7 @@ const getLogs = asyncHandler(async (req, res) => { // module exports module.exports = { + getUserHistoryVCs, list, get, update, diff --git a/src/rest-server/src/utils/manager/user/crudK8sSecret.js b/src/rest-server/src/utils/manager/user/crudK8sSecret.js index cc4d07e0..549b41db 100644 --- a/src/rest-server/src/utils/manager/user/crudK8sSecret.js +++ b/src/rest-server/src/utils/manager/user/crudK8sSecret.js @@ -20,6 +20,7 @@ const logger = require('@pai/config/logger'); const groupModel = require('@pai/models/v2/group'); const k8sModel = require('@pai/models/kubernetes/kubernetes'); const { Mutex } = require('async-mutex'); +const jobController = require('@pai/controllers/v2/job'); const USER_NAMESPACE = process.env.PAI_USER_NAMESPACE || 'pai-user-v2'; @@ -201,9 +202,13 @@ async function create(key, value) { grouplist: value.grouplist, email: value.email, extension: value.extension, + history_vclist: [], }); await User.encryptUserPassword(userInstance); + // retrieve VC list from the history job list belonging to the user if exists + userInstance.history_vclist = await jobController.getUserHistoryVCs(userInstance.username); + // retrieve VC from group list const vcSet = new Set(); const vcPromises = value.grouplist.map(async group => { @@ -217,6 +222,13 @@ async function create(key, value) { const vcResults = await Promise.all(vcPromises); vcResults.forEach(vcs => vcs.forEach(vc => vcSet.add(vc))); + // Merge vcResults into userInstance.history_vclist and remove duplicates + const mergedVCList = new Set([ + ...userInstance.history_vclist, + ...Array.from(vcSet), + ]); + userInstance.history_vclist = Array.from(mergedVCList); + const userData = { metadata: { name: hexKey }, type: 'Opaque', @@ -230,7 +242,7 @@ async function create(key, value) { extension: Buffer.from(JSON.stringify(userInstance.extension)).toString( 'base64', ), - history_vclist: Buffer.from(JSON.stringify(Array.from(vcSet))).toString('base64'), + history_vclist: Buffer.from(JSON.stringify(userInstance.history_vclist)).toString('base64'), }, }; const logId = Math.floor(Math.random() * 100000); @@ -280,6 +292,12 @@ async function update(key, value, updatePassword = false) { await User.encryptUserPassword(userInstance); } + // if userInstance.history_vclist is empty, set it to the retrieved VC list + // retrieve VC list from the job list belonging to the user + if (!userInstance.history_vclist || userInstance.history_vclist.length === 0) { + userInstance.history_vclist = await jobController.getUserHistoryVCs(userInstance.username); + } + // retrieve VC from group list const vcSet = new Set(); const vcPromises = value.grouplist.map(async group => { From c53684ddeb0bfbee9ad21e70627d4d389e543af0 Mon Sep 17 00:00:00 2001 From: Rui Gao Date: Tue, 28 Oct 2025 12:04:35 +0000 Subject: [PATCH 05/11] add history vc list retrieve when user login/update --- src/rest-server/src/controllers/v2/job.js | 8 ----- src/rest-server/src/models/v2/job/k8s.js | 33 +++++++++++++++++++ .../src/utils/manager/user/crudK8sSecret.js | 11 ++++--- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/rest-server/src/controllers/v2/job.js b/src/rest-server/src/controllers/v2/job.js index 5389dc35..60142e39 100644 --- a/src/rest-server/src/controllers/v2/job.js +++ b/src/rest-server/src/controllers/v2/job.js @@ -26,13 +26,6 @@ const { Op } = require('sequelize'); const { userProperty } = require('@pai/config/token'); const userController = require('@pai/controllers/v2/user'); -const getUserHistoryVCs = async (username) => { - const jobs = await job.list(['virtualCluster'], { userName: username }); - const vcs = Array.from(new Set(jobs.map((j) => j.virtualCluster))); - logger.info(`User ${username} has accessed historical virtual clusters: ${vcs}`); - return vcs; -}; - const list = asyncHandler(async (req, res) => { // ?keyword=&username=,&vc=, // &state=,&offset=&limit=&withTotalCount=true @@ -392,7 +385,6 @@ const getLogs = asyncHandler(async (req, res) => { // module exports module.exports = { - getUserHistoryVCs, list, get, update, diff --git a/src/rest-server/src/models/v2/job/k8s.js b/src/rest-server/src/models/v2/job/k8s.js index 86ad75f8..67cf692d 100644 --- a/src/rest-server/src/models/v2/job/k8s.js +++ b/src/rest-server/src/models/v2/job/k8s.js @@ -1556,6 +1556,38 @@ const getEvents = async (frameworkName, attributes, filters) => { } }; +const listVCsFromJob = async (username) => { + try { + logger.info(`Start to list jobs for user ${username}`); + // Remove limit: 0 so it fetches all records, and optionally add a sensible order + const frameworks = await databaseModel.Framework.findAll({ + attributes: [ + 'name', + 'jobName', + 'userName', + 'virtualCluster', + ], + where: { userName: username }, + }); + + logger.info(`Frameworks raw result for user ${username}: ${JSON.stringify(frameworks, null, 2)}`); + + const vcsSet = new Set(); + frameworks.forEach((framework) => { + if (framework.virtualCluster) { + vcsSet.add(framework.virtualCluster); + } + }); + const vcs = Array.from(vcsSet); + + logger.info(`User ${username} has accessed historical virtual clusters: ${vcs}`); + return vcs; + } catch (error) { + logger.error(`Failed to get historical virtual clusters for user ${username}: ${error}`); + return []; + } +}; + // module exports module.exports = { list, @@ -1567,4 +1599,5 @@ module.exports = { addTag, deleteTag, getEvents, + listVCsFromJob, }; diff --git a/src/rest-server/src/utils/manager/user/crudK8sSecret.js b/src/rest-server/src/utils/manager/user/crudK8sSecret.js index 549b41db..9af2cd0f 100644 --- a/src/rest-server/src/utils/manager/user/crudK8sSecret.js +++ b/src/rest-server/src/utils/manager/user/crudK8sSecret.js @@ -20,7 +20,7 @@ const logger = require('@pai/config/logger'); const groupModel = require('@pai/models/v2/group'); const k8sModel = require('@pai/models/kubernetes/kubernetes'); const { Mutex } = require('async-mutex'); -const jobController = require('@pai/controllers/v2/job'); +const { job } = require('@pai/models/v2/job'); const USER_NAMESPACE = process.env.PAI_USER_NAMESPACE || 'pai-user-v2'; @@ -207,7 +207,8 @@ async function create(key, value) { await User.encryptUserPassword(userInstance); // retrieve VC list from the history job list belonging to the user if exists - userInstance.history_vclist = await jobController.getUserHistoryVCs(userInstance.username); + const vcsFromJob = await job.listVCsFromJob(userInstance.username); + userInstance.history_vclist = vcsFromJob; // retrieve VC from group list const vcSet = new Set(); @@ -286,7 +287,8 @@ async function update(key, value, updatePassword = false) { grouplist: value.grouplist, email: value.email, extension: value.extension, - history_vclist: value.history_vclist || [], + //history_vclist: value.history_vclist || [], + history_vclist: [], }); if (updatePassword) { await User.encryptUserPassword(userInstance); @@ -295,7 +297,8 @@ async function update(key, value, updatePassword = false) { // if userInstance.history_vclist is empty, set it to the retrieved VC list // retrieve VC list from the job list belonging to the user if (!userInstance.history_vclist || userInstance.history_vclist.length === 0) { - userInstance.history_vclist = await jobController.getUserHistoryVCs(userInstance.username); + const vcsFromJob = await job.listVCsFromJob(userInstance.username); + userInstance.history_vclist = vcsFromJob; } // retrieve VC from group list From ecdcd9e69e7526aa767d28cf83edb1078eed134e Mon Sep 17 00:00:00 2001 From: Rui Gao Date: Wed, 29 Oct 2025 01:14:31 +0000 Subject: [PATCH 06/11] clean the code --- src/rest-server/src/models/v2/job/k8s.js | 2 - .../src/utils/manager/user/crudK8sSecret.js | 89 +++++++++---------- 2 files changed, 42 insertions(+), 49 deletions(-) diff --git a/src/rest-server/src/models/v2/job/k8s.js b/src/rest-server/src/models/v2/job/k8s.js index 67cf692d..3aa31e44 100644 --- a/src/rest-server/src/models/v2/job/k8s.js +++ b/src/rest-server/src/models/v2/job/k8s.js @@ -1570,8 +1570,6 @@ const listVCsFromJob = async (username) => { where: { userName: username }, }); - logger.info(`Frameworks raw result for user ${username}: ${JSON.stringify(frameworks, null, 2)}`); - const vcsSet = new Set(); frameworks.forEach((framework) => { if (framework.virtualCluster) { diff --git a/src/rest-server/src/utils/manager/user/crudK8sSecret.js b/src/rest-server/src/utils/manager/user/crudK8sSecret.js index 9af2cd0f..c26115b4 100644 --- a/src/rest-server/src/utils/manager/user/crudK8sSecret.js +++ b/src/rest-server/src/utils/manager/user/crudK8sSecret.js @@ -43,6 +43,36 @@ const cache = new Map(); const readMutex = new Mutex(); +async function getHistoryVCs(name, grouplist, retrieveFromHistory=true) { + // Retrieve VC list from the user's job history + let vcsFromJob = []; + if (retrieveFromHistory) { + logger.info(`Retrieving VC list from job history for user: ${name}`); + vcsFromJob = await job.listVCsFromJob(name); + } + + // Retrieve VC list from each group the user belongs to + const vcSet = new Set(); + const vcPromises = grouplist.map(async group => { + try { + return await groupModel.getGroupVCs(group); + } catch (error) { + console.error(`Failed to fetch VCs for group ${group}:`, error); + return []; // Return an empty array on failure + } + }); + const vcResults = await Promise.all(vcPromises); + vcResults.forEach(vcs => vcs.forEach(vc => vcSet.add(vc))); + + // Merge VC lists and remove duplicates + const mergedVCList = new Set([ + ...vcsFromJob, + ...Array.from(vcSet), + ]); + + return Array.from(mergedVCList); +} + async function read(key) { if (cache.has(key)) { logger.info(`Read user info from cache: ${key}`); @@ -207,28 +237,7 @@ async function create(key, value) { await User.encryptUserPassword(userInstance); // retrieve VC list from the history job list belonging to the user if exists - const vcsFromJob = await job.listVCsFromJob(userInstance.username); - userInstance.history_vclist = vcsFromJob; - - // retrieve VC from group list - const vcSet = new Set(); - const vcPromises = value.grouplist.map(async group => { - try { - return await groupModel.getGroupVCs(group); - } catch (error) { - console.error(`Failed to fetch VCs for group ${group}:`, error); - return []; // Return an empty array on failure - } - }); - const vcResults = await Promise.all(vcPromises); - vcResults.forEach(vcs => vcs.forEach(vc => vcSet.add(vc))); - - // Merge vcResults into userInstance.history_vclist and remove duplicates - const mergedVCList = new Set([ - ...userInstance.history_vclist, - ...Array.from(vcSet), - ]); - userInstance.history_vclist = Array.from(mergedVCList); + userInstance.history_vclist = await getHistoryVCs(userInstance.username, userInstance.grouplist); const userData = { metadata: { name: hexKey }, @@ -287,8 +296,7 @@ async function update(key, value, updatePassword = false) { grouplist: value.grouplist, email: value.email, extension: value.extension, - //history_vclist: value.history_vclist || [], - history_vclist: [], + history_vclist: value.history_vclist || [], }); if (updatePassword) { await User.encryptUserPassword(userInstance); @@ -296,30 +304,17 @@ async function update(key, value, updatePassword = false) { // if userInstance.history_vclist is empty, set it to the retrieved VC list // retrieve VC list from the job list belonging to the user - if (!userInstance.history_vclist || userInstance.history_vclist.length === 0) { - const vcsFromJob = await job.listVCsFromJob(userInstance.username); - userInstance.history_vclist = vcsFromJob; - } + const vclist = await getHistoryVCs( + userInstance.username, + userInstance.grouplist, + userInstance.history_vclist.length === 0 + ); - // retrieve VC from group list - const vcSet = new Set(); - const vcPromises = value.grouplist.map(async group => { - try { - return await groupModel.getGroupVCs(group); - } catch (error) { - console.error(`Failed to fetch VCs for group ${group}:`, error); - return []; // Return an empty array on failure - } - }); - const vcResults = await Promise.all(vcPromises); - vcResults.forEach(vcs => vcs.forEach(vc => vcSet.add(vc))); - - // Merge vcResults into userInstance.history_vclist and remove duplicates - const mergedVCList = new Set([ - ...userInstance.history_vclist, - ...Array.from(vcSet), - ]); - userInstance.history_vclist = Array.from(mergedVCList); + // Merge userInstance.history_vclist with vclist and remove duplicates + userInstance.history_vclist = Array.from(new Set([ + ...(userInstance.history_vclist || []), + ...(vclist || []), + ])); const userData = { metadata: { name: hexKey }, From c28c8c1ce2587dc49e951c5f41ef412a13a01e64 Mon Sep 17 00:00:00 2001 From: Rui Gao Date: Tue, 11 Nov 2025 13:16:54 +0800 Subject: [PATCH 07/11] add user access checking when retrieve job information including job config and logs --- src/rest-server/src/controllers/v2/job.js | 143 ++++++++++++++++++++- src/rest-server/src/controllers/v2/user.js | 4 +- 2 files changed, 138 insertions(+), 9 deletions(-) diff --git a/src/rest-server/src/controllers/v2/job.js b/src/rest-server/src/controllers/v2/job.js index 60142e39..7233aa4f 100644 --- a/src/rest-server/src/controllers/v2/job.js +++ b/src/rest-server/src/controllers/v2/job.js @@ -26,6 +26,48 @@ const { Op } = require('sequelize'); const { userProperty } = require('@pai/config/token'); const userController = require('@pai/controllers/v2/user'); +const retrieveJobInfo = async (frameworkName, jobAttemptId, requestingUser, isAdmin, vcAdmins) => { + let data; + try { + data = await job.get( + frameworkName, + jobAttemptId ? Number(jobAttemptId) : undefined, + ); + } catch (error) { + logger.error(`Error retrieving job: ${error.message}`); + throw createError( + 'Internal Server Error', + 'UnknownError', + `Failed to retrieve job ${frameworkName}.`, + ); + } + + if (data && data.jobStatus) { + const virtualCluster = data.jobStatus.virtualCluster; + const userName = data.jobStatus.username; + logger.info(`Job belongs to user: ${userName}, virtual cluster: ${virtualCluster}`); + + const isAdminOfVC = vcAdmins.includes(virtualCluster); + + if (userName !== requestingUser && !isAdmin && !isAdminOfVC) { + logger.warn(`User ${requestingUser} is not allowed to access job ${frameworkName}`); + throw createError( + 'Forbidden', + 'ForbiddenUserError', + `User ${requestingUser} is not allowed to access job ${frameworkName}.`, + ); + } + } else { + throw createError( + 'Not Found', + 'NoJobStatusError', + `Job status for ${frameworkName} is not found.`, + ); + } + logger.info(`Job ${frameworkName} retrieved successfully.`); + return data; +}; + const list = asyncHandler(async (req, res) => { // ?keyword=&username=,&vc=, // &state=,&offset=&limit=&withTotalCount=true @@ -50,10 +92,10 @@ const list = asyncHandler(async (req, res) => { } const username = req[userProperty].username; - currentvcs = await userController.getUserVCs(username); - let myvcs = await userController.getUserHistoryVCs(username); + const currentvcs = await userController.getUserVCs(username); + let myvcs = await userController.getUserHistoryVCsFromUserInfo(username); if (!myvcs || myvcs.length === 0) { - myvcs = currentvcs; + myvcs = [...currentvcs]; } else { myvcs = Array.from(new Set([...myvcs, ...currentvcs])); @@ -67,6 +109,34 @@ const list = asyncHandler(async (req, res) => { ); } + // if the user tries to access other users' jobs in his/her history VCs, + // we will block the request here + // if filtering on username is not applied, which means accessing all users' jobs, + // or filtering on multiple usernames besides of himself/herself, + // we need to do the checking + const userFilterChecking = !filters.userName || filters.userName.some((name) => name !== username); + + if (userFilterChecking) { + const isAdmin = req[userProperty].admin; + // for admin user, no need to check + if (!isAdmin) { + if (filters.virtualCluster && filters.virtualCluster.length > 0) { + // now check if the vc list only contains user's current VCs + // vc filter is already applied above, so we don't need to check empty case here + const otherVcs = filters.virtualCluster.filter( + (vc) => !currentvcs.includes(vc), + ); + if (otherVcs.length > 0) { + throw createError( + 'Forbidden', + 'ForbiddenUserError', + `User ${username} is not allowed to access other users' jobs in ${otherVcs.join(', ')}.`, + ); + } + } + } + } + if ('state' in req.query) { filters.state = req.query.state.split(','); } @@ -196,10 +266,18 @@ const list = asyncHandler(async (req, res) => { }); const get = asyncHandler(async (req, res) => { - const data = await job.get( - req.params.frameworkName, - req.params.jobAttemptId ? Number(req.params.jobAttemptId) : undefined, - ); + let data; + try { + data = await retrieveJobInfo( + req.params.frameworkName, + req.params.jobAttemptId ? Number(req.params.jobAttemptId) : undefined, + req[userProperty].username, + req[userProperty].admin, + req[userProperty].vcadmins || [], + ); + } catch (error) { + throw error; + } res.json(data); }); @@ -272,6 +350,23 @@ const execute = asyncHandler(async (req, res) => { }); const getConfig = asyncHandler(async (req, res) => { + try { + await retrieveJobInfo( + req.params.frameworkName, + undefined, + req[userProperty].username, + req[userProperty].admin, + req[userProperty].vcadmins || [], + ); + } + catch (error) { + throw createError( + 'Forbidden', + 'ForbiddenUserError', + `User ${req[userProperty].username} is not allowed to access the config file for job ${req.params.frameworkName}.`, + ); + } + try { const data = await job.getConfig(req.params.frameworkName); return res.status(200).type('text/yaml').send(data); @@ -330,6 +425,23 @@ const deleteTag = asyncHandler(async (req, res) => { }); const getEvents = asyncHandler(async (req, res) => { + try { + await retrieveJobInfo( + req.params.frameworkName, + undefined, + req[userProperty].username, + req[userProperty].admin, + req[userProperty].vcadmins || [], + ); + } + catch (error) { + throw createError( + 'Forbidden', + 'ForbiddenUserError', + `User ${req[userProperty].username} is not allowed to access the events for job ${req.params.frameworkName}.`, + ); + } + const filters = {}; if (req.query) { if ('type' in req.query) { @@ -361,6 +473,23 @@ const getEvents = asyncHandler(async (req, res) => { }); const getLogs = asyncHandler(async (req, res) => { + try { + await retrieveJobInfo( + req.params.frameworkName, + undefined, + req[userProperty].username, + req[userProperty].admin, + req[userProperty].vcadmins || [], + ); + } + catch (error) { + throw createError( + 'Forbidden', + 'ForbiddenUserError', + `User ${req[userProperty].username} is not allowed to access the logs for job ${req.params.frameworkName}.`, + ); + } + try { const data = await log.getLogListFromLogServer( req.params.frameworkName, diff --git a/src/rest-server/src/controllers/v2/user.js b/src/rest-server/src/controllers/v2/user.js index c4316a4f..682cc194 100644 --- a/src/rest-server/src/controllers/v2/user.js +++ b/src/rest-server/src/controllers/v2/user.js @@ -35,7 +35,7 @@ const getUserVCs = async (username) => { return [...virtualClusters]; }; -const getUserHistoryVCs = async (username) => { +const getUserHistoryVCsFromUserInfo = async (username) => { const userInfo = await userModel.getUser(username); return userInfo.history_vclist || []; }; @@ -816,5 +816,5 @@ module.exports = { updateUserPassword, createUser, getUserVCs, - getUserHistoryVCs, + getUserHistoryVCsFromUserInfo, }; From 7e39728dbd0d5d3e5cd1248e9a9b8f76e312c18f Mon Sep 17 00:00:00 2001 From: Rui Gao Date: Wed, 12 Nov 2025 15:01:08 +0800 Subject: [PATCH 08/11] Update src/rest-server/src/controllers/v2/job.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/rest-server/src/controllers/v2/job.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/rest-server/src/controllers/v2/job.js b/src/rest-server/src/controllers/v2/job.js index 7233aa4f..fa557660 100644 --- a/src/rest-server/src/controllers/v2/job.js +++ b/src/rest-server/src/controllers/v2/job.js @@ -96,8 +96,7 @@ const list = asyncHandler(async (req, res) => { let myvcs = await userController.getUserHistoryVCsFromUserInfo(username); if (!myvcs || myvcs.length === 0) { myvcs = [...currentvcs]; - } - else { + } else { myvcs = Array.from(new Set([...myvcs, ...currentvcs])); } From bec63ed29c956fcb0c424ffe2a1227964545651c Mon Sep 17 00:00:00 2001 From: Rui Gao Date: Wed, 12 Nov 2025 15:04:52 +0800 Subject: [PATCH 09/11] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/rest-server/src/controllers/v2/job.js | 42 +++++++------------ .../src/utils/manager/user/crudK8sSecret.js | 2 +- 2 files changed, 15 insertions(+), 29 deletions(-) diff --git a/src/rest-server/src/controllers/v2/job.js b/src/rest-server/src/controllers/v2/job.js index fa557660..9aaac7ef 100644 --- a/src/rest-server/src/controllers/v2/job.js +++ b/src/rest-server/src/controllers/v2/job.js @@ -265,18 +265,13 @@ const list = asyncHandler(async (req, res) => { }); const get = asyncHandler(async (req, res) => { - let data; - try { - data = await retrieveJobInfo( - req.params.frameworkName, - req.params.jobAttemptId ? Number(req.params.jobAttemptId) : undefined, - req[userProperty].username, - req[userProperty].admin, - req[userProperty].vcadmins || [], - ); - } catch (error) { - throw error; - } + const data = await retrieveJobInfo( + req.params.frameworkName, + req.params.jobAttemptId ? Number(req.params.jobAttemptId) : undefined, + req[userProperty].username, + req[userProperty].admin, + req[userProperty].vcadmins || [], + ); res.json(data); }); @@ -349,22 +344,13 @@ const execute = asyncHandler(async (req, res) => { }); const getConfig = asyncHandler(async (req, res) => { - try { - await retrieveJobInfo( - req.params.frameworkName, - undefined, - req[userProperty].username, - req[userProperty].admin, - req[userProperty].vcadmins || [], - ); - } - catch (error) { - throw createError( - 'Forbidden', - 'ForbiddenUserError', - `User ${req[userProperty].username} is not allowed to access the config file for job ${req.params.frameworkName}.`, - ); - } + await retrieveJobInfo( + req.params.frameworkName, + undefined, + req[userProperty].username, + req[userProperty].admin, + req[userProperty].vcadmins || [], + ); try { const data = await job.getConfig(req.params.frameworkName); diff --git a/src/rest-server/src/utils/manager/user/crudK8sSecret.js b/src/rest-server/src/utils/manager/user/crudK8sSecret.js index c26115b4..07ed4532 100644 --- a/src/rest-server/src/utils/manager/user/crudK8sSecret.js +++ b/src/rest-server/src/utils/manager/user/crudK8sSecret.js @@ -57,7 +57,7 @@ async function getHistoryVCs(name, grouplist, retrieveFromHistory=true) { try { return await groupModel.getGroupVCs(group); } catch (error) { - console.error(`Failed to fetch VCs for group ${group}:`, error); + logger.error(`Failed to fetch VCs for group ${group}:`, error); return []; // Return an empty array on failure } }); From bf42fa38a75dc04140e4978232d8402be73c7e17 Mon Sep 17 00:00:00 2001 From: Rui Gao Date: Wed, 12 Nov 2025 07:15:31 +0000 Subject: [PATCH 10/11] fix the try-catch following copilot suggestions --- src/rest-server/src/controllers/v2/job.js | 46 +++++++---------------- 1 file changed, 14 insertions(+), 32 deletions(-) diff --git a/src/rest-server/src/controllers/v2/job.js b/src/rest-server/src/controllers/v2/job.js index 9aaac7ef..8e5024fb 100644 --- a/src/rest-server/src/controllers/v2/job.js +++ b/src/rest-server/src/controllers/v2/job.js @@ -410,22 +410,13 @@ const deleteTag = asyncHandler(async (req, res) => { }); const getEvents = asyncHandler(async (req, res) => { - try { - await retrieveJobInfo( - req.params.frameworkName, - undefined, - req[userProperty].username, - req[userProperty].admin, - req[userProperty].vcadmins || [], - ); - } - catch (error) { - throw createError( - 'Forbidden', - 'ForbiddenUserError', - `User ${req[userProperty].username} is not allowed to access the events for job ${req.params.frameworkName}.`, - ); - } + await retrieveJobInfo( + req.params.frameworkName, + undefined, + req[userProperty].username, + req[userProperty].admin, + req[userProperty].vcadmins || [], + ); const filters = {}; if (req.query) { @@ -458,22 +449,13 @@ const getEvents = asyncHandler(async (req, res) => { }); const getLogs = asyncHandler(async (req, res) => { - try { - await retrieveJobInfo( - req.params.frameworkName, - undefined, - req[userProperty].username, - req[userProperty].admin, - req[userProperty].vcadmins || [], - ); - } - catch (error) { - throw createError( - 'Forbidden', - 'ForbiddenUserError', - `User ${req[userProperty].username} is not allowed to access the logs for job ${req.params.frameworkName}.`, - ); - } + await retrieveJobInfo( + req.params.frameworkName, + undefined, + req[userProperty].username, + req[userProperty].admin, + req[userProperty].vcadmins || [], + ); try { const data = await log.getLogListFromLogServer( From 34dc20d4d4f392ad8e027612c77694a0bb33d9f5 Mon Sep 17 00:00:00 2001 From: Rui Gao Date: Wed, 12 Nov 2025 07:19:54 +0000 Subject: [PATCH 11/11] fix PR comments to remove unused attributes from database --- src/rest-server/src/models/v2/job/k8s.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/rest-server/src/models/v2/job/k8s.js b/src/rest-server/src/models/v2/job/k8s.js index 3aa31e44..eb37c5ab 100644 --- a/src/rest-server/src/models/v2/job/k8s.js +++ b/src/rest-server/src/models/v2/job/k8s.js @@ -1562,9 +1562,6 @@ const listVCsFromJob = async (username) => { // Remove limit: 0 so it fetches all records, and optionally add a sensible order const frameworks = await databaseModel.Framework.findAll({ attributes: [ - 'name', - 'jobName', - 'userName', 'virtualCluster', ], where: { userName: username },