Files
ThothII/tools/tht/internal/safeio/private_windows_test.go
T

243 lines
7.1 KiB
Go

//go:build windows
package safeio
import (
"errors"
"os"
"path/filepath"
"runtime"
"testing"
"golang.org/x/sys/windows"
)
func TestPrivateWindowsDACLRejectsPermissiveDirectoryAndRegularFile(t *testing.T) {
directory := filepath.Join(t.TempDir(), "auth")
if err := os.Mkdir(directory, 0o700); err != nil {
t.Fatal(err)
}
if err := ProtectPrivateDirectory(directory); err != nil {
t.Fatal(err)
}
if err := ValidatePrivateDirectory(directory); err != nil {
t.Fatalf("ValidatePrivateDirectory() protected directory error = %v", err)
}
path := filepath.Join(directory, "users.yaml")
if err := os.WriteFile(path, []byte("private"), 0o600); err != nil {
t.Fatal(err)
}
if err := ProtectPrivateRegular(path); err != nil {
t.Fatal(err)
}
if err := ValidatePrivateRegular(path); err != nil {
t.Fatalf("ValidatePrivateRegular() protected file error = %v", err)
}
for name, path := range map[string]string{"directory": directory, "regular file": path} {
t.Run(name, func(t *testing.T) {
if err := setPermissiveDACL(path); err != nil {
t.Fatal(err)
}
var err error
if name == "directory" {
err = ValidatePrivateDirectory(path)
} else {
err = ValidatePrivateRegular(path)
}
if !errors.Is(err, ErrUnsafeFile) {
t.Fatalf("private validation error = %v, want ErrUnsafeFile", err)
}
})
}
}
func TestCreateCanonicalNewPrivateFileInstallsOwnerOnlyDACLAtCreation(t *testing.T) {
directory := filepath.Join(t.TempDir(), "auth")
if err := os.Mkdir(directory, 0o700); err != nil {
t.Fatal(err)
}
if err := ProtectPrivateDirectory(directory); err != nil {
t.Fatal(err)
}
path := filepath.Join(directory, ".auth.lock")
file, err := createCanonicalNewPrivateFile(path, 0o600)
if err != nil {
t.Fatal(err)
}
defer file.Close()
if err := ValidatePrivateRegular(path); err != nil {
t.Fatalf("new lock DACL error = %v", err)
}
}
func TestGenericNewFileAllowsInheritedOperatorParentButAuthNewFileRequiresPrivateParent(t *testing.T) {
directory := filepath.Join(t.TempDir(), "operator-output")
if err := os.Mkdir(directory, 0o700); err != nil {
t.Fatal(err)
}
if err := setPermissiveDACL(directory); err != nil {
t.Fatal(err)
}
if err := WriteCanonicalNewFile(filepath.Join(directory, "candidates.yaml"), []byte("reviewed: []\n"), 0o600); err != nil {
t.Fatalf("generic operator output error = %v", err)
}
if err := WriteCanonicalNewPrivateFile(filepath.Join(directory, "auth.json"), []byte("record"), 0o600); !errors.Is(err, ErrUnsafeFile) {
t.Fatalf("auth record in inherited directory error = %v, want ErrUnsafeFile", err)
}
}
func TestWithWindowsSecurityDescriptorKeepsOwnedDescriptorValidDuringInspection(t *testing.T) {
directory := filepath.Join(t.TempDir(), "auth")
if err := os.Mkdir(directory, 0o700); err != nil {
t.Fatal(err)
}
if err := ProtectPrivateDirectory(directory); err != nil {
t.Fatal(err)
}
path := filepath.Join(directory, "users.yaml")
if err := os.WriteFile(path, []byte("private"), 0o600); err != nil {
t.Fatal(err)
}
if err := ProtectPrivateRegular(path); err != nil {
t.Fatal(err)
}
handle, err := openWindowsComponent(path, false)
if err != nil {
t.Fatal(err)
}
defer windows.CloseHandle(handle)
if err := withWindowsSecurityDescriptor(handle, func(descriptor *windows.SECURITY_DESCRIPTOR) error {
runtime.GC()
owner, _, err := descriptor.Owner()
if err != nil || owner == nil {
t.Fatalf("descriptor owner error = %v", err)
}
dacl, _, err := descriptor.DACL()
if err != nil || dacl == nil || dacl.AceCount != 1 {
t.Fatalf("descriptor DACL error = %v", err)
}
return nil
}); err != nil {
t.Fatalf("withWindowsSecurityDescriptor() error = %v", err)
}
}
func TestReplaceCanonicalRegularCreatesPrivateTemporaryAndReplacement(t *testing.T) {
directory := filepath.Join(t.TempDir(), "auth")
if err := os.Mkdir(directory, 0o700); err != nil {
t.Fatal(err)
}
if err := ProtectPrivateDirectory(directory); err != nil {
t.Fatal(err)
}
path := filepath.Join(directory, "users.yaml")
if err := os.WriteFile(path, []byte("old"), 0o600); err != nil {
t.Fatal(err)
}
if err := ProtectPrivateRegular(path); err != nil {
t.Fatal(err)
}
temporary, err := writePrivateTemporary(directory, []byte("temporary"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Remove(temporary) })
if err := ValidatePrivateRegular(temporary); err != nil {
t.Fatalf("temporary DACL error = %v", err)
}
if err := ReplaceCanonicalRegular(path, []byte("replacement"), 0o600); err != nil {
t.Fatal(err)
}
if err := ValidatePrivateRegular(path); err != nil {
t.Fatalf("replacement DACL error = %v", err)
}
}
func TestReplaceCanonicalRegularRejectsReparseParent(t *testing.T) {
root := t.TempDir()
realDirectory := filepath.Join(root, "real")
if err := os.Mkdir(realDirectory, 0o700); err != nil {
t.Fatal(err)
}
if err := ProtectPrivateDirectory(realDirectory); err != nil {
t.Fatal(err)
}
target := filepath.Join(realDirectory, "users.yaml")
if err := os.WriteFile(target, []byte("old"), 0o600); err != nil {
t.Fatal(err)
}
if err := ProtectPrivateRegular(target); err != nil {
t.Fatal(err)
}
parentLink := filepath.Join(root, "reparse-parent")
if err := os.Symlink(realDirectory, parentLink); err != nil {
t.Skipf("Windows host does not permit test symlink creation: %v", err)
}
if err := ReplaceCanonicalRegular(filepath.Join(parentLink, "users.yaml"), []byte("new"), 0o600); !errors.Is(err, ErrUnsafeFile) {
t.Fatalf("reparse-parent replacement error = %v, want ErrUnsafeFile", err)
}
}
func TestOpenCanonicalWindowsParentBlocksParentRename(t *testing.T) {
root := t.TempDir()
realDirectory := filepath.Join(root, "real")
if err := os.Mkdir(realDirectory, 0o700); err != nil {
t.Fatal(err)
}
if err := ProtectPrivateDirectory(realDirectory); err != nil {
t.Fatal(err)
}
target := filepath.Join(realDirectory, "users.yaml")
if err := os.WriteFile(target, []byte("old"), 0o600); err != nil {
t.Fatal(err)
}
if err := ProtectPrivateRegular(target); err != nil {
t.Fatal(err)
}
parents, _, err := openCanonicalWindowsParent(target)
if err != nil {
t.Fatal(err)
}
renamed := realDirectory + "-renamed"
if err := os.Rename(realDirectory, renamed); err == nil {
parents.Close()
t.Fatal("parent rename succeeded while canonical replacement handles were retained")
}
parents.Close()
if err := os.Rename(realDirectory, renamed); err != nil {
t.Fatalf("parent rename after closing canonical replacement handles: %v", err)
}
}
func setPermissiveDACL(path string) error {
world, err := windows.StringToSid("S-1-1-0")
if err != nil {
return err
}
var pinner runtime.Pinner
pinner.Pin(world)
defer pinner.Unpin()
acl, err := windows.ACLFromEntries([]windows.EXPLICIT_ACCESS{{
AccessPermissions: windows.GENERIC_READ | windows.GENERIC_WRITE,
AccessMode: windows.GRANT_ACCESS,
Trustee: windows.TRUSTEE{
TrusteeForm: windows.TRUSTEE_IS_SID,
TrusteeType: windows.TRUSTEE_IS_GROUP,
TrusteeValue: windows.TrusteeValueFromSID(world),
},
}}, nil)
if err != nil {
return err
}
return windows.SetNamedSecurityInfo(path, windows.SE_FILE_OBJECT,
windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION,
nil, nil, acl, nil)
}