返回 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 // Lock identity must be available before ACL repair, without reading the
106 // protected contents or silently lifting the deny as part of resolution.
107 if _, err := resolveConfigAccessPathUnpinned(path, true); err != nil {
108 t.Fatalf("resolve identity before credential ACL repair: %v", err)
109 }
110 if _, err := os.ReadFile(path); err == nil {
111 t.Fatal("identity resolution removed the credential read deny")
112 }
113 markerDir := filepath.Join(os.TempDir(), "windows-sandbox-denylocks")
114 if err := os.MkdirAll(markerDir, 0o700); err != nil {
115 t.Fatal(err)
116 }
117 marker := filepath.Join(markerDir, strconv.Itoa(os.Getpid())+"-credential-test.txt")
118 if err := os.WriteFile(marker, []byte("deny\t"+path+"\n"), 0o600); err != nil {
119 t.Fatal(err)
120 }
121 switch operation {
122 case "load":
123 if file, ok := readDotEnvFile(path); !ok || file.Values["EXISTING_KEY"] != "old" {
124 t.Fatal("credential load failed to repair legacy deny")
125 }
126 case "revision":
127 if revision := CredentialStoreRevision(); !strings.HasPrefix(revision, "sha256:") {
128 t.Fatalf("credential revision after legacy deny = %s", revision)
129 }
130 }
131
132 if _, err := SetCredential("ACL_REPAIR_INTEGRATION_KEY", "new"); err != nil {
133 t.Fatalf("SetCredential after legacy deny ACL: %v", err)
134 }
135 data, err := os.ReadFile(path)
136 if err != nil {
137 t.Fatalf("read repaired credential file: %v", err)
138 }
139 contents := string(data)
140 if !strings.Contains(contents, "EXISTING_KEY=old") || !strings.Contains(contents, "ACL_REPAIR_INTEGRATION_KEY=new") {
141 t.Fatalf("credential contents after repair = %q", contents)
142 }
143 }
144
145 func TestReadableCredentialsDoNotWaitForSandboxLock(t *testing.T) {
146 home := t.TempDir()
147 t.Setenv("REASONIX_HOME", home)
148 path := UserCredentialsPath()
149 if err := os.WriteFile(path, []byte("KEY=value\n"), 0o600); err != nil {
150 t.Fatal(err)
151 }
152 // Hold one of the sandbox's ancestor mutexes on this thread while another
153 // goroutine exercises each credential reader. The lock remains held until
154 // all readers finish, so completion cannot depend on a scheduling race.
155 digest := sha1.Sum([]byte(strings.ToLower(filepath.Clean(home))))
156 name, err := windows.UTF16PtrFromString(fmt.Sprintf(`Local\windows-sandbox.%x`, digest[:16]))
157 if err != nil {
158 t.Fatal(err)
159 }
160 runtime.LockOSThread()
161 defer runtime.UnlockOSThread()
162 mutex, err := windows.CreateMutex(nil, true, name)
163 if err != nil {
164 t.Fatal(err)
165 }
166 defer windows.CloseHandle(mutex)
167 defer windows.ReleaseMutex(mutex)
168 done := make(chan error, 1)
169 go func() {
170 if revision := CredentialStoreRevision(); !strings.HasPrefix(revision, "sha256:") {
171 done <- fmt.Errorf("readable store revision = %s", revision)
172 return
173 }
174 if _, ok := readDotEnvFile(path); !ok {
175 done <- fmt.Errorf("readDotEnvFile failed while unrelated sandbox lock was held")
176 return
177 }
178 _, err := readCredentialFileLines(path)
179 done <- err
180 }()
181 select {
182 case err := <-done:
183 if err != nil {
184 t.Fatal(err)
185 }
186 case <-time.After(5 * time.Second):
187 t.Fatal("readable credentials waited for an unrelated sandbox lock")
188 }
189 }
190
191 // A deny with no sandbox record cannot be attributed to Reasonix, so the
192 // provenance-checked repair refuses; an explicit save must still succeed by
193 // resetting the ACL without reading it and keeping the stored values.
194 func TestCredentialSaveResetsDenyWithoutSandboxRecord(t *testing.T) {
195 tmp := t.TempDir()
196 t.Setenv("TMP", tmp)
197 t.Setenv("TEMP", tmp)
198 t.Setenv("REASONIX_HOME", t.TempDir())
199 t.Setenv("ACL_RESET_INTEGRATION_KEY", "")
200 path := UserCredentialsPath()
201 if err := os.WriteFile(path, []byte("EXISTING_KEY=old\n"), 0o600); err != nil {
202 t.Fatal(err)
203 }
204 user, err := windows.GetCurrentProcessToken().GetTokenUser()
205 if err != nil {
206 t.Fatal(err)
207 }
208 trustee := "*" + user.User.Sid.String()
209 if output, err := exec.Command("icacls", path, "/deny", trustee+":(RX)").CombinedOutput(); err != nil {
210 t.Fatalf("install deny ACL: %v: %s", err, strings.TrimSpace(string(output)))
211 }
212 t.Cleanup(func() { _ = exec.Command("icacls", path, "/remove:d", trustee, "/C").Run() })
213 if _, err := os.ReadFile(path); err == nil {
214 t.Fatal("deny ACL did not block credential reads")
215 }
216 if _, err := SetCredential("ACL_RESET_INTEGRATION_KEY", "new"); err != nil {
217 t.Fatalf("SetCredential with an unattributed deny: %v", err)
218 }
219 data, err := os.ReadFile(path)
220 if err != nil {
221 t.Fatalf("read reset credential file: %v", err)
222 }
223 if got := string(data); !strings.Contains(got, "EXISTING_KEY=old") || !strings.Contains(got, "ACL_RESET_INTEGRATION_KEY=new") {
224 t.Fatalf("credential contents after reset = %q", got)
225 }
226 if quarantined, _ := filepath.Glob(path + ".locked-*"); len(quarantined) != 0 {
227 t.Fatalf("reset path must not quarantine the store: %v", quarantined)
228 }
229 }
230
230 lines GO