From f72d6349fef86fa099cc0ecb205115649341af9b Mon Sep 17 00:00:00 2001 From: Ben McClelland Date: Sat, 30 Mar 2024 20:54:56 -0700 Subject: [PATCH 1/2] feat: consolidate scoutfs/posix mkdir in backend We had some duplicated code that we can bring into the backend package so that we can remove duplications. This moves the mkdir implementation into backend so that both posix and scoutfs can call the same implementation. --- backend/common.go | 46 ++++++++++++++++++++++++++++++++++ backend/posix/posix.go | 51 +++----------------------------------- backend/scoutfs/scoutfs.go | 47 +---------------------------------- 3 files changed, 50 insertions(+), 94 deletions(-) diff --git a/backend/common.go b/backend/common.go index b4ebf91e..e3e8b18e 100644 --- a/backend/common.go +++ b/backend/common.go @@ -19,6 +19,7 @@ import ( "encoding/hex" "fmt" "io/fs" + "os" "strconv" "strings" "time" @@ -120,3 +121,48 @@ 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/posix/posix.go b/backend/posix/posix.go index 5e3c97e3..fb3dfdc5 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -376,7 +376,7 @@ func (p *Posix) CompleteMultipartUpload(_ context.Context, input *s3.CompleteMul objname := filepath.Join(bucket, object) dir := filepath.Dir(objname) if dir != "" { - err = mkdirAll(dir, os.FileMode(0755), bucket, object) + err = backend.MkdirAll(dir, os.FileMode(0755)) if err != nil { return nil, s3err.GetAPIError(s3err.ErrExistingObjectIsDirectory) } @@ -497,51 +497,6 @@ func isValidMeta(val string) bool { return false } -// mkdirAll is similar to os.MkdirAll but it will return ErrObjectParentIsFile -// when appropriate -func mkdirAll(path string, perm os.FileMode, bucket, object string) 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, bucket, object) - 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 -} - func (p *Posix) AbortMultipartUpload(_ context.Context, mpu *s3.AbortMultipartUploadInput) error { if mpu.Bucket == nil { return s3err.GetAPIError(s3err.ErrInvalidBucketName) @@ -1066,7 +1021,7 @@ func (p *Posix) PutObject(ctx context.Context, po *s3.PutObjectInput) (string, e return "", s3err.GetAPIError(s3err.ErrDirectoryObjectContainsData) } - err = mkdirAll(name, os.FileMode(0755), *po.Bucket, *po.Key) + err = backend.MkdirAll(name, os.FileMode(0755)) if err != nil { return "", err } @@ -1102,7 +1057,7 @@ func (p *Posix) PutObject(ctx context.Context, po *s3.PutObjectInput) (string, e } dir := filepath.Dir(name) if dir != "" { - err = mkdirAll(dir, os.FileMode(0755), *po.Bucket, *po.Key) + err = backend.MkdirAll(dir, os.FileMode(0755)) if err != nil { return "", s3err.GetAPIError(s3err.ErrExistingObjectIsDirectory) } diff --git a/backend/scoutfs/scoutfs.go b/backend/scoutfs/scoutfs.go index 9b248363..22ae8646 100644 --- a/backend/scoutfs/scoutfs.go +++ b/backend/scoutfs/scoutfs.go @@ -203,7 +203,7 @@ func (s *ScoutFS) CompleteMultipartUpload(_ context.Context, input *s3.CompleteM objname := filepath.Join(bucket, object) dir := filepath.Dir(objname) if dir != "" { - err = mkdirAll(dir, os.FileMode(0755), bucket, object) + err = backend.MkdirAll(dir, os.FileMode(0755)) if err != nil { return nil, s3err.GetAPIError(s3err.ErrExistingObjectIsDirectory) } @@ -310,51 +310,6 @@ func isValidMeta(val string) bool { return false } -// mkdirAll is similar to os.MkdirAll but it will return ErrObjectParentIsFile -// when appropriate -func mkdirAll(path string, perm os.FileMode, bucket, object string) 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, bucket, object) - 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 -} - func (s *ScoutFS) HeadObject(_ context.Context, input *s3.HeadObjectInput) (*s3.HeadObjectOutput, error) { bucket := *input.Bucket object := *input.Key From c228bbfd7961b2fa8f0849054c73bd355b800b07 Mon Sep 17 00:00:00 2001 From: Ben McClelland Date: Tue, 27 Feb 2024 15:41:21 -0800 Subject: [PATCH 2/2] 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 e3e8b18e..b4ebf91e 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 00000000..09e4e655 --- /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 fb3dfdc5..d83e262d 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 256a6364..a772800a 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 3c793b95..d6d41944 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 22ae8646..89872c0e 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 92652ebf..194a440d 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 f88abfa8..4a5fda2b 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 be2bff64..e29d59f2 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 6f85292e..c9fe4165 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 65041dee..8ab1470e 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 c930446a..eac11285 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 # ######