Compare commits

...

1 Commits

Author SHA1 Message Date
Ben McClelland
e9b41d53b6 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.
2024-04-08 10:39:22 -07:00
7 changed files with 107 additions and 0 deletions

View File

@@ -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)

View File

@@ -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{}

View File

@@ -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
}

View File

@@ -42,6 +42,7 @@ func (s *ScoutFS) openTmpFile(_, _, _ string, _ int64, _ auth.Account) (*tmpfile
_ = s.chowngid
_ = s.euid
_ = s.egid
_ = s.readonly
return nil, errNotSupported
}

View File

@@ -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)

View File

@@ -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 {

View File

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