diff --git a/conf/defaults.ini b/conf/defaults.ini
index cc2c8f09466..3bc86a006d0 100644
--- a/conf/defaults.ini
+++ b/conf/defaults.ini
@@ -1109,6 +1109,7 @@ signed_url_expiration =
account_name =
account_key =
container_name =
+sas_token_expiration_days =
[external_image_storage.local]
# does not require any configuration
diff --git a/conf/sample.ini b/conf/sample.ini
index 60699bd0a11..b8033e7d08b 100644
--- a/conf/sample.ini
+++ b/conf/sample.ini
@@ -226,7 +226,7 @@
;google_analytics_ua_id =
# Google Analytics 4 tracking code, only enabled if you specify an id here
-;google_analytics_4_id =
+;google_analytics_4_id =
# Google Tag Manager ID, only enabled if you specify an id here
;google_tag_manager_id =
@@ -1074,6 +1074,7 @@
;account_name =
;account_key =
;container_name =
+;sas_token_expiration_days =
[external_image_storage.local]
# does not require any configuration
@@ -1241,13 +1242,13 @@
# Enable or disable loading other base map layers
;enable_custom_baselayers = true
-# Move an app plugin referenced by its id (including all its pages) to a specific navigation section
+# Move an app plugin referenced by its id (including all its pages) to a specific navigation section
# Dependencies: needs the `topnav` feature to be enabled
[navigation.app_sections]
# The following will move an app plugin with the id of `my-app-id` under the `starred` section
# my-app-id = admin
-# Move a specific app plugin page (referenced by its `path` field) to a specific navigation section
+# Move a specific app plugin page (referenced by its `path` field) to a specific navigation section
[navigation.app_standalone_pages]
# The following will move the page with the path "/a/my-app-id/starred-content" from `my-app-id` to the `starred` section
# /a/my-app-id/starred-content = starred
\ No newline at end of file
diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md
index 5297b8a7d22..afea7e772e7 100644
--- a/docs/sources/setup-grafana/configure-grafana/_index.md
+++ b/docs/sources/setup-grafana/configure-grafana/_index.md
@@ -1747,6 +1747,10 @@ Storage account key
Container name where to store "Blob" images with random names. Creating the blob container beforehand is required. Only public containers are supported.
+### sas_token_expiration_days
+
+Number of days for SAS token validity. If specified SAS token will be attached to image URL. Allow storing images in private containers.
+
## [external_image_storage.local]
diff --git a/go.mod b/go.mod
index 38dc40395dc..d2dcdb311cb 100644
--- a/go.mod
+++ b/go.mod
@@ -44,7 +44,7 @@ require (
github.com/go-sql-driver/mysql v1.6.0
github.com/go-stack/stack v1.8.1
github.com/gobwas/glob v0.2.3
- github.com/gofrs/uuid v4.3.0+incompatible
+ github.com/gofrs/uuid v4.3.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2
github.com/golang/mock v1.6.0
github.com/golang/snappy v0.0.4
@@ -240,6 +240,7 @@ require (
cloud.google.com/go/kms v1.4.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.2
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.4.0
+ github.com/Azure/azure-storage-blob-go v0.15.0
github.com/Azure/go-autorest/autorest/adal v0.9.20
github.com/armon/go-radix v1.0.0
github.com/blugelabs/bluge v0.1.9
@@ -261,11 +262,11 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.7.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.7.0
gocloud.dev v0.25.0
- gotest.tools v2.2.0+incompatible
)
require (
cloud.google.com/go v0.100.2 // indirect
+ github.com/Azure/azure-pipeline-go v0.2.3 // indirect
github.com/armon/go-metrics v0.3.10 // indirect
github.com/bmatcuk/doublestar v1.1.1 // indirect
github.com/buildkite/yaml v2.1.0+incompatible // indirect
@@ -279,6 +280,7 @@ require (
github.com/hashicorp/memberlist v0.4.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
+ github.com/mattn/go-ieproxy v0.0.3 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
diff --git a/go.sum b/go.sum
index 439e28c8caf..526a2f3caa7 100644
--- a/go.sum
+++ b/go.sum
@@ -96,6 +96,7 @@ github.com/Azure/azure-amqp-common-go/v3 v3.2.1/go.mod h1:O6X1iYHP7s2x7NjUKsXVhk
github.com/Azure/azure-amqp-common-go/v3 v3.2.2/go.mod h1:O6X1iYHP7s2x7NjUKsXVhkwWrQhxrd+d8/3rRadj4CI=
github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4=
github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc=
+github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go v23.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
@@ -135,6 +136,8 @@ github.com/Azure/azure-service-bus-go v0.11.5/go.mod h1:MI6ge2CuQWBVq+ly456MY7Xq
github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0=
github.com/Azure/azure-storage-blob-go v0.13.0/go.mod h1:pA9kNqtjUeQF2zOSu4s//nUdBD+e64lEuc4sVnuOfNs=
github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck=
+github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7OgcSXPpwp3tx6qk=
+github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58=
github.com/Azure/go-amqp v0.16.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg=
github.com/Azure/go-amqp v0.16.4/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
@@ -1805,6 +1808,7 @@ github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HN
github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
github.com/mattn/go-ieproxy v0.0.0-20191113090002-7c0f6868bffe/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
+github.com/mattn/go-ieproxy v0.0.3 h1:YkaHmK1CzE5C4O7A3hv3TCbfNDPSCf0RKZFX+VhBeYk=
github.com/mattn/go-ieproxy v0.0.3/go.mod h1:6ZpRmhBaYuBX1U2za+9rC9iCGLsSp2tftelZne7CPko=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
diff --git a/pkg/components/imguploader/azureblobuploader.go b/pkg/components/imguploader/azureblobuploader.go
index 8598665df21..b6b76def636 100644
--- a/pkg/components/imguploader/azureblobuploader.go
+++ b/pkg/components/imguploader/azureblobuploader.go
@@ -19,23 +19,26 @@ import (
"strings"
"time"
+ "github.com/Azure/azure-storage-blob-go/azblob"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/util"
)
type AzureBlobUploader struct {
- account_name string
- account_key string
- container_name string
- log log.Logger
+ account_name string
+ account_key string
+ container_name string
+ sas_token_expiration_days int
+ log log.Logger
}
-func NewAzureBlobUploader(account_name string, account_key string, container_name string) *AzureBlobUploader {
+func NewAzureBlobUploader(account_name string, account_key string, container_name string, sas_token_expiration_days int) *AzureBlobUploader {
return &AzureBlobUploader{
- account_name: account_name,
- account_key: account_key,
- container_name: container_name,
- log: log.New("azureBlobUploader"),
+ account_name: account_name,
+ account_key: account_key,
+ container_name: container_name,
+ sas_token_expiration_days: sas_token_expiration_days,
+ log: log.New("azureBlobUploader"),
}
}
@@ -91,9 +94,49 @@ func (az *AzureBlobUploader) Upload(ctx context.Context, imageDiskPath string) (
}
url := fmt.Sprintf("https://%s.blob.core.windows.net/%s/%s", az.account_name, az.container_name, randomFileName)
+
+ if az.sas_token_expiration_days > 0 {
+ url, err = blob.GetBlobSasUrl(ctx, az.container_name, randomFileName, az.sas_token_expiration_days)
+ if err != nil {
+ return "", err
+ }
+ }
return url, nil
}
+// SignWithSharedKey uses an account's SharedKeyCredential to sign this signature values to produce the proper SAS query parameters.
+func (c *StorageClient) GetBlobSasUrl(ctx context.Context, containerName, blobName string, sasTokenExpiration int) (string, error) {
+ if c.Auth == nil {
+ return "", fmt.Errorf("cannot sign SAS query without Shared Key Credential")
+ }
+
+ // create source blob SAS url
+ credential, err := azblob.NewSharedKeyCredential(c.Auth.Account, c.Auth.Key)
+ if err != nil {
+ return "", err
+ }
+
+ // Set the desired SAS signature values and sign them with the shared key credentials to get the SAS query parameters.
+ sasQueryParams, err := azblob.BlobSASSignatureValues{
+ Protocol: azblob.SASProtocolHTTPS, // Users MUST use HTTPS (not HTTP)
+ ExpiryTime: time.Now().UTC().AddDate(0, 0, sasTokenExpiration), // Expiration time
+ ContainerName: containerName,
+ BlobName: blobName,
+ Permissions: azblob.BlobSASPermissions{Add: false, Read: true, Write: false}.String(), // Read only permissions
+ }.NewSASQueryParameters(credential)
+ if err != nil {
+ return "", err
+ }
+
+ // Create the URL of the resource you wish to access and append the SAS query parameters.
+ // Since this is a blob SAS, the URL is to the Azure storage blob.
+ qp := sasQueryParams.Encode()
+ blobSasUrl := fmt.Sprintf("https://%s.blob.core.windows.net/%s/%s?%s", c.Auth.Account, containerName, blobName, qp)
+
+ // Return Blob SAS token URL
+ return blobSasUrl, nil
+}
+
// --- AZURE LIBRARY
type Error struct {
Code int
diff --git a/pkg/components/imguploader/imguploader.go b/pkg/components/imguploader/imguploader.go
index ae8eb44d282..7e0aee0028b 100644
--- a/pkg/components/imguploader/imguploader.go
+++ b/pkg/components/imguploader/imguploader.go
@@ -110,8 +110,10 @@ func NewImageUploader() (ImageUploader, error) {
account_name := azureBlobSec.Key("account_name").MustString("")
account_key := azureBlobSec.Key("account_key").MustString("")
container_name := azureBlobSec.Key("container_name").MustString("")
+ sas_token_expiration_days := azureBlobSec.Key("sas_token_expiration_days").MustInt(-1)
+
+ return NewAzureBlobUploader(account_name, account_key, container_name, sas_token_expiration_days), nil
- return NewAzureBlobUploader(account_name, account_key, container_name), nil
case "local":
return NewLocalImageUploader()
}
diff --git a/pkg/components/imguploader/imguploader_test.go b/pkg/components/imguploader/imguploader_test.go
index 11d9dff4fe6..a6acb346c34 100644
--- a/pkg/components/imguploader/imguploader_test.go
+++ b/pkg/components/imguploader/imguploader_test.go
@@ -154,6 +154,8 @@ func TestImageUploaderFactory(t *testing.T) {
require.NoError(t, err)
_, err = azureBlobSec.NewKey("container_name", "container_name")
require.NoError(t, err)
+ _, err = azureBlobSec.NewKey("sas_token_expiration_days", "sas_token_expiration_days")
+ require.NoError(t, err)
uploader, err := NewImageUploader()
require.NoError(t, err)
@@ -163,6 +165,7 @@ func TestImageUploaderFactory(t *testing.T) {
require.Equal(t, "account_name", original.account_name)
require.Equal(t, "account_key", original.account_key)
require.Equal(t, "container_name", original.container_name)
+ require.Equal(t, -1, original.sas_token_expiration_days)
})
})