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) }) })