mirror of
https://github.com/hansjone/dsh-ops-cron.git
synced 2026-10-16 05:13:19 +08:00
Isolate scheduled tasks by empNo, aligned with uds-auth workspaces.
Super/fallback admins see all jobs grouped by user folder; admin/user only see their own. Stamp ownership on create, filter HTTP/tools, and prefer the owner's provisioned cwd on fire. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0ffc48b43a
commit
7d27179e51
12 changed files with 785 additions and 141 deletions
112
lib/ownership.js
Normal file
112
lib/ownership.js
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* Job ownership helpers for per-user cron isolation (aligns with uds-auth roles).
|
||||
*/
|
||||
|
||||
export const UNASSIGNED_OWNER = '__unassigned__'
|
||||
|
||||
export function normalizeOwnerEmpNo(value) {
|
||||
const empNo = String(value || '').trim()
|
||||
return empNo || UNASSIGNED_OWNER
|
||||
}
|
||||
|
||||
export function canViewAllJobs(identity) {
|
||||
return !!identity?.permissions?.canViewAllSessions
|
||||
}
|
||||
|
||||
export function jobVisibleToIdentity(job, identity) {
|
||||
if (!identity?.empNo) return false
|
||||
if (canViewAllJobs(identity)) return true
|
||||
const owner = normalizeOwnerEmpNo(job?.ownerEmpNo)
|
||||
if (owner === UNASSIGNED_OWNER) return false
|
||||
return owner === String(identity.empNo)
|
||||
}
|
||||
|
||||
export function assertCanAccessJob(job, identity) {
|
||||
if (!job) {
|
||||
const error = new Error('job not found')
|
||||
error.code = 'NOT_FOUND'
|
||||
throw error
|
||||
}
|
||||
if (!identity?.empNo) {
|
||||
const error = new Error('login_required')
|
||||
error.code = 'LOGIN_REQUIRED'
|
||||
throw error
|
||||
}
|
||||
if (!jobVisibleToIdentity(job, identity)) {
|
||||
const error = new Error('job not found')
|
||||
error.code = 'NOT_FOUND'
|
||||
throw error
|
||||
}
|
||||
return job
|
||||
}
|
||||
|
||||
export function filterJobsForIdentity(jobs, identity) {
|
||||
const list = Array.isArray(jobs) ? jobs : []
|
||||
if (!identity?.empNo) return []
|
||||
if (canViewAllJobs(identity)) return [...list]
|
||||
return list.filter((job) => jobVisibleToIdentity(job, identity))
|
||||
}
|
||||
|
||||
export function filterRunsForJobs(runs, jobs) {
|
||||
const allowed = new Set((jobs || []).map((job) => job.id))
|
||||
return (Array.isArray(runs) ? runs : []).filter((run) => allowed.has(run.jobId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer owner for legacy jobs missing ownerEmpNo.
|
||||
* @param {object} job
|
||||
* @param {{ getSessionOwner?: (sessionId: string) => string|null }} [deps]
|
||||
*/
|
||||
export function inferOwnerEmpNo(job, deps = {}) {
|
||||
if (job?.ownerEmpNo) return normalizeOwnerEmpNo(job.ownerEmpNo)
|
||||
const origin = job?.origin
|
||||
if (origin?.kind === 'im') return UNASSIGNED_OWNER
|
||||
const sessionId = origin?.kind === 'web' && typeof origin.sessionId === 'string'
|
||||
? origin.sessionId.trim()
|
||||
: ''
|
||||
if (sessionId && typeof deps.getSessionOwner === 'function') {
|
||||
const owner = deps.getSessionOwner(sessionId)
|
||||
if (owner) return normalizeOwnerEmpNo(owner)
|
||||
}
|
||||
return UNASSIGNED_OWNER
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate jobs in-place; returns { state, changed }.
|
||||
*/
|
||||
export function migrateJobOwners(state, deps = {}) {
|
||||
const jobs = Array.isArray(state?.jobs) ? state.jobs : []
|
||||
let changed = false
|
||||
const nextJobs = jobs.map((job) => {
|
||||
if (!job || typeof job !== 'object') return job
|
||||
if (job.ownerEmpNo) {
|
||||
const normalized = normalizeOwnerEmpNo(job.ownerEmpNo)
|
||||
if (normalized === job.ownerEmpNo && job.ownerDisplayName !== undefined) return job
|
||||
changed = true
|
||||
return {
|
||||
...job,
|
||||
ownerEmpNo: normalized,
|
||||
ownerDisplayName: job.ownerDisplayName || '',
|
||||
}
|
||||
}
|
||||
changed = true
|
||||
return {
|
||||
...job,
|
||||
ownerEmpNo: inferOwnerEmpNo(job, deps),
|
||||
ownerDisplayName: job.ownerDisplayName || '',
|
||||
}
|
||||
})
|
||||
if (!changed) return { state, changed: false }
|
||||
return { state: { ...state, jobs: nextJobs }, changed: true }
|
||||
}
|
||||
|
||||
export function viewerPayload(identity) {
|
||||
if (!identity?.empNo) return null
|
||||
return {
|
||||
empNo: identity.empNo,
|
||||
role: identity.role || 'user',
|
||||
displayName: identity.displayName || identity.empNo,
|
||||
canViewAll: canViewAllJobs(identity),
|
||||
workspacePath: identity.workspacePath || null,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue