Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions ci/e2e-config-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,74 @@ else
exit 1
fi

echo ""
echo "=== Verify parent directory ownership ==="
DIR_OWNERSHIP_OK=true
DIR_CHECKED=0

# Collect unique parent directories (up to but not including HOME)
PARENT_DIRS_FILE="$(mktemp)"
for f in "${CREATED_FILES[@]}"; do
dir="$(dirname "$f")"
while [ "$dir" != "$HOME" ] && [ "$dir" != "/" ] && [ ${#dir} -gt ${#HOME} ]; do
echo "$dir"
dir="$(dirname "$dir")"
done
done | sort -u > "$PARENT_DIRS_FILE"

case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*)
echo "Expected owner: $EXPECTED_OWNER"
while IFS= read -r d; do
WIN_D=$(cygpath -w "$d")
ACTUAL_OWNER=$(powershell.exe -NoProfile -NonInteractive -Command \
"(Get-Acl -LiteralPath '${WIN_D}').Owner" | tr -d '\r')
DIR_CHECKED=$((DIR_CHECKED + 1))
if [ "$ACTUAL_OWNER" = "$EXPECTED_OWNER" ]; then
echo " OK: $d"
else
echo " FAIL: $d (expected: $EXPECTED_OWNER, actual: $ACTUAL_OWNER)"
DIR_OWNERSHIP_OK=false
fi
done < "$PARENT_DIRS_FILE"
;;
Darwin)
echo "Expected owner: $EXPECTED_OWNER"
while IFS= read -r d; do
ACTUAL_OWNER=$(stat -f '%Su' "$d")
DIR_CHECKED=$((DIR_CHECKED + 1))
if [ "$ACTUAL_OWNER" = "$EXPECTED_OWNER" ]; then
echo " OK: $d"
else
echo " FAIL: $d (expected: $EXPECTED_OWNER, actual: $ACTUAL_OWNER)"
DIR_OWNERSHIP_OK=false
fi
done < "$PARENT_DIRS_FILE"
;;
Linux)
echo "Expected owner: $EXPECTED_OWNER"
while IFS= read -r d; do
ACTUAL_OWNER=$(stat -c '%U' "$d")
DIR_CHECKED=$((DIR_CHECKED + 1))
if [ "$ACTUAL_OWNER" = "$EXPECTED_OWNER" ]; then
echo " OK: $d"
else
echo " FAIL: $d (expected: $EXPECTED_OWNER, actual: $ACTUAL_OWNER)"
DIR_OWNERSHIP_OK=false
fi
done < "$PARENT_DIRS_FILE"
;;
esac

rm -f "$PARENT_DIRS_FILE"

if [ "$DIR_OWNERSHIP_OK" = true ]; then
echo "PASS [dir-ownership]: All $DIR_CHECKED parent dir(s) owned by $EXPECTED_OWNER"
else
echo "FAIL [dir-ownership]: Some parent directories have incorrect ownership"
exit 1
fi

echo ""
echo "=== Compute Run 1 checksums ==="
# Deduplicate paths (e.g. cursor and cursor-agent share the same file)
Expand Down
45 changes: 28 additions & 17 deletions src/hosts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,23 +64,23 @@ function resolveProfileOwner(homeDir: string): string | null {
}
}

function setOwnerWindows(filePath: string, owner: string): void {
function setOwnerWindowsBatch(paths: string[], owner: string): void {
if (paths.length === 0) return
const escapedOwner = owner.replace(/'/g, "''")
const pathsList = paths.map((p) => `'${p.replace(/'/g, "''")}'`).join(',')
try {
execFileSync('icacls', [filePath, '/setowner', owner], {
stdio: 'pipe',
timeout: 30_000,
})
execFileSync(
'powershell.exe',
[
'-NoProfile',
'-NonInteractive',
'-Command',
`[Console]::OutputEncoding = [Text.Encoding]::UTF8; $o = [System.Security.Principal.NTAccount]'${escapedOwner}'; @(${pathsList}) | ForEach-Object { $p = $_; try { $a = Get-Acl -LiteralPath $p; $a.SetOwner($o); Set-Acl -LiteralPath $p -AclObject $a } catch { Write-Warning "Failed to set owner on $p : $_" } }`,
],
{ encoding: 'utf-8', stdio: 'pipe', timeout: 60_000 },
)
} catch (err) {
log.warn(`Failed to set owner of ${filePath} to ${owner}: ${err}`)
}
}

function setOwnerWindowsAncestors(filePath: string, stopAt: string, owner: string): void {
const stopDir = resolve(stopAt)
let dir = dirname(resolve(filePath))
while (dir.length > stopDir.length && dir.startsWith(stopDir)) {
setOwnerWindows(dir, owner)
dir = dirname(dir)
log.warn(`Failed to batch-set ownership to ${owner}: ${err}`)
}
}

Expand Down Expand Up @@ -112,6 +112,7 @@ export function configureHosts(options: ConfigureHostsOptions): ConfigureResult[
// creates with the correct principal.
const windowsOwner =
currentPlatform === 'win32' ? resolveProfileOwner(userHomeDir) ?? username : undefined
const windowsOwnerPaths: string[] = []

for (const client of clients) {
const configPath = client.configPath[currentPlatform]
Expand Down Expand Up @@ -160,9 +161,15 @@ export function configureHosts(options: ConfigureHostsOptions): ConfigureResult[

if (currentPlatform === 'win32' && windowsOwner) {
if (!lstatSync(resolvedPath).isSymbolicLink()) {
setOwnerWindows(resolvedPath, windowsOwner)
windowsOwnerPaths.push(resolvedPath)
}
// Collect ancestor directories up to (but not including) the home dir
const stopDir = resolve(userHomeDir)
let dir = dirname(resolve(resolvedPath))
while (dir.length > stopDir.length && dir.startsWith(stopDir)) {
windowsOwnerPaths.push(dir)
dir = dirname(dir)
}
setOwnerWindowsAncestors(resolvedPath, userHomeDir, windowsOwner)
} else if (uid !== undefined && gid !== undefined) {
if (!lstatSync(resolvedPath).isSymbolicLink()) {
chownSync(resolvedPath, uid, gid)
Expand All @@ -178,5 +185,9 @@ export function configureHosts(options: ConfigureHostsOptions): ConfigureResult[
}
}

if (windowsOwner && windowsOwnerPaths.length > 0) {
setOwnerWindowsBatch([...new Set(windowsOwnerPaths)], windowsOwner)
}

return results
}