返回 DeepSeek-Reasonix
credential_acl_repair_windows_test.go
根目录 / internal / config / credential_acl_repair_windows_test.go
1 //go:build windows
2
3 package config
4
5 import (
6 "crypto/sha1"
7 "fmt"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "runtime"
12 "strconv"
13 "strings"
14 "testing"
15 "time"
16
17 "golang.org/x/sys/windows"
18 )
19
20 func TestCredentialAccessRepairsLegacyCredentialDeny(t *testing.T) {
21 for _, operation := range []string{"save", "load", "revision"} {
22 t.Run(operation, func(t *testing.T) {
23 testCredentialAccessRepairsLegacyDeny(t, operation)
24 })
25 }
26 }
27
28 func TestCredentialAccessRepairsLegacyDenyThroughLinkedHome(t *testing.T) {
29 tmp := t.TempDir()
30 t.Setenv("TMP", tmp)
31 t.Setenv("TEMP", tmp)
32 realHome := t.TempDir()
33 linkedHome := filepath.Join(t.TempDir(), "reasonix-home")
34 if err := os.Symlink(realHome, linkedHome); err != nil {
35 t.Skipf("directory symlinks unavailable: %v", err)
36 }
37 t.Setenv("REASONIX_HOME", linkedHome)
38 path := UserCredentialsPath()
39 if err := os.WriteFile(path, []byte("EXISTING_KEY=old\n"), 0o600); err != nil {
40 t.Fatal(err)
41 }
42 realPath, err := filepath.EvalSymlinks(path)
43 if err != nil {
44 t.Fatal(err)
45 }
46 user, err := windows.GetCurrentProcessToken().GetTokenUser()
47 if err != nil {
48 t.Fatal(err)
49 }
50 if user == nil || user.User.Sid == nil {
51 t.Fatal("current process token has no user SID")
52 }
53 trustee := "*" + user.User.Sid.String()
54 if output, err := exec.Command("icacls", realPath, "/deny", trustee+":(RX)").CombinedOutput(); err != nil {
55 t.Fatalf("install legacy credential deny ACL: %v: %s", err, strings.TrimSpace(string(output)))
56 }
57 t.Cleanup(func() {
58 _ = exec.Command("icacls", realPath, "/remove:d", trustee, "/C").Run()
59 })
60 markerDir := filepath.Join(os.TempDir(), "windows-sandbox-denylocks")
61 if err := os.MkdirAll(markerDir, 0o700); err != nil {
62 t.Fatal(err)
63 }
64 marker := filepath.Join(markerDir, strconv.Itoa(os.Getpid())+"-credential-linked-home-test.txt")
65 if err := os.WriteFile(marker, []byte("deny\t"+realPath+"\n"), 0o600); err != nil {
66 t.Fatal(err)
67 }
68
69 file, ok := readDotEnvFile(path)
70 if !ok || file.Values["EXISTING_KEY"] != "old" {
71 t.Fatal("credential load through linked home failed to repair legacy deny")
72 }
73 }
74
75 func testCredentialAccessRepairsLegacyDeny(t *testing.T, operation string) {
76 t.Helper()
77 tmp := t.TempDir()
78 t.Setenv("TMP", tmp)
79 t.Setenv("TEMP", tmp)
80 home := t.TempDir()
81 t.Setenv("REASONIX_HOME", home)
82 t.Setenv("ACL_REPAIR_INTEGRATION_KEY", "")
83 path := UserCredentialsPath()
84 if err := os.WriteFile(path, []byte("EXISTING_KEY=old\n"), 0o600); err != nil {
85 t.Fatal(err)
86 }
87
88 user, err := windows.GetCurrentProcessToken().GetTokenUser()
89 if err != nil {
90 t.Fatal(err)
91 }
92 if user == nil || user.User.Sid == nil {
93 t.Fatal("current process token has no user SID")
94 }
95 trustee := "*" + user.User.Sid.String()
96 if output, err := exec.Command("icacls", path, "/deny", trustee+":(RX)").CombinedOutput(); err != nil {
97 t.Fatalf("install legacy credential deny ACL: %v: %s", err, strings.TrimSpace(string(output)))
98 }
99 t.Cleanup(func() {
100 _ = exec.Command("icacls", path, "/remove:d", trustee, "/C").Run()
101 })
102 if _, err := os.ReadFile(path); err == nil {
103 t.Fatal("legacy deny ACL did not block credential reads")
104 }
105 markerDir := filepath.Join(os.TempDir(), "windows-sandbox-denylocks")
106 if err := os.MkdirAll(markerDir, 0o700); err != nil {
107 t.Fatal(err)
108 }
109 marker := filepath.Join(markerDir, strconv.Itoa(os.Getpid())+"-credential-test.txt")
110 if err := os.WriteFile(marker, []byte("deny\t"+path+"\n"), 0o600); err != nil {
111 t.Fatal(err)
112 }
113 switch operation {
114 case "load":
115 if file, ok := readDotEnvFile(path); !ok || file.Values["EXISTING_KEY"] != "old" {
116 t.Fatal("credential load failed to repair legacy deny")
117 }
118 case "revision":
119 if revision := CredentialStoreRevision(); !strings.HasPrefix(revision, "sha256:") {
120 t.Fatalf("credential revision after legacy deny = %s", revision)
121 }
122 }
123
124 if _, err := SetCredential("ACL_REPAIR_INTEGRATION_KEY", "new"); err != nil {
125 t.Fatalf("SetCredential after legacy deny ACL: %v", err)
126 }
127 data, err := os.ReadFile(path)
128 if err != nil {
129 t.Fatalf("read repaired credential file: %v", err)
130 }
131 contents := string(data)
132 if !strings.Contains(contents, "EXISTING_KEY=old") || !strings.Contains(contents, "ACL_REPAIR_INTEGRATION_KEY=new") {
133 t.Fatalf("credential contents after repair = %q", contents)
134 }
135 }
136
137 func TestReadableCredentialsDoNotWaitForSandboxLock(t *testing.T) {
138 home := t.TempDir()
139 t.Setenv("REASONIX_HOME", home)
140 path := UserCredentialsPath()
141 if err := os.WriteFile(path, []byte("KEY=value\n"), 0o600); err != nil {
142 t.Fatal(err)
143 }
144 // Hold one of the sandbox's ancestor mutexes on this thread while another
145 // goroutine exercises each credential reader. The lock remains held until
146 // all readers finish, so completion cannot depend on a scheduling race.
147 digest := sha1.Sum([]byte(strings.ToLower(filepath.Clean(home))))
148 name, err := windows.UTF16PtrFromString(fmt.Sprintf(`Local\windows-sandbox.%x`, digest[:16]))
149 if err != nil {
150 t.Fatal(err)
151 }
152 runtime.LockOSThread()
153 defer runtime.UnlockOSThread()
154 mutex, err := windows.CreateMutex(nil, true, name)
155 if err != nil {
156 t.Fatal(err)
157 }
158 defer windows.CloseHandle(mutex)
159 defer windows.ReleaseMutex(mutex)
160 done := make(chan error, 1)
161 go func() {
162 if revision := CredentialStoreRevision(); !strings.HasPrefix(revision, "sha256:") {
163 done <- fmt.Errorf("readable store revision = %s", revision)
164 return
165 }
166 if _, ok := readDotEnvFile(path); !ok {
167 done <- fmt.Errorf("readDotEnvFile failed while unrelated sandbox lock was held")
168 return
169 }
170 _, err := readCredentialFileLines(path)
171 done <- err
172 }()
173 select {
174 case err := <-done:
175 if err != nil {
176 t.Fatal(err)
177 }
178 case <-time.After(5 * time.Second):
179 t.Fatal("readable credentials waited for an unrelated sandbox lock")
180 }
181 }
182
183 // A deny with no sandbox record cannot be attributed to Reasonix, so the
184 // provenance-checked repair refuses; an explicit save must still succeed by
185 // resetting the ACL without reading it and keeping the stored values.
186 func TestCredentialSaveResetsDenyWithoutSandboxRecord(t *testing.T) {
187 tmp := t.TempDir()
188 t.Setenv("TMP", tmp)
189 t.Setenv("TEMP", tmp)
190 t.Setenv("REASONIX_HOME", t.TempDir())
191 t.Setenv("ACL_RESET_INTEGRATION_KEY", "")
192 path := UserCredentialsPath()
193 if err := os.WriteFile(path, []byte("EXISTING_KEY=old\n"), 0o600); err != nil {
194 t.Fatal(err)
195 }
196 user, err := windows.GetCurrentProcessToken().GetTokenUser()
197 if err != nil {
198 t.Fatal(err)
199 }
200 trustee := "*" + user.User.Sid.String()
201 if output, err := exec.Command("icacls", path, "/deny", trustee+":(RX)").CombinedOutput(); err != nil {
202 t.Fatalf("install deny ACL: %v: %s", err, strings.TrimSpace(string(output)))
203 }
204 t.Cleanup(func() { _ = exec.Command("icacls", path, "/remove:d", trustee, "/C").Run() })
205 if _, err := os.ReadFile(path); err == nil {
206 t.Fatal("deny ACL did not block credential reads")
207 }
208 if _, err := SetCredential("ACL_RESET_INTEGRATION_KEY", "new"); err != nil {
209 t.Fatalf("SetCredential with an unattributed deny: %v", err)
210 }
211 data, err := os.ReadFile(path)
212 if err != nil {
213 t.Fatalf("read reset credential file: %v", err)
214 }
215 if got := string(data); !strings.Contains(got, "EXISTING_KEY=old") || !strings.Contains(got, "ACL_RESET_INTEGRATION_KEY=new") {
216 t.Fatalf("credential contents after reset = %q", got)
217 }
218 if quarantined, _ := filepath.Glob(path + ".locked-*"); len(quarantined) != 0 {
219 t.Fatalf("reset path must not quarantine the store: %v", quarantined)
220 }
221 }
222
222 lines GO