all: Support syncing ownership (fixes #1329) (#8434)

This adds support for syncing ownership on Unixes and on Windows. The
scanner always picks up ownership information, but it is not applied
unless the new folder option "Sync Ownership" is set.

Ownership data is stored in a new FileInfo field called "platform data". This
is intended to hold further platform-specific data in the future
(specifically, extended attributes), which is why the whole design is a
bit overkill for just ownership.
This commit is contained in:
Jakob Borg
2022-07-26 08:24:58 +02:00
committed by GitHub
parent 34a5f087c8
commit adce6fa473
35 changed files with 1896 additions and 500 deletions
-8
View File
@@ -129,14 +129,6 @@ func (f *BasicFilesystem) Chmod(name string, mode FileMode) error {
return os.Chmod(name, os.FileMode(mode))
}
func (f *BasicFilesystem) Lchown(name string, uid, gid int) error {
name, err := f.rooted(name)
if err != nil {
return err
}
return os.Lchown(name, uid, gid)
}
func (f *BasicFilesystem) Chtimes(name string, atime time.Time, mtime time.Time) error {
name, err := f.rooted(name)
if err != nil {
+18
View File
@@ -0,0 +1,18 @@
// Copyright (C) 2022 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build !windows
// +build !windows
package fs
import (
"github.com/syncthing/syncthing/lib/protocol"
)
func (f *BasicFilesystem) PlatformData(name string) (protocol.PlatformData, error) {
return unixPlatformData(f, name)
}
+69
View File
@@ -0,0 +1,69 @@
// Copyright (C) 2022 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package fs
import (
"fmt"
"os/user"
"github.com/syncthing/syncthing/lib/protocol"
"golang.org/x/sys/windows"
)
func (f *BasicFilesystem) PlatformData(name string) (protocol.PlatformData, error) {
rootedName, err := f.rooted(name)
if err != nil {
return protocol.PlatformData{}, fmt.Errorf("rooted for %s: %w", name, err)
}
hdl, err := openReadOnlyWithBackupSemantics(rootedName)
if err != nil {
return protocol.PlatformData{}, fmt.Errorf("open %s: %w", rootedName, err)
}
defer windows.Close(hdl)
// GetSecurityInfo returns an owner SID.
sd, err := windows.GetSecurityInfo(hdl, windows.SE_FILE_OBJECT, windows.OWNER_SECURITY_INFORMATION)
if err != nil {
return protocol.PlatformData{}, fmt.Errorf("get security info for %s: %w", rootedName, err)
}
owner, _, err := sd.Owner()
if err != nil {
return protocol.PlatformData{}, fmt.Errorf("get owner for %s: %w", rootedName, err)
}
// The owner SID might represent a user or a group. We try to look it up
// as both, and set the appropriate fields in the OS data.
pd := &protocol.WindowsData{}
if us, err := user.LookupId(owner.String()); err == nil {
pd.OwnerName = us.Username
} else if gr, err := user.LookupGroupId(owner.String()); err == nil {
pd.OwnerName = gr.Name
pd.OwnerIsGroup = true
} else {
l.Debugf("Failed to resolve owner for %s: %v", rootedName, err)
}
return protocol.PlatformData{Windows: pd}, nil
}
func openReadOnlyWithBackupSemantics(path string) (fd windows.Handle, err error) {
// This is windows.Open but simplified to read-only only, and adding
// FILE_FLAG_BACKUP_SEMANTICS which is required to open directories.
if len(path) == 0 {
return windows.InvalidHandle, windows.ERROR_FILE_NOT_FOUND
}
pathp, err := windows.UTF16PtrFromString(path)
if err != nil {
return windows.InvalidHandle, err
}
var access uint32 = windows.GENERIC_READ
var sharemode uint32 = windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE
var sa *windows.SecurityAttributes
var createmode uint32 = windows.OPEN_EXISTING
var attrs uint32 = windows.FILE_ATTRIBUTE_READONLY | windows.FILE_FLAG_BACKUP_SEMANTICS
return windows.CreateFile(pathp, access, sharemode, sa, createmode, attrs, 0)
}
+2 -1
View File
@@ -11,6 +11,7 @@ import (
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"testing"
"time"
@@ -84,7 +85,7 @@ func TestChownFile(t *testing.T) {
newUID := 1000 + rand.Intn(30000)
newGID := 1000 + rand.Intn(30000)
if err := fs.Lchown("file", newUID, newGID); err != nil {
if err := fs.Lchown("file", strconv.Itoa(newUID), strconv.Itoa(newGID)); err != nil {
t.Error("Unexpected error:", err)
}
+17
View File
@@ -12,6 +12,7 @@ package fs
import (
"os"
"path/filepath"
"strconv"
"strings"
)
@@ -57,6 +58,22 @@ func (f *BasicFilesystem) Roots() ([]string, error) {
return []string{"/"}, nil
}
func (f *BasicFilesystem) Lchown(name, uid, gid string) error {
name, err := f.rooted(name)
if err != nil {
return err
}
nuid, err := strconv.Atoi(uid)
if err != nil {
return err
}
ngid, err := strconv.Atoi(gid)
if err != nil {
return err
}
return os.Lchown(name, nuid, ngid)
}
// unrootedChecked returns the path relative to the folder root (same as
// unrooted) or an error if the given path is not a subpath and handles the
// special case when the given path is the folder root without a trailing
+38
View File
@@ -17,6 +17,8 @@ import (
"strings"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
var errNotSupported = errors.New("symlinks not supported")
@@ -152,6 +154,42 @@ func (f *BasicFilesystem) Roots() ([]string, error) {
return drives, nil
}
func (f *BasicFilesystem) Lchown(name, uid, gid string) error {
name, err := f.rooted(name)
if err != nil {
return err
}
hdl, err := windows.Open(name, windows.O_WRONLY, 0)
if err != nil {
return err
}
defer windows.Close(hdl)
// Depending on whether we got an uid or a gid, we need to set the
// appropriate flag and parse the corresponding SID. The one we're not
// setting remains nil, which is what we want in the call to
// SetSecurityInfo.
var si windows.SECURITY_INFORMATION
var ownerSID, groupSID *syscall.SID
if uid != "" {
ownerSID, err = syscall.StringToSid(uid)
if err == nil {
si |= windows.OWNER_SECURITY_INFORMATION
}
} else if gid != "" {
groupSID, err = syscall.StringToSid(uid)
if err == nil {
si |= windows.GROUP_SECURITY_INFORMATION
}
} else {
return errors.New("neither uid nor gid specified")
}
return windows.SetSecurityInfo(hdl, windows.SE_FILE_OBJECT, si, (*windows.SID)(ownerSID), (*windows.SID)(groupSID), nil, nil)
}
// unrootedChecked returns the path relative to the folder root (same as
// unrooted) or an error if the given path is not a subpath and handles the
// special case when the given path is the folder root without a trailing
+1 -1
View File
@@ -153,7 +153,7 @@ func (f *caseFilesystem) Chmod(name string, mode FileMode) error {
return f.Filesystem.Chmod(name, mode)
}
func (f *caseFilesystem) Lchown(name string, uid, gid int) error {
func (f *caseFilesystem) Lchown(name, uid, gid string) error {
if err := f.checkCase(name); err != nil {
return err
}
+6 -1
View File
@@ -9,6 +9,8 @@ package fs
import (
"context"
"time"
"github.com/syncthing/syncthing/lib/protocol"
)
type errorFilesystem struct {
@@ -18,7 +20,7 @@ type errorFilesystem struct {
}
func (fs *errorFilesystem) Chmod(name string, mode FileMode) error { return fs.err }
func (fs *errorFilesystem) Lchown(name string, uid, gid int) error { return fs.err }
func (fs *errorFilesystem) Lchown(name, uid, gid string) error { return fs.err }
func (fs *errorFilesystem) Chtimes(name string, atime time.Time, mtime time.Time) error {
return fs.err
}
@@ -52,6 +54,9 @@ func (fs *errorFilesystem) SameFile(fi1, fi2 FileInfo) bool { return false }
func (fs *errorFilesystem) Watch(path string, ignore Matcher, ctx context.Context, ignorePerms bool) (<-chan Event, <-chan error, error) {
return nil, nil, fs.err
}
func (fs *errorFilesystem) PlatformData(name string) (protocol.PlatformData, error) {
return protocol.PlatformData{}, fs.err
}
func (fs *errorFilesystem) underlying() (Filesystem, bool) {
return nil, false
+18 -13
View File
@@ -21,6 +21,8 @@ import (
"sync"
"testing"
"time"
"github.com/syncthing/syncthing/lib/protocol"
)
// see readShortAt()
@@ -29,19 +31,19 @@ const randomBlockShift = 14 // 128k
// fakeFS is a fake filesystem for testing and benchmarking. It has the
// following properties:
//
// - File metadata is kept in RAM. Specifically, we remember which files and
// directories exist, their dates, permissions and sizes. Symlinks are
// not supported.
// - File metadata is kept in RAM. Specifically, we remember which files and
// directories exist, their dates, permissions and sizes. Symlinks are
// not supported.
//
// - File contents are generated pseudorandomly with just the file name as
// seed. Writes are discarded, other than having the effect of increasing
// the file size. If you only write data that you've read from a file with
// the same name on a different fakeFS, you'll never know the difference...
// - File contents are generated pseudorandomly with just the file name as
// seed. Writes are discarded, other than having the effect of increasing
// the file size. If you only write data that you've read from a file with
// the same name on a different fakeFS, you'll never know the difference...
//
// - We totally ignore permissions - pretend you are root.
//
// - The root path can contain URL query-style parameters that pre populate
// the filesystem at creation with a certain amount of random data:
// - The root path can contain URL query-style parameters that pre populate
// the filesystem at creation with a certain amount of random data:
//
// files=n to generate n random files (default 0)
// maxsize=n to generate files up to a total of n MiB (default 0)
@@ -51,7 +53,6 @@ const randomBlockShift = 14 // 128k
// latency=d to set the amount of time each "disk" operation takes, where d is time.ParseDuration format
//
// - Two fakeFS:s pointing at the same root path see the same files.
//
type fakeFS struct {
counters fakeFSCounters
uri string
@@ -220,7 +221,7 @@ func (fs *fakeFS) Chmod(name string, mode FileMode) error {
return nil
}
func (fs *fakeFS) Lchown(name string, uid, gid int) error {
func (fs *fakeFS) Lchown(name, uid, gid string) error {
fs.mut.Lock()
defer fs.mut.Unlock()
fs.counters.Lchown++
@@ -229,8 +230,8 @@ func (fs *fakeFS) Lchown(name string, uid, gid int) error {
if entry == nil {
return os.ErrNotExist
}
entry.uid = uid
entry.gid = gid
entry.uid, _ = strconv.Atoi(uid)
entry.gid, _ = strconv.Atoi(gid)
return nil
}
@@ -656,6 +657,10 @@ func (fs *fakeFS) SameFile(fi1, fi2 FileInfo) bool {
return ok && fi1.ModTime().Equal(fi2.ModTime()) && fi1.Mode() == fi2.Mode() && fi1.IsDir() == fi2.IsDir() && fi1.IsRegular() == fi2.IsRegular() && fi1.IsSymlink() == fi2.IsSymlink() && fi1.Owner() == fi2.Owner() && fi1.Group() == fi2.Group()
}
func (fs *fakeFS) PlatformData(name string) (protocol.PlatformData, error) {
return unixPlatformData(fs, name)
}
func (fs *fakeFS) underlying() (Filesystem, bool) {
return nil, false
}
+1 -1
View File
@@ -119,7 +119,7 @@ func TestFakeFS(t *testing.T) {
}
// Chown
if err := fs.Lchown("dira", 1234, 5678); err != nil {
if err := fs.Lchown("dira", "1234", "5678"); err != nil {
t.Fatal(err)
}
if info, err := fs.Lstat("dira"); err != nil {
+4 -1
View File
@@ -14,6 +14,8 @@ import (
"path/filepath"
"strings"
"time"
"github.com/syncthing/syncthing/lib/protocol"
)
type filesystemWrapperType int32
@@ -30,7 +32,7 @@ const (
// The Filesystem interface abstracts access to the file system.
type Filesystem interface {
Chmod(name string, mode FileMode) error
Lchown(name string, uid, gid int) error
Lchown(name string, uid, gid string) error // uid/gid as strings; numeric on POSIX, SID on Windows, like in os/user package
Chtimes(name string, atime time.Time, mtime time.Time) error
Create(name string) (File, error)
CreateSymlink(target, name string) error
@@ -60,6 +62,7 @@ type Filesystem interface {
URI() string
Options() []Option
SameFile(fi1, fi2 FileInfo) bool
PlatformData(name string) (protocol.PlatformData, error)
// Used for unwrapping things
underlying() (Filesystem, bool)
+1 -1
View File
@@ -59,7 +59,7 @@ func (o *optionMtime) apply(fs Filesystem) Filesystem {
return f
}
func (_ *optionMtime) String() string {
func (*optionMtime) String() string {
return "mtime"
}
+55
View File
@@ -0,0 +1,55 @@
// Copyright (C) 2022 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package fs
import (
"os/user"
"strconv"
"github.com/syncthing/syncthing/lib/protocol"
)
// unixPlatformData is used on all platforms, because apart from being the
// implementation for BasicFilesystem on Unixes it's also the implementation
// in fakeFS.
func unixPlatformData(fs Filesystem, name string) (protocol.PlatformData, error) {
stat, err := fs.Lstat(name)
if err != nil {
return protocol.PlatformData{}, err
}
ownerUID := stat.Owner()
ownerName := ""
if u, err := user.LookupId(strconv.Itoa(ownerUID)); err == nil && u.Username != "" {
ownerName = u.Username
} else if ownerUID == 0 {
// We couldn't look up a name, but UID zero should be "root". This
// fixup works around the (unlikely) situation where the ownership
// is 0:0 but we can't look up a name for either uid zero or gid
// zero. If that were the case we'd return a zero PlatformData which
// wouldn't get serialized over the wire and the other side would
// assume a lack of ownership info...
ownerName = "root"
}
groupID := stat.Group()
groupName := ""
if g, err := user.LookupGroupId(strconv.Itoa(groupID)); err == nil && g.Name != "" {
groupName = g.Name
} else if groupID == 0 {
groupName = "root"
}
return protocol.PlatformData{
Unix: &protocol.UnixData{
OwnerName: ownerName,
GroupName: groupName,
UID: ownerUID,
GID: groupID,
},
}, nil
}