Plugins: Pass PDC file contents in requests (#84783)

* Plugins: Pass PDC file contents in requests

* go mod tidy

* undo go.mod changes

* fix linter

* fix tests

* undo unnecessary changes

* update dep

* join with comma

* update naming

* bump SDK
This commit is contained in:
Will Browne 2024-03-22 13:52:24 +01:00 committed by GitHub
parent 0b4c81158e
commit b765c21d4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 198 additions and 71 deletions

8
go.mod
View File

@ -51,7 +51,7 @@ require (
github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code
github.com/grafana/grafana-aws-sdk v0.25.0 // @grafana/aws-datasources
github.com/grafana/grafana-azure-sdk-go/v2 v2.0.1 // @grafana/partner-datasources
github.com/grafana/grafana-plugin-sdk-go v0.216.0 // @grafana/plugins-platform-backend
github.com/grafana/grafana-plugin-sdk-go v0.217.0 // @grafana/plugins-platform-backend
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/backend-platform
github.com/hashicorp/go-hclog v1.6.2 // @grafana/plugins-platform-backend
github.com/hashicorp/go-plugin v1.6.0 // @grafana/plugins-platform-backend
@ -109,7 +109,7 @@ require (
gopkg.in/mail.v2 v2.3.1 // @grafana/backend-platform
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-squad-backend
xorm.io/builder v0.3.6 // indirect; @grafana/backend-platform
xorm.io/builder v0.3.6 // @grafana/backend-platform
xorm.io/core v0.7.3 // @grafana/backend-platform
xorm.io/xorm v0.8.2 // @grafana/alerting-squad-backend
)
@ -161,7 +161,7 @@ require (
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-msgpack v0.5.5 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect; @grafana/alerting-squad
github.com/hashicorp/go-multierror v1.1.1 // @grafana/alerting-squad
github.com/hashicorp/go-sockaddr v1.0.6 // indirect
github.com/hashicorp/golang-lru v0.6.0 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
@ -317,7 +317,7 @@ require (
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect; @grafana/alerting-squad-backend
github.com/hashicorp/golang-lru/v2 v2.0.7 // @grafana/alerting-squad-backend
github.com/hashicorp/memberlist v0.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/invopop/yaml v0.2.0 // indirect

4
go.sum
View File

@ -2184,8 +2184,8 @@ github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkr
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs=
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ=
github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
github.com/grafana/grafana-plugin-sdk-go v0.216.0 h1:DftszjdGZM6hikBazyB7CdrhyYOwUct9s5lA/BwSl2c=
github.com/grafana/grafana-plugin-sdk-go v0.216.0/go.mod h1:FdvSvOliqpVLnytM7e89zCFyYPDE6VOn9SIjVQRvVxM=
github.com/grafana/grafana-plugin-sdk-go v0.217.0 h1:oQjq5KRrVrhweXHxFtEMgjv1KW7hujGiRPIYrsPZ9PE=
github.com/grafana/grafana-plugin-sdk-go v0.217.0/go.mod h1:FdvSvOliqpVLnytM7e89zCFyYPDE6VOn9SIjVQRvVxM=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240226124929-648abdbd0ea4 h1:hpyusz8c3yRFoJPlA0o34rWnsLbaOOBZleqRhFBi5Lg=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240226124929-648abdbd0ea4/go.mod h1:vrRQJuNprTWqwm6JPxHf3BoTJhvO15QMEjQ7Q/YUOnI=
github.com/grafana/grafana/pkg/apiserver v0.0.0-20240226124929-648abdbd0ea4 h1:tIbI5zgos92vwJ8lV3zwHwuxkV03GR3FGLkFW9V5LxY=

View File

@ -534,7 +534,7 @@ func (s *Service) httpClientOptions(ctx context.Context, ds *datasources.DataSou
ClientCfg: &sdkproxy.ClientCfg{
ClientCert: s.cfg.SecureSocksDSProxy.ClientCert,
ClientKey: s.cfg.SecureSocksDSProxy.ClientKey,
RootCA: s.cfg.SecureSocksDSProxy.RootCA,
RootCAs: s.cfg.SecureSocksDSProxy.RootCAs,
ProxyAddress: s.cfg.SecureSocksDSProxy.ProxyAddress,
ServerName: s.cfg.SecureSocksDSProxy.ServerName,
AllowInsecure: s.cfg.SecureSocksDSProxy.AllowInsecure,

View File

@ -122,13 +122,20 @@ func (p *EnvVarsProvider) awsEnvVars(pluginID string) []string {
func (p *EnvVarsProvider) secureSocksProxyEnvVars() []string {
if p.cfg.ProxySettings.Enabled {
return []string{
envVar(proxy.PluginSecureSocksProxyClientCert, p.cfg.ProxySettings.ClientCert),
envVar(proxy.PluginSecureSocksProxyClientKey, p.cfg.ProxySettings.ClientKey),
envVar(proxy.PluginSecureSocksProxyRootCACert, p.cfg.ProxySettings.RootCA),
envVar(proxy.PluginSecureSocksProxyProxyAddress, p.cfg.ProxySettings.ProxyAddress),
envVar(proxy.PluginSecureSocksProxyServerName, p.cfg.ProxySettings.ServerName),
envVar(proxy.PluginSecureSocksProxyEnabled, strconv.FormatBool(p.cfg.ProxySettings.Enabled)),
envVar(proxy.PluginSecureSocksProxyAllowInsecure, strconv.FormatBool(p.cfg.ProxySettings.AllowInsecure)),
// nolint:staticcheck
envVar(proxy.PluginSecureSocksProxyClientCertFilePathEnvVarName, p.cfg.ProxySettings.ClientCertFilePath),
// nolint:staticcheck
envVar(proxy.PluginSecureSocksProxyClientKeyFilePathEnvVarName, p.cfg.ProxySettings.ClientKeyFilePath),
// nolint:staticcheck
envVar(proxy.PluginSecureSocksProxyRootCACertFilePathsEnvVarName, strings.Join(p.cfg.ProxySettings.RootCAFilePaths, " ")),
// nolint:staticcheck
envVar(proxy.PluginSecureSocksProxyAddressEnvVarName, p.cfg.ProxySettings.ProxyAddress),
// nolint:staticcheck
envVar(proxy.PluginSecureSocksProxyServerNameEnvVarName, p.cfg.ProxySettings.ServerName),
// nolint:staticcheck
envVar(proxy.PluginSecureSocksProxyEnabledEnvVarName, strconv.FormatBool(p.cfg.ProxySettings.Enabled)),
// nolint:staticcheck
envVar(proxy.PluginSecureSocksProxyAllowInsecureEnvVarName, strconv.FormatBool(p.cfg.ProxySettings.AllowInsecure)),
}
}
return nil

View File

@ -75,7 +75,7 @@ func (s *RequestConfigProvider) PluginRequestConfig(ctx context.Context, pluginI
m[proxy.PluginSecureSocksProxyEnabled] = "true"
m[proxy.PluginSecureSocksProxyClientCert] = s.cfg.ProxySettings.ClientCert
m[proxy.PluginSecureSocksProxyClientKey] = s.cfg.ProxySettings.ClientKey
m[proxy.PluginSecureSocksProxyRootCACert] = s.cfg.ProxySettings.RootCA
m[proxy.PluginSecureSocksProxyRootCAs] = strings.Join(s.cfg.ProxySettings.RootCAs, ",")
m[proxy.PluginSecureSocksProxyProxyAddress] = s.cfg.ProxySettings.ProxyAddress
m[proxy.PluginSecureSocksProxyServerName] = s.cfg.ProxySettings.ServerName
m[proxy.PluginSecureSocksProxyAllowInsecure] = strconv.FormatBool(s.cfg.ProxySettings.AllowInsecure)

View File

@ -39,7 +39,7 @@ func TestRequestConfigProvider_PluginRequestConfig(t *testing.T) {
ShowUI: true,
ClientCert: "c3rt",
ClientKey: "k3y",
RootCA: "ca",
RootCAs: []string{"ca"},
ProxyAddress: "https://proxy.grafana.com",
ServerName: "secureProxy",
AllowInsecure: true,
@ -65,7 +65,7 @@ func TestRequestConfigProvider_PluginRequestConfig(t *testing.T) {
ShowUI: true,
ClientCert: "c3rt",
ClientKey: "k3y",
RootCA: "ca",
RootCAs: []string{"ca"},
ProxyAddress: "https://proxy.grafana.com",
ServerName: "secureProxy",
},
@ -83,7 +83,7 @@ func TestRequestConfigProvider_PluginRequestConfig(t *testing.T) {
ShowUI: true,
ClientCert: "c3rt",
ClientKey: "k3y",
RootCA: "ca",
RootCAs: []string{"ca"},
ProxyAddress: "https://proxy.grafana.com",
ServerName: "secureProxy",
},

View File

@ -1,33 +1,41 @@
package setting
import (
"encoding/pem"
"errors"
"os"
"gopkg.in/ini.v1"
)
type SecureSocksDSProxySettings struct {
Enabled bool
ShowUI bool
AllowInsecure bool
ClientCert string
ClientKey string
RootCA string
ProxyAddress string
ServerName string
Enabled bool
ShowUI bool
AllowInsecure bool
ClientCert string
ClientCertFilePath string
ClientKey string
ClientKeyFilePath string
RootCAs []string
RootCAFilePaths []string
ProxyAddress string
ServerName string
}
func readSecureSocksDSProxySettings(iniFile *ini.File) (SecureSocksDSProxySettings, error) {
s := SecureSocksDSProxySettings{}
s := SecureSocksDSProxySettings{
RootCAs: []string{},
RootCAFilePaths: []string{},
}
secureSocksProxySection := iniFile.Section("secure_socks_datasource_proxy")
s.Enabled = secureSocksProxySection.Key("enabled").MustBool(false)
s.ClientCert = secureSocksProxySection.Key("client_cert").MustString("")
s.ClientKey = secureSocksProxySection.Key("client_key").MustString("")
s.RootCA = secureSocksProxySection.Key("root_ca_cert").MustString("")
s.ProxyAddress = secureSocksProxySection.Key("proxy_address").MustString("")
s.ServerName = secureSocksProxySection.Key("server_name").MustString("")
s.ShowUI = secureSocksProxySection.Key("show_ui").MustBool(true)
s.AllowInsecure = secureSocksProxySection.Key("allow_insecure").MustBool(false)
s.ClientCertFilePath = secureSocksProxySection.Key("client_cert").MustString("")
s.ClientKeyFilePath = secureSocksProxySection.Key("client_key").MustString("")
s.RootCAFilePaths = secureSocksProxySection.Key("root_ca_cert").Strings(" ")
if !s.Enabled {
return s, nil
@ -40,14 +48,50 @@ func readSecureSocksDSProxySettings(iniFile *ini.File) (SecureSocksDSProxySettin
// If the proxy is going to use TLS.
if !s.AllowInsecure {
// all fields must be specified to use the proxy
if s.RootCA == "" {
return s, errors.New("rootCA required")
} else if s.ClientCert == "" || s.ClientKey == "" {
if len(s.RootCAFilePaths) == 0 {
return s, errors.New("one or more rootCA required")
} else if s.ClientCertFilePath == "" || s.ClientKeyFilePath == "" {
return s, errors.New("client key pair required")
} else if s.ServerName == "" {
return s, errors.New("server name required")
}
} else {
return s, nil
}
if s.ClientCertFilePath != "" {
certPEMBlock, err := os.ReadFile(s.ClientCertFilePath)
if err != nil {
return s, err
}
s.ClientCert = string(certPEMBlock)
}
if s.ClientKeyFilePath != "" {
keyPEMBlock, err := os.ReadFile(s.ClientKeyFilePath)
if err != nil {
return s, err
}
s.ClientKey = string(keyPEMBlock)
}
var rootCAs []string
for _, rootCAFile := range s.RootCAFilePaths {
// nolint:gosec
// The gosec G304 warning can be ignored because `rootCAFile` comes from config ini, and we check below if
// it's the right file type.
pemBytes, err := os.ReadFile(rootCAFile)
if err != nil {
return s, err
}
pemDecoded, _ := pem.Decode(pemBytes)
if pemDecoded == nil || pemDecoded.Type != "CERTIFICATE" {
return s, errors.New("root ca is invalid")
}
rootCAs = append(rootCAs, string(pemBytes))
}
s.RootCAs = rootCAs
return s, nil
}

View File

@ -1,25 +1,46 @@
package setting
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"math/big"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/ini.v1"
)
func mustNewIniFile(fileContents string) *ini.File {
file, err := ini.Load([]byte(fileContents))
if err != nil {
panic(fmt.Sprintf("creating ini file for test: %s", err))
}
return file
}
func TestReadSecureSocksDSProxySettings(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
testFilePath := filepath.Join(tempDir, "test")
testFileData := "foobar"
err := os.WriteFile(testFilePath, []byte(testFileData), 0600)
require.NoError(t, err)
rootCACertFilePath := filepath.Join(tempDir, "ca.cert")
// nolint:gosec
caCertFile, err := os.Create(rootCACertFilePath)
require.NoError(t, err)
defer func() {
err = caCertFile.Close()
require.NoError(t, err)
}()
rootCaFileData := createTestRootCAFile(t, rootCACertFilePath)
require.NoError(t, err)
cases := []struct {
description string
iniFile *ini.File
@ -32,24 +53,27 @@ func TestReadSecureSocksDSProxySettings(t *testing.T) {
[secure_socks_datasource_proxy]
`),
expectedSettings: SecureSocksDSProxySettings{
Enabled: false,
ClientCert: "",
ClientKey: "",
RootCA: "",
ProxyAddress: "",
ServerName: "",
ShowUI: true,
AllowInsecure: false,
Enabled: false,
ShowUI: true,
AllowInsecure: false,
ClientCertFilePath: "",
ClientCert: "",
ClientKey: "",
ClientKeyFilePath: "",
RootCAs: []string{},
RootCAFilePaths: []string{},
ProxyAddress: "",
ServerName: "",
},
},
{
description: "root ca is required",
description: "one or more root ca is required",
iniFile: mustNewIniFile(`
[secure_socks_datasource_proxy]
enabled = true
proxy_address = address
`),
expectedErr: errors.New("rootCA required"),
expectedErr: errors.New("one or more rootCA required"),
},
{
description: "client cert is required",
@ -106,35 +130,40 @@ server_name = name
allow_insecure = true
`),
expectedSettings: SecureSocksDSProxySettings{
Enabled: true,
ProxyAddress: "address",
ServerName: "name",
ShowUI: true,
AllowInsecure: true,
Enabled: true,
ProxyAddress: "address",
ServerName: "name",
ShowUI: true,
AllowInsecure: true,
RootCAFilePaths: []string{},
RootCAs: []string{},
},
},
{
description: "custom values",
iniFile: mustNewIniFile(`
iniFile: mustNewIniFile(fmt.Sprintf(`
[secure_socks_datasource_proxy]
enabled = true
client_cert = cert
client_key = key
root_ca_cert = root_ca
client_cert = %s
client_key = %s
root_ca_cert = %s
proxy_address = proxy_address
server_name = server_name
show_ui = false
allow_insecure = true
`),
allow_insecure = false
`, testFilePath, testFilePath, rootCACertFilePath)),
expectedSettings: SecureSocksDSProxySettings{
Enabled: true,
ClientCert: "cert",
ClientKey: "key",
RootCA: "root_ca",
ProxyAddress: "proxy_address",
ServerName: "server_name",
ShowUI: false,
AllowInsecure: true,
Enabled: true,
ShowUI: false,
AllowInsecure: false,
ClientCert: testFileData,
ClientCertFilePath: testFilePath,
ClientKey: testFileData,
ClientKeyFilePath: testFilePath,
RootCAs: []string{rootCaFileData},
RootCAFilePaths: []string{rootCACertFilePath},
ProxyAddress: "proxy_address",
ServerName: "server_name",
},
},
}
@ -146,9 +175,56 @@ allow_insecure = true
if tt.expectedErr != nil {
assert.Equal(t, tt.expectedErr, err)
} else {
assert.Equal(t, tt.expectedSettings, settings)
assert.NoError(t, err)
assert.Equal(t, tt.expectedSettings, settings)
}
})
}
}
func createTestRootCAFile(t *testing.T, path string) string {
t.Helper()
ca := &x509.Certificate{
SerialNumber: big.NewInt(2019),
Subject: pkix.Name{
Organization: []string{"Grafana Labs"},
CommonName: "Grafana",
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}
caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
require.NoError(t, err)
caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
require.NoError(t, err)
// nolint:gosec
caCertFile, err := os.Create(path)
require.NoError(t, err)
block := &pem.Block{
Type: "CERTIFICATE",
Bytes: caBytes,
}
err = pem.Encode(caCertFile, block)
require.NoError(t, err)
buf := new(bytes.Buffer)
err = pem.Encode(buf, block)
require.NoError(t, err)
return buf.String()
}
func mustNewIniFile(fileContents string) *ini.File {
file, err := ini.Load([]byte(fileContents))
if err != nil {
panic(fmt.Sprintf("creating ini file for test: %s", err))
}
return file
}