From 09ce1b4e5811bc5e988511172198fc5ea1a3a9d9 Mon Sep 17 00:00:00 2001 From: Aneesh Nireshwalia <99904+aneesh-n@users.noreply.github.com> Date: Sat, 24 Feb 2024 13:16:25 -0700 Subject: [PATCH 1/7] Create helper for SecurityDescriptor related functions --- internal/fs/sd_windows.go | 471 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 471 insertions(+) create mode 100644 internal/fs/sd_windows.go diff --git a/internal/fs/sd_windows.go b/internal/fs/sd_windows.go new file mode 100644 index 000000000..361631766 --- /dev/null +++ b/internal/fs/sd_windows.go @@ -0,0 +1,471 @@ +package fs + +import ( + "bytes" + "encoding/binary" + "fmt" + "sync" + "syscall" + "unicode/utf16" + "unsafe" + + "github.com/restic/restic/internal/errors" + "golang.org/x/sys/windows" +) + +// GetSecurityDescriptor takes the path of the file and returns the SecurityDescriptor for the file. +// This needs admin permissions or SeBackupPrivilege for getting the full SD. +// If there are no admin permissions, only the current user's owner, group and DACL will be got. +func GetSecurityDescriptor(filePath string) (securityDescriptor *[]byte, err error) { + onceBackup.Do(enableBackupPrivilege) + + var sd *windows.SECURITY_DESCRIPTOR + + if lowerPrivileges { + sd, err = getNamedSecurityInfoLow(sd, err, filePath) + } else { + sd, err = getNamedSecurityInfoHigh(sd, err, filePath) + } + if err != nil { + if isHandlePrivilegeNotHeldError(err) { + lowerPrivileges = true + sd, err = getNamedSecurityInfoLow(sd, err, filePath) + if err != nil { + return nil, fmt.Errorf("get low-level named security info failed with: %w", err) + } + } else { + return nil, fmt.Errorf("get named security info failed with: %w", err) + } + } + + sdBytes, err := securityDescriptorStructToBytes(sd) + if err != nil { + return nil, fmt.Errorf("convert security descriptor to bytes failed: %w", err) + } + return &sdBytes, nil +} + +// SetSecurityDescriptor sets the SecurityDescriptor for the file at the specified path. +// This needs admin permissions or SeRestorePrivilege, SeSecurityPrivilege and SeTakeOwnershipPrivilege +// for setting the full SD. +// If there are no admin permissions/required privileges, only the DACL from the SD can be set and +// owner and group will be set based on the current user. +func SetSecurityDescriptor(filePath string, securityDescriptor *[]byte) error { + onceRestore.Do(enableRestorePrivilege) + // Set the security descriptor on the file + sd, err := SecurityDescriptorBytesToStruct(*securityDescriptor) + if err != nil { + return fmt.Errorf("error converting bytes to security descriptor: %w", err) + } + + owner, _, err := sd.Owner() + if err != nil { + //Do not set partial values. + owner = nil + } + group, _, err := sd.Group() + if err != nil { + //Do not set partial values. + group = nil + } + dacl, _, err := sd.DACL() + if err != nil { + //Do not set partial values. + dacl = nil + } + sacl, _, err := sd.SACL() + if err != nil { + //Do not set partial values. + sacl = nil + } + + if lowerPrivileges { + err = setNamedSecurityInfoLow(filePath, dacl) + } else { + err = setNamedSecurityInfoHigh(filePath, owner, group, dacl, sacl) + } + + if err != nil { + if isHandlePrivilegeNotHeldError(err) { + lowerPrivileges = true + err = setNamedSecurityInfoLow(filePath, dacl) + if err != nil { + return fmt.Errorf("set low-level named security info failed with: %w", err) + } + } else { + return fmt.Errorf("set named security info failed with: %w", err) + } + } + return nil +} + +var ( + onceBackup sync.Once + onceRestore sync.Once + + // SeBackupPrivilege allows the application to bypass file and directory ACLs to back up files and directories. + SeBackupPrivilege = "SeBackupPrivilege" + // SeRestorePrivilege allows the application to bypass file and directory ACLs to restore files and directories. + SeRestorePrivilege = "SeRestorePrivilege" + // SeSecurityPrivilege allows read and write access to all SACLs. + SeSecurityPrivilege = "SeSecurityPrivilege" + // SeTakeOwnershipPrivilege allows the application to take ownership of files and directories, regardless of the permissions set on them. + SeTakeOwnershipPrivilege = "SeTakeOwnershipPrivilege" + + backupPrivilegeError error + restorePrivilegeError error + lowerPrivileges bool +) + +// Flags for backup and restore with admin permissions +var highSecurityFlags windows.SECURITY_INFORMATION = windows.OWNER_SECURITY_INFORMATION | windows.GROUP_SECURITY_INFORMATION | windows.DACL_SECURITY_INFORMATION | windows.SACL_SECURITY_INFORMATION | windows.LABEL_SECURITY_INFORMATION | windows.ATTRIBUTE_SECURITY_INFORMATION | windows.SCOPE_SECURITY_INFORMATION | windows.BACKUP_SECURITY_INFORMATION | windows.PROTECTED_DACL_SECURITY_INFORMATION | windows.PROTECTED_SACL_SECURITY_INFORMATION + +// Flags for backup without admin permissions. If there are no admin permissions, only the current user's owner, group and DACL will be backed up. +var lowBackupSecurityFlags windows.SECURITY_INFORMATION = windows.OWNER_SECURITY_INFORMATION | windows.GROUP_SECURITY_INFORMATION | windows.DACL_SECURITY_INFORMATION | windows.LABEL_SECURITY_INFORMATION | windows.ATTRIBUTE_SECURITY_INFORMATION | windows.SCOPE_SECURITY_INFORMATION | windows.PROTECTED_DACL_SECURITY_INFORMATION + +// Flags for restore without admin permissions. If there are no admin permissions, only the DACL from the SD can be restored and owner and group will be set based on the current user. +var lowRestoreSecurityFlags windows.SECURITY_INFORMATION = windows.DACL_SECURITY_INFORMATION | windows.ATTRIBUTE_SECURITY_INFORMATION | windows.PROTECTED_DACL_SECURITY_INFORMATION + +// getNamedSecurityInfoHigh gets the higher level SecurityDescriptor which requires admin permissions. +func getNamedSecurityInfoHigh(sd *windows.SECURITY_DESCRIPTOR, err error, filePath string) (*windows.SECURITY_DESCRIPTOR, error) { + return windows.GetNamedSecurityInfo(filePath, windows.SE_FILE_OBJECT, highSecurityFlags) +} + +// getNamedSecurityInfoLow gets the lower level SecurityDescriptor which requires no admin permissions. +func getNamedSecurityInfoLow(sd *windows.SECURITY_DESCRIPTOR, err error, filePath string) (*windows.SECURITY_DESCRIPTOR, error) { + return windows.GetNamedSecurityInfo(filePath, windows.SE_FILE_OBJECT, lowBackupSecurityFlags) +} + +// setNamedSecurityInfoHigh sets the higher level SecurityDescriptor which requires admin permissions. +func setNamedSecurityInfoHigh(filePath string, owner *windows.SID, group *windows.SID, dacl *windows.ACL, sacl *windows.ACL) error { + return windows.SetNamedSecurityInfo(filePath, windows.SE_FILE_OBJECT, highSecurityFlags, owner, group, dacl, sacl) +} + +// setNamedSecurityInfoLow sets the lower level SecurityDescriptor which requires no admin permissions. +func setNamedSecurityInfoLow(filePath string, dacl *windows.ACL) error { + return windows.SetNamedSecurityInfo(filePath, windows.SE_FILE_OBJECT, lowRestoreSecurityFlags, nil, nil, dacl, nil) +} + +// enableBackupPrivilege enables privilege for backing up security descriptors +func enableBackupPrivilege() { + err := enableProcessPrivileges([]string{SeBackupPrivilege}) + if err != nil { + backupPrivilegeError = fmt.Errorf("error enabling backup privilege: %w", err) + } +} + +// enableBackupPrivilege enables privilege for restoring security descriptors +func enableRestorePrivilege() { + err := enableProcessPrivileges([]string{SeRestorePrivilege, SeSecurityPrivilege, SeTakeOwnershipPrivilege}) + if err != nil { + restorePrivilegeError = fmt.Errorf("error enabling restore/security privilege: %w", err) + } +} + +// DisableBackupPrivileges disables privileges that are needed for backup operations. +// They may be reenabled if GetSecurityDescriptor is called again. +func DisableBackupPrivileges() error { + //Reset the once so that backup privileges can be enabled again if needed. + onceBackup = sync.Once{} + return enableDisableProcessPrivilege([]string{SeBackupPrivilege}, 0) +} + +// DisableRestorePrivileges disables privileges that are needed for restore operations. +// They may be reenabled if SetSecurityDescriptor is called again. +func DisableRestorePrivileges() error { + //Reset the once so that restore privileges can be enabled again if needed. + onceRestore = sync.Once{} + return enableDisableProcessPrivilege([]string{SeRestorePrivilege, SeSecurityPrivilege}, 0) +} + +// isHandlePrivilegeNotHeldError checks if the error is ERROR_PRIVILEGE_NOT_HELD +func isHandlePrivilegeNotHeldError(err error) bool { + // Use a type assertion to check if the error is of type syscall.Errno + if errno, ok := err.(syscall.Errno); ok { + // Compare the error code to the expected value + return errno == windows.ERROR_PRIVILEGE_NOT_HELD + } + return false +} + +// IsAdmin checks if current user is an administrator. +func IsAdmin() (isAdmin bool, err error) { + var sid *windows.SID + err = windows.AllocateAndInitializeSid(&windows.SECURITY_NT_AUTHORITY, 2, windows.SECURITY_BUILTIN_DOMAIN_RID, windows.DOMAIN_ALIAS_RID_ADMINS, + 0, 0, 0, 0, 0, 0, &sid) + if err != nil { + return false, errors.Errorf("sid error: %s", err) + } + token := windows.Token(0) + member, err := token.IsMember(sid) + if err != nil { + return false, errors.Errorf("token membership error: %s", err) + } + return member, nil +} + +// The code below was adapted from github.com/Microsoft/go-winio under MIT license. + +// The MIT License (MIT) + +// Copyright (c) 2015 Microsoft + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +var ( + modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") + + procLookupPrivilegeValueW = modadvapi32.NewProc("LookupPrivilegeValueW") + procAdjustTokenPrivileges = modadvapi32.NewProc("AdjustTokenPrivileges") + procLookupPrivilegeDisplayNameW = modadvapi32.NewProc("LookupPrivilegeDisplayNameW") + procLookupPrivilegeNameW = modadvapi32.NewProc("LookupPrivilegeNameW") +) + +// Do the interface allocations only once for common +// Errno values. +const ( + errnoErrorIOPending = 997 + + //revive:disable-next-line:var-naming ALL_CAPS + SE_PRIVILEGE_ENABLED = windows.SE_PRIVILEGE_ENABLED + + //revive:disable-next-line:var-naming ALL_CAPS + ERROR_NOT_ALL_ASSIGNED syscall.Errno = windows.ERROR_NOT_ALL_ASSIGNED +) + +var ( + errErrorIOPending error = syscall.Errno(errnoErrorIOPending) + errErrorEinval error = syscall.EINVAL + + privNames = make(map[string]uint64) + privNameMutex sync.Mutex +) + +// PrivilegeError represents an error enabling privileges. +type PrivilegeError struct { + privileges []uint64 +} + +// SecurityDescriptorBytesToStruct converts the security descriptor bytes representation +// into a pointer to windows SECURITY_DESCRIPTOR. +func SecurityDescriptorBytesToStruct(sd []byte) (*windows.SECURITY_DESCRIPTOR, error) { + if l := int(unsafe.Sizeof(windows.SECURITY_DESCRIPTOR{})); len(sd) < l { + return nil, fmt.Errorf("securityDescriptor (%d) smaller than expected (%d): %w", len(sd), l, windows.ERROR_INCORRECT_SIZE) + } + s := (*windows.SECURITY_DESCRIPTOR)(unsafe.Pointer(&sd[0])) + return s, nil +} + +// securityDescriptorStructToBytes converts the pointer to windows SECURITY_DESCRIPTOR +// into a security descriptor bytes representation. +func securityDescriptorStructToBytes(sd *windows.SECURITY_DESCRIPTOR) ([]byte, error) { + b := unsafe.Slice((*byte)(unsafe.Pointer(sd)), sd.Length()) + return b, nil +} + +// Error returns the string message for the error. +func (e *PrivilegeError) Error() string { + s := "Could not enable privilege " + if len(e.privileges) > 1 { + s = "Could not enable privileges " + } + for i, p := range e.privileges { + if i != 0 { + s += ", " + } + s += `"` + s += getPrivilegeName(p) + s += `"` + } + if backupPrivilegeError != nil { + s += " backupPrivilegeError:" + backupPrivilegeError.Error() + } + if restorePrivilegeError != nil { + s += " restorePrivilegeError:" + restorePrivilegeError.Error() + } + return s +} + +func mapPrivileges(names []string) ([]uint64, error) { + privileges := make([]uint64, 0, len(names)) + privNameMutex.Lock() + defer privNameMutex.Unlock() + for _, name := range names { + p, ok := privNames[name] + if !ok { + err := lookupPrivilegeValue("", name, &p) + if err != nil { + return nil, err + } + privNames[name] = p + } + privileges = append(privileges, p) + } + return privileges, nil +} + +// enableProcessPrivileges enables privileges globally for the process. +func enableProcessPrivileges(names []string) error { + return enableDisableProcessPrivilege(names, SE_PRIVILEGE_ENABLED) +} + +// DisableProcessPrivileges disables privileges globally for the process. +func DisableProcessPrivileges(names []string) error { + return enableDisableProcessPrivilege(names, 0) +} + +func enableDisableProcessPrivilege(names []string, action uint32) error { + privileges, err := mapPrivileges(names) + if err != nil { + return err + } + + p := windows.CurrentProcess() + var token windows.Token + err = windows.OpenProcessToken(p, windows.TOKEN_ADJUST_PRIVILEGES|windows.TOKEN_QUERY, &token) + if err != nil { + return err + } + + defer func() { + _ = token.Close() + }() + return adjustPrivileges(token, privileges, action) +} + +func adjustPrivileges(token windows.Token, privileges []uint64, action uint32) error { + var b bytes.Buffer + _ = binary.Write(&b, binary.LittleEndian, uint32(len(privileges))) + for _, p := range privileges { + _ = binary.Write(&b, binary.LittleEndian, p) + _ = binary.Write(&b, binary.LittleEndian, action) + } + prevState := make([]byte, b.Len()) + reqSize := uint32(0) + success, err := adjustTokenPrivileges(token, false, &b.Bytes()[0], uint32(len(prevState)), &prevState[0], &reqSize) + if !success { + return err + } + if err == ERROR_NOT_ALL_ASSIGNED { //nolint:errorlint // err is Errno + return &PrivilegeError{privileges} + } + return nil +} + +func getPrivilegeName(luid uint64) string { + var nameBuffer [256]uint16 + bufSize := uint32(len(nameBuffer)) + err := lookupPrivilegeName("", &luid, &nameBuffer[0], &bufSize) + if err != nil { + return fmt.Sprintf("", luid) + } + + var displayNameBuffer [256]uint16 + displayBufSize := uint32(len(displayNameBuffer)) + var langID uint32 + err = lookupPrivilegeDisplayName("", &nameBuffer[0], &displayNameBuffer[0], &displayBufSize, &langID) + if err != nil { + return fmt.Sprintf("", string(utf16.Decode(nameBuffer[:bufSize]))) + } + + return string(utf16.Decode(displayNameBuffer[:displayBufSize])) +} + +func adjustTokenPrivileges(token windows.Token, releaseAll bool, input *byte, outputSize uint32, output *byte, requiredSize *uint32) (success bool, err error) { + var _p0 uint32 + if releaseAll { + _p0 = 1 + } + r0, _, e1 := syscall.SyscallN(procAdjustTokenPrivileges.Addr(), uintptr(token), uintptr(_p0), uintptr(unsafe.Pointer(input)), uintptr(outputSize), uintptr(unsafe.Pointer(output)), uintptr(unsafe.Pointer(requiredSize))) + success = r0 != 0 + if !success { + err = errnoErr(e1) + } + return +} + +func lookupPrivilegeDisplayName(systemName string, name *uint16, buffer *uint16, size *uint32, languageID *uint32) (err error) { + var _p0 *uint16 + _p0, err = syscall.UTF16PtrFromString(systemName) + if err != nil { + return + } + return _lookupPrivilegeDisplayName(_p0, name, buffer, size, languageID) +} + +func _lookupPrivilegeDisplayName(systemName *uint16, name *uint16, buffer *uint16, size *uint32, languageID *uint32) (err error) { + r1, _, e1 := syscall.SyscallN(procLookupPrivilegeDisplayNameW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(size)), uintptr(unsafe.Pointer(languageID)), 0) + if r1 == 0 { + err = errnoErr(e1) + } + return +} + +func lookupPrivilegeName(systemName string, luid *uint64, buffer *uint16, size *uint32) (err error) { + var _p0 *uint16 + _p0, err = syscall.UTF16PtrFromString(systemName) + if err != nil { + return + } + return _lookupPrivilegeName(_p0, luid, buffer, size) +} + +func _lookupPrivilegeName(systemName *uint16, luid *uint64, buffer *uint16, size *uint32) (err error) { + r1, _, e1 := syscall.SyscallN(procLookupPrivilegeNameW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(luid)), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(size)), 0, 0) + if r1 == 0 { + err = errnoErr(e1) + } + return +} + +func lookupPrivilegeValue(systemName string, name string, luid *uint64) (err error) { + var _p0 *uint16 + _p0, err = syscall.UTF16PtrFromString(systemName) + if err != nil { + return + } + var _p1 *uint16 + _p1, err = syscall.UTF16PtrFromString(name) + if err != nil { + return + } + return _lookupPrivilegeValue(_p0, _p1, luid) +} + +func _lookupPrivilegeValue(systemName *uint16, name *uint16, luid *uint64) (err error) { + r1, _, e1 := syscall.SyscallN(procLookupPrivilegeValueW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(luid))) + if r1 == 0 { + err = errnoErr(e1) + } + return +} + +// errnoErr returns common boxed Errno values, to prevent +// allocations at runtime. +func errnoErr(e syscall.Errno) error { + switch e { + case 0: + return errErrorEinval + case errnoErrorIOPending: + return errErrorIOPending + } + // TODO: add more here, after collecting data on the common + // error values see on Windows. (perhaps when running + // all.bat?) + return e +} From e3e59fef242ead47d2a227f88d73a7c638c4e9a5 Mon Sep 17 00:00:00 2001 From: Aneesh Nireshwalia <99904+aneesh-n@users.noreply.github.com> Date: Sat, 24 Feb 2024 13:22:34 -0700 Subject: [PATCH 2/7] Fix CombineErrors and fillExtendedAttr error handling --- internal/errors/errors.go | 27 +++++++++++++++++---------- internal/restic/node.go | 7 +------ 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 3c669f861..68a48b325 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -43,22 +43,29 @@ func Is(x, y error) bool { return stderrors.Is(x, y) } // unwrap errors returned by [Join]. func Unwrap(err error) error { return stderrors.Unwrap(err) } -// CombineErrors combines multiple errors into a single error. -func CombineErrors(errors ...error) error { +// CombineErrors combines multiple errors into a single error after filtering out any nil values. +// If no errors are passed, it returns nil. +// If one error is passed, it simply returns that same error. +func CombineErrors(errors ...error) (err error) { var combinedErrorMsg string - - for _, err := range errors { - if err != nil { + var multipleErrors bool + for _, errVal := range errors { + if errVal != nil { if combinedErrorMsg != "" { combinedErrorMsg += "; " // Separate error messages with a delimiter + multipleErrors = true + } else { + // Set the first error + err = errVal } - combinedErrorMsg += err.Error() + combinedErrorMsg += errVal.Error() } } - if combinedErrorMsg == "" { - return nil // No errors, return nil + return nil // If no errors, return nil + } else if !multipleErrors { + return err // If only one error, return that first error + } else { + return fmt.Errorf("multiple errors occurred: [%s]", combinedErrorMsg) } - - return fmt.Errorf("multiple errors occurred: [%s]", combinedErrorMsg) } diff --git a/internal/restic/node.go b/internal/restic/node.go index cbe9ef363..8fc06df79 100644 --- a/internal/restic/node.go +++ b/internal/restic/node.go @@ -719,12 +719,7 @@ func (node *Node) fillExtra(path string, fi os.FileInfo) error { allowExtended, err := node.fillGenericAttributes(path, fi, stat) if allowExtended { // Skip processing ExtendedAttributes if allowExtended is false. - errEx := node.fillExtendedAttributes(path) - if err == nil { - err = errEx - } else { - debug.Log("Error filling extended attributes for %v at %v : %v", node.Name, path, errEx) - } + err = errors.CombineErrors(err, node.fillExtendedAttributes(path)) } return err } From 70cf8e37887cd574c055337f37427eca94d54052 Mon Sep 17 00:00:00 2001 From: Aneesh Nireshwalia <99904+aneesh-n@users.noreply.github.com> Date: Sat, 24 Feb 2024 13:25:28 -0700 Subject: [PATCH 3/7] Add support for backup/restore of security descriptors --- internal/restic/node.go | 4 +++- internal/restic/node_windows.go | 21 ++++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/internal/restic/node.go b/internal/restic/node.go index 8fc06df79..a0e658b9b 100644 --- a/internal/restic/node.go +++ b/internal/restic/node.go @@ -48,13 +48,15 @@ const ( TypeCreationTime GenericAttributeType = "windows.creation_time" // TypeFileAttributes is the GenericAttributeType used for storing file attributes for windows files within the generic attributes map. TypeFileAttributes GenericAttributeType = "windows.file_attributes" + // TypeSecurityDescriptor is the GenericAttributeType used for storing security descriptors including owner, group, discretionary access control list (DACL), system access control list (SACL)) for windows files within the generic attributes map. + TypeSecurityDescriptor GenericAttributeType = "windows.security_descriptor" // Generic Attributes for other OS types should be defined here. ) // init is called when the package is initialized. Any new GenericAttributeTypes being created must be added here as well. func init() { - storeGenericAttributeType(TypeCreationTime, TypeFileAttributes) + storeGenericAttributeType(TypeCreationTime, TypeFileAttributes, TypeSecurityDescriptor) } // genericAttributesForOS maintains a map of known genericAttributesForOS to the OSType diff --git a/internal/restic/node_windows.go b/internal/restic/node_windows.go index 5875c3ccd..f4797c0d7 100644 --- a/internal/restic/node_windows.go +++ b/internal/restic/node_windows.go @@ -23,6 +23,9 @@ type WindowsAttributes struct { CreationTime *syscall.Filetime `generic:"creation_time"` // FileAttributes is used for storing file attributes for windows files. FileAttributes *uint32 `generic:"file_attributes"` + // SecurityDescriptor is used for storing security descriptors which includes + // owner, group, discretionary access control list (DACL), system access control list (SACL)) + SecurityDescriptor *[]byte `generic:"security_descriptor"` } var ( @@ -114,7 +117,7 @@ func (s statT) mtim() syscall.Timespec { func (s statT) ctim() syscall.Timespec { // Windows does not have the concept of a "change time" in the sense Unix uses it, so we're using the LastWriteTime here. - return syscall.NsecToTimespec(s.LastWriteTime.Nanoseconds()) + return s.mtim() } // restoreGenericAttributes restores generic attributes for Windows @@ -137,6 +140,11 @@ func (node Node) restoreGenericAttributes(path string, warn func(msg string)) (e errs = append(errs, fmt.Errorf("error restoring file attributes for: %s : %v", path, err)) } } + if windowsAttributes.SecurityDescriptor != nil { + if err := fs.SetSecurityDescriptor(path, windowsAttributes.SecurityDescriptor); err != nil { + errs = append(errs, fmt.Errorf("error restoring security descriptor for: %s : %v", path, err)) + } + } HandleUnknownGenericAttributesFound(unknownAttribs, warn) return errors.CombineErrors(errs...) @@ -270,11 +278,18 @@ func (node *Node) fillGenericAttributes(path string, fi os.FileInfo, stat *statT // Do not process file attributes and created time for windows directories like // C:, D: // Filepath.Clean(path) ends with '\' for Windows root drives only. + var sd *[]byte + if node.Type == "file" || node.Type == "dir" { + if sd, err = fs.GetSecurityDescriptor(path); err != nil { + return true, err + } + } // Add Windows attributes node.GenericAttributes, err = WindowsAttrsToGenericAttributes(WindowsAttributes{ - CreationTime: getCreationTime(fi, path), - FileAttributes: &stat.FileAttributes, + CreationTime: getCreationTime(fi, path), + FileAttributes: &stat.FileAttributes, + SecurityDescriptor: sd, }) } return true, err 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 4/7] 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() From c0a1b9ada51f0d4f03ebd05e45b18b72c1c1142d Mon Sep 17 00:00:00 2001 From: Aneesh Nireshwalia <99904+aneesh-n@users.noreply.github.com> Date: Sat, 24 Feb 2024 13:28:18 -0700 Subject: [PATCH 5/7] Update docs for security descriptors --- changelog/unreleased/pull-4611 | 2 +- doc/040_backup.rst | 7 ++++++- doc/050_restore.rst | 5 +++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/changelog/unreleased/pull-4611 b/changelog/unreleased/pull-4611 index 940de9c26..a3c7a24d0 100644 --- a/changelog/unreleased/pull-4611 +++ b/changelog/unreleased/pull-4611 @@ -1,7 +1,7 @@ Enhancement: Back up windows created time and file attributes like hidden flag Restic did not back up windows-specific meta-data like created time and file attributes like hidden flag. -Restic now backs up file created time and file attributes like hidden, readonly and encrypted flag when backing up files and folders on windows. +Restic now backs up file created time and file attributes like hidden, readonly and encrypted flag when backing up files and folders on Windows. https://github.com/restic/restic/pull/4611 diff --git a/doc/040_backup.rst b/doc/040_backup.rst index d0bd4b2e2..b697e38bd 100644 --- a/doc/040_backup.rst +++ b/doc/040_backup.rst @@ -481,12 +481,17 @@ written, and the next backup needs to write new metadata again. If you really want to save the access time for files and directories, you can pass the ``--with-atime`` option to the ``backup`` command. +Backing up full security descriptors on windows is only possible when the user +has ``SeBackupPrivilege``privilege or is running as admin. This is a restriction +of windows not restic. +If either of these conditions are not met, only the owner, group and DACL will +be backed up. + Note that ``restic`` does not back up some metadata associated with files. Of particular note are: * File creation date on Unix platforms * Inode flags on Unix platforms -* File ownership and ACLs on Windows Reading data from a command *************************** diff --git a/doc/050_restore.rst b/doc/050_restore.rst index 916b11c86..5ab0286f1 100644 --- a/doc/050_restore.rst +++ b/doc/050_restore.rst @@ -72,6 +72,11 @@ Restoring symbolic links on windows is only possible when the user has ``SeCreateSymbolicLinkPrivilege`` privilege or is running as admin. This is a restriction of windows not restic. +Restoring full security descriptors on windows is only possible when the user has +``SeRestorePrivilege``, ``SeSecurityPrivilege`` and ``SeTakeOwnershipPrivilege`` +privilege or is running as admin. This is a restriction of windows not restic. +If either of these conditions are not met, only the DACL will be restored. + By default, restic does not restore files as sparse. Use ``restore --sparse`` to enable the creation of sparse files if supported by the filesystem. Then restic will restore long runs of zero bytes as holes in the corresponding files. From 5764300022f7afceb2e54babe5ffed6e56b7aff9 Mon Sep 17 00:00:00 2001 From: Aneesh Nireshwalia <99904+aneesh-n@users.noreply.github.com> Date: Sat, 24 Feb 2024 13:47:49 -0700 Subject: [PATCH 6/7] Add changelog and fix lint error --- changelog/unreleased/pull-4708 | 12 ++++++++++++ internal/errors/errors.go | 3 +-- 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 changelog/unreleased/pull-4708 diff --git a/changelog/unreleased/pull-4708 b/changelog/unreleased/pull-4708 new file mode 100644 index 000000000..2c666c300 --- /dev/null +++ b/changelog/unreleased/pull-4708 @@ -0,0 +1,12 @@ +Enhancement: Back up and restore SecurityDescriptors on Windows + +Restic did not back up SecurityDescriptors of files on Windows. +Restic now backs up and restores SecurityDescriptors (which includes owner, group, +discretionary access control list (DACL), system access control list (SACL)) +when backing up files and folders on Windows. This requires the user to be +a member of backup operators or the application must be run as admin. +If that is not the case, only the current user's owner, group and DACL will be backed up +and during restore only the DACL of the backed file will be restored while the current +user's owner and group will be set during the restore. + +https://github.com/restic/restic/pull/4708 diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 68a48b325..ca36611eb 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -65,7 +65,6 @@ func CombineErrors(errors ...error) (err error) { return nil // If no errors, return nil } else if !multipleErrors { return err // If only one error, return that first error - } else { - return fmt.Errorf("multiple errors occurred: [%s]", combinedErrorMsg) } + return fmt.Errorf("multiple errors occurred: [%s]", combinedErrorMsg) } From 062d40898723997ccb63acf8c36fb6a25c3f0aa7 Mon Sep 17 00:00:00 2001 From: Aneesh Nireshwalia <99904+aneesh-n@users.noreply.github.com> Date: Sat, 24 Feb 2024 14:23:04 -0700 Subject: [PATCH 7/7] Clean up SecurityDescriptor helper --- internal/fs/sd_windows.go | 58 +++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/internal/fs/sd_windows.go b/internal/fs/sd_windows.go index 361631766..9d53b3974 100644 --- a/internal/fs/sd_windows.go +++ b/internal/fs/sd_windows.go @@ -13,6 +13,33 @@ import ( "golang.org/x/sys/windows" ) +var ( + onceBackup sync.Once + onceRestore sync.Once + + // SeBackupPrivilege allows the application to bypass file and directory ACLs to back up files and directories. + SeBackupPrivilege = "SeBackupPrivilege" + // SeRestorePrivilege allows the application to bypass file and directory ACLs to restore files and directories. + SeRestorePrivilege = "SeRestorePrivilege" + // SeSecurityPrivilege allows read and write access to all SACLs. + SeSecurityPrivilege = "SeSecurityPrivilege" + // SeTakeOwnershipPrivilege allows the application to take ownership of files and directories, regardless of the permissions set on them. + SeTakeOwnershipPrivilege = "SeTakeOwnershipPrivilege" + + backupPrivilegeError error + restorePrivilegeError error + lowerPrivileges bool +) + +// Flags for backup and restore with admin permissions +var highSecurityFlags windows.SECURITY_INFORMATION = windows.OWNER_SECURITY_INFORMATION | windows.GROUP_SECURITY_INFORMATION | windows.DACL_SECURITY_INFORMATION | windows.SACL_SECURITY_INFORMATION | windows.LABEL_SECURITY_INFORMATION | windows.ATTRIBUTE_SECURITY_INFORMATION | windows.SCOPE_SECURITY_INFORMATION | windows.BACKUP_SECURITY_INFORMATION | windows.PROTECTED_DACL_SECURITY_INFORMATION | windows.PROTECTED_SACL_SECURITY_INFORMATION + +// Flags for backup without admin permissions. If there are no admin permissions, only the current user's owner, group and DACL will be backed up. +var lowBackupSecurityFlags windows.SECURITY_INFORMATION = windows.OWNER_SECURITY_INFORMATION | windows.GROUP_SECURITY_INFORMATION | windows.DACL_SECURITY_INFORMATION | windows.LABEL_SECURITY_INFORMATION | windows.ATTRIBUTE_SECURITY_INFORMATION | windows.SCOPE_SECURITY_INFORMATION | windows.PROTECTED_DACL_SECURITY_INFORMATION + +// Flags for restore without admin permissions. If there are no admin permissions, only the DACL from the SD can be restored and owner and group will be set based on the current user. +var lowRestoreSecurityFlags windows.SECURITY_INFORMATION = windows.DACL_SECURITY_INFORMATION | windows.ATTRIBUTE_SECURITY_INFORMATION | windows.PROTECTED_DACL_SECURITY_INFORMATION + // GetSecurityDescriptor takes the path of the file and returns the SecurityDescriptor for the file. // This needs admin permissions or SeBackupPrivilege for getting the full SD. // If there are no admin permissions, only the current user's owner, group and DACL will be got. @@ -87,6 +114,7 @@ func SetSecurityDescriptor(filePath string, securityDescriptor *[]byte) error { if err != nil { if isHandlePrivilegeNotHeldError(err) { + // If ERROR_PRIVILEGE_NOT_HELD is encountered, fallback to backups/restores using lower non-admin privileges. lowerPrivileges = true err = setNamedSecurityInfoLow(filePath, dacl) if err != nil { @@ -99,33 +127,6 @@ func SetSecurityDescriptor(filePath string, securityDescriptor *[]byte) error { return nil } -var ( - onceBackup sync.Once - onceRestore sync.Once - - // SeBackupPrivilege allows the application to bypass file and directory ACLs to back up files and directories. - SeBackupPrivilege = "SeBackupPrivilege" - // SeRestorePrivilege allows the application to bypass file and directory ACLs to restore files and directories. - SeRestorePrivilege = "SeRestorePrivilege" - // SeSecurityPrivilege allows read and write access to all SACLs. - SeSecurityPrivilege = "SeSecurityPrivilege" - // SeTakeOwnershipPrivilege allows the application to take ownership of files and directories, regardless of the permissions set on them. - SeTakeOwnershipPrivilege = "SeTakeOwnershipPrivilege" - - backupPrivilegeError error - restorePrivilegeError error - lowerPrivileges bool -) - -// Flags for backup and restore with admin permissions -var highSecurityFlags windows.SECURITY_INFORMATION = windows.OWNER_SECURITY_INFORMATION | windows.GROUP_SECURITY_INFORMATION | windows.DACL_SECURITY_INFORMATION | windows.SACL_SECURITY_INFORMATION | windows.LABEL_SECURITY_INFORMATION | windows.ATTRIBUTE_SECURITY_INFORMATION | windows.SCOPE_SECURITY_INFORMATION | windows.BACKUP_SECURITY_INFORMATION | windows.PROTECTED_DACL_SECURITY_INFORMATION | windows.PROTECTED_SACL_SECURITY_INFORMATION - -// Flags for backup without admin permissions. If there are no admin permissions, only the current user's owner, group and DACL will be backed up. -var lowBackupSecurityFlags windows.SECURITY_INFORMATION = windows.OWNER_SECURITY_INFORMATION | windows.GROUP_SECURITY_INFORMATION | windows.DACL_SECURITY_INFORMATION | windows.LABEL_SECURITY_INFORMATION | windows.ATTRIBUTE_SECURITY_INFORMATION | windows.SCOPE_SECURITY_INFORMATION | windows.PROTECTED_DACL_SECURITY_INFORMATION - -// Flags for restore without admin permissions. If there are no admin permissions, only the DACL from the SD can be restored and owner and group will be set based on the current user. -var lowRestoreSecurityFlags windows.SECURITY_INFORMATION = windows.DACL_SECURITY_INFORMATION | windows.ATTRIBUTE_SECURITY_INFORMATION | windows.PROTECTED_DACL_SECURITY_INFORMATION - // getNamedSecurityInfoHigh gets the higher level SecurityDescriptor which requires admin permissions. func getNamedSecurityInfoHigh(sd *windows.SECURITY_DESCRIPTOR, err error, filePath string) (*windows.SECURITY_DESCRIPTOR, error) { return windows.GetNamedSecurityInfo(filePath, windows.SE_FILE_OBJECT, highSecurityFlags) @@ -464,8 +465,5 @@ func errnoErr(e syscall.Errno) error { case errnoErrorIOPending: return errErrorIOPending } - // TODO: add more here, after collecting data on the common - // error values see on Windows. (perhaps when running - // all.bat?) return e }