From 90916f53ded94c95a858458a192c0b91f96e01be Mon Sep 17 00:00:00 2001 From: Aneesh Nireshwalia <99904+aneesh-n@users.noreply.github.com> Date: Sat, 24 Feb 2024 13:27:01 -0700 Subject: [PATCH] Add test cases for security descriptors --- internal/fs/sd_windows_test.go | 60 ++++++++++++++ internal/fs/sd_windows_test_helpers.go | 109 +++++++++++++++++++++++++ internal/restic/node_windows_test.go | 57 +++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 internal/fs/sd_windows_test.go create mode 100644 internal/fs/sd_windows_test_helpers.go diff --git a/internal/fs/sd_windows_test.go b/internal/fs/sd_windows_test.go new file mode 100644 index 000000000..e4e37cb4a --- /dev/null +++ b/internal/fs/sd_windows_test.go @@ -0,0 +1,60 @@ +//go:build windows +// +build windows + +package fs + +import ( + "encoding/base64" + "os" + "path/filepath" + "testing" + + "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/test" +) + +func Test_SetGetFileSecurityDescriptors(t *testing.T) { + tempDir := t.TempDir() + testfilePath := filepath.Join(tempDir, "testfile.txt") + // create temp file + testfile, err := os.Create(testfilePath) + if err != nil { + t.Fatalf("failed to create temporary file: %s", err) + } + defer func() { + err := testfile.Close() + if err != nil { + t.Logf("Error closing file %s: %v\n", testfilePath, err) + } + }() + + testSecurityDescriptors(t, TestFileSDs, testfilePath) +} + +func Test_SetGetFolderSecurityDescriptors(t *testing.T) { + tempDir := t.TempDir() + testfolderPath := filepath.Join(tempDir, "testfolder") + // create temp folder + err := os.Mkdir(testfolderPath, os.ModeDir) + if err != nil { + t.Fatalf("failed to create temporary file: %s", err) + } + + testSecurityDescriptors(t, TestDirSDs, testfolderPath) +} + +func testSecurityDescriptors(t *testing.T, testSDs []string, testPath string) { + for _, testSD := range testSDs { + sdInputBytes, err := base64.StdEncoding.DecodeString(testSD) + test.OK(t, errors.Wrapf(err, "Error decoding SD: %s", testPath)) + + err = SetSecurityDescriptor(testPath, &sdInputBytes) + test.OK(t, errors.Wrapf(err, "Error setting file security descriptor for: %s", testPath)) + + var sdOutputBytes *[]byte + sdOutputBytes, err = GetSecurityDescriptor(testPath) + test.OK(t, errors.Wrapf(err, "Error getting file security descriptor for: %s", testPath)) + + CompareSecurityDescriptors(t, testPath, sdInputBytes, *sdOutputBytes) + } +} diff --git a/internal/fs/sd_windows_test_helpers.go b/internal/fs/sd_windows_test_helpers.go new file mode 100644 index 000000000..877408796 --- /dev/null +++ b/internal/fs/sd_windows_test_helpers.go @@ -0,0 +1,109 @@ +//go:build windows +// +build windows + +package fs + +import ( + "os/user" + "testing" + + "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/test" + "golang.org/x/sys/windows" +) + +var ( + TestFileSDs = []string{"AQAUvBQAAAAwAAAAAAAAAEwAAAABBQAAAAAABRUAAACIn1iuVqCC6sy9JqvqAwAAAQUAAAAAAAUVAAAAiJ9YrlaggurMvSarAQIAAAIAfAAEAAAAAAAkAKkAEgABBQAAAAAABRUAAACIn1iuVqCC6sy9JqvtAwAAABAUAP8BHwABAQAAAAAABRIAAAAAEBgA/wEfAAECAAAAAAAFIAAAACACAAAAECQA/wEfAAEFAAAAAAAFFQAAAIifWK5WoILqzL0mq+oDAAA=", + "AQAUvBQAAAAwAAAAAAAAAEwAAAABBQAAAAAABRUAAACIn1iuVqCC6sy9JqvqAwAAAQUAAAAAAAUVAAAAiJ9YrlaggurMvSarAQIAAAIAyAAHAAAAAAAUAKkAEgABAQAAAAAABQcAAAAAABQAiQASAAEBAAAAAAAFBwAAAAAAJACpABIAAQUAAAAAAAUVAAAAiJ9YrlaggurMvSar7QMAAAAAJAC/ARMAAQUAAAAAAAUVAAAAiJ9YrlaggurMvSar6gMAAAAAFAD/AR8AAQEAAAAAAAUSAAAAAAAYAP8BHwABAgAAAAAABSAAAAAgAgAAAAAkAP8BHwABBQAAAAAABRUAAACIn1iuVqCC6sy9JqvqAwAA", + "AQAUvBQAAAAwAAAA7AAAAEwAAAABBQAAAAAABRUAAAAvr7t03PyHGk2FokNHCAAAAQUAAAAAAAUVAAAAiJ9YrlaggurMvSarAQIAAAIAoAAFAAAAAAAkAP8BHwABBQAAAAAABRUAAAAvr7t03PyHGk2FokNHCAAAAAAkAKkAEgABBQAAAAAABRUAAACIn1iuVqCC6sy9JqvtAwAAABAUAP8BHwABAQAAAAAABRIAAAAAEBgA/wEfAAECAAAAAAAFIAAAACACAAAAECQA/wEfAAEFAAAAAAAFFQAAAIifWK5WoILqzL0mq+oDAAACAHQAAwAAAAKAJAC/AQIAAQUAAAAAAAUVAAAAL6+7dNz8hxpNhaJDtgQAAALAJAC/AQMAAQUAAAAAAAUVAAAAL6+7dNz8hxpNhaJDPgkAAAJAJAD/AQ8AAQUAAAAAAAUVAAAAL6+7dNz8hxpNhaJDtQQAAA==", + } + TestDirSDs = []string{"AQAUvBQAAAAwAAAAAAAAAEwAAAABBQAAAAAABRUAAACIn1iuVqCC6sy9JqvqAwAAAQUAAAAAAAUVAAAAiJ9YrlaggurMvSarAQIAAAIAfAAEAAAAAAAkAKkAEgABBQAAAAAABRUAAACIn1iuVqCC6sy9JqvtAwAAABMUAP8BHwABAQAAAAAABRIAAAAAExgA/wEfAAECAAAAAAAFIAAAACACAAAAEyQA/wEfAAEFAAAAAAAFFQAAAIifWK5WoILqzL0mq+oDAAA=", + "AQAUvBQAAAAwAAAAAAAAAEwAAAABBQAAAAAABRUAAACIn1iuVqCC6sy9JqvqAwAAAQUAAAAAAAUVAAAAiJ9YrlaggurMvSarAQIAAAIA3AAIAAAAAAIUAKkAEgABAQAAAAAABQcAAAAAAxQAiQASAAEBAAAAAAAFBwAAAAAAJACpABIAAQUAAAAAAAUVAAAAiJ9YrlaggurMvSar7QMAAAAAJAC/ARMAAQUAAAAAAAUVAAAAiJ9YrlaggurMvSar6gMAAAALFAC/ARMAAQEAAAAAAAMAAAAAABMUAP8BHwABAQAAAAAABRIAAAAAExgA/wEfAAECAAAAAAAFIAAAACACAAAAEyQA/wEfAAEFAAAAAAAFFQAAAIifWK5WoILqzL0mq+oDAAA=", + "AQAUvBQAAAAwAAAA7AAAAEwAAAABBQAAAAAABRUAAAAvr7t03PyHGk2FokNHCAAAAQUAAAAAAAUVAAAAiJ9YrlaggurMvSarAQIAAAIAoAAFAAAAAAAkAP8BHwABBQAAAAAABRUAAAAvr7t03PyHGk2FokNHCAAAAAAkAKkAEgABBQAAAAAABRUAAACIn1iuVqCC6sy9JqvtAwAAABMUAP8BHwABAQAAAAAABRIAAAAAExgA/wEfAAECAAAAAAAFIAAAACACAAAAEyQA/wEfAAEFAAAAAAAFFQAAAIifWK5WoILqzL0mq+oDAAACAHQAAwAAAAKAJAC/AQIAAQUAAAAAAAUVAAAAL6+7dNz8hxpNhaJDtgQAAALAJAC/AQMAAQUAAAAAAAUVAAAAL6+7dNz8hxpNhaJDPgkAAAJAJAD/AQ8AAQUAAAAAAAUVAAAAL6+7dNz8hxpNhaJDtQQAAA==", + } +) + +// CompareSecurityDescriptors runs tests for comparing 2 security descriptors in []byte format. +func CompareSecurityDescriptors(t *testing.T, testPath string, sdInputBytes, sdOutputBytes []byte) { + sdInput, err := SecurityDescriptorBytesToStruct(sdInputBytes) + test.OK(t, errors.Wrapf(err, "Error converting SD to struct for: %s", testPath)) + + sdOutput, err := SecurityDescriptorBytesToStruct(sdOutputBytes) + test.OK(t, errors.Wrapf(err, "Error converting SD to struct for: %s", testPath)) + + isAdmin, err := IsAdmin() + test.OK(t, errors.Wrapf(err, "Error checking if user is admin: %s", testPath)) + + var ownerExpected *windows.SID + var defaultedOwnerExpected bool + var groupExpected *windows.SID + var defaultedGroupExpected bool + var daclExpected *windows.ACL + var defaultedDaclExpected bool + var saclExpected *windows.ACL + var defaultedSaclExpected bool + + // The Dacl is set correctly whether or not application is running as admin. + daclExpected, defaultedDaclExpected, err = sdInput.DACL() + test.OK(t, errors.Wrapf(err, "Error getting input dacl for: %s", testPath)) + + if isAdmin { + // If application is running as admin, all sd values including owner, group, dacl, sacl are set correctly during restore. + // Hence we will use the input values for comparison with the output values. + ownerExpected, defaultedOwnerExpected, err = sdInput.Owner() + test.OK(t, errors.Wrapf(err, "Error getting input owner for: %s", testPath)) + groupExpected, defaultedGroupExpected, err = sdInput.Group() + test.OK(t, errors.Wrapf(err, "Error getting input group for: %s", testPath)) + saclExpected, defaultedSaclExpected, err = sdInput.SACL() + test.OK(t, errors.Wrapf(err, "Error getting input sacl for: %s", testPath)) + } else { + // If application is not running as admin, owner and group are set as current user's SID/GID during restore and sacl is empty. + // Get the current user + user, err := user.Current() + test.OK(t, errors.Wrapf(err, "Could not get current user for: %s", testPath)) + // Get current user's SID + currentUserSID, err := windows.StringToSid(user.Uid) + test.OK(t, errors.Wrapf(err, "Error getting output group for: %s", testPath)) + // Get current user's Group SID + currentGroupSID, err := windows.StringToSid(user.Gid) + test.OK(t, errors.Wrapf(err, "Error getting output group for: %s", testPath)) + + // Set owner and group as current user's SID and GID during restore. + ownerExpected = currentUserSID + defaultedOwnerExpected = false + groupExpected = currentGroupSID + defaultedGroupExpected = false + + // If application is not running as admin, SACL is returned empty. + saclExpected = nil + defaultedSaclExpected = false + } + // Now do all the comparisons + // Get owner SID from output file + ownerOut, defaultedOwnerOut, err := sdOutput.Owner() + test.OK(t, errors.Wrapf(err, "Error getting output owner for: %s", testPath)) + // Compare owner SIDs. We must use the Equals method for comparison as a syscall is made for comparing SIDs. + test.Assert(t, ownerExpected.Equals(ownerOut), "Owner from SDs read from test path don't match: %s, cur:%s, exp: %s", testPath, ownerExpected.String(), ownerOut.String()) + test.Equals(t, defaultedOwnerExpected, defaultedOwnerOut, "Defaulted for owner from SDs read from test path don't match: %s", testPath) + + // Get group SID from output file + groupOut, defaultedGroupOut, err := sdOutput.Group() + test.OK(t, errors.Wrapf(err, "Error getting output group for: %s", testPath)) + // Compare group SIDs. We must use the Equals method for comparison as a syscall is made for comparing SIDs. + test.Assert(t, groupExpected.Equals(groupOut), "Group from SDs read from test path don't match: %s, cur:%s, exp: %s", testPath, groupExpected.String(), groupOut.String()) + test.Equals(t, defaultedGroupExpected, defaultedGroupOut, "Defaulted for group from SDs read from test path don't match: %s", testPath) + + // Get dacl from output file + daclOut, defaultedDaclOut, err := sdOutput.DACL() + test.OK(t, errors.Wrapf(err, "Error getting output dacl for: %s", testPath)) + // Compare dacls + test.Equals(t, daclExpected, daclOut, "DACL from SDs read from test path don't match: %s", testPath) + test.Equals(t, defaultedDaclExpected, defaultedDaclOut, "Defaulted for DACL from SDs read from test path don't match: %s", testPath) + + // Get sacl from output file + saclOut, defaultedSaclOut, err := sdOutput.SACL() + test.OK(t, errors.Wrapf(err, "Error getting output sacl for: %s", testPath)) + // Compare sacls + test.Equals(t, saclExpected, saclOut, "DACL from SDs read from test path don't match: %s", testPath) + test.Equals(t, defaultedSaclExpected, defaultedSaclOut, "Defaulted for SACL from SDs read from test path don't match: %s", testPath) +} diff --git a/internal/restic/node_windows_test.go b/internal/restic/node_windows_test.go index 501d5a98a..5fd1fe376 100644 --- a/internal/restic/node_windows_test.go +++ b/internal/restic/node_windows_test.go @@ -4,6 +4,7 @@ package restic import ( + "encoding/base64" "encoding/json" "fmt" "os" @@ -12,10 +13,66 @@ import ( "testing" "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/test" "golang.org/x/sys/windows" ) +func TestRestoreSecurityDescriptors(t *testing.T) { + t.Parallel() + tempDir := t.TempDir() + for i, sd := range fs.TestFileSDs { + testRestoreSecurityDescriptor(t, sd, tempDir, "file", fmt.Sprintf("testfile%d", i)) + } + for i, sd := range fs.TestDirSDs { + testRestoreSecurityDescriptor(t, sd, tempDir, "dir", fmt.Sprintf("testdir%d", i)) + } +} + +func testRestoreSecurityDescriptor(t *testing.T, sd string, tempDir, fileType, fileName string) { + // Decode the encoded string SD to get the security descriptor input in bytes. + sdInputBytes, err := base64.StdEncoding.DecodeString(sd) + test.OK(t, errors.Wrapf(err, "Error decoding SD for: %s", fileName)) + // Wrap the security descriptor bytes in windows attributes and convert to generic attributes. + genericAttributes, err := WindowsAttrsToGenericAttributes(WindowsAttributes{CreationTime: nil, FileAttributes: nil, SecurityDescriptor: &sdInputBytes}) + test.OK(t, errors.Wrapf(err, "Error constructing windows attributes for: %s", fileName)) + // Construct a Node with the generic attributes. + expectedNode := getNode(fileName, fileType, genericAttributes) + + // Restore the file/dir and restore the meta data including the security descriptors. + testPath, node := restoreAndGetNode(t, tempDir, expectedNode, false) + // Get the security descriptor from the node constructed from the file info of the restored path. + sdByteFromRestoredNode := getWindowsAttr(t, testPath, node).SecurityDescriptor + + // Get the security descriptor for the test path after the restore. + sdBytesFromRestoredPath, err := fs.GetSecurityDescriptor(testPath) + test.OK(t, errors.Wrapf(err, "Error while getting the security descriptor for: %s", testPath)) + + // Compare the input SD and the SD got from the restored file. + fs.CompareSecurityDescriptors(t, testPath, sdInputBytes, *sdBytesFromRestoredPath) + // Compare the SD got from node constructed from the restored file info and the SD got directly from the restored file. + fs.CompareSecurityDescriptors(t, testPath, *sdByteFromRestoredNode, *sdBytesFromRestoredPath) +} + +func getNode(name string, fileType string, genericAttributes map[GenericAttributeType]json.RawMessage) Node { + return Node{ + Name: name, + Type: fileType, + Mode: 0644, + ModTime: parseTime("2024-02-21 6:30:01.111"), + AccessTime: parseTime("2024-02-22 7:31:02.222"), + ChangeTime: parseTime("2024-02-23 8:32:03.333"), + GenericAttributes: genericAttributes, + } +} + +func getWindowsAttr(t *testing.T, testPath string, node *Node) WindowsAttributes { + windowsAttributes, unknownAttribs, err := genericAttributesToWindowsAttrs(node.GenericAttributes) + test.OK(t, errors.Wrapf(err, "Error getting windows attr from generic attr: %s", testPath)) + test.Assert(t, len(unknownAttribs) == 0, "Unkown attribs found: %s for: %s", unknownAttribs, testPath) + return windowsAttributes +} + func TestRestoreCreationTime(t *testing.T) { t.Parallel() path := t.TempDir()