Image Store: Add support for using signed URLs when uploading images to GCS (#26840)
Enables creating signed URLs when uploading images to Google Cloud Storage. By using signed urls, not only is the public URL expiration configurable but the images in the bucket are not publicly accessible. Fixes #26773 Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
This commit is contained in:
co-authored by
Diana Payton
parent
8253b9ddb3
commit
4e94c0959a
@@ -3,35 +3,57 @@ package imguploader
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2/jwt"
|
||||
|
||||
"cloud.google.com/go/storage"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"golang.org/x/oauth2/google"
|
||||
)
|
||||
|
||||
const (
|
||||
tokenUrl string = "https://www.googleapis.com/auth/devstorage.read_write" // #nosec
|
||||
uploadUrl string = "https://www.googleapis.com/upload/storage/v1/b/%s/o?uploadType=media&name=%s&predefinedAcl=publicRead"
|
||||
tokenUrl string = "https://www.googleapis.com/auth/devstorage.read_write" // #nosec
|
||||
uploadUrl string = "https://www.googleapis.com/upload/storage/v1/b/%s/o?uploadType=media&name=%s"
|
||||
publicReadOption string = "&predefinedAcl=publicRead"
|
||||
bodySizeLimit = 1 << 20
|
||||
)
|
||||
|
||||
type GCSUploader struct {
|
||||
keyFile string
|
||||
bucket string
|
||||
path string
|
||||
log log.Logger
|
||||
keyFile string
|
||||
bucket string
|
||||
path string
|
||||
log log.Logger
|
||||
enableSignedUrls bool
|
||||
signedUrlExpiration time.Duration
|
||||
}
|
||||
|
||||
func NewGCSUploader(keyFile, bucket, path string) *GCSUploader {
|
||||
return &GCSUploader{
|
||||
keyFile: keyFile,
|
||||
bucket: bucket,
|
||||
path: path,
|
||||
log: log.New("gcsuploader"),
|
||||
func NewGCSUploader(keyFile, bucket, path string, enableSignedUrls bool, signedUrlExpiration string) (*GCSUploader, error) {
|
||||
expiration, err := time.ParseDuration(signedUrlExpiration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if expiration <= 0 {
|
||||
return nil, fmt.Errorf("invalid signed url expiration: %q", expiration)
|
||||
}
|
||||
uploader := &GCSUploader{
|
||||
keyFile: keyFile,
|
||||
bucket: bucket,
|
||||
path: path,
|
||||
log: log.New("gcsuploader"),
|
||||
enableSignedUrls: enableSignedUrls,
|
||||
signedUrlExpiration: expiration,
|
||||
}
|
||||
|
||||
uploader.log.Debug(fmt.Sprintf("Created GCSUploader key=%q bucket=%q path=%q, enable_signed_urls=%v signed_url_expiration=%q", keyFile, bucket, path, enableSignedUrls, expiration.String()))
|
||||
|
||||
return uploader, nil
|
||||
}
|
||||
|
||||
func (u *GCSUploader) Upload(ctx context.Context, imageDiskPath string) (string, error) {
|
||||
@@ -73,7 +95,43 @@ func (u *GCSUploader) Upload(ctx context.Context, imageDiskPath string) (string,
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("https://storage.googleapis.com/%s/%s", u.bucket, key), nil
|
||||
if !u.enableSignedUrls {
|
||||
return fmt.Sprintf("https://storage.googleapis.com/%s/%s", u.bucket, key), nil
|
||||
}
|
||||
|
||||
u.log.Debug("Signing GCS URL")
|
||||
var conf *jwt.Config
|
||||
if u.keyFile != "" {
|
||||
jsonKey, err := ioutil.ReadFile(u.keyFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ioutil.ReadFile: %v", err)
|
||||
}
|
||||
conf, err = google.JWTConfigFromJSON(jsonKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("google.JWTConfigFromJSON: %v", err)
|
||||
}
|
||||
} else {
|
||||
creds, err := google.FindDefaultCredentials(ctx, storage.ScopeReadWrite)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("google.FindDefaultCredentials: %v", err)
|
||||
}
|
||||
conf, err = google.JWTConfigFromJSON(creds.JSON)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("google.JWTConfigFromJSON: %v", err)
|
||||
}
|
||||
}
|
||||
opts := &storage.SignedURLOptions{
|
||||
Scheme: storage.SigningSchemeV4,
|
||||
Method: "GET",
|
||||
GoogleAccessID: conf.Email,
|
||||
PrivateKey: conf.PrivateKey,
|
||||
Expires: time.Now().Add(u.signedUrlExpiration),
|
||||
}
|
||||
signedUrl, err := storage.SignedURL(u.bucket, key, opts)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("storage.SignedURL: %v", err)
|
||||
}
|
||||
return signedUrl, nil
|
||||
}
|
||||
|
||||
func (u *GCSUploader) uploadFile(client *http.Client, imageDiskPath, key string) error {
|
||||
@@ -86,6 +144,9 @@ func (u *GCSUploader) uploadFile(client *http.Client, imageDiskPath, key string)
|
||||
defer fileReader.Close()
|
||||
|
||||
reqUrl := fmt.Sprintf(uploadUrl, u.bucket, key)
|
||||
if !u.enableSignedUrls {
|
||||
reqUrl += publicReadOption
|
||||
}
|
||||
u.log.Debug("Request URL: ", reqUrl)
|
||||
|
||||
req, err := http.NewRequest("POST", reqUrl, fileReader)
|
||||
@@ -100,9 +161,13 @@ func (u *GCSUploader) uploadFile(client *http.Client, imageDiskPath, key string)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Body.Close()
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, bodySizeLimit))
|
||||
if err == nil && len(respBody) > 0 {
|
||||
u.log.Error(fmt.Sprintf("GCS response: url=%q status=%d, body=%q", reqUrl, resp.StatusCode, string(respBody)))
|
||||
}
|
||||
return fmt.Errorf("GCS response status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,16 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
const pngExt = ".png"
|
||||
const (
|
||||
pngExt = ".png"
|
||||
defaultSGcsSignedUrlExpiration = 7 * 24 * time.Hour //7 days
|
||||
)
|
||||
|
||||
type ImageUploader interface {
|
||||
Upload(ctx context.Context, path string) (string, error)
|
||||
@@ -82,8 +86,10 @@ func NewImageUploader() (ImageUploader, error) {
|
||||
keyFile := gcssec.Key("key_file").MustString("")
|
||||
bucketName := gcssec.Key("bucket").MustString("")
|
||||
path := gcssec.Key("path").MustString("")
|
||||
enableSignedUrls := gcssec.Key("enable_signed_urls").MustBool(false)
|
||||
signedUrlExpiration := gcssec.Key("signed_url_expiration").MustString(defaultSGcsSignedUrlExpiration.String())
|
||||
|
||||
return NewGCSUploader(keyFile, bucketName, path), nil
|
||||
return NewGCSUploader(keyFile, bucketName, path, enableSignedUrls, signedUrlExpiration)
|
||||
case "azure_blob":
|
||||
azureBlobSec, err := setting.Raw.GetSection("external_image_storage.azure_blob")
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user