From e9b41d53b65d437110ec7b5f683d82bdf52947c9 Mon Sep 17 00:00:00 2001 From: Ben McClelland Date: Sun, 31 Mar 2024 15:13:01 -0700 Subject: [PATCH] feat: implement read only option for posix/scoutfs Fixes #484. When read only option selected, backend will return AccessDenied for any operations that would update data or metadata in the backend. This include creating and deleting objects and buckets as well as updating object metadata. This will still allow admin to change bucket owner since read only is mainly intended for s3 api only. --- backend/posix/posix.go | 65 +++++++++++++++++++++++++++++ backend/scoutfs/scoutfs.go | 8 ++++ backend/scoutfs/scoutfs_compat.go | 2 + backend/scoutfs/scoutfs_incompat.go | 1 + cmd/versitygw/posix.go | 8 ++++ cmd/versitygw/scoutfs.go | 11 +++++ extra/example.conf | 12 ++++++ 7 files changed, 107 insertions(+) diff --git a/backend/posix/posix.go b/backend/posix/posix.go index 1ce09f33..b0ba5500 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -52,6 +52,9 @@ type Posix struct { chownuid bool chowngid bool + // read only mode prevents any backend modifications + readonly bool + // euid/egid are the effective uid/gid of the running versitygw process // used to determine if chowning is needed euid int @@ -77,6 +80,7 @@ const ( type PosixOpts struct { ChownUID bool ChownGID bool + ReadOnly bool } func New(rootdir string, opts PosixOpts) (*Posix, error) { @@ -103,6 +107,7 @@ func New(rootdir string, opts PosixOpts) (*Posix, error) { egid: os.Getegid(), chownuid: opts.ChownUID, chowngid: opts.ChownGID, + readonly: opts.ReadOnly, }, nil } @@ -196,6 +201,10 @@ var ( ) func (p *Posix) CreateBucket(ctx context.Context, input *s3.CreateBucketInput, acl []byte) error { + if p.readonly { + return s3err.GetAPIError(s3err.ErrAccessDenied) + } + if input.Bucket == nil { return s3err.GetAPIError(s3err.ErrInvalidBucketName) } @@ -232,6 +241,10 @@ func (p *Posix) CreateBucket(ctx context.Context, input *s3.CreateBucketInput, a } func (p *Posix) DeleteBucket(_ context.Context, input *s3.DeleteBucketInput) error { + if p.readonly { + return s3err.GetAPIError(s3err.ErrAccessDenied) + } + if input.Bucket == nil { return s3err.GetAPIError(s3err.ErrInvalidBucketName) } @@ -265,6 +278,10 @@ func (p *Posix) DeleteBucket(_ context.Context, input *s3.DeleteBucketInput) err } func (p *Posix) CreateMultipartUpload(_ context.Context, mpu *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) { + if p.readonly { + return nil, s3err.GetAPIError(s3err.ErrAccessDenied) + } + if mpu.Bucket == nil { return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) } @@ -348,6 +365,10 @@ func (p *Posix) getChownIDs(acct auth.Account) (int, int, bool) { } func (p *Posix) CompleteMultipartUpload(ctx context.Context, input *s3.CompleteMultipartUploadInput) (*s3.CompleteMultipartUploadOutput, error) { + if p.readonly { + return nil, s3err.GetAPIError(s3err.ErrAccessDenied) + } + acct, ok := ctx.Value("account").(auth.Account) if !ok { acct = auth.Account{} @@ -571,6 +592,10 @@ func isValidMeta(val string) bool { } func (p *Posix) AbortMultipartUpload(_ context.Context, mpu *s3.AbortMultipartUploadInput) error { + if p.readonly { + return s3err.GetAPIError(s3err.ErrAccessDenied) + } + if mpu.Bucket == nil { return s3err.GetAPIError(s3err.ErrInvalidBucketName) } @@ -870,6 +895,10 @@ func (p *Posix) ListParts(_ context.Context, input *s3.ListPartsInput) (s3respon } func (p *Posix) UploadPart(ctx context.Context, input *s3.UploadPartInput) (string, error) { + if p.readonly { + return "", s3err.GetAPIError(s3err.ErrAccessDenied) + } + acct, ok := ctx.Value("account").(auth.Account) if !ok { acct = auth.Account{} @@ -947,6 +976,10 @@ func (p *Posix) UploadPart(ctx context.Context, input *s3.UploadPartInput) (stri } func (p *Posix) UploadPartCopy(ctx context.Context, upi *s3.UploadPartCopyInput) (s3response.CopyObjectResult, error) { + if p.readonly { + return s3response.CopyObjectResult{}, s3err.GetAPIError(s3err.ErrAccessDenied) + } + acct, ok := ctx.Value("account").(auth.Account) if !ok { acct = auth.Account{} @@ -1070,6 +1103,10 @@ func (p *Posix) UploadPartCopy(ctx context.Context, upi *s3.UploadPartCopyInput) } func (p *Posix) PutObject(ctx context.Context, po *s3.PutObjectInput) (string, error) { + if p.readonly { + return "", s3err.GetAPIError(s3err.ErrAccessDenied) + } + acct, ok := ctx.Value("account").(auth.Account) if !ok { acct = auth.Account{} @@ -1198,6 +1235,10 @@ func (p *Posix) PutObject(ctx context.Context, po *s3.PutObjectInput) (string, e } func (p *Posix) DeleteObject(_ context.Context, input *s3.DeleteObjectInput) error { + if p.readonly { + return s3err.GetAPIError(s3err.ErrAccessDenied) + } + if input.Bucket == nil { return s3err.GetAPIError(s3err.ErrInvalidBucketName) } @@ -1262,6 +1303,10 @@ func (p *Posix) removeParents(bucket, object string) error { } func (p *Posix) DeleteObjects(ctx context.Context, input *s3.DeleteObjectsInput) (s3response.DeleteResult, error) { + if p.readonly { + return s3response.DeleteResult{}, s3err.GetAPIError(s3err.ErrAccessDenied) + } + // delete object already checks bucket delResult, errs := []types.DeletedObject{}, []types.Error{} for _, obj := range input.Delete.Objects { @@ -1477,6 +1522,10 @@ func (p *Posix) HeadObject(_ context.Context, input *s3.HeadObjectInput) (*s3.He } func (p *Posix) CopyObject(ctx context.Context, input *s3.CopyObjectInput) (*s3.CopyObjectOutput, error) { + if p.readonly { + return nil, s3err.GetAPIError(s3err.ErrAccessDenied) + } + if input.Bucket == nil { return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) } @@ -1746,6 +1795,10 @@ func (p *Posix) ListObjectsV2(_ context.Context, input *s3.ListObjectsV2Input) ( } func (p *Posix) PutBucketAcl(_ context.Context, bucket string, data []byte) error { + if p.readonly { + return s3err.GetAPIError(s3err.ErrAccessDenied) + } + _, err := os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { return s3err.GetAPIError(s3err.ErrNoSuchBucket) @@ -1784,6 +1837,10 @@ func (p *Posix) GetBucketAcl(_ context.Context, input *s3.GetBucketAclInput) ([] } func (p *Posix) PutBucketTagging(_ context.Context, bucket string, tags map[string]string) error { + if p.readonly { + return s3err.GetAPIError(s3err.ErrAccessDenied) + } + _, err := os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { return s3err.GetAPIError(s3err.ErrNoSuchBucket) @@ -1869,6 +1926,10 @@ func (p *Posix) getXattrTags(bucket, object string) (map[string]string, error) { } func (p *Posix) PutObjectTagging(_ context.Context, bucket, object string, tags map[string]string) error { + if p.readonly { + return s3err.GetAPIError(s3err.ErrAccessDenied) + } + _, err := os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { return s3err.GetAPIError(s3err.ErrNoSuchBucket) @@ -1909,6 +1970,10 @@ func (p *Posix) DeleteObjectTagging(ctx context.Context, bucket, object string) } func (p *Posix) PutBucketPolicy(ctx context.Context, bucket string, policy []byte) error { + if p.readonly { + return s3err.GetAPIError(s3err.ErrAccessDenied) + } + _, err := os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { return s3err.GetAPIError(s3err.ErrNoSuchBucket) diff --git a/backend/scoutfs/scoutfs.go b/backend/scoutfs/scoutfs.go index b5c065e0..13af65c4 100644 --- a/backend/scoutfs/scoutfs.go +++ b/backend/scoutfs/scoutfs.go @@ -39,6 +39,7 @@ import ( type ScoutfsOpts struct { ChownUID bool ChownGID bool + ReadOnly bool GlacierMode bool } @@ -63,6 +64,9 @@ type ScoutFS struct { chownuid bool chowngid bool + // read only mode prevents any backend modifications + readonly bool + // euid/egid are the effective uid/gid of the running versitygw process // used to determine if chowning is needed euid int @@ -143,6 +147,10 @@ func (s *ScoutFS) getChownIDs(acct auth.Account) (int, int, bool) { // 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(ctx context.Context, input *s3.CompleteMultipartUploadInput) (*s3.CompleteMultipartUploadOutput, error) { + if s.readonly { + return nil, s3err.GetAPIError(s3err.ErrAccessDenied) + } + acct, ok := ctx.Value("account").(auth.Account) if !ok { acct = auth.Account{} diff --git a/backend/scoutfs/scoutfs_compat.go b/backend/scoutfs/scoutfs_compat.go index 194a440d..d1809895 100644 --- a/backend/scoutfs/scoutfs_compat.go +++ b/backend/scoutfs/scoutfs_compat.go @@ -37,6 +37,7 @@ func New(rootdir string, opts ScoutfsOpts) (*ScoutFS, error) { p, err := posix.New(rootdir, posix.PosixOpts{ ChownUID: opts.ChownUID, ChownGID: opts.ChownGID, + ReadOnly: opts.ReadOnly, }) if err != nil { return nil, err @@ -53,6 +54,7 @@ func New(rootdir string, opts ScoutfsOpts) (*ScoutFS, error) { rootdir: rootdir, chownuid: opts.ChownUID, chowngid: opts.ChownGID, + readonly: opts.ReadOnly, }, nil } diff --git a/backend/scoutfs/scoutfs_incompat.go b/backend/scoutfs/scoutfs_incompat.go index 4a5fda2b..32bad1ca 100644 --- a/backend/scoutfs/scoutfs_incompat.go +++ b/backend/scoutfs/scoutfs_incompat.go @@ -42,6 +42,7 @@ func (s *ScoutFS) openTmpFile(_, _, _ string, _ int64, _ auth.Account) (*tmpfile _ = s.chowngid _ = s.euid _ = s.egid + _ = s.readonly return nil, errNotSupported } diff --git a/cmd/versitygw/posix.go b/cmd/versitygw/posix.go index c9fe4165..5cc71c42 100644 --- a/cmd/versitygw/posix.go +++ b/cmd/versitygw/posix.go @@ -23,6 +23,7 @@ import ( var ( chownuid, chowngid bool + readonly bool ) func posixCommand() *cli.Command { @@ -53,6 +54,12 @@ will be translated into the file /mnt/fs/gwroot/mybucket/a/b/c/myobject`, EnvVars: []string{"VGW_CHOWN_GID"}, Destination: &chowngid, }, + &cli.BoolFlag{ + Name: "readonly", + Usage: "allow only read operations to backend", + EnvVars: []string{"VGW_READ_ONLY"}, + Destination: &readonly, + }, }, } } @@ -65,6 +72,7 @@ func runPosix(ctx *cli.Context) error { be, err := posix.New(ctx.Args().Get(0), posix.PosixOpts{ ChownUID: chownuid, ChownGID: chowngid, + ReadOnly: readonly, }) if err != nil { return fmt.Errorf("init posix: %v", err) diff --git a/cmd/versitygw/scoutfs.go b/cmd/versitygw/scoutfs.go index 8ab1470e..cbb3ed59 100644 --- a/cmd/versitygw/scoutfs.go +++ b/cmd/versitygw/scoutfs.go @@ -23,6 +23,10 @@ import ( var ( glacier bool + + // defined in posix.go: + // chownuid, chowngid bool + // readonly bool ) func scoutfsCommand() *cli.Command { @@ -63,6 +67,12 @@ move interfaces as well as support for tiered filesystems.`, EnvVars: []string{"VGW_CHOWN_GID"}, Destination: &chowngid, }, + &cli.BoolFlag{ + Name: "readonly", + Usage: "allow only read operations to backend", + EnvVars: []string{"VGW_READ_ONLY"}, + Destination: &readonly, + }, }, } } @@ -76,6 +86,7 @@ func runScoutfs(ctx *cli.Context) error { opts.GlacierMode = glacier opts.ChownUID = chownuid opts.ChownGID = chowngid + opts.ReadOnly = readonly be, err := scoutfs.New(ctx.Args().Get(0), opts) if err != nil { diff --git a/extra/example.conf b/extra/example.conf index 9378e1f7..ce83a256 100644 --- a/extra/example.conf +++ b/extra/example.conf @@ -254,6 +254,12 @@ ROOT_SECRET_ACCESS_KEY= #VGW_CHOWN_UID=false #VGW_CHOWN_GID=false +# The VGW_READ_ONLY option will disable all write operations to the backend +# filesystem. This is useful for creating a read-only gateway for clients. +# This will prevent any PUT, POST, DELETE, and multipart upload operations +# for objects and buckets as well as preventing updating metadata for objects. +#VGW_READ_ONLY=false + ########### # scoutfs # ########### @@ -285,6 +291,12 @@ ROOT_SECRET_ACCESS_KEY= #VGW_CHOWN_UID=false #VGW_CHOWN_GID=false +# The VGW_READ_ONLY option will disable all write operations to the backend +# filesystem. This is useful for creating a read-only gateway for clients. +# This will prevent any PUT, POST, DELETE, and multipart upload operations +# for objects and buckets as well as preventing updating metadata for objects. +#VGW_READ_ONLY=false + ###### # s3 # ######