mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
176 lines
4.5 KiB
Go
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
|
|
}
|