Skip to content

fix(credentials): restore Windows ACL inheritance to fix EPERM on read - #301

Open
aythin wants to merge 2 commits into
TestSprite:mainfrom
aythin:fix/windows-credentials-acl-eperm
Open

fix(credentials): restore Windows ACL inheritance to fix EPERM on read#301
aythin wants to merge 2 commits into
TestSprite:mainfrom
aythin:fix/windows-credentials-acl-eperm

Conversation

@aythin

@aythin aythin commented Aug 3, 2026

Copy link
Copy Markdown

What does this PR do?

ensureWindowsRestrictiveAcl previously called:

icacls credentials /inheritance:r /grant:r USERNAME:F

/inheritance:r strips all inherited ACEs and leaves the file protected solely by the USERNAME env-var grant. On Windows with Microsoft Accounts or domain-joined machines, USERNAME may not resolve to the same SID that owns the file — the resulting ACL locks out the file owner, causing every subsequent credential read to fail with EPERM: operation not permitted.

Fix: call icacls /reset first (re-enables inheritance from the parent directory), then /grant:r USERNAME:F as an explicit belt-and-suspenders Full Control entry.

Verified on Windows 11 Pro:

  • testsprite setup --from-env --yes + testsprite doctor previously failed immediately with EPERM
  • After this fix, testsprite doctor reports all checks passed ✅

Related issue

None — small bug fix, no issue required per CONTRIBUTING.md.

Type of change

  • Bug fix (non-breaking change that fixes an issue)

Checklist

  • PR targets the main branch.
  • Commits follow Conventional Commits (fix(credentials): ...).
  • npm run lint and npm run format:check pass.
  • npm run typecheck passes.
  • npm test passes and coverage stays at or above the 80% gate.
  • New behavior is covered by unit tests (updated credentials.test.ts to expect two icacls calls).
  • No secrets, API keys, internal endpoints, or personal data are included.
  • User-facing changes are reflected in README.md / DOCUMENTATION.md where relevant. (N/A — internal Windows ACL fix)

Notes for reviewers

Only two files changed: src/lib/credentials.ts (the fix) and src/lib/credentials.test.ts (updated test assertions to match the new two-call sequence: /reset then /grant:r).

Summary by CodeRabbit

  • Bug Fixes

    • Improved Windows credential file permission handling by resetting inherited permissions before granting the current user full control.
    • Strengthened ACL configuration to help prevent unintended access to stored credentials.
  • Tests

    • Updated Windows permission checks to verify the revised command sequence and secure execution options.

yaxin.liu and others added 2 commits August 3, 2026 17:22
The previous icacls call used /inheritance:r which strips all inherited
ACEs and relies solely on the USERNAME env grant. On Windows, USERNAME
may not resolve to the same SID that owns the file (e.g. Microsoft
Account or domain account mismatches), leaving the file unreadable by
anyone including the file owner.

Fix: call icacls /reset first to re-enable inherited permissions from
the parent directory, then add an explicit /grant:r entry as
belt-and-suspenders full-control grant.

Verified on Windows 11 Pro: testsprite doctor passes after fix.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Thanks for the PR, @aythin! A quick note on our workflow: for features and behavior changes we require contributors to open an issue first, claim it by commenting /assign on the issue, then submit a PR that links it (e.g. Closes #123). This PR isn't linked to any issue yet, so it is not review-ready. After fixing it, edit the PR description or push a commit to re-run this check. See CONTRIBUTING → Contribution model.

@github-actions github-actions Bot added the needs-issue PR not linked to an issue yet — please open one first and claim it (see CONTRIBUTING) label Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Changes

Windows ACL hardening

Layer / File(s) Summary
Reset and grant ACL permissions
src/lib/credentials.ts, src/lib/credentials.test.ts
Windows ACL handling now runs icacls /reset, then icacls /grant:r <username>:F. The test verifies call order, secure spawn options, arguments, and the two-call count.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: ruili-testsprite, zeshi-du

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the Windows ACL inheritance change and its purpose of preventing EPERM errors during credential reads.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/credentials.ts`:
- Around line 242-246: The ACL update flow around run('icacls', [path,
'/reset']) must validate the reset before issuing /grant:r. Capture the /reset
result, handle both an execution error and a non-zero status by reporting the
failure and stopping the ACL transition, and add tests covering each failure
form.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6de32c2f-8823-409f-9734-67e69f459548

📥 Commits

Reviewing files that changed from the base of the PR and between fe07bc9 and 4010631.

📒 Files selected for processing (2)
  • src/lib/credentials.test.ts
  • src/lib/credentials.ts

Comment thread src/lib/credentials.ts
Comment on lines +242 to +246
run('icacls', [path, '/reset'], {
shell: false,
stdio: 'ignore',
windowsHide: true,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)credentials\.ts$|DOCUMENTATION\.md$|samples\.ts$|test|spec' || true

echo "== credentials outline =="
ast-grep outline src/lib/credentials.ts --view expanded || true

echo "== relevant lines 200-270 =="
sed -n '200,270p' src/lib/credentials.ts | nl -ba -v200

echo "== icacls usages =="
rg -n "icacls|/reset|grant:r|windowsHide" src/lib/credentials.ts src test tests . --glob '!node_modules' --glob '!dist' --glob '!build' || true

Repository: TestSprite/testsprite-cli

Length of output: 5182


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== credentials.ts lines 200-270 =="
sed -n '200,270p' src/lib/credentials.ts > /tmp/cred_slice.txt || true
awk '{printf "%6d\t%s\n",$1,$0}' /tmp/cred_slice.txt

echo "== credentials tests around windows acl =="
rg -n "ensureRestrictiveMode|Windows|icacls|reset|grant:r|restrictiveMode|acl" src/lib/credentials.test.ts src/lib/credentials.ts test --glob '!node_modules' || true

echo "== credentials tests surrounding ensureWindowsRestrictiveAcl/reset/grant =="
rg -n -C 8 "ensureRestrictiveMode|Windows|icacls|reset|grant:r|restrictiveMode|acl" src/lib/credentials.test.ts || true

Repository: TestSprite/testsprite-cli

Length of output: 11992


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== mock parent ACL references =="
git ls-files | rg '(^|/)parent(-acl|-mode|-permission).*|acl.*parent|ensureWindowsRestrictiveAcl|samples\.ts' || true

echo "== read-only behavior probe for current code path =="
python3 - <<'PY'
import ast, pathlib

text = pathlib.Path('src/lib/credentials.ts').read_text()
tree = ast.parse(text)
# Minimal read-only AST scan: identify icacls run calls and whether first call result is inspected.
for node in ast.walk(tree):
    if isinstance(node, ast.FunctionDef) and node.name == 'ensureWindowsRestrictiveAcl':
        calls = [n for n in ast.walk(node) if isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id == 'run']
        first = calls[0] if calls else None
        print(f"ensureWindowsRestrictiveAcl called {len(calls)} times")
        for i, call in enumerate(calls, 1):
            print(f"  call {i} args:", [str(a) for a in call.args])
        print("  assigns result:", [str(n.targets[0].value) for n in ast.walk(node) if isinstance(n, ast.Assign) and isinstance(n.value, ast.Call) and isinstance(n.value.func, ast.Name) and n.value.func.id == 'run'])
        print("  result inspected before grant?",
              [str(n.test) for n in ast.walk(node) if isinstance(n, ast.If) and isinstance(n.test, ast.Attribute) and n.test.value.id == 'result'])

# Node semantics probe: spawnSync-like result fields are available on returned object;
# omitting assignment means /reset error/status are never read by this function.
class Result:
    error = RuntimeError("icacls not found")
    status = 1606
result = Result()
print("Node-like result fields:", result.error, result.status)
PY

Repository: TestSprite/testsprite-cli

Length of output: 536


Handle /reset failures before issuing /grant:r.

/reset may leave the credentials file with inherited ACLs, including broader directory permissions. If /reset fails and /grant:r succeeds, the current code only checks the grant result, so it does not report the failed reset or avoid issuing the wrong ACL transition. Capture the /reset result, handle error and non-zero status, and add tests for both failure forms.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/credentials.ts` around lines 242 - 246, The ACL update flow around
run('icacls', [path, '/reset']) must validate the reset before issuing /grant:r.
Capture the /reset result, handle both an execution error and a non-zero status
by reporting the failure and stopping the ACL transition, and add tests covering
each failure form.

Source: Path instructions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-issue PR not linked to an issue yet — please open one first and claim it (see CONTRIBUTING)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant