From c228bbfd7961b2fa8f0849054c73bd355b800b07 Mon Sep 17 00:00:00 2001 From: Ben McClelland Date: Tue, 27 Feb 2024 15:41:21 -0800 Subject: [PATCH] feat: add option to change ownership of dir/files to acct settings When enabled, any new directories or files created through the gateway will change ownership based on the account uid/gid. Fixes #238. --- backend/common.go | 46 ----------- backend/mkdir.go | 81 +++++++++++++++++++ backend/posix/posix.go | 120 +++++++++++++++++++++++----- backend/posix/with_otmpfile.go | 82 ++++++++++++++++--- backend/posix/without_otmpfile.go | 28 ++++++- backend/scoutfs/scoutfs.go | 71 +++++++++++++--- backend/scoutfs/scoutfs_compat.go | 118 +++++++++++++-------------- backend/scoutfs/scoutfs_incompat.go | 11 ++- cmd/versitygw/gateway_test.go | 2 +- cmd/versitygw/posix.go | 23 +++++- cmd/versitygw/scoutfs.go | 22 +++-- extra/example.conf | 18 ++++- 12 files changed, 456 insertions(+), 166 deletions(-) create mode 100644 backend/mkdir.go diff --git a/backend/common.go b/backend/common.go index e3e8b18..b4ebf91 100644 --- a/backend/common.go +++ b/backend/common.go @@ -19,7 +19,6 @@ import ( "encoding/hex" "fmt" "io/fs" - "os" "strconv" "strings" "time" @@ -121,48 +120,3 @@ func md5String(data []byte) string { sum := md5.Sum(data) return hex.EncodeToString(sum[:]) } - -// MkdirAll is similar to os.MkdirAll but it will return ErrObjectParentIsFile -// when appropriate -func MkdirAll(path string, perm os.FileMode) error { - // Fast path: if we can tell whether path is a directory or file, stop with success or error. - dir, err := os.Stat(path) - if err == nil { - if dir.IsDir() { - return nil - } - return s3err.GetAPIError(s3err.ErrObjectParentIsFile) - } - - // Slow path: make sure parent exists and then call Mkdir for path. - i := len(path) - for i > 0 && os.IsPathSeparator(path[i-1]) { // Skip trailing path separator. - i-- - } - - j := i - for j > 0 && !os.IsPathSeparator(path[j-1]) { // Scan backward over element. - j-- - } - - if j > 1 { - // Create parent. - err = MkdirAll(path[:j-1], perm) - if err != nil { - return err - } - } - - // Parent now exists; invoke Mkdir and use its result. - err = os.Mkdir(path, perm) - if err != nil { - // Handle arguments like "foo/." by - // double-checking that directory doesn't exist. - dir, err1 := os.Lstat(path) - if err1 == nil && dir.IsDir() { - return nil - } - return s3err.GetAPIError(s3err.ErrObjectParentIsFile) - } - return nil -} diff --git a/backend/mkdir.go b/backend/mkdir.go new file mode 100644 index 0000000..09e4e65 --- /dev/null +++ b/backend/mkdir.go @@ -0,0 +1,81 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// MkdirAll borrowed from stdlib to add ability to set ownership +// as directories are created + +package backend + +import ( + "io/fs" + "os" + + "github.com/versity/versitygw/s3err" +) + +var ( + // TODO: make this configurable + defaultDirPerm fs.FileMode = 0755 +) + +// MkdirAll is similar to os.MkdirAll but it will return +// ErrObjectParentIsFile when appropriate +// MkdirAll creates a directory named path, +// along with any necessary parents, and returns nil, +// or else returns an error. +// The permission bits perm (before umask) are used for all +// directories that MkdirAll creates. +// Any newly created directory is set to provided uid/gid ownership. +// If path is already a directory, MkdirAll does nothing +// and returns nil. +// Any directoy created will be set to provided uid/gid ownership +// if doChown is true. +func MkdirAll(path string, uid, gid int, doChown bool) error { + // Fast path: if we can tell whether path is a directory or file, stop with success or error. + dir, err := os.Stat(path) + if err == nil { + if dir.IsDir() { + return nil + } + return s3err.GetAPIError(s3err.ErrObjectParentIsFile) + } + + // Slow path: make sure parent exists and then call Mkdir for path. + i := len(path) + for i > 0 && os.IsPathSeparator(path[i-1]) { // Skip trailing path separator. + i-- + } + + j := i + for j > 0 && !os.IsPathSeparator(path[j-1]) { // Scan backward over element. + j-- + } + + if j > 1 { + // Create parent. + err = MkdirAll(path[:j-1], uid, gid, doChown) + if err != nil { + return err + } + } + + // Parent now exists; invoke Mkdir and use its result. + err = os.Mkdir(path, defaultDirPerm) + if err != nil { + // Handle arguments like "foo/." by + // double-checking that directory doesn't exist. + dir, err1 := os.Lstat(path) + if err1 == nil && dir.IsDir() { + return nil + } + return err + } + if doChown { + err = os.Chown(path, uid, gid) + if err != nil { + return err + } + } + return nil +} diff --git a/backend/posix/posix.go b/backend/posix/posix.go index fb3dfdc..d83e262 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -46,6 +46,16 @@ type Posix struct { rootfd *os.File rootdir string + + // chownuid/gid enable chowning of files to the account uid/gid + // when objects are uploaded + chownuid bool + chowngid bool + + // euid/egid are the effective uid/gid of the running versitygw process + // used to determine if chowning is needed + euid int + egid int } var _ backend.Backend = &Posix{} @@ -64,7 +74,12 @@ const ( policykey = "user.policy" ) -func New(rootdir string) (*Posix, error) { +type PosixOpts struct { + ChownUID bool + ChownGID bool +} + +func New(rootdir string, opts PosixOpts) (*Posix, error) { err := os.Chdir(rootdir) if err != nil { return nil, fmt.Errorf("chdir %v: %w", rootdir, err) @@ -81,7 +96,14 @@ func New(rootdir string) (*Posix, error) { return nil, fmt.Errorf("xattr not supported on %v", rootdir) } - return &Posix{rootfd: f, rootdir: rootdir}, nil + return &Posix{ + rootfd: f, + rootdir: rootdir, + euid: os.Geteuid(), + egid: os.Getegid(), + chownuid: opts.ChownUID, + chowngid: opts.ChownGID, + }, nil } func (p *Posix) Shutdown() { @@ -168,14 +190,26 @@ func (p *Posix) HeadBucket(_ context.Context, input *s3.HeadBucketInput) (*s3.He return &s3.HeadBucketOutput{}, nil } -func (p *Posix) CreateBucket(_ context.Context, input *s3.CreateBucketInput, acl []byte) error { +var ( + // TODO: make this configurable + defaultDirPerm fs.FileMode = 0755 +) + +func (p *Posix) CreateBucket(ctx context.Context, input *s3.CreateBucketInput, acl []byte) error { if input.Bucket == nil { return s3err.GetAPIError(s3err.ErrInvalidBucketName) } + acct, ok := ctx.Value("account").(auth.Account) + if !ok { + acct = auth.Account{} + } + + uid, gid, doChown := p.getChownIDs(acct) + bucket := *input.Bucket - err := os.Mkdir(bucket, 0777) + err := os.Mkdir(bucket, defaultDirPerm) if err != nil && os.IsExist(err) { return s3err.GetAPIError(s3err.ErrBucketAlreadyExists) } @@ -183,6 +217,13 @@ func (p *Posix) CreateBucket(_ context.Context, input *s3.CreateBucketInput, acl return fmt.Errorf("mkdir bucket: %w", err) } + if doChown { + err := os.Chown(bucket, uid, gid) + if err != nil { + return fmt.Errorf("chown bucket: %w", err) + } + } + if err := xattr.Set(bucket, aclkey, acl); err != nil { return fmt.Errorf("set acl: %w", err) } @@ -287,7 +328,31 @@ func (p *Posix) CreateMultipartUpload(_ context.Context, mpu *s3.CreateMultipart }, nil } -func (p *Posix) CompleteMultipartUpload(_ context.Context, input *s3.CompleteMultipartUploadInput) (*s3.CompleteMultipartUploadOutput, error) { +// getChownIDs returns the uid and gid that should be used for chowning +// the object to the account uid/gid. It also returns a boolean indicating +// if chowning is needed. +func (p *Posix) getChownIDs(acct auth.Account) (int, int, bool) { + uid := p.euid + gid := p.egid + var needsChown bool + if p.chownuid && acct.UserID != p.euid { + uid = acct.UserID + needsChown = true + } + if p.chowngid && acct.GroupID != p.egid { + gid = acct.GroupID + needsChown = true + } + + return uid, gid, needsChown +} + +func (p *Posix) CompleteMultipartUpload(ctx context.Context, input *s3.CompleteMultipartUploadInput) (*s3.CompleteMultipartUploadOutput, error) { + acct, ok := ctx.Value("account").(auth.Account) + if !ok { + acct = auth.Account{} + } + if input.Bucket == nil { return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) } @@ -351,7 +416,8 @@ func (p *Posix) CompleteMultipartUpload(_ context.Context, input *s3.CompleteMul } } - f, err := openTmpFile(filepath.Join(bucket, metaTmpDir), bucket, object, totalsize) + f, err := p.openTmpFile(filepath.Join(bucket, metaTmpDir), bucket, object, + totalsize, acct) if err != nil { return nil, fmt.Errorf("open temp file: %w", err) } @@ -376,9 +442,10 @@ func (p *Posix) CompleteMultipartUpload(_ context.Context, input *s3.CompleteMul objname := filepath.Join(bucket, object) dir := filepath.Dir(objname) if dir != "" { - err = backend.MkdirAll(dir, os.FileMode(0755)) + uid, gid, doChown := p.getChownIDs(acct) + err = backend.MkdirAll(dir, uid, gid, doChown) if err != nil { - return nil, s3err.GetAPIError(s3err.ErrExistingObjectIsDirectory) + return nil, err } } err = f.link() @@ -796,7 +863,12 @@ func (p *Posix) ListParts(_ context.Context, input *s3.ListPartsInput) (s3respon }, nil } -func (p *Posix) UploadPart(_ context.Context, input *s3.UploadPartInput) (string, error) { +func (p *Posix) UploadPart(ctx context.Context, input *s3.UploadPartInput) (string, error) { + acct, ok := ctx.Value("account").(auth.Account) + if !ok { + acct = auth.Account{} + } + if input.Bucket == nil { return "", s3err.GetAPIError(s3err.ErrInvalidBucketName) } @@ -835,8 +907,8 @@ func (p *Posix) UploadPart(_ context.Context, input *s3.UploadPartInput) (string partPath := filepath.Join(objdir, uploadID, fmt.Sprintf("%v", *part)) - f, err := openTmpFile(filepath.Join(bucket, objdir), - bucket, partPath, length) + f, err := p.openTmpFile(filepath.Join(bucket, objdir), + bucket, partPath, length, acct) if err != nil { return "", fmt.Errorf("open temp file: %w", err) } @@ -862,7 +934,12 @@ func (p *Posix) UploadPart(_ context.Context, input *s3.UploadPartInput) (string return etag, nil } -func (p *Posix) UploadPartCopy(_ context.Context, upi *s3.UploadPartCopyInput) (s3response.CopyObjectResult, error) { +func (p *Posix) UploadPartCopy(ctx context.Context, upi *s3.UploadPartCopyInput) (s3response.CopyObjectResult, error) { + acct, ok := ctx.Value("account").(auth.Account) + if !ok { + acct = auth.Account{} + } + if upi.Bucket == nil { return s3response.CopyObjectResult{}, s3err.GetAPIError(s3err.ErrInvalidBucketName) } @@ -929,8 +1006,8 @@ func (p *Posix) UploadPartCopy(_ context.Context, upi *s3.UploadPartCopyInput) ( return s3response.CopyObjectResult{}, s3err.GetAPIError(s3err.ErrInvalidRange) } - f, err := openTmpFile(filepath.Join(*upi.Bucket, objdir), - *upi.Bucket, partPath, length) + f, err := p.openTmpFile(filepath.Join(*upi.Bucket, objdir), + *upi.Bucket, partPath, length, acct) if err != nil { return s3response.CopyObjectResult{}, fmt.Errorf("open temp file: %w", err) } @@ -975,6 +1052,11 @@ func (p *Posix) UploadPartCopy(_ context.Context, upi *s3.UploadPartCopyInput) ( } func (p *Posix) PutObject(ctx context.Context, po *s3.PutObjectInput) (string, error) { + acct, ok := ctx.Value("account").(auth.Account) + if !ok { + acct = auth.Account{} + } + if po.Bucket == nil { return "", s3err.GetAPIError(s3err.ErrInvalidBucketName) } @@ -1008,6 +1090,8 @@ func (p *Posix) PutObject(ctx context.Context, po *s3.PutObjectInput) (string, e name := filepath.Join(*po.Bucket, *po.Key) + uid, gid, doChown := p.getChownIDs(acct) + contentLength := int64(0) if po.ContentLength != nil { contentLength = *po.ContentLength @@ -1021,7 +1105,7 @@ func (p *Posix) PutObject(ctx context.Context, po *s3.PutObjectInput) (string, e return "", s3err.GetAPIError(s3err.ErrDirectoryObjectContainsData) } - err = backend.MkdirAll(name, os.FileMode(0755)) + err = backend.MkdirAll(name, uid, gid, doChown) if err != nil { return "", err } @@ -1042,8 +1126,8 @@ func (p *Posix) PutObject(ctx context.Context, po *s3.PutObjectInput) (string, e return "", s3err.GetAPIError(s3err.ErrExistingObjectIsDirectory) } - f, err := openTmpFile(filepath.Join(*po.Bucket, metaTmpDir), - *po.Bucket, *po.Key, contentLength) + f, err := p.openTmpFile(filepath.Join(*po.Bucket, metaTmpDir), + *po.Bucket, *po.Key, contentLength, acct) if err != nil { return "", fmt.Errorf("open temp file: %w", err) } @@ -1057,7 +1141,7 @@ func (p *Posix) PutObject(ctx context.Context, po *s3.PutObjectInput) (string, e } dir := filepath.Dir(name) if dir != "" { - err = backend.MkdirAll(dir, os.FileMode(0755)) + err = backend.MkdirAll(dir, uid, gid, doChown) if err != nil { return "", s3err.GetAPIError(s3err.ErrExistingObjectIsDirectory) } diff --git a/backend/posix/with_otmpfile.go b/backend/posix/with_otmpfile.go index 256a636..a772800 100644 --- a/backend/posix/with_otmpfile.go +++ b/backend/posix/with_otmpfile.go @@ -27,30 +27,42 @@ import ( "strconv" "syscall" + "github.com/versity/versitygw/auth" + "github.com/versity/versitygw/backend" "golang.org/x/sys/unix" ) const procfddir = "/proc/self/fd" type tmpfile struct { - f *os.File - bucket string - objname string - isOTmp bool - size int64 + f *os.File + bucket string + objname string + isOTmp bool + size int64 + needsChown bool + uid int + gid int } -func openTmpFile(dir, bucket, obj string, size int64) (*tmpfile, error) { +var ( + // TODO: make this configurable + defaultFilePerm uint32 = 0644 +) + +func (p *Posix) openTmpFile(dir, bucket, obj string, size int64, acct auth.Account) (*tmpfile, error) { + uid, gid, doChown := p.getChownIDs(acct) + // O_TMPFILE allows for a file handle to an unnamed file in the filesystem. // This can help reduce contention within the namespace (parent directories), // etc. And will auto cleanup the inode on close if we never link this // file descriptor into the namespace. // Not all filesystems support this, so fallback to CreateTemp for when // this is not supported. - fd, err := unix.Open(dir, unix.O_RDWR|unix.O_TMPFILE|unix.O_CLOEXEC, 0666) + fd, err := unix.Open(dir, unix.O_RDWR|unix.O_TMPFILE|unix.O_CLOEXEC, defaultFilePerm) if err != nil { // O_TMPFILE not supported, try fallback - err := os.MkdirAll(dir, 0700) + err = backend.MkdirAll(dir, uid, gid, doChown) if err != nil { return nil, fmt.Errorf("make temp dir: %w", err) } @@ -59,11 +71,27 @@ func openTmpFile(dir, bucket, obj string, size int64) (*tmpfile, error) { if err != nil { return nil, err } - tmp := &tmpfile{f: f, bucket: bucket, objname: obj, size: size} + tmp := &tmpfile{ + f: f, + bucket: bucket, + objname: obj, + size: size, + needsChown: doChown, + uid: uid, + gid: gid, + } // falloc is best effort, its fine if this fails if size > 0 { tmp.falloc() } + + if doChown { + err := f.Chown(uid, gid) + if err != nil { + return nil, fmt.Errorf("set temp file ownership: %w", err) + } + } + return tmp, nil } @@ -71,11 +99,29 @@ func openTmpFile(dir, bucket, obj string, size int64) (*tmpfile, error) { // later to link file into namespace f := os.NewFile(uintptr(fd), filepath.Join(procfddir, strconv.Itoa(fd))) - tmp := &tmpfile{f: f, bucket: bucket, objname: obj, isOTmp: true, size: size} + tmp := &tmpfile{ + f: f, + bucket: bucket, + objname: obj, + isOTmp: true, + size: size, + needsChown: doChown, + uid: uid, + gid: gid, + } + // falloc is best effort, its fine if this fails if size > 0 { tmp.falloc() } + + if doChown { + err := f.Chown(uid, gid) + if err != nil { + return nil, fmt.Errorf("set temp file ownership: %w", err) + } + } + return tmp, nil } @@ -100,6 +146,13 @@ func (tmp *tmpfile) link() error { return fmt.Errorf("remove stale path: %w", err) } + dir := filepath.Dir(objPath) + + err = backend.MkdirAll(dir, tmp.uid, tmp.gid, tmp.needsChown) + if err != nil { + return fmt.Errorf("make parent dir: %w", err) + } + if !tmp.isOTmp { // O_TMPFILE not suported, use fallback return tmp.fallbackLink() @@ -111,14 +164,14 @@ func (tmp *tmpfile) link() error { } defer procdir.Close() - dir, err := os.Open(filepath.Dir(objPath)) + dirf, err := os.Open(dir) if err != nil { return fmt.Errorf("open parent dir: %w", err) } - defer dir.Close() + defer dirf.Close() err = unix.Linkat(int(procdir.Fd()), filepath.Base(tmp.f.Name()), - int(dir.Fd()), filepath.Base(objPath), unix.AT_SYMLINK_FOLLOW) + int(dirf.Fd()), filepath.Base(objPath), unix.AT_SYMLINK_FOLLOW) if err != nil { return fmt.Errorf("link tmpfile (%q in %q): %w", filepath.Dir(objPath), filepath.Base(tmp.f.Name()), err) @@ -138,6 +191,9 @@ func (tmp *tmpfile) fallbackLink() error { // this will no longer exist defer os.Remove(tempname) + // reset default file mode because CreateTemp uses 0600 + tmp.f.Chmod(fs.FileMode(defaultFilePerm)) + err := tmp.f.Close() if err != nil { return fmt.Errorf("close tmpfile: %w", err) diff --git a/backend/posix/without_otmpfile.go b/backend/posix/without_otmpfile.go index 3c793b9..d6d4194 100644 --- a/backend/posix/without_otmpfile.go +++ b/backend/posix/without_otmpfile.go @@ -24,6 +24,9 @@ import ( "io/fs" "os" "path/filepath" + + "github.com/versity/versitygw/auth" + "github.com/versity/versitygw/backend" ) type tmpfile struct { @@ -33,20 +36,36 @@ type tmpfile struct { size int64 } -func openTmpFile(dir, bucket, obj string, size int64) (*tmpfile, error) { +func (p *Posix) openTmpFile(dir, bucket, obj string, size int64, acct auth.Account) (*tmpfile, error) { + uid, gid, doChown := p.getChownIDs(acct) + // Create a temp file for upload while in progress (see link comments below). - err := os.MkdirAll(dir, 0700) + var err error + err = backend.MkdirAll(dir, uid, gid, doChown) if err != nil { return nil, fmt.Errorf("make temp dir: %w", err) } f, err := os.CreateTemp(dir, fmt.Sprintf("%x.", sha256.Sum256([]byte(obj)))) if err != nil { - return nil, err + return nil, fmt.Errorf("create temp file: %w", err) } + + if doChown { + err := f.Chown(uid, gid) + if err != nil { + return nil, fmt.Errorf("set temp file ownership: %w", err) + } + } + return &tmpfile{f: f, bucket: bucket, objname: obj, size: size}, nil } +var ( + // TODO: make this configurable + defaultFilePerm fs.FileMode = 0644 +) + func (tmp *tmpfile) link() error { tempname := tmp.f.Name() // cleanup in case anything goes wrong, if rename succeeds then @@ -64,6 +83,9 @@ func (tmp *tmpfile) link() error { return fmt.Errorf("remove stale path: %w", err) } + // reset default file mode because CreateTemp uses 0600 + tmp.f.Chmod(defaultFilePerm) + err = tmp.f.Close() if err != nil { return fmt.Errorf("close tmpfile: %w", err) diff --git a/backend/scoutfs/scoutfs.go b/backend/scoutfs/scoutfs.go index 22ae864..89872c0 100644 --- a/backend/scoutfs/scoutfs.go +++ b/backend/scoutfs/scoutfs.go @@ -29,11 +29,18 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/pkg/xattr" + "github.com/versity/versitygw/auth" "github.com/versity/versitygw/backend" "github.com/versity/versitygw/backend/posix" "github.com/versity/versitygw/s3err" ) +type ScoutfsOpts struct { + ChownUID bool + ChownGID bool + GlacierMode bool +} + type ScoutFS struct { *posix.Posix rootfd *os.File @@ -49,6 +56,16 @@ type ScoutFS struct { // ListObjects: if file offline, set obj storage class to GLACIER // RestoreObject: add batch stage request to file glaciermode bool + + // chownuid/gid enable chowning of files to the account uid/gid + // when objects are uploaded + chownuid bool + chowngid bool + + // euid/egid are the effective uid/gid of the running versitygw process + // used to determine if chowning is needed + euid int + egid int } var _ backend.Backend = &ScoutFS{} @@ -92,14 +109,6 @@ const ( ExtCacheDone ) -// Option sets various options for scoutfs -type Option func(s *ScoutFS) - -// WithGlacierEmulation sets glacier mode emulation -func WithGlacierEmulation() Option { - return func(s *ScoutFS) { s.glaciermode = true } -} - func (s *ScoutFS) Shutdown() { s.Posix.Shutdown() s.rootfd.Close() @@ -110,10 +119,47 @@ func (*ScoutFS) String() string { return "ScoutFS Gateway" } +// getChownIDs returns the uid and gid that should be used for chowning +// the object to the account uid/gid. It also returns a boolean indicating +// if chowning is needed. +func (s *ScoutFS) getChownIDs(acct auth.Account) (int, int, bool) { + uid := s.euid + gid := s.egid + var needsChown bool + if s.chownuid && acct.UserID != s.euid { + uid = acct.UserID + needsChown = true + } + if s.chowngid && acct.GroupID != s.egid { + gid = acct.GroupID + needsChown = true + } + + return uid, gid, needsChown +} + // CompleteMultipartUpload scoutfs complete upload uses scoutfs move blocks // ioctl to not have to read and copy the part data to the final object. This // saves a read and write cycle for all mutlipart uploads. -func (s *ScoutFS) CompleteMultipartUpload(_ context.Context, input *s3.CompleteMultipartUploadInput) (*s3.CompleteMultipartUploadOutput, error) { +func (s *ScoutFS) CompleteMultipartUpload(ctx context.Context, input *s3.CompleteMultipartUploadInput) (*s3.CompleteMultipartUploadOutput, error) { + acct, ok := ctx.Value("account").(auth.Account) + if !ok { + acct = auth.Account{} + } + + if input.Bucket == nil { + return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) + } + if input.Key == nil { + return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) + } + if input.UploadId == nil { + return nil, s3err.GetAPIError(s3err.ErrNoSuchUpload) + } + if input.MultipartUpload == nil { + return nil, s3err.GetAPIError(s3err.ErrInvalidRequest) + } + bucket := *input.Bucket object := *input.Key uploadID := *input.UploadId @@ -174,7 +220,7 @@ func (s *ScoutFS) CompleteMultipartUpload(_ context.Context, input *s3.CompleteM // use totalsize=0 because we wont be writing to the file, only moving // extents around. so we dont want to fallocate this. - f, err := openTmpFile(filepath.Join(bucket, metaTmpDir), bucket, object, 0) + f, err := s.openTmpFile(filepath.Join(bucket, metaTmpDir), bucket, object, 0, acct) if err != nil { return nil, fmt.Errorf("open temp file: %w", err) } @@ -203,9 +249,10 @@ func (s *ScoutFS) CompleteMultipartUpload(_ context.Context, input *s3.CompleteM objname := filepath.Join(bucket, object) dir := filepath.Dir(objname) if dir != "" { - err = backend.MkdirAll(dir, os.FileMode(0755)) + uid, gid, doChown := s.getChownIDs(acct) + err = backend.MkdirAll(dir, uid, gid, doChown) if err != nil { - return nil, s3err.GetAPIError(s3err.ErrExistingObjectIsDirectory) + return nil, err } } err = f.link() diff --git a/backend/scoutfs/scoutfs_compat.go b/backend/scoutfs/scoutfs_compat.go index 92652eb..194a440 100644 --- a/backend/scoutfs/scoutfs_compat.go +++ b/backend/scoutfs/scoutfs_compat.go @@ -17,7 +17,6 @@ package scoutfs import ( - "crypto/sha256" "errors" "fmt" "io/fs" @@ -29,11 +28,16 @@ import ( "golang.org/x/sys/unix" "github.com/versity/scoutfs-go" + "github.com/versity/versitygw/auth" + "github.com/versity/versitygw/backend" "github.com/versity/versitygw/backend/posix" ) -func New(rootdir string, opts ...Option) (*ScoutFS, error) { - p, err := posix.New(rootdir) +func New(rootdir string, opts ScoutfsOpts) (*ScoutFS, error) { + p, err := posix.New(rootdir, posix.PosixOpts{ + ChownUID: opts.ChownUID, + ChownGID: opts.ChownGID, + }) if err != nil { return nil, err } @@ -43,60 +47,70 @@ func New(rootdir string, opts ...Option) (*ScoutFS, error) { return nil, fmt.Errorf("open %v: %w", rootdir, err) } - s := &ScoutFS{Posix: p, rootfd: f, rootdir: rootdir} - for _, opt := range opts { - opt(s) - } - - return s, nil + return &ScoutFS{ + Posix: p, + rootfd: f, + rootdir: rootdir, + chownuid: opts.ChownUID, + chowngid: opts.ChownGID, + }, nil } const procfddir = "/proc/self/fd" type tmpfile struct { - f *os.File - bucket string - objname string - isOTmp bool - size int64 + f *os.File + bucket string + objname string + size int64 + needsChown bool + uid int + gid int } -func openTmpFile(dir, bucket, obj string, size int64) (*tmpfile, error) { +var ( + // TODO: make this configurable + defaultFilePerm uint32 = 0644 +) + +func (s *ScoutFS) openTmpFile(dir, bucket, obj string, size int64, acct auth.Account) (*tmpfile, error) { + uid, gid, doChown := s.getChownIDs(acct) + // O_TMPFILE allows for a file handle to an unnamed file in the filesystem. // This can help reduce contention within the namespace (parent directories), // etc. And will auto cleanup the inode on close if we never link this // file descriptor into the namespace. - // Not all filesystems support this, so fallback to CreateTemp for when - // this is not supported. - fd, err := unix.Open(dir, unix.O_RDWR|unix.O_TMPFILE|unix.O_CLOEXEC, 0666) + fd, err := unix.Open(dir, unix.O_RDWR|unix.O_TMPFILE|unix.O_CLOEXEC, defaultFilePerm) if err != nil { - // O_TMPFILE not supported, try fallback - err := os.MkdirAll(dir, 0700) - if err != nil { - return nil, fmt.Errorf("make temp dir: %w", err) - } - f, err := os.CreateTemp(dir, - fmt.Sprintf("%x.", sha256.Sum256([]byte(obj)))) - if err != nil { - return nil, err - } - tmp := &tmpfile{f: f, bucket: bucket, objname: obj, size: size} - // falloc is best effort, its fine if this fails - if size > 0 { - tmp.falloc() - } - return tmp, nil + return nil, err } // for O_TMPFILE, filename is /proc/self/fd/ to be used // later to link file into namespace f := os.NewFile(uintptr(fd), filepath.Join(procfddir, strconv.Itoa(fd))) - tmp := &tmpfile{f: f, bucket: bucket, objname: obj, isOTmp: true, size: size} + tmp := &tmpfile{ + f: f, + bucket: bucket, + objname: obj, + size: size, + needsChown: doChown, + uid: uid, + gid: gid, + } + // falloc is best effort, its fine if this fails if size > 0 { tmp.falloc() } + + if doChown { + err := f.Chown(uid, gid) + if err != nil { + return nil, fmt.Errorf("set temp file ownership: %w", err) + } + } + return tmp, nil } @@ -121,9 +135,11 @@ func (tmp *tmpfile) link() error { return fmt.Errorf("remove stale path: %w", err) } - if !tmp.isOTmp { - // O_TMPFILE not suported, use fallback - return tmp.fallbackLink() + dir := filepath.Dir(objPath) + + err = backend.MkdirAll(dir, tmp.uid, tmp.gid, tmp.needsChown) + if err != nil { + return fmt.Errorf("make parent dir: %w", err) } procdir, err := os.Open(procfddir) @@ -132,14 +148,14 @@ func (tmp *tmpfile) link() error { } defer procdir.Close() - dir, err := os.Open(filepath.Dir(objPath)) + dirf, err := os.Open(dir) if err != nil { return fmt.Errorf("open parent dir: %w", err) } - defer dir.Close() + defer dirf.Close() err = unix.Linkat(int(procdir.Fd()), filepath.Base(tmp.f.Name()), - int(dir.Fd()), filepath.Base(objPath), unix.AT_SYMLINK_FOLLOW) + int(dirf.Fd()), filepath.Base(objPath), unix.AT_SYMLINK_FOLLOW) if err != nil { return fmt.Errorf("link tmpfile: %w", err) } @@ -152,26 +168,6 @@ func (tmp *tmpfile) link() error { return nil } -func (tmp *tmpfile) fallbackLink() error { - tempname := tmp.f.Name() - // cleanup in case anything goes wrong, if rename succeeds then - // this will no longer exist - defer os.Remove(tempname) - - err := tmp.f.Close() - if err != nil { - return fmt.Errorf("close tmpfile: %w", err) - } - - objPath := filepath.Join(tmp.bucket, tmp.objname) - err = os.Rename(tempname, objPath) - if err != nil { - return fmt.Errorf("rename tmpfile: %w", err) - } - - return nil -} - func (tmp *tmpfile) Write(b []byte) (int, error) { if int64(len(b)) > tmp.size { return 0, fmt.Errorf("write exceeds content length %v", tmp.size) diff --git a/backend/scoutfs/scoutfs_incompat.go b/backend/scoutfs/scoutfs_incompat.go index f88abfa..4a5fda2 100644 --- a/backend/scoutfs/scoutfs_incompat.go +++ b/backend/scoutfs/scoutfs_incompat.go @@ -20,9 +20,11 @@ import ( "errors" "fmt" "os" + + "github.com/versity/versitygw/auth" ) -func New(rootdir string, opts ...Option) (*ScoutFS, error) { +func New(rootdir string, opts ScoutfsOpts) (*ScoutFS, error) { return nil, fmt.Errorf("scoutfs only available on linux") } @@ -34,7 +36,12 @@ var ( errNotSupported = errors.New("not supported") ) -func openTmpFile(_, _, _ string, _ int64) (*tmpfile, error) { +func (s *ScoutFS) openTmpFile(_, _, _ string, _ int64, _ auth.Account) (*tmpfile, error) { + // make these look used for static check + _ = s.chownuid + _ = s.chowngid + _ = s.euid + _ = s.egid return nil, errNotSupported } diff --git a/cmd/versitygw/gateway_test.go b/cmd/versitygw/gateway_test.go index be2bff6..e29d59f 100644 --- a/cmd/versitygw/gateway_test.go +++ b/cmd/versitygw/gateway_test.go @@ -56,7 +56,7 @@ func initPosix(ctx context.Context) { log.Fatalf("make temp directory: %v", err) } - be, err := posix.New(tempdir) + be, err := posix.New(tempdir, posix.PosixOpts{}) if err != nil { log.Fatalf("init posix: %v", err) } diff --git a/cmd/versitygw/posix.go b/cmd/versitygw/posix.go index 6f85292..c9fe416 100644 --- a/cmd/versitygw/posix.go +++ b/cmd/versitygw/posix.go @@ -21,6 +21,10 @@ import ( "github.com/versity/versitygw/backend/posix" ) +var ( + chownuid, chowngid bool +) + func posixCommand() *cli.Command { return &cli.Command{ Name: "posix", @@ -36,6 +40,20 @@ bucket: mybucket object: a/b/c/myobject will be translated into the file /mnt/fs/gwroot/mybucket/a/b/c/myobject`, Action: runPosix, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "chuid", + Usage: "chown newly created files and directories to client account UID", + EnvVars: []string{"VGW_CHOWN_UID"}, + Destination: &chownuid, + }, + &cli.BoolFlag{ + Name: "chgid", + Usage: "chown newly created files and directories to client account GID", + EnvVars: []string{"VGW_CHOWN_GID"}, + Destination: &chowngid, + }, + }, } } @@ -44,7 +62,10 @@ func runPosix(ctx *cli.Context) error { return fmt.Errorf("no directory provided for operation") } - be, err := posix.New(ctx.Args().Get(0)) + be, err := posix.New(ctx.Args().Get(0), posix.PosixOpts{ + ChownUID: chownuid, + ChownGID: chowngid, + }) if err != nil { return fmt.Errorf("init posix: %v", err) } diff --git a/cmd/versitygw/scoutfs.go b/cmd/versitygw/scoutfs.go index 65041de..8ab1470 100644 --- a/cmd/versitygw/scoutfs.go +++ b/cmd/versitygw/scoutfs.go @@ -51,6 +51,18 @@ move interfaces as well as support for tiered filesystems.`, EnvVars: []string{"VGW_SCOUTFS_GLACIER"}, Destination: &glacier, }, + &cli.BoolFlag{ + Name: "chuid", + Usage: "chown newly created files and directories to client account UID", + EnvVars: []string{"VGW_CHOWN_UID"}, + Destination: &chownuid, + }, + &cli.BoolFlag{ + Name: "chgid", + Usage: "chown newly created files and directories to client account GID", + EnvVars: []string{"VGW_CHOWN_GID"}, + Destination: &chowngid, + }, }, } } @@ -60,12 +72,12 @@ func runScoutfs(ctx *cli.Context) error { return fmt.Errorf("no directory provided for operation") } - var opts []scoutfs.Option - if glacier { - opts = append(opts, scoutfs.WithGlacierEmulation()) - } + var opts scoutfs.ScoutfsOpts + opts.GlacierMode = glacier + opts.ChownUID = chownuid + opts.ChownGID = chowngid - be, err := scoutfs.New(ctx.Args().Get(0), opts...) + be, err := scoutfs.New(ctx.Args().Get(0), opts) if err != nil { return fmt.Errorf("init scoutfs: %v", err) } diff --git a/extra/example.conf b/extra/example.conf index c930446..eac1128 100644 --- a/extra/example.conf +++ b/extra/example.conf @@ -216,20 +216,24 @@ ROOT_SECRET_ACCESS_KEY= # below the "bucket directory" are treated as the objects. The object # name is split on "/" separator to translate to posix storage. # For example: -# top level: /mnt/fs/gwroot +# top level (VGW_BACKEND_ARG): /mnt/fs/gwroot # bucket: mybucket # object: a/b/c/myobject # will be translated into the file /mnt/fs/gwroot/mybucket/a/b/c/myobject -# There are currently no further options other than VGW_BACKEND_ARG for the -# posix backend. +# The VGW_CHOWN_UID and VGW_CHOWN_GID options will enable the gateway to +# change the ownership of newly created files and directories to the IAM +# account UID/GID. +#VGW_CHOWN_UID=false +#VGW_CHOWN_GID=false ########### # scoutfs # ########### # The scoutfs backend requires a ScoutFS filesystem type for the backend -# path. The glacier mode functionality requires ScoutAM to be configured +# path. The object to posix name mappings follow the same rules as posix for +# scoutfs. The glacier mode functionality requires ScoutAM to be configured # for tiering data from the ScoutFS filesystem to a mass stroage system. # The mass storage system is often one or more tape libraries. Due to the # high latency of tape, the glacier mode functionality is designed to @@ -248,6 +252,12 @@ ROOT_SECRET_ACCESS_KEY= # RestoreObject: add batch stage request to file #VGW_SCOUTFS_GLACIER=false +# The VGW_CHOWN_UID and VGW_CHOWN_GID options will enable the gateway to +# change the ownership of newly created files and directories to the IAM +# account UID/GID. +#VGW_CHOWN_UID=false +#VGW_CHOWN_GID=false + ###### # s3 # ######