返回 DeepSeek-Reasonix
credential_windows_test.go
根目录 / internal / winaclresidue / credential_windows_test.go
1 //go:build windows
2
3 package winaclresidue
4
5 import (
6 "fmt"
7 "os"
8 "path/filepath"
9 "runtime"
10 "strconv"
11 "strings"
12 "testing"
13 "unsafe"
14
15 "golang.org/x/sys/windows"
16 )
17
18 func installLegacyDeny(t *testing.T, path, userSID string) {
19 t.Helper()
20 if err := icacls(path, "/deny", "*"+userSID+":(RX)"); err != nil {
21 t.Fatal(err)
22 }
23 t.Cleanup(func() { _ = icacls(path, "/remove:d", "*"+userSID, "/C") })
24 }
25
26 // writeMarker records a deny for path under a marker owned by pid. Our own PID
27 // stands for a crashed predecessor after PID reuse; the parent test runner's
28 // PID stands for a live owner.
29 func writeMarker(t *testing.T, pid int, path string) string {
30 t.Helper()
31 if err := os.MkdirAll(markerDir(), 0o700); err != nil {
32 t.Fatal(err)
33 }
34 marker := filepath.Join(markerDir(), strconv.Itoa(pid)+"-credential-test.txt")
35 if err := os.WriteFile(marker, []byte("deny\t"+path+"\n"), 0o600); err != nil {
36 t.Fatal(err)
37 }
38 return marker
39 }
40
41 func TestRepairLegacyCredentialDenyRemovesExactCurrentUserACE(t *testing.T) {
42 t.Setenv("TEMP", t.TempDir())
43 path := filepath.Join(t.TempDir(), ".env")
44 if err := os.WriteFile(path, []byte("KEY=value\n"), 0o600); err != nil {
45 t.Fatal(err)
46 }
47 userSID, err := currentProcessUserSIDString()
48 if err != nil {
49 t.Fatal(err)
50 }
51 installLegacyDeny(t, path, userSID)
52 legacy, other, err := currentUserDenyACECounts(path, userSID)
53 if err != nil || legacy != 1 || other != 0 {
54 t.Fatalf("deny counts before repair = legacy:%d other:%d err:%v", legacy, other, err)
55 }
56 marker := writeMarker(t, os.Getpid(), path)
57
58 if err := RepairLegacyCredentialDeny(path); err != nil {
59 t.Fatal(err)
60 }
61 legacy, other, err = currentUserDenyACECounts(path, userSID)
62 if err != nil || legacy != 0 || other != 0 {
63 t.Fatalf("deny counts after repair = legacy:%d other:%d err:%v", legacy, other, err)
64 }
65 if data, err := os.ReadFile(path); err != nil || string(data) != "KEY=value\n" {
66 t.Fatalf("credential after repair = %q, %v", data, err)
67 }
68 if _, err := os.Stat(marker); !os.IsNotExist(err) {
69 t.Fatalf("stale marker survived repair: %v", err)
70 }
71 installLegacyDeny(t, path, userSID)
72 if err := RepairLegacyCredentialDeny(path); err == nil {
73 t.Fatal("repair reused a consumed stale marker")
74 }
75 }
76
77 func TestRepairLegacyCredentialDenyMatchesFileAcrossPathAliases(t *testing.T) {
78 tmp := t.TempDir()
79 t.Setenv("TMP", tmp)
80 t.Setenv("TEMP", tmp)
81 path := filepath.Join(t.TempDir(), ".env")
82 if err := os.WriteFile(path, []byte("KEY=value\n"), 0o600); err != nil {
83 t.Fatal(err)
84 }
85 pathUTF16, err := windows.UTF16PtrFromString(path)
86 if err != nil {
87 t.Fatal(err)
88 }
89 shortBuffer := make([]uint16, 32768)
90 n, err := windows.GetShortPathName(pathUTF16, &shortBuffer[0], uint32(len(shortBuffer)))
91 if err != nil || n == 0 || n >= uint32(len(shortBuffer)) {
92 t.Skipf("8.3 path aliases unavailable: %v", err)
93 }
94 alias := windows.UTF16ToString(shortBuffer[:n])
95 if strings.EqualFold(filepath.Clean(alias), filepath.Clean(path)) {
96 t.Skip("fixture path has no distinct 8.3 alias")
97 }
98 userSID, err := currentProcessUserSIDString()
99 if err != nil {
100 t.Fatal(err)
101 }
102 installLegacyDeny(t, path, userSID)
103 writeMarker(t, os.Getpid(), alias)
104 if err := RepairLegacyCredentialDeny(path); err != nil {
105 t.Fatal(err)
106 }
107 if data, err := os.ReadFile(path); err != nil || string(data) != "KEY=value\n" {
108 t.Fatalf("credential after aliased repair = %q, %v", data, err)
109 }
110 }
111
112 func TestRepairLegacyCredentialDenyPreservesUnattributedAndLiveACL(t *testing.T) {
113 for _, source := range []string{"missing marker", "live marker", "wrong path"} {
114 t.Run(source, func(t *testing.T) {
115 t.Setenv("TEMP", t.TempDir())
116 path := filepath.Join(t.TempDir(), ".env")
117 if err := os.WriteFile(path, []byte("KEY=value\n"), 0o600); err != nil {
118 t.Fatal(err)
119 }
120 userSID, err := currentProcessUserSIDString()
121 if err != nil {
122 t.Fatal(err)
123 }
124 installLegacyDeny(t, path, userSID)
125 switch source {
126 case "live marker":
127 writeMarker(t, os.Getppid(), path)
128 case "wrong path":
129 writeMarker(t, os.Getpid(), path+"-other")
130 }
131 before, err := pathDACLSDDL(path)
132 if err != nil {
133 t.Fatal(err)
134 }
135 if err := RepairLegacyCredentialDeny(path); err == nil {
136 t.Fatal("repair accepted an unattributed or live deny")
137 }
138 after, err := pathDACLSDDL(path)
139 if err != nil || after != before {
140 t.Fatalf("DACL changed: before %s, after %s, err %v", before, after, err)
141 }
142 })
143 }
144 }
145
146 func TestRepairLegacyCredentialDenyLeavesOrdinaryACLAlone(t *testing.T) {
147 path := filepath.Join(t.TempDir(), ".env")
148 if err := os.WriteFile(path, []byte("KEY=value\n"), 0o600); err != nil {
149 t.Fatal(err)
150 }
151 before, err := pathDACLSDDL(path)
152 if err != nil {
153 t.Fatal(err)
154 }
155 if err := RepairLegacyCredentialDeny(path); err != nil {
156 t.Fatal(err)
157 }
158 after, err := pathDACLSDDL(path)
159 if err != nil {
160 t.Fatal(err)
161 }
162 if after != before {
163 t.Fatalf("ordinary DACL changed:\nbefore %s\nafter %s", before, after)
164 }
165 }
166
167 func TestRepairLegacyCredentialDenyRefusesMixedCurrentUserDenyACL(t *testing.T) {
168 path := filepath.Join(t.TempDir(), ".env")
169 if err := os.WriteFile(path, []byte("KEY=value\n"), 0o600); err != nil {
170 t.Fatal(err)
171 }
172 userSID, err := currentProcessUserSIDString()
173 if err != nil {
174 t.Fatal(err)
175 }
176 sd, err := windows.SecurityDescriptorFromString(fmt.Sprintf(
177 "D:(D;;0x%x;;;%s)(D;;0x2;;;%s)(A;;FA;;;%s)",
178 uint32(legacyCredentialDenyMask), userSID, userSID, userSID,
179 ))
180 if err != nil {
181 t.Fatal(err)
182 }
183 dacl, _, err := sd.DACL()
184 if err != nil {
185 t.Fatal(err)
186 }
187 if err := windows.SetNamedSecurityInfo(path, windows.SE_FILE_OBJECT, windows.DACL_SECURITY_INFORMATION, nil, nil, dacl, nil); err != nil {
188 t.Fatal(err)
189 }
190 runtime.KeepAlive(sd)
191 legacy, other, err := currentUserDenyACECounts(path, userSID)
192 if err != nil || legacy != 1 || other == 0 {
193 t.Fatalf("deny counts before repair = legacy:%d other:%d err:%v", legacy, other, err)
194 }
195 before, err := pathDACLSDDL(path)
196 if err != nil {
197 t.Fatal(err)
198 }
199 if err := RepairLegacyCredentialDeny(path); err == nil {
200 t.Fatal("RepairLegacyCredentialDeny accepted mixed current-user deny ACL")
201 }
202 after, err := pathDACLSDDL(path)
203 if err != nil {
204 t.Fatal(err)
205 }
206 if after != before {
207 t.Fatalf("mixed DACL changed:\nbefore %s\nafter %s", before, after)
208 }
209 }
210
211 func pathDACLSDDL(path string) (string, error) {
212 sd, err := windows.GetNamedSecurityInfo(path, windows.SE_FILE_OBJECT, windows.DACL_SECURITY_INFORMATION)
213 if err != nil || sd == nil {
214 return "", err
215 }
216 return sd.String(), nil
217 }
218
219 func TestResetCredentialDACLRestoresAccessWithoutReadingACL(t *testing.T) {
220 path := filepath.Join(t.TempDir(), ".env")
221 if err := os.WriteFile(path, []byte("KEY=value\n"), 0o600); err != nil {
222 t.Fatal(err)
223 }
224 userSID, err := currentProcessUserSIDString()
225 if err != nil {
226 t.Fatal(err)
227 }
228 installLegacyDeny(t, path, userSID)
229 if _, err := os.ReadFile(path); err == nil {
230 t.Fatal("deny did not block reads")
231 }
232 if err := ResetCredentialDACL(path); err != nil {
233 t.Fatal(err)
234 }
235 if data, err := os.ReadFile(path); err != nil || string(data) != "KEY=value\n" {
236 t.Fatalf("credential after reset = %q, %v", data, err)
237 }
238 // SDDL renders well-known accounts as aliases (the CI runner's admin is
239 // "LA"), so inspect the ACEs instead of matching the SID string.
240 sd, err := windows.GetNamedSecurityInfo(path, windows.SE_FILE_OBJECT, windows.DACL_SECURITY_INFORMATION)
241 if err != nil {
242 t.Fatal(err)
243 }
244 control, _, err := sd.Control()
245 if err != nil || control&windows.SE_DACL_PROTECTED == 0 {
246 t.Fatalf("reset DACL control = %#x err=%v, want SE_DACL_PROTECTED", control, err)
247 }
248 acl, _, err := sd.DACL()
249 if err != nil || acl == nil || acl.AceCount != 1 {
250 t.Fatalf("reset DACL = %s, want exactly one ACE", sd.String())
251 }
252 var ace *windows.ACCESS_ALLOWED_ACE
253 if err := windows.GetAce(acl, 0, &ace); err != nil {
254 t.Fatal(err)
255 }
256 want, err := windows.StringToSid(userSID)
257 if err != nil {
258 t.Fatal(err)
259 }
260 if ace.Header.AceType != windows.ACCESS_ALLOWED_ACE_TYPE || !windows.EqualSid((*windows.SID)(unsafe.Pointer(&ace.SidStart)), want) {
261 t.Fatalf("reset DACL = %s, want an allow entry for the current user", sd.String())
262 }
263 runtime.KeepAlive(sd)
264 }
265
266 func TestRenameLockedFileMovesDeniedStore(t *testing.T) {
267 dir := t.TempDir()
268 path := filepath.Join(dir, ".env")
269 if err := os.WriteFile(path, []byte("KEY=value\n"), 0o600); err != nil {
270 t.Fatal(err)
271 }
272 userSID, err := currentProcessUserSIDString()
273 if err != nil {
274 t.Fatal(err)
275 }
276 installLegacyDeny(t, path, userSID)
277 target := path + ".locked-test"
278 if err := RenameLockedFile(path, target); err != nil {
279 t.Fatal(err)
280 }
281 t.Cleanup(func() { _ = icacls(target, "/remove:d", "*"+userSID, "/C") })
282 if _, err := os.Stat(path); !os.IsNotExist(err) {
283 t.Fatalf("source still present after rename: %v", err)
284 }
285 if _, err := os.Stat(target); err != nil {
286 t.Fatalf("target missing after rename: %v", err)
287 }
288 if err := RenameLockedFile(filepath.Join(dir, "missing"), target); err == nil {
289 t.Fatal("rename of a missing file must fail")
290 }
291 }
292
292 lines GO