Files
2023-04-28 16:00:48 +02:00

237 lines
5.3 KiB
Go

package dynamic
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"sync"
"time"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
)
const publicKeySyncInterval = 10 * 24 * time.Hour // 10 days
// ManifestKeys is the database representation of public keys
// used to verify plugin manifests.
type ManifestKeys struct {
KeyID string `json:"keyId"`
PublicKey string `json:"public"`
Since int64 `json:"since"`
}
type KeyRetriever struct {
cfg *setting.Cfg
log log.Logger
flags featuremgmt.FeatureToggles
lock sync.Mutex
cli http.Client
kv plugins.KeyStore
hasKeys bool
}
var _ plugins.KeyRetriever = (*KeyRetriever)(nil)
func ProvideService(cfg *setting.Cfg, kv plugins.KeyStore, flags featuremgmt.FeatureToggles) *KeyRetriever {
kr := &KeyRetriever{
cfg: cfg,
flags: flags,
log: log.New("plugin.signature.key_retriever"),
cli: makeHttpClient(),
kv: kv,
}
return kr
}
// IsDisabled disables dynamic retrieval of public keys from the API server.
func (kr *KeyRetriever) IsDisabled() bool {
return !kr.flags.IsEnabled(featuremgmt.FlagPluginsAPIManifestKey)
}
func (kr *KeyRetriever) Run(ctx context.Context) error {
// do an initial update if necessary
err := kr.updateKeys(ctx)
if err != nil {
kr.log.Error("Error downloading plugin manifest keys", "error", err)
}
// calculate initial send delay
lastUpdated, err := kr.kv.GetLastUpdated(ctx)
if err != nil {
return err
}
nextSendInterval := time.Until(lastUpdated.Add(publicKeySyncInterval))
if nextSendInterval < time.Minute {
nextSendInterval = time.Minute
}
downloadKeysTicker := time.NewTicker(nextSendInterval)
defer downloadKeysTicker.Stop()
select {
case <-downloadKeysTicker.C:
err = kr.updateKeys(ctx)
if err != nil {
kr.log.Error("Error downloading plugin manifest keys", "error", err)
}
if nextSendInterval != publicKeySyncInterval {
nextSendInterval = publicKeySyncInterval
downloadKeysTicker.Reset(nextSendInterval)
}
case <-ctx.Done():
return ctx.Err()
}
return ctx.Err()
}
func (kr *KeyRetriever) updateKeys(ctx context.Context) error {
kr.lock.Lock()
defer kr.lock.Unlock()
lastUpdated, err := kr.kv.GetLastUpdated(ctx)
if err != nil {
return err
}
if !kr.cfg.PluginForcePublicKeyDownload && time.Since(*lastUpdated) < publicKeySyncInterval {
// Cache is still valid
return nil
}
return kr.downloadKeys(ctx)
}
// Retrieve the key from the API and store it in the database
func (kr *KeyRetriever) downloadKeys(ctx context.Context) error {
var data struct {
Items []ManifestKeys
}
url, err := url.JoinPath(kr.cfg.GrafanaComURL, "/api/plugins/ci/keys") // nolint:gosec URL is provided by config
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := kr.cli.Do(req)
if err != nil {
return err
}
defer func() {
err := resp.Body.Close()
if err != nil {
kr.log.Warn("error closing response body", "error", err)
}
}()
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return err
}
if len(data.Items) == 0 {
return errors.New("missing public key")
}
cachedKeys, err := kr.kv.ListKeys(ctx)
if err != nil {
return err
}
shouldKeep := make(map[string]bool)
for _, key := range data.Items {
err = kr.kv.Set(ctx, key.KeyID, key.PublicKey)
if err != nil {
return err
}
shouldKeep[key.KeyID] = true
}
// Delete keys that are no longer in the API
for _, key := range cachedKeys {
if !shouldKeep[key] {
err = kr.kv.Del(ctx, key)
if err != nil {
return err
}
}
}
// Update the last updated timestamp
return kr.kv.SetLastUpdated(ctx)
}
func (kr *KeyRetriever) ensureKeys(ctx context.Context) error {
if kr.hasKeys {
return nil
}
keys, err := kr.kv.ListKeys(ctx)
if err != nil {
return err
}
if len(keys) == 0 {
// Populate with the default key
err := kr.kv.Set(ctx, statickey.GetDefaultKeyID(), statickey.GetDefaultKey())
if err != nil {
return err
}
}
kr.hasKeys = true
return nil
}
// GetPublicKey loads public keys from:
// - The hard-coded value if the feature flag is not enabled.
// - A cached value from kv storage if it has been already retrieved. This cache is populated from the grafana.com API.
func (kr *KeyRetriever) GetPublicKey(ctx context.Context, keyID string) (string, error) {
kr.lock.Lock()
defer kr.lock.Unlock()
err := kr.ensureKeys(ctx)
if err != nil {
return "", err
}
key, exist, err := kr.kv.Get(ctx, keyID)
if err != nil {
return "", err
}
if exist {
return key, nil
}
return "", fmt.Errorf("missing public key for %s", keyID)
}
// Same configuration as pkg/plugins/repo/client.go
func makeHttpClient() http.Client {
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
return http.Client{
Timeout: 10 * time.Second,
Transport: tr,
}
}