mirror of
https://github.com/grafana/grafana.git
synced 2025-09-20 07:44:29 +08:00
237 lines
5.3 KiB
Go
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,
|
|
}
|
|
}
|