Skip to content

Commit

Permalink
all: Support syncing ownership (fixes syncthing#1329) (syncthing#8434)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
calmh authored and Martchus committed Aug 4, 2022
1 parent 7da9a62 commit 93b789c
Show file tree
Hide file tree
Showing 35 changed files with 1,895 additions and 499 deletions.
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -52,7 +52,7 @@ require (
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
golang.org/x/mod v0.5.1 // indirect
golang.org/x/net v0.0.0-20220607020251-c690dde0001d
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b
golang.org/x/text v0.3.7
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
golang.org/x/tools v0.1.7
Expand Down
3 changes: 2 additions & 1 deletion go.sum
Expand Up @@ -603,8 +603,9 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8=
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand Down
5 changes: 1 addition & 4 deletions lib/config/config.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

296 changes: 167 additions & 129 deletions lib/config/folderconfiguration.pb.go

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions lib/db/db_test.go
Expand Up @@ -212,7 +212,7 @@ func TestUpdate0to3(t *testing.T) {
t.Error("Unexpected additional file via sequence", f.FileName())
return true
}
if e := haveUpdate0to3[protocol.LocalDeviceID][0]; f.IsEquivalentOptional(e, 0, true, true, 0) {
if e := haveUpdate0to3[protocol.LocalDeviceID][0]; f.IsEquivalentOptional(e, protocol.FileInfoComparison{IgnorePerms: true, IgnoreBlocks: true}) {
found = true
} else {
t.Errorf("Wrong file via sequence, got %v, expected %v", f, e)
Expand Down Expand Up @@ -281,7 +281,7 @@ func TestUpdate0to3(t *testing.T) {
}
f := fi.(protocol.FileInfo)
delete(need, f.Name)
if !f.IsEquivalentOptional(e, 0, true, true, 0) {
if !f.IsEquivalentOptional(e, protocol.FileInfoComparison{IgnorePerms: true, IgnoreBlocks: true}) {
t.Errorf("Wrong needed file, got %v, expected %v", f, e)
}
}
Expand Down
256 changes: 152 additions & 104 deletions lib/db/structs.pb.go

Large diffs are not rendered by default.

8 changes: 0 additions & 8 deletions lib/fs/basicfs.go
Expand Up @@ -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 {
Expand Down
18 changes: 18 additions & 0 deletions lib/fs/basicfs_platformdata_unix.go
@@ -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 changes: 69 additions & 0 deletions lib/fs/basicfs_platformdata_windows.go
@@ -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)
}
3 changes: 2 additions & 1 deletion lib/fs/basicfs_test.go
Expand Up @@ -11,6 +11,7 @@ import (
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -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)
}

Expand Down
17 changes: 17 additions & 0 deletions lib/fs/basicfs_unix.go
Expand Up @@ -12,6 +12,7 @@ package fs
import (
"os"
"path/filepath"
"strconv"
"strings"
)

Expand Down Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions lib/fs/basicfs_windows.go
Expand Up @@ -17,6 +17,8 @@ import (
"strings"
"syscall"
"unsafe"

"golang.org/x/sys/windows"
)

var errNotSupported = errors.New("symlinks not supported")
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/fs/casefs.go
Expand Up @@ -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
}
Expand Down
7 changes: 6 additions & 1 deletion lib/fs/errorfs.go
Expand Up @@ -9,6 +9,8 @@ package fs
import (
"context"
"time"

"github.com/syncthing/syncthing/lib/protocol"
)

type errorFilesystem struct {
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
31 changes: 18 additions & 13 deletions lib/fs/fakefs.go
Expand Up @@ -21,6 +21,8 @@ import (
"sync"
"testing"
"time"

"github.com/syncthing/syncthing/lib/protocol"
)

// see readShortAt()
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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++
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion lib/fs/fakefs_test.go
Expand Up @@ -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 {
Expand Down

0 comments on commit 93b789c

Please sign in to comment.