Browse Source

S3: add ACL support

Fixes #610
Nicola Murino 3 years ago
parent
commit
ee5c5e033d

+ 3 - 0
cmd/portable.go

@@ -44,6 +44,7 @@ var (
 	portableS3AccessSecret             string
 	portableS3AccessSecret             string
 	portableS3Endpoint                 string
 	portableS3Endpoint                 string
 	portableS3StorageClass             string
 	portableS3StorageClass             string
+	portableS3ACL                      string
 	portableS3KeyPrefix                string
 	portableS3KeyPrefix                string
 	portableS3ULPartSize               int
 	portableS3ULPartSize               int
 	portableS3ULConcurrency            int
 	portableS3ULConcurrency            int
@@ -169,6 +170,7 @@ Please take a look at the usage below to customize the serving parameters`,
 								AccessSecret:      kms.NewPlainSecret(portableS3AccessSecret),
 								AccessSecret:      kms.NewPlainSecret(portableS3AccessSecret),
 								Endpoint:          portableS3Endpoint,
 								Endpoint:          portableS3Endpoint,
 								StorageClass:      portableS3StorageClass,
 								StorageClass:      portableS3StorageClass,
+								ACL:               portableS3ACL,
 								KeyPrefix:         portableS3KeyPrefix,
 								KeyPrefix:         portableS3KeyPrefix,
 								UploadPartSize:    int64(portableS3ULPartSize),
 								UploadPartSize:    int64(portableS3ULPartSize),
 								UploadConcurrency: portableS3ULConcurrency,
 								UploadConcurrency: portableS3ULConcurrency,
@@ -288,6 +290,7 @@ sftpfs => SFTP (legacy: 5)`)
 	portableCmd.Flags().StringVar(&portableS3AccessSecret, "s3-access-secret", "", "")
 	portableCmd.Flags().StringVar(&portableS3AccessSecret, "s3-access-secret", "", "")
 	portableCmd.Flags().StringVar(&portableS3Endpoint, "s3-endpoint", "", "")
 	portableCmd.Flags().StringVar(&portableS3Endpoint, "s3-endpoint", "", "")
 	portableCmd.Flags().StringVar(&portableS3StorageClass, "s3-storage-class", "", "")
 	portableCmd.Flags().StringVar(&portableS3StorageClass, "s3-storage-class", "", "")
+	portableCmd.Flags().StringVar(&portableS3ACL, "s3-acl", "", "")
 	portableCmd.Flags().StringVar(&portableS3KeyPrefix, "s3-key-prefix", "", `Allows to restrict access to the
 	portableCmd.Flags().StringVar(&portableS3KeyPrefix, "s3-key-prefix", "", `Allows to restrict access to the
 virtual folder identified by this
 virtual folder identified by this
 prefix and its contents`)
 prefix and its contents`)

+ 1 - 0
docs/portable-mode.md

@@ -81,6 +81,7 @@ Flags:
   -k, --public-key strings
   -k, --public-key strings
       --s3-access-key string
       --s3-access-key string
       --s3-access-secret string
       --s3-access-secret string
+      --s3-acl string
       --s3-bucket string
       --s3-bucket string
       --s3-endpoint string
       --s3-endpoint string
       --s3-key-prefix string            Allows to restrict access to the
       --s3-key-prefix string            Allows to restrict access to the

+ 7 - 0
httpd/httpd_test.go

@@ -1543,6 +1543,7 @@ func TestUserRedactedPassword(t *testing.T) {
 	u.FsConfig.S3Config.AccessSecret = kms.NewSecret(kms.SecretStatusRedacted, "access-secret", "", "")
 	u.FsConfig.S3Config.AccessSecret = kms.NewSecret(kms.SecretStatusRedacted, "access-secret", "", "")
 	u.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000/path?k=m"
 	u.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000/path?k=m"
 	u.FsConfig.S3Config.StorageClass = "Standard"
 	u.FsConfig.S3Config.StorageClass = "Standard"
+	u.FsConfig.S3Config.ACL = "bucket-owner-full-control"
 	_, resp, err := httpdtest.AddUser(u, http.StatusBadRequest)
 	_, resp, err := httpdtest.AddUser(u, http.StatusBadRequest)
 	assert.NoError(t, err, string(resp))
 	assert.NoError(t, err, string(resp))
 	assert.Contains(t, string(resp), "cannot save a user with a redacted secret")
 	assert.Contains(t, string(resp), "cannot save a user with a redacted secret")
@@ -13181,6 +13182,7 @@ func TestWebUserS3Mock(t *testing.T) {
 	user.FsConfig.S3Config.DownloadPartSize = 6
 	user.FsConfig.S3Config.DownloadPartSize = 6
 	user.FsConfig.S3Config.DownloadConcurrency = 3
 	user.FsConfig.S3Config.DownloadConcurrency = 3
 	user.FsConfig.S3Config.ForcePathStyle = true
 	user.FsConfig.S3Config.ForcePathStyle = true
+	user.FsConfig.S3Config.ACL = "public-read"
 	user.Description = "s3 tèst user"
 	user.Description = "s3 tèst user"
 	form := make(url.Values)
 	form := make(url.Values)
 	form.Set(csrfFormToken, csrfToken)
 	form.Set(csrfFormToken, csrfToken)
@@ -13205,6 +13207,7 @@ func TestWebUserS3Mock(t *testing.T) {
 	form.Set("s3_access_key", user.FsConfig.S3Config.AccessKey)
 	form.Set("s3_access_key", user.FsConfig.S3Config.AccessKey)
 	form.Set("s3_access_secret", user.FsConfig.S3Config.AccessSecret.GetPayload())
 	form.Set("s3_access_secret", user.FsConfig.S3Config.AccessSecret.GetPayload())
 	form.Set("s3_storage_class", user.FsConfig.S3Config.StorageClass)
 	form.Set("s3_storage_class", user.FsConfig.S3Config.StorageClass)
+	form.Set("s3_acl", user.FsConfig.S3Config.ACL)
 	form.Set("s3_endpoint", user.FsConfig.S3Config.Endpoint)
 	form.Set("s3_endpoint", user.FsConfig.S3Config.Endpoint)
 	form.Set("s3_key_prefix", user.FsConfig.S3Config.KeyPrefix)
 	form.Set("s3_key_prefix", user.FsConfig.S3Config.KeyPrefix)
 	form.Set("pattern_path0", "/dir1")
 	form.Set("pattern_path0", "/dir1")
@@ -13282,6 +13285,7 @@ func TestWebUserS3Mock(t *testing.T) {
 	assert.Equal(t, updateUser.FsConfig.S3Config.Region, user.FsConfig.S3Config.Region)
 	assert.Equal(t, updateUser.FsConfig.S3Config.Region, user.FsConfig.S3Config.Region)
 	assert.Equal(t, updateUser.FsConfig.S3Config.AccessKey, user.FsConfig.S3Config.AccessKey)
 	assert.Equal(t, updateUser.FsConfig.S3Config.AccessKey, user.FsConfig.S3Config.AccessKey)
 	assert.Equal(t, updateUser.FsConfig.S3Config.StorageClass, user.FsConfig.S3Config.StorageClass)
 	assert.Equal(t, updateUser.FsConfig.S3Config.StorageClass, user.FsConfig.S3Config.StorageClass)
+	assert.Equal(t, updateUser.FsConfig.S3Config.ACL, user.FsConfig.S3Config.ACL)
 	assert.Equal(t, updateUser.FsConfig.S3Config.Endpoint, user.FsConfig.S3Config.Endpoint)
 	assert.Equal(t, updateUser.FsConfig.S3Config.Endpoint, user.FsConfig.S3Config.Endpoint)
 	assert.Equal(t, updateUser.FsConfig.S3Config.KeyPrefix, user.FsConfig.S3Config.KeyPrefix)
 	assert.Equal(t, updateUser.FsConfig.S3Config.KeyPrefix, user.FsConfig.S3Config.KeyPrefix)
 	assert.Equal(t, updateUser.FsConfig.S3Config.UploadPartSize, user.FsConfig.S3Config.UploadPartSize)
 	assert.Equal(t, updateUser.FsConfig.S3Config.UploadPartSize, user.FsConfig.S3Config.UploadPartSize)
@@ -13931,6 +13935,7 @@ func TestS3WebFolderMock(t *testing.T) {
 	S3AccessSecret := kms.NewPlainSecret("folder-access-secret")
 	S3AccessSecret := kms.NewPlainSecret("folder-access-secret")
 	S3Endpoint := "http://127.0.0.1:9000/path?b=c"
 	S3Endpoint := "http://127.0.0.1:9000/path?b=c"
 	S3StorageClass := "Standard"
 	S3StorageClass := "Standard"
+	S3ACL := "public-read-write"
 	S3KeyPrefix := "somedir/subdir/"
 	S3KeyPrefix := "somedir/subdir/"
 	S3UploadPartSize := 5
 	S3UploadPartSize := 5
 	S3UploadConcurrency := 4
 	S3UploadConcurrency := 4
@@ -13947,6 +13952,7 @@ func TestS3WebFolderMock(t *testing.T) {
 	form.Set("s3_access_key", S3AccessKey)
 	form.Set("s3_access_key", S3AccessKey)
 	form.Set("s3_access_secret", S3AccessSecret.GetPayload())
 	form.Set("s3_access_secret", S3AccessSecret.GetPayload())
 	form.Set("s3_storage_class", S3StorageClass)
 	form.Set("s3_storage_class", S3StorageClass)
+	form.Set("s3_acl", S3ACL)
 	form.Set("s3_endpoint", S3Endpoint)
 	form.Set("s3_endpoint", S3Endpoint)
 	form.Set("s3_key_prefix", S3KeyPrefix)
 	form.Set("s3_key_prefix", S3KeyPrefix)
 	form.Set("s3_upload_part_size", strconv.Itoa(S3UploadPartSize))
 	form.Set("s3_upload_part_size", strconv.Itoa(S3UploadPartSize))
@@ -13991,6 +13997,7 @@ func TestS3WebFolderMock(t *testing.T) {
 	assert.NotEmpty(t, folder.FsConfig.S3Config.AccessSecret.GetPayload())
 	assert.NotEmpty(t, folder.FsConfig.S3Config.AccessSecret.GetPayload())
 	assert.Equal(t, S3Endpoint, folder.FsConfig.S3Config.Endpoint)
 	assert.Equal(t, S3Endpoint, folder.FsConfig.S3Config.Endpoint)
 	assert.Equal(t, S3StorageClass, folder.FsConfig.S3Config.StorageClass)
 	assert.Equal(t, S3StorageClass, folder.FsConfig.S3Config.StorageClass)
+	assert.Equal(t, S3ACL, folder.FsConfig.S3Config.ACL)
 	assert.Equal(t, S3KeyPrefix, folder.FsConfig.S3Config.KeyPrefix)
 	assert.Equal(t, S3KeyPrefix, folder.FsConfig.S3Config.KeyPrefix)
 	assert.Equal(t, S3UploadConcurrency, folder.FsConfig.S3Config.UploadConcurrency)
 	assert.Equal(t, S3UploadConcurrency, folder.FsConfig.S3Config.UploadConcurrency)
 	assert.Equal(t, int64(S3UploadPartSize), folder.FsConfig.S3Config.UploadPartSize)
 	assert.Equal(t, int64(S3UploadPartSize), folder.FsConfig.S3Config.UploadPartSize)

+ 3 - 0
httpd/schema/openapi.yaml

@@ -4181,6 +4181,9 @@ components:
           description: optional endpoint
           description: optional endpoint
         storage_class:
         storage_class:
           type: string
           type: string
+        acl:
+          type: string
+          description: 'The canned ACL to apply to uploaded objects. Leave empty to use the default ACL. For more information and available ACLs, see here: https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl'
         upload_part_size:
         upload_part_size:
           type: integer
           type: integer
           description: 'the buffer size (in MB) to use for multipart uploads. The minimum allowed part size is 5MB, and if this value is set to zero, the default value (5MB) for the AWS SDK will be used. The minimum allowed value is 5.'
           description: 'the buffer size (in MB) to use for multipart uploads. The minimum allowed part size is 5MB, and if this value is set to zero, the default value (5MB) for the AWS SDK will be used. The minimum allowed value is 5.'

+ 1 - 0
httpd/webadmin.go

@@ -830,6 +830,7 @@ func getS3Config(r *http.Request) (vfs.S3FsConfig, error) {
 	config.AccessSecret = getSecretFromFormField(r, "s3_access_secret")
 	config.AccessSecret = getSecretFromFormField(r, "s3_access_secret")
 	config.Endpoint = r.Form.Get("s3_endpoint")
 	config.Endpoint = r.Form.Get("s3_endpoint")
 	config.StorageClass = r.Form.Get("s3_storage_class")
 	config.StorageClass = r.Form.Get("s3_storage_class")
+	config.ACL = r.Form.Get("s3_acl")
 	config.KeyPrefix = r.Form.Get("s3_key_prefix")
 	config.KeyPrefix = r.Form.Get("s3_key_prefix")
 	config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("s3_upload_part_size"), 10, 64)
 	config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("s3_upload_part_size"), 10, 64)
 	if err != nil {
 	if err != nil {

+ 4 - 1
httpdtest/httpdtest.go

@@ -1245,7 +1245,7 @@ func compareFsConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error {
 	return compareSFTPFsConfig(expected, actual)
 	return compareSFTPFsConfig(expected, actual)
 }
 }
 
 
-func compareS3Config(expected *vfs.Filesystem, actual *vfs.Filesystem) error {
+func compareS3Config(expected *vfs.Filesystem, actual *vfs.Filesystem) error { //nolint:gocyclo
 	if expected.S3Config.Bucket != actual.S3Config.Bucket {
 	if expected.S3Config.Bucket != actual.S3Config.Bucket {
 		return errors.New("fs S3 bucket mismatch")
 		return errors.New("fs S3 bucket mismatch")
 	}
 	}
@@ -1264,6 +1264,9 @@ func compareS3Config(expected *vfs.Filesystem, actual *vfs.Filesystem) error {
 	if expected.S3Config.StorageClass != actual.S3Config.StorageClass {
 	if expected.S3Config.StorageClass != actual.S3Config.StorageClass {
 		return errors.New("fs S3 storage class mismatch")
 		return errors.New("fs S3 storage class mismatch")
 	}
 	}
+	if expected.S3Config.ACL != actual.S3Config.ACL {
+		return errors.New("fs S3 ACL mismatch")
+	}
 	if expected.S3Config.UploadPartSize != actual.S3Config.UploadPartSize {
 	if expected.S3Config.UploadPartSize != actual.S3Config.UploadPartSize {
 		return errors.New("fs S3 upload part size mismatch")
 		return errors.New("fs S3 upload part size mismatch")
 	}
 	}

+ 4 - 0
sdk/filesystem.go

@@ -99,6 +99,10 @@ type S3FsConfig struct {
 	AccessSecret *kms.Secret `json:"access_secret,omitempty"`
 	AccessSecret *kms.Secret `json:"access_secret,omitempty"`
 	Endpoint     string      `json:"endpoint,omitempty"`
 	Endpoint     string      `json:"endpoint,omitempty"`
 	StorageClass string      `json:"storage_class,omitempty"`
 	StorageClass string      `json:"storage_class,omitempty"`
+	// The canned ACL to apply to uploaded objects. Leave empty to use the default ACL.
+	// For more information and available ACLs, see here:
+	// https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl
+	ACL string `json:"acl,omitempty"`
 	// The buffer size (in MB) to use for multipart uploads. The minimum allowed part size is 5MB,
 	// The buffer size (in MB) to use for multipart uploads. The minimum allowed part size is 5MB,
 	// and if this value is set to zero, the default value (5MB) for the AWS SDK will be used.
 	// and if this value is set to zero, the default value (5MB) for the AWS SDK will be used.
 	// The minimum allowed value is 5.
 	// The minimum allowed value is 5.

+ 13 - 1
templates/webadmin/fsconfig.html

@@ -109,8 +109,19 @@
                 </small>
                 </small>
             </div>
             </div>
             <div class="col-sm-2"></div>
             <div class="col-sm-2"></div>
-            <label for="idS3KeyPrefix" class="col-sm-2 col-form-label">Key Prefix</label>
+            <label for="idS3KeyPrefix" class="col-sm-2 col-form-label">ACL</label>
             <div class="col-sm-3">
             <div class="col-sm-3">
+                <input type="text" class="form-control" id="idS3ACL" name="s3_acl" placeholder=""
+                    value="{{.S3Config.ACL}}" maxlength="255" aria-describedby="S3ACLHelpBlock">
+                <small id="S3ACLHelpBlock" class="form-text text-muted">
+                    ACL for uploaded objects. For more info take a look <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl" target="_blank">here</a>
+                </small>
+            </div>
+        </div>
+
+        <div class="form-group row fsconfig fsconfig-s3fs">
+            <label for="idS3KeyPrefix" class="col-sm-2 col-form-label">Key Prefix</label>
+            <div class="col-sm-10">
                 <input type="text" class="form-control" id="idS3KeyPrefix" name="s3_key_prefix" placeholder=""
                 <input type="text" class="form-control" id="idS3KeyPrefix" name="s3_key_prefix" placeholder=""
                     value="{{.S3Config.KeyPrefix}}" maxlength="255" aria-describedby="S3KeyPrefixHelpBlock">
                     value="{{.S3Config.KeyPrefix}}" maxlength="255" aria-describedby="S3KeyPrefixHelpBlock">
                 <small id="S3KeyPrefixHelpBlock" class="form-text text-muted">
                 <small id="S3KeyPrefixHelpBlock" class="form-text text-muted">
@@ -119,6 +130,7 @@
             </div>
             </div>
         </div>
         </div>
 
 
+
         <div class="form-group fsconfig fsconfig-s3fs">
         <div class="form-group fsconfig fsconfig-s3fs">
             <div class="form-check">
             <div class="form-check">
                 <input type="checkbox" class="form-check-input" id="idS3ForcePathStyle" name="s3_force_path_style"
                 <input type="checkbox" class="form-check-input" id="idS3ForcePathStyle" name="s3_force_path_style"

+ 1 - 0
vfs/filesystem.go

@@ -235,6 +235,7 @@ func (f *Filesystem) GetACopy() Filesystem {
 				AccessSecret:        f.S3Config.AccessSecret.Clone(),
 				AccessSecret:        f.S3Config.AccessSecret.Clone(),
 				Endpoint:            f.S3Config.Endpoint,
 				Endpoint:            f.S3Config.Endpoint,
 				StorageClass:        f.S3Config.StorageClass,
 				StorageClass:        f.S3Config.StorageClass,
+				ACL:                 f.S3Config.ACL,
 				KeyPrefix:           f.S3Config.KeyPrefix,
 				KeyPrefix:           f.S3Config.KeyPrefix,
 				UploadPartSize:      f.S3Config.UploadPartSize,
 				UploadPartSize:      f.S3Config.UploadPartSize,
 				UploadConcurrency:   f.S3Config.UploadConcurrency,
 				UploadConcurrency:   f.S3Config.UploadConcurrency,

+ 4 - 2
vfs/s3fs.go

@@ -244,6 +244,7 @@ func (fs *S3Fs) Create(name string, flag int) (File, *PipeWriter, func(), error)
 			Bucket:       aws.String(fs.config.Bucket),
 			Bucket:       aws.String(fs.config.Bucket),
 			Key:          aws.String(key),
 			Key:          aws.String(key),
 			Body:         r,
 			Body:         r,
+			ACL:          util.NilIfEmpty(fs.config.ACL),
 			StorageClass: util.NilIfEmpty(fs.config.StorageClass),
 			StorageClass: util.NilIfEmpty(fs.config.StorageClass),
 			ContentType:  util.NilIfEmpty(contentType),
 			ContentType:  util.NilIfEmpty(contentType),
 		}, func(u *s3manager.Uploader) {
 		}, func(u *s3manager.Uploader) {
@@ -252,8 +253,8 @@ func (fs *S3Fs) Create(name string, flag int) (File, *PipeWriter, func(), error)
 		})
 		})
 		r.CloseWithError(err) //nolint:errcheck
 		r.CloseWithError(err) //nolint:errcheck
 		p.Done(err)
 		p.Done(err)
-		fsLog(fs, logger.LevelDebug, "upload completed, path: %#v, response: %v, readed bytes: %v, err: %+v",
-			name, response, r.GetReadedBytes(), err)
+		fsLog(fs, logger.LevelDebug, "upload completed, path: %#v, acl: %#v, response: %v, readed bytes: %v, err: %+v",
+			name, fs.config.ACL, response, r.GetReadedBytes(), err)
 		metric.S3TransferCompleted(r.GetReadedBytes(), 0, err)
 		metric.S3TransferCompleted(r.GetReadedBytes(), 0, err)
 	}()
 	}()
 	return nil, p, cancelFn, nil
 	return nil, p, cancelFn, nil
@@ -306,6 +307,7 @@ func (fs *S3Fs) Rename(source, target string) error {
 		CopySource:   aws.String(pathEscape(copySource)),
 		CopySource:   aws.String(pathEscape(copySource)),
 		Key:          aws.String(target),
 		Key:          aws.String(target),
 		StorageClass: util.NilIfEmpty(fs.config.StorageClass),
 		StorageClass: util.NilIfEmpty(fs.config.StorageClass),
+		ACL:          util.NilIfEmpty(fs.config.ACL),
 		ContentType:  util.NilIfEmpty(contentType),
 		ContentType:  util.NilIfEmpty(contentType),
 	})
 	})
 	if err != nil {
 	if err != nil {

+ 10 - 0
vfs/vfs.go

@@ -169,6 +169,9 @@ func (c *S3FsConfig) isEqual(other *S3FsConfig) bool {
 	if c.StorageClass != other.StorageClass {
 	if c.StorageClass != other.StorageClass {
 		return false
 		return false
 	}
 	}
+	if c.ACL != other.ACL {
+		return false
+	}
 	if c.UploadPartSize != other.UploadPartSize {
 	if c.UploadPartSize != other.UploadPartSize {
 		return false
 		return false
 	}
 	}
@@ -187,6 +190,10 @@ func (c *S3FsConfig) isEqual(other *S3FsConfig) bool {
 	if c.ForcePathStyle != other.ForcePathStyle {
 	if c.ForcePathStyle != other.ForcePathStyle {
 		return false
 		return false
 	}
 	}
+	return c.isSecretEqual(other)
+}
+
+func (c *S3FsConfig) isSecretEqual(other *S3FsConfig) bool {
 	if c.AccessSecret == nil {
 	if c.AccessSecret == nil {
 		c.AccessSecret = kms.NewEmptySecret()
 		c.AccessSecret = kms.NewEmptySecret()
 	}
 	}
@@ -263,6 +270,8 @@ func (c *S3FsConfig) Validate() error {
 			c.KeyPrefix += "/"
 			c.KeyPrefix += "/"
 		}
 		}
 	}
 	}
+	c.StorageClass = strings.TrimSpace(c.StorageClass)
+	c.ACL = strings.TrimSpace(c.ACL)
 	return c.checkPartSizeAndConcurrency()
 	return c.checkPartSizeAndConcurrency()
 }
 }
 
 
@@ -329,6 +338,7 @@ func (c *GCSFsConfig) Validate(credentialsFilePath string) error {
 			return errors.New("credentials cannot be empty")
 			return errors.New("credentials cannot be empty")
 		}
 		}
 	}
 	}
+	c.StorageClass = strings.TrimSpace(c.StorageClass)
 	return nil
 	return nil
 }
 }