Browse Source

S3: add support for assume role

Fixes #736

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino 3 years ago
parent
commit
4519bffa39
11 changed files with 44 additions and 6 deletions
  1. 3 1
      docs/s3.md
  2. 2 2
      go.mod
  3. 4 3
      go.sum
  4. 8 0
      httpd/httpd_test.go
  5. 1 0
      httpd/webadmin.go
  6. 3 0
      httpdtest/httpdtest.go
  7. 3 0
      openapi/openapi.yaml
  8. 11 0
      templates/webadmin/fsconfig.html
  9. 1 0
      vfs/filesystem.go
  10. 5 0
      vfs/s3fs.go
  11. 3 0
      vfs/vfs.go

+ 3 - 1
docs/s3.md

@@ -2,7 +2,7 @@
 
 
 To connect SFTPGo to AWS, you need to specify credentials, a `bucket` and a `region`. Here is the list of available [AWS regions](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions). For example, if your bucket is at `Frankfurt`, you have to set the region to `eu-central-1`. You can specify an AWS [storage class](https://docs.aws.amazon.com/AmazonS3/latest/dev/storage-class-intro.html) too. Leave it blank to use the default AWS storage class. An endpoint is required if you are connecting to a Compatible AWS Storage such as [MinIO](https://min.io/).
 To connect SFTPGo to AWS, you need to specify credentials, a `bucket` and a `region`. Here is the list of available [AWS regions](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions). For example, if your bucket is at `Frankfurt`, you have to set the region to `eu-central-1`. You can specify an AWS [storage class](https://docs.aws.amazon.com/AmazonS3/latest/dev/storage-class-intro.html) too. Leave it blank to use the default AWS storage class. An endpoint is required if you are connecting to a Compatible AWS Storage such as [MinIO](https://min.io/).
 
 
-AWS SDK has different options for credentials. [More Detail](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html). We support:
+AWS SDK has different options for credentials. We support:
 
 
 1. Providing [Access Keys](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html#access-keys-and-secret-access-keys).
 1. Providing [Access Keys](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html#access-keys-and-secret-access-keys).
 2. Use IAM roles for Amazon EC2
 2. Use IAM roles for Amazon EC2
@@ -10,6 +10,8 @@ AWS SDK has different options for credentials. [More Detail](https://docs.aws.am
 
 
 So, you need to provide access keys to activate option 1, or leave them blank to use the other ways to specify credentials.
 So, you need to provide access keys to activate option 1, or leave them blank to use the other ways to specify credentials.
 
 
+You can also use a temporary session token or assume a role by setting its ARN.
+
 Specifying a different `key_prefix`, you can assign different "folders" of the same bucket to different users. This is similar to a chroot directory for local filesystem. Each SFTP/SCP user can only access the assigned folder and its contents. The folder identified by `key_prefix` does not need to be pre-created.
 Specifying a different `key_prefix`, you can assign different "folders" of the same bucket to different users. This is similar to a chroot directory for local filesystem. Each SFTP/SCP user can only access the assigned folder and its contents. The folder identified by `key_prefix` does not need to be pre-created.
 
 
 SFTPGo uses multipart uploads and parallel downloads for storing and retrieving files from S3.
 SFTPGo uses multipart uploads and parallel downloads for storing and retrieving files from S3.

+ 2 - 2
go.mod

@@ -41,7 +41,7 @@ require (
 	github.com/rs/cors v1.8.2
 	github.com/rs/cors v1.8.2
 	github.com/rs/xid v1.3.0
 	github.com/rs/xid v1.3.0
 	github.com/rs/zerolog v1.26.2-0.20220227173336-263b0bde3672
 	github.com/rs/zerolog v1.26.2-0.20220227173336-263b0bde3672
-	github.com/sftpgo/sdk v0.1.1-0.20220225141305-cca7ba31466c
+	github.com/sftpgo/sdk v0.1.1-0.20220228183957-d7251ba29961
 	github.com/shirou/gopsutil/v3 v3.22.1
 	github.com/shirou/gopsutil/v3 v3.22.1
 	github.com/spf13/afero v1.8.1
 	github.com/spf13/afero v1.8.1
 	github.com/spf13/cobra v1.3.0
 	github.com/spf13/cobra v1.3.0
@@ -130,7 +130,7 @@ require (
 	golang.org/x/tools v0.1.9 // indirect
 	golang.org/x/tools v0.1.9 // indirect
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf // indirect
+	google.golang.org/genproto v0.0.0-20220228155957-1da8797a5878 // indirect
 	google.golang.org/grpc v1.44.0 // indirect
 	google.golang.org/grpc v1.44.0 // indirect
 	google.golang.org/protobuf v1.27.1 // indirect
 	google.golang.org/protobuf v1.27.1 // indirect
 	gopkg.in/ini.v1 v1.66.4 // indirect
 	gopkg.in/ini.v1 v1.66.4 // indirect

+ 4 - 3
go.sum

@@ -700,8 +700,8 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
 github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
 github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY=
 github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY=
-github.com/sftpgo/sdk v0.1.1-0.20220225141305-cca7ba31466c h1:aSWi1VB6DXmPmscawueKEhoyMTZjsMTiRaWFfhHmB4Y=
-github.com/sftpgo/sdk v0.1.1-0.20220225141305-cca7ba31466c/go.mod h1:zqCRMcwS28IViwekJHNkFu4GqSfyVmOQTlh8h3icAXE=
+github.com/sftpgo/sdk v0.1.1-0.20220228183957-d7251ba29961 h1:XpSoX58U9KR5qbexs3VUBZvgcRogjgbALWzQO4TIZKo=
+github.com/sftpgo/sdk v0.1.1-0.20220228183957-d7251ba29961/go.mod h1:zqCRMcwS28IViwekJHNkFu4GqSfyVmOQTlh8h3icAXE=
 github.com/shirou/gopsutil/v3 v3.22.1 h1:33y31Q8J32+KstqPfscvFwBlNJ6xLaBy4xqBXzlYV5w=
 github.com/shirou/gopsutil/v3 v3.22.1 h1:33y31Q8J32+KstqPfscvFwBlNJ6xLaBy4xqBXzlYV5w=
 github.com/shirou/gopsutil/v3 v3.22.1/go.mod h1:WapW1AOOPlHyXr+yOyw3uYx36enocrtSoSBy0L5vUHY=
 github.com/shirou/gopsutil/v3 v3.22.1/go.mod h1:WapW1AOOPlHyXr+yOyw3uYx36enocrtSoSBy0L5vUHY=
 github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
 github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
@@ -1189,8 +1189,9 @@ google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ6
 google.golang.org/genproto v0.0.0-20220211171837-173942840c17/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
 google.golang.org/genproto v0.0.0-20220211171837-173942840c17/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
 google.golang.org/genproto v0.0.0-20220216160803-4663080d8bc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
 google.golang.org/genproto v0.0.0-20220216160803-4663080d8bc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
 google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
 google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
-google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf h1:SVYXkUz2yZS9FWb2Gm8ivSlbNQzL2Z/NpPKE3RG2jWk=
 google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
 google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
+google.golang.org/genproto v0.0.0-20220228155957-1da8797a5878 h1:gERY0VtsF9UyyyCsPSjRk9/RWlcKSa/Gw/aenR/5z48=
+google.golang.org/genproto v0.0.0-20220228155957-1da8797a5878/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
 google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=

+ 8 - 0
httpd/httpd_test.go

@@ -1788,6 +1788,7 @@ func TestUserRedactedPassword(t *testing.T) {
 	u.FsConfig.S3Config.Region = "eu-west-1"
 	u.FsConfig.S3Config.Region = "eu-west-1"
 	u.FsConfig.S3Config.AccessKey = "access-key"
 	u.FsConfig.S3Config.AccessKey = "access-key"
 	u.FsConfig.S3Config.SessionToken = "session token"
 	u.FsConfig.S3Config.SessionToken = "session token"
+	u.FsConfig.S3Config.RoleARN = "myRoleARN"
 	u.FsConfig.S3Config.AccessSecret = kms.NewSecret(sdkkms.SecretStatusRedacted, "access-secret", "", "")
 	u.FsConfig.S3Config.AccessSecret = kms.NewSecret(sdkkms.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"
@@ -2566,6 +2567,7 @@ func TestUserS3Config(t *testing.T) {
 	user.FsConfig.S3Config.AccessKey = "Server-Access-Key"
 	user.FsConfig.S3Config.AccessKey = "Server-Access-Key"
 	user.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret("Server-Access-Secret")
 	user.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret("Server-Access-Secret")
 	user.FsConfig.S3Config.SessionToken = "Session token"
 	user.FsConfig.S3Config.SessionToken = "Session token"
+	user.FsConfig.S3Config.RoleARN = "myRoleARN"
 	user.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000"
 	user.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000"
 	user.FsConfig.S3Config.UploadPartSize = 8
 	user.FsConfig.S3Config.UploadPartSize = 8
 	user.FsConfig.S3Config.DownloadPartMaxTime = 60
 	user.FsConfig.S3Config.DownloadPartMaxTime = 60
@@ -15100,6 +15102,7 @@ func TestWebUserS3Mock(t *testing.T) {
 	user.FsConfig.S3Config.AccessKey = "access-key"
 	user.FsConfig.S3Config.AccessKey = "access-key"
 	user.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret("access-secret")
 	user.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret("access-secret")
 	user.FsConfig.S3Config.SessionToken = "new session token"
 	user.FsConfig.S3Config.SessionToken = "new session token"
+	user.FsConfig.S3Config.RoleARN = "arn:aws:iam::123456789012:user/Development/product_1234/*"
 	user.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000/path?a=b"
 	user.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000/path?a=b"
 	user.FsConfig.S3Config.StorageClass = "Standard"
 	user.FsConfig.S3Config.StorageClass = "Standard"
 	user.FsConfig.S3Config.KeyPrefix = "somedir/subdir/"
 	user.FsConfig.S3Config.KeyPrefix = "somedir/subdir/"
@@ -15139,6 +15142,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_session_token", user.FsConfig.S3Config.SessionToken)
 	form.Set("s3_session_token", user.FsConfig.S3Config.SessionToken)
+	form.Set("s3_role_arn", user.FsConfig.S3Config.RoleARN)
 	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_acl", user.FsConfig.S3Config.ACL)
 	form.Set("s3_endpoint", user.FsConfig.S3Config.Endpoint)
 	form.Set("s3_endpoint", user.FsConfig.S3Config.Endpoint)
@@ -15229,6 +15233,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.SessionToken, user.FsConfig.S3Config.SessionToken)
 	assert.Equal(t, updateUser.FsConfig.S3Config.SessionToken, user.FsConfig.S3Config.SessionToken)
+	assert.Equal(t, updateUser.FsConfig.S3Config.RoleARN, user.FsConfig.S3Config.RoleARN)
 	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.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)
@@ -15930,6 +15935,7 @@ func TestS3WebFolderMock(t *testing.T) {
 	S3AccessKey := "access-key"
 	S3AccessKey := "access-key"
 	S3AccessSecret := kms.NewPlainSecret("folder-access-secret")
 	S3AccessSecret := kms.NewPlainSecret("folder-access-secret")
 	S3SessionToken := "fake session token"
 	S3SessionToken := "fake session token"
+	S3RoleARN := "arn:aws:iam::123456789012:user/Development/product_1234/*"
 	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"
 	S3ACL := "public-read-write"
@@ -15950,6 +15956,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_session_token", S3SessionToken)
 	form.Set("s3_session_token", S3SessionToken)
+	form.Set("s3_role_arn", S3RoleARN)
 	form.Set("s3_storage_class", S3StorageClass)
 	form.Set("s3_storage_class", S3StorageClass)
 	form.Set("s3_acl", S3ACL)
 	form.Set("s3_acl", S3ACL)
 	form.Set("s3_endpoint", S3Endpoint)
 	form.Set("s3_endpoint", S3Endpoint)
@@ -16044,6 +16051,7 @@ func TestS3WebFolderMock(t *testing.T) {
 	assert.Equal(t, S3Region, folder.FsConfig.S3Config.Region)
 	assert.Equal(t, S3Region, folder.FsConfig.S3Config.Region)
 	assert.Equal(t, S3AccessKey, folder.FsConfig.S3Config.AccessKey)
 	assert.Equal(t, S3AccessKey, folder.FsConfig.S3Config.AccessKey)
 	assert.Equal(t, S3SessionToken, folder.FsConfig.S3Config.SessionToken)
 	assert.Equal(t, S3SessionToken, folder.FsConfig.S3Config.SessionToken)
+	assert.Equal(t, S3RoleARN, folder.FsConfig.S3Config.RoleARN)
 	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)

+ 1 - 0
httpd/webadmin.go

@@ -969,6 +969,7 @@ func getS3Config(r *http.Request) (vfs.S3FsConfig, error) {
 	config.Region = r.Form.Get("s3_region")
 	config.Region = r.Form.Get("s3_region")
 	config.AccessKey = r.Form.Get("s3_access_key")
 	config.AccessKey = r.Form.Get("s3_access_key")
 	config.SessionToken = strings.TrimSpace(r.Form.Get("s3_session_token"))
 	config.SessionToken = strings.TrimSpace(r.Form.Get("s3_session_token"))
+	config.RoleARN = r.Form.Get("s3_role_arn")
 	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")

+ 3 - 0
httpdtest/httpdtest.go

@@ -1278,6 +1278,9 @@ func compareS3Config(expected *vfs.Filesystem, actual *vfs.Filesystem) error { /
 	if expected.S3Config.SessionToken != actual.S3Config.SessionToken {
 	if expected.S3Config.SessionToken != actual.S3Config.SessionToken {
 		return errors.New("fs S3 session token mismatch")
 		return errors.New("fs S3 session token mismatch")
 	}
 	}
+	if expected.S3Config.RoleARN != actual.S3Config.RoleARN {
+		return errors.New("fs S3 role ARN mismatch")
+	}
 	if err := checkEncryptedSecret(expected.S3Config.AccessSecret, actual.S3Config.AccessSecret); err != nil {
 	if err := checkEncryptedSecret(expected.S3Config.AccessSecret, actual.S3Config.AccessSecret); err != nil {
 		return fmt.Errorf("fs S3 access secret mismatch: %v", err)
 		return fmt.Errorf("fs S3 access secret mismatch: %v", err)
 	}
 	}

+ 3 - 0
openapi/openapi.yaml

@@ -4720,6 +4720,9 @@ components:
           $ref: '#/components/schemas/Secret'
           $ref: '#/components/schemas/Secret'
         session_token:
         session_token:
           type: string
           type: string
+        role_arn:
+          type: string
+          description: 'IAM Role ARN to assume'
         endpoint:
         endpoint:
           type: string
           type: string
           description: optional endpoint
           description: optional endpoint

+ 11 - 0
templates/webadmin/fsconfig.html

@@ -144,6 +144,17 @@
             </div>
             </div>
         </div>
         </div>
 
 
+        <div class="form-group row fsconfig fsconfig-s3fs">
+            <label for="idS3RoleARN" class="col-sm-2 col-form-label">Role ARN</label>
+            <div class="col-sm-10">
+                <input type="text" class="form-control" id="idS3RoleARN" name="s3_role_arn" placeholder=""
+                    value="{{.S3Config.RoleARN}}" aria-describedby="S3RoleARNHelpBlock">
+                <small id="S3RoleARNHelpBlock" class="form-text text-muted">
+                    IAM Role ARN to assume
+                </small>
+            </div>
+        </div>
+
         <div class="form-group row fsconfig fsconfig-s3fs">
         <div class="form-group row fsconfig fsconfig-s3fs">
             <label for="idS3ACL" class="col-sm-2 col-form-label">ACL</label>
             <label for="idS3ACL" class="col-sm-2 col-form-label">ACL</label>
             <div class="col-sm-10">
             <div class="col-sm-10">

+ 1 - 0
vfs/filesystem.go

@@ -245,6 +245,7 @@ func (f *Filesystem) GetACopy() Filesystem {
 				Region:              f.S3Config.Region,
 				Region:              f.S3Config.Region,
 				AccessKey:           f.S3Config.AccessKey,
 				AccessKey:           f.S3Config.AccessKey,
 				SessionToken:        f.S3Config.SessionToken,
 				SessionToken:        f.S3Config.SessionToken,
+				RoleARN:             f.S3Config.RoleARN,
 				Endpoint:            f.S3Config.Endpoint,
 				Endpoint:            f.S3Config.Endpoint,
 				StorageClass:        f.S3Config.StorageClass,
 				StorageClass:        f.S3Config.StorageClass,
 				ACL:                 f.S3Config.ACL,
 				ACL:                 f.S3Config.ACL,

+ 5 - 0
vfs/s3fs.go

@@ -17,6 +17,7 @@ import (
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/awserr"
 	"github.com/aws/aws-sdk-go/aws/awserr"
 	"github.com/aws/aws-sdk-go/aws/credentials"
 	"github.com/aws/aws-sdk-go/aws/credentials"
+	"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
 	"github.com/aws/aws-sdk-go/aws/request"
 	"github.com/aws/aws-sdk-go/aws/request"
 	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/aws/aws-sdk-go/service/s3"
 	"github.com/aws/aws-sdk-go/service/s3"
@@ -101,6 +102,10 @@ func NewS3Fs(connectionID, localTempDir, mountPath string, config S3FsConfig) (F
 	if err != nil {
 	if err != nil {
 		return fs, err
 		return fs, err
 	}
 	}
+	if fs.config.RoleARN != "" {
+		creds := stscreds.NewCredentials(sess, fs.config.RoleARN)
+		sess.Config.Credentials = creds
+	}
 	fs.svc = s3.New(sess)
 	fs.svc = s3.New(sess)
 	return fs, nil
 	return fs, nil
 }
 }

+ 3 - 0
vfs/vfs.go

@@ -176,6 +176,9 @@ func (c *S3FsConfig) isEqual(other *S3FsConfig) bool {
 	if c.SessionToken != other.SessionToken {
 	if c.SessionToken != other.SessionToken {
 		return false
 		return false
 	}
 	}
+	if c.RoleARN != other.RoleARN {
+		return false
+	}
 	if c.Endpoint != other.Endpoint {
 	if c.Endpoint != other.Endpoint {
 		return false
 		return false
 	}
 	}