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 # ######