mirror of
https://github.com/hansjone/dsh-ops-cron.git
synced 2026-10-15 18:40:51 +08:00
Make uds-auth optional: standalone local mode for cron.
Without udsAuth, trusted HTTP uses a synthetic __local__ viewer that sees all jobs; with uds-auth, keep multi-user empNo isolation and admin folders. Document that IM channels are not default UDS accounts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
1c504eb1fd
commit
933bad9201
8 changed files with 132 additions and 27 deletions
|
|
@ -1,29 +1,48 @@
|
|||
/**
|
||||
* Job ownership helpers for per-user cron isolation (aligns with uds-auth roles).
|
||||
*
|
||||
* Two modes:
|
||||
* - standalone (no uds-auth): identity empNo is LOCAL_EMP_NO → everyone sees all jobs
|
||||
* - multi-user (uds-auth present): filter/claim by real empNo
|
||||
*/
|
||||
|
||||
export const UNASSIGNED_OWNER = '__unassigned__'
|
||||
export const LOCAL_EMP_NO = '__local__'
|
||||
|
||||
export function normalizeOwnerEmpNo(value) {
|
||||
const empNo = String(value || '').trim()
|
||||
return empNo || UNASSIGNED_OWNER
|
||||
}
|
||||
|
||||
export function canViewAllJobs(identity) {
|
||||
return !!identity?.permissions?.canViewAllSessions
|
||||
/** True when uds-auth supplied a real user identity (not standalone local). */
|
||||
export function isMultiUserIdentity(identity) {
|
||||
const empNo = String(identity?.empNo || '').trim()
|
||||
return !!empNo && empNo !== LOCAL_EMP_NO
|
||||
}
|
||||
|
||||
function empNoFromUserWorkspacePath(cwd) {
|
||||
const norm = String(cwd || '').replace(/\\/g, '/')
|
||||
const match = norm.match(/\/user-workspaces\/([^/]+)(?:\/|$)/)
|
||||
return match ? decodeURIComponent(match[1]) : null
|
||||
export function localIdentity(overrides = {}) {
|
||||
return {
|
||||
empNo: LOCAL_EMP_NO,
|
||||
role: 'local',
|
||||
displayName: 'local',
|
||||
permissions: { canViewAllSessions: true },
|
||||
workspacePath: null,
|
||||
mode: 'local',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
export function canViewAllJobs(identity) {
|
||||
if (!isMultiUserIdentity(identity)) return true
|
||||
return !!identity?.permissions?.canViewAllSessions
|
||||
}
|
||||
|
||||
export function jobVisibleToIdentity(job, identity) {
|
||||
if (!identity?.empNo) return false
|
||||
if (canViewAllJobs(identity)) return true
|
||||
// Standalone / local: no empNo isolation.
|
||||
if (!isMultiUserIdentity(identity) || canViewAllJobs(identity)) return true
|
||||
const owner = normalizeOwnerEmpNo(job?.ownerEmpNo)
|
||||
// Unclaimed jobs are admin-only (super_admin / fallback_admin via canViewAll).
|
||||
// Unclaimed jobs are admin-only in multi-user mode.
|
||||
if (owner === UNASSIGNED_OWNER) return false
|
||||
return owner === String(identity.empNo)
|
||||
}
|
||||
|
|
@ -50,7 +69,7 @@ export function assertCanAccessJob(job, identity) {
|
|||
export function filterJobsForIdentity(jobs, identity) {
|
||||
const list = Array.isArray(jobs) ? jobs : []
|
||||
if (!identity?.empNo) return []
|
||||
if (canViewAllJobs(identity)) return [...list]
|
||||
if (!isMultiUserIdentity(identity) || canViewAllJobs(identity)) return [...list]
|
||||
return list.filter((job) => jobVisibleToIdentity(job, identity))
|
||||
}
|
||||
|
||||
|
|
@ -109,21 +128,23 @@ export function migrateJobOwners(state, deps = {}) {
|
|||
|
||||
export function viewerPayload(identity) {
|
||||
if (!identity?.empNo) return null
|
||||
const multi = isMultiUserIdentity(identity)
|
||||
return {
|
||||
empNo: identity.empNo,
|
||||
role: identity.role || 'user',
|
||||
role: identity.role || (multi ? 'user' : 'local'),
|
||||
displayName: identity.displayName || identity.empNo,
|
||||
canViewAll: canViewAllJobs(identity),
|
||||
workspacePath: identity.workspacePath || null,
|
||||
mode: multi ? 'multi' : 'local',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim unassigned jobs that clearly belong to the viewer (origin session / cwd).
|
||||
* Returns { state, changed }.
|
||||
* No-op in standalone / canViewAll. Returns { state, changed }.
|
||||
*/
|
||||
export function claimUnassignedForViewer(state, identity, deps = {}) {
|
||||
if (!identity?.empNo || canViewAllJobs(identity)) {
|
||||
if (!isMultiUserIdentity(identity) || canViewAllJobs(identity)) {
|
||||
return { state, changed: false }
|
||||
}
|
||||
const jobs = Array.isArray(state?.jobs) ? state.jobs : []
|
||||
|
|
@ -134,14 +155,14 @@ export function claimUnassignedForViewer(state, identity, deps = {}) {
|
|||
? job.origin.sessionId.trim()
|
||||
: ''
|
||||
let mine = false
|
||||
// Only claim when evidence ties the job to this viewer. Remaining unassigned
|
||||
// jobs stay admin-only until a super/fallback admin reassigns them.
|
||||
// Only claim when evidence ties the job to this viewer.
|
||||
if (sessionId && typeof deps.getSessionOwner === 'function') {
|
||||
mine = deps.getSessionOwner(sessionId) === identity.empNo
|
||||
}
|
||||
if (!mine && job.cwd) {
|
||||
const pathOwner = empNoFromUserWorkspacePath(job.cwd)
|
||||
if (pathOwner === identity.empNo) mine = true
|
||||
const norm = String(job.cwd).replace(/\\/g, '/')
|
||||
const match = norm.match(/\/user-workspaces\/([^/]+)(?:\/|$)/)
|
||||
mine = !!(match && decodeURIComponent(match[1]) === identity.empNo)
|
||||
}
|
||||
if (!mine) return job
|
||||
changed = true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue