Keep foreign cron cwd for super_admin via live role resolve and session default.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-10 11:09:56 +08:00
parent 9b8f937a65
commit 7a2a673ac2
9 changed files with 189 additions and 18 deletions

View file

@ -978,12 +978,16 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
return String(row?.cwd || '').trim()
}
/** Prefer provisioned viewer workspace over the currently open project. */
/** Prefer open session cwd for elevated viewers; else provisioned path. */
function preferredNewJobCwd(viewer, sessionCwd) {
const session = String(sessionCwd || '').trim()
const provisioned = String(viewer?.workspacePath || '').trim()
if (viewer?.canViewAll && session) return session
const role = String(viewer?.role || '').trim()
if ((role === 'super_admin' || role === 'fallback_admin') && session) return session
if (viewer?.mode === 'multi' && provisioned) return provisioned
if (provisioned) return provisioned
return String(sessionCwd || '').trim()
return session
}
function cwdSelectValue(cwd, workspaces) {

View file

@ -30,6 +30,7 @@ import {
claimUnassignedForViewer,
filterJobsForIdentity,
filterRunsForJobs,
isElevatedCronRole,
isMultiUserIdentity,
localIdentity,
migrateJobOwners,
@ -175,6 +176,67 @@ async function requireIdentity(request, write, getUdsAuth) {
return { ...identity, mode: 'multi', lang: identity.lang || locale }
}
/**
* Whether this identity / job owner may keep an explicit cwd outside the
* provisioned tree. Re-resolves via uds-auth so stale tool identities (role
* missing permissions, or Host with an older caller path) still work.
*/
function allowsForeignJobCwd(identity, getUdsAuth, ownerEmpNo) {
if (canViewAllJobs(identity)) return true
const uds = typeof getUdsAuth === 'function' ? getUdsAuth() : null
const empNo = String(ownerEmpNo || identity?.empNo || '').trim()
if (!empNo || empNo.startsWith('__') || !uds) return false
if (typeof uds.resolveIdentityForEmpNo === 'function') {
const live = uds.resolveIdentityForEmpNo(empNo)
if (live && canViewAllJobs(live)) return true
}
return false
}
/** Merge live role/permissions onto a caller identity when uds-auth can look them up. */
function enrichIdentity(identity, getUdsAuth) {
if (!isMultiUserIdentity(identity)) return identity
const uds = typeof getUdsAuth === 'function' ? getUdsAuth() : null
if (typeof uds?.resolveIdentityForEmpNo === 'function') {
const live = uds.resolveIdentityForEmpNo(identity.empNo)
if (live?.empNo) {
const canView = !!(
live.permissions?.canViewAllSessions
|| identity.permissions?.canViewAllSessions
|| isElevatedCronRole(live.role)
|| isElevatedCronRole(identity.role)
)
return {
...identity,
role: live.role || identity.role,
displayName: live.displayName || identity.displayName || identity.empNo,
permissions: {
...(identity.permissions || {}),
...(live.permissions || {}),
canViewAllSessions: canView,
canCreateWorkspace: !!(
live.permissions?.canCreateWorkspace
|| identity.permissions?.canCreateWorkspace
|| canView
),
},
workspacePath: live.workspacePath || identity.workspacePath || null,
}
}
}
if (isElevatedCronRole(identity.role)) {
return {
...identity,
permissions: {
...(identity.permissions || {}),
canViewAllSessions: true,
canCreateWorkspace: true,
},
}
}
return identity
}
function sanitizeJobCwd(cwd, identity, getUdsAuth, { forOwnerEmpNo } = {}) {
const raw = typeof cwd === 'string' ? cwd.trim() : ''
const uds = typeof getUdsAuth === 'function' ? getUdsAuth() : null
@ -183,7 +245,7 @@ function sanitizeJobCwd(cwd, identity, getUdsAuth, { forOwnerEmpNo } = {}) {
? uds.getProvisionedWorkspacePath(owner)
: null
if (!raw) return ownerPath || ''
if (canViewAllJobs(identity)) return raw
if (allowsForeignJobCwd(identity, getUdsAuth, owner)) return raw
// Without uds-auth, keep the caller cwd (tools may stamp owner from path only).
if (!uds) return raw
if (owner && uds?.isUserPath?.(owner, raw)) return raw
@ -350,9 +412,12 @@ export function createHostService(options = {}) {
safeInput.ownerDisplayName = ''
// Keep cwd as the bot workspace path; do not sanitize via empNo.
} else {
const ownerIdentity = isMultiUserIdentity(identity)
? identity
: inferIdentityFromJobInput(safeInput, getUdsAuth)
const ownerIdentity = enrichIdentity(
isMultiUserIdentity(identity)
? identity
: inferIdentityFromJobInput(safeInput, getUdsAuth),
getUdsAuth,
)
if (isMultiUserIdentity(ownerIdentity)) {
const delivery = normalizeDelivery(safeInput.delivery || { kind: 'dsh' })
assertDeliveryAllowedForIdentity(delivery, ownerIdentity)
@ -412,18 +477,19 @@ export function createHostService(options = {}) {
ownerDisplayName: job.ownerDisplayName || '',
}
if (patch._identity) {
const enriched = enrichIdentity(patch._identity, getUdsAuth)
if (patch.delivery !== undefined) {
nextInput.delivery = mergeDeliveryMention(job.delivery, patch.delivery)
assertDeliveryAllowedForIdentity(nextInput.delivery, patch._identity)
assertDeliveryAllowedForIdentity(nextInput.delivery, enriched)
}
nextInput.cwd = sanitizeJobCwd(
nextInput.cwd,
patch._identity,
enriched,
getUdsAuth,
{ forOwnerEmpNo: nextInput.ownerEmpNo },
)
}
if (patch.ownerEmpNo !== undefined && canViewAllJobs(patch._identity)) {
if (patch.ownerEmpNo !== undefined && canViewAllJobs(enrichIdentity(patch._identity, getUdsAuth))) {
nextInput.ownerEmpNo = normalizeOwnerEmpNo(patch.ownerEmpNo)
if (patch.ownerDisplayName !== undefined) {
nextInput.ownerDisplayName = String(patch.ownerDisplayName || '').trim().slice(0, 80)
@ -1004,16 +1070,18 @@ function sessionCwdOf(ctx, sessionId) {
/**
* True when the job owner may keep an explicit cwd outside their provisioned tree
* (super_admin / fallback_admin via canViewAllSessions or canCreateWorkspace).
* (super_admin / fallback_admin via canViewAllSessions, canCreateWorkspace, or role).
*/
export function ownerAllowsForeignCwd(ownerEmpNo, uds) {
const empNo = String(ownerEmpNo || '').trim()
if (!empNo || empNo.startsWith('__') || !uds) return false
if (typeof uds.resolveIdentityForEmpNo === 'function') {
const identity = uds.resolveIdentityForEmpNo(empNo)
if (canViewAllJobs(identity)) return true
return !!(
identity?.permissions?.canViewAllSessions
|| identity?.permissions?.canCreateWorkspace
|| isElevatedCronRole(identity?.role)
)
}
return false

1
lib/index.d.ts vendored
View file

@ -24,6 +24,7 @@ export function localIdentity(overrides?: object): object
export function claimUnassignedForViewer(state: object, identity: object, deps?: object): { state: object, changed: boolean }
export function jobVisibleToIdentity(job: object, identity: object): boolean
export function canViewAllJobs(identity: object): boolean
export function isElevatedCronRole(role?: string | null): boolean
export function assertCanAccessJob(job: object, identity: object): object
export function filterJobsForIdentity(jobs: object[], identity: object): object[]
export function migrateJobOwners(state: object, deps?: object): { state: object, changed: boolean }

View file

@ -54,6 +54,7 @@ export {
canViewAllJobs,
claimUnassignedForViewer,
filterJobsForIdentity,
isElevatedCronRole,
isMultiUserIdentity,
jobVisibleToIdentity,
LOCAL_EMP_NO,

View file

@ -32,9 +32,17 @@ export function localIdentity(overrides = {}) {
}
}
/** Roles that may keep foreign cwd and see every job (aligns with uds-auth). */
export function isElevatedCronRole(role) {
const value = String(role || '').trim()
return value === 'super_admin' || value === 'fallback_admin'
}
export function canViewAllJobs(identity) {
if (!isMultiUserIdentity(identity)) return true
return !!identity?.permissions?.canViewAllSessions
if (identity?.permissions?.canViewAllSessions) return true
// Defense: some callers stamp role without copying permissions.
return isElevatedCronRole(identity?.role)
}
export function jobVisibleToIdentity(job, identity) {
@ -141,14 +149,18 @@ export function viewerPayload(identity) {
/**
* Default cwd when creating a job in the sidebar.
* Prefer the viewer's provisioned workspace over whatever project the session
* happens to have open (admins often sit in an unrelated clone).
* - super_admin / fallback_admin: prefer the currently open session workspace
* (they often work in a shared tree like D:\\code\\gpt, not their empty provisioned dir).
* - ordinary multi-user: prefer provisioned path so a random open clone is not stamped.
*/
export function preferredNewJobCwd({ viewer = null, sessionCwd = '' } = {}) {
const session = String(sessionCwd || '').trim()
const provisioned = String(viewer?.workspacePath || '').trim()
if (viewer?.canViewAll && session) return session
if (isElevatedCronRole(viewer?.role) && session) return session
if (viewer?.mode === 'multi' && provisioned) return provisioned
if (provisioned) return provisioned
return String(sessionCwd || '').trim()
return session
}
/**

View file

@ -174,13 +174,14 @@ export function resolveToolIdentity(exec, service) {
if (typeof uds?.resolveIdentityForEmpNo === 'function') {
const resolved = uds.resolveIdentityForEmpNo(id)
if (resolved?.empNo) {
const elevated = resolved.role === 'super_admin' || resolved.role === 'fallback_admin'
return {
empNo: String(resolved.empNo),
displayName: String(resolved.displayName || resolved.empNo),
role: resolved.role || 'user',
permissions: {
canViewAllSessions: !!resolved.permissions?.canViewAllSessions,
canCreateWorkspace: !!resolved.permissions?.canCreateWorkspace,
canViewAllSessions: !!(resolved.permissions?.canViewAllSessions || elevated),
canCreateWorkspace: !!(resolved.permissions?.canCreateWorkspace || elevated),
},
workspacePath: resolved.workspacePath || uds.getProvisionedWorkspacePath?.(id) || null,
}