grafana/pkg/components/imguploader/gcsuploader.go
Marcos Mendez 4e94c0959a
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>
2020-09-07 19:10:14 +02:00

176 lines
4.5 KiB
Go

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"
publicReadOption string = "&predefinedAcl=publicRead"
bodySizeLimit = 1 << 20
)
type GCSUploader struct {
keyFile string
bucket string
path string
log log.Logger
enableSignedUrls bool
signedUrlExpiration time.Duration
}
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) {
fileName, err := util.GetRandomString(20)
if err != nil {
return "", err
}
fileName += pngExt
key := path.Join(u.path, fileName)
var client *http.Client
if u.keyFile != "" {
u.log.Debug("Opening key file ", u.keyFile)
data, err := ioutil.ReadFile(u.keyFile)
if err != nil {
return "", err
}
u.log.Debug("Creating JWT conf")
conf, err := google.JWTConfigFromJSON(data, tokenUrl)
if err != nil {
return "", err
}
u.log.Debug("Creating HTTP client")
client = conf.Client(ctx)
} else {
u.log.Debug("Key file is empty, trying to use application default credentials")
client, err = google.DefaultClient(ctx)
if err != nil {
return "", err
}
}
err = u.uploadFile(client, imageDiskPath, key)
if err != nil {
return "", err
}
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 {
u.log.Debug("Opening image file ", imageDiskPath)
fileReader, err := os.Open(imageDiskPath)
if err != nil {
return err
}
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)
if err != nil {
return err
}
req.Header.Add("Content-Type", "image/png")
u.log.Debug("Sending POST request to GCS")
resp, err := client.Do(req)
if err != nil {
return err
}
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)
}
return nil
}