Files

188 lines
6.8 KiB
Go

//go:build windows
package authstorage
import (
"encoding/base64"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
"golang.org/x/sys/windows"
)
func TestProtocolCreatesRecordsWithOwnerOnlyDACLAndRejectsReparseRoot(t *testing.T) {
root := filepath.Join(t.TempDir(), "auth")
filename := "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc.json"
runRequest(t, request{Version: 1, Operation: "create", Root: root, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("private"))})
if err := safeio.ValidatePrivateDirectory(root); err != nil {
t.Fatalf("root DACL = %v", err)
}
if err := safeio.ValidatePrivateDirectory(filepath.Join(root, "sessions")); err != nil {
t.Fatalf("sessions DACL = %v", err)
}
if err := safeio.ValidatePrivateRegular(filepath.Join(root, "sessions", filename)); err != nil {
t.Fatalf("record DACL = %v", err)
}
}
func TestProtocolRejectsPermissiveDACLAndReparseRoot(t *testing.T) {
root := filepath.Join(t.TempDir(), "auth")
filename := "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff.json"
runRequest(t, request{Version: 1, Operation: "create", Root: root, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("private"))})
path := filepath.Join(root, "sessions", filename)
if err := setPermissiveDACL(path); err != nil {
t.Fatal(err)
}
runRejected(t, request{Version: 1, Operation: "read", Root: root, Directory: "sessions", Filename: filename})
linkedRoot := filepath.Join(t.TempDir(), "reparse-auth")
if err := os.Symlink(root, linkedRoot); err != nil {
t.Skipf("Windows host does not permit test symlink creation: %v", err)
}
runRejected(t, request{Version: 1, Operation: "create", Root: linkedRoot, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("private"))})
}
func TestProtocolRejectsRecordCreationAfterSessionsDirectoryDACLBecomesPermissive(t *testing.T) {
root := filepath.Join(t.TempDir(), "auth")
filename := "abababababababababababababababababababababababababababababababab.json"
runRequest(t, request{Version: 1, Operation: "list", Root: root, Directory: "sessions"})
if err := setPermissiveDACL(filepath.Join(root, "sessions")); err != nil {
t.Fatal(err)
}
runRejected(t, request{Version: 1, Operation: "create", Root: root, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("record"))})
}
func TestProtocolValidatesAndCreatesTheCompleteWindowsSessionLayout(t *testing.T) {
root := filepath.Join(t.TempDir(), "auth")
runRequest(t, request{Version: 1, Operation: "ensure-layout", Root: root})
runRequest(t, request{Version: 1, Operation: "validate-root", Root: root})
for _, directory := range []string{"sessions", "oidc"} {
if err := safeio.ValidatePrivateDirectory(filepath.Join(root, directory)); err != nil {
t.Fatalf("%s DACL = %v", directory, err)
}
}
if err := setPermissiveDACL(filepath.Join(root, "oidc")); err != nil {
t.Fatal(err)
}
runRejected(t, request{Version: 1, Operation: "validate-root", Root: root})
}
func TestProtocolReadsWindowsLocalUsersWithOwnerOnlyDACLAndNoReparseFallback(t *testing.T) {
root := filepath.Join(t.TempDir(), "auth")
if err := safeio.EnsurePrivateDirectory(root); err != nil {
t.Fatal(err)
}
path := filepath.Join(root, "users.yaml")
contents := []byte("version: 1\nusers: []\n")
if err := safeio.WriteCanonicalNewPrivateFile(path, contents, 0o600); err != nil {
t.Fatal(err)
}
read := runRequest(t, request{Version: 1, Operation: "read-local-users", Root: root, Filename: "users.yaml"})
if !read.Found || decodeContent(t, read) != string(contents) {
t.Fatalf("read-local-users = %#v", read)
}
if err := setPermissiveDACL(path); err != nil {
t.Fatal(err)
}
runRejected(t, request{Version: 1, Operation: "read-local-users", Root: root, Filename: "users.yaml"})
if err := os.Remove(path); err != nil {
t.Fatal(err)
}
if err := os.Symlink(filepath.Join(root, "missing-target.yaml"), path); err != nil {
t.Skipf("Windows host does not permit test symlink creation: %v", err)
}
runRejected(t, request{Version: 1, Operation: "read-local-users", Root: root, Filename: "users.yaml"})
}
func TestProtocolValidateLayoutPinsWindowsRootAcrossMissingAndUnsafeChildren(t *testing.T) {
for _, scenario := range []struct {
name string
originalUnsafe bool
replacementUnsafe bool
wantAccepted bool
}{
{name: "rejects unsafe retained child", originalUnsafe: true, wantAccepted: false},
{name: "accepts missing retained child while replacement is unsafe", replacementUnsafe: true, wantAccepted: true},
} {
t.Run(scenario.name, func(t *testing.T) {
parent := t.TempDir()
root := filepath.Join(parent, "auth")
replacement := filepath.Join(parent, "replacement")
moved := filepath.Join(parent, "auth-original")
for _, directory := range []string{root, replacement} {
if err := safeio.EnsurePrivateDirectory(directory); err != nil {
t.Fatal(err)
}
}
if scenario.originalUnsafe {
path := filepath.Join(root, "sessions")
if err := safeio.EnsurePrivateDirectory(path); err != nil {
t.Fatal(err)
}
if err := setPermissiveDACL(path); err != nil {
t.Fatal(err)
}
}
if scenario.replacementUnsafe {
path := filepath.Join(replacement, "sessions")
if err := safeio.EnsurePrivateDirectory(path); err != nil {
t.Fatal(err)
}
if err := setPermissiveDACL(path); err != nil {
t.Fatal(err)
}
}
blocked := false
restore := safeio.SetPrivateDirectoryTestHookForTest(func(stage string) {
if stage != "after-auth-root-open" || blocked {
return
}
if err := os.Rename(root, moved); err == nil {
t.Fatal("retained Windows root handle permitted rename")
}
blocked = true
})
t.Cleanup(restore)
input := request{Version: 1, Operation: "validate-root", Root: root}
if scenario.wantAccepted {
runRequest(t, input)
} else {
runRejected(t, input)
}
if !blocked {
t.Fatal("layout validation did not retain the Windows root handle")
}
})
}
}
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)
}