mirror of https://github.com/grafana/grafana
Chore: Refactor manifest verifier (#67218)
parent
62587eee88
commit
aa9838bd25
@ -1,286 +0,0 @@ |
|||||||
package manifestverifier |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"context" |
|
||||||
"encoding/json" |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"net" |
|
||||||
"net/http" |
|
||||||
"net/url" |
|
||||||
"sync" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/ProtonMail/go-crypto/openpgp" |
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/clearsign" |
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/packet" |
|
||||||
"github.com/grafana/grafana/pkg/plugins" |
|
||||||
"github.com/grafana/grafana/pkg/plugins/config" |
|
||||||
"github.com/grafana/grafana/pkg/plugins/log" |
|
||||||
|
|
||||||
// Only used for getting the feature flag value
|
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt" |
|
||||||
) |
|
||||||
|
|
||||||
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 ManifestVerifier struct { |
|
||||||
cfg *config.Cfg |
|
||||||
mlog log.Logger |
|
||||||
|
|
||||||
lock sync.Mutex |
|
||||||
cli http.Client |
|
||||||
kv plugins.KeyStore |
|
||||||
hasKeys bool |
|
||||||
} |
|
||||||
|
|
||||||
func New(cfg *config.Cfg, mlog log.Logger, kv plugins.KeyStore) *ManifestVerifier { |
|
||||||
pmv := &ManifestVerifier{ |
|
||||||
cfg: cfg, |
|
||||||
mlog: mlog, |
|
||||||
cli: makeHttpClient(), |
|
||||||
kv: kv, |
|
||||||
} |
|
||||||
return pmv |
|
||||||
} |
|
||||||
|
|
||||||
// IsDisabled disables dynamic retrieval of public keys from the API server.
|
|
||||||
func (pmv *ManifestVerifier) IsDisabled() bool { |
|
||||||
return pmv.cfg == nil || pmv.cfg.Features == nil || !pmv.cfg.Features.IsEnabled(featuremgmt.FlagPluginsAPIManifestKey) |
|
||||||
} |
|
||||||
|
|
||||||
func (pmv *ManifestVerifier) Run(ctx context.Context) error { |
|
||||||
// do an initial update if necessary
|
|
||||||
err := pmv.updateKeys(ctx) |
|
||||||
if err != nil { |
|
||||||
pmv.mlog.Error("Error downloading plugin manifest keys", "error", err) |
|
||||||
} |
|
||||||
|
|
||||||
// calculate initial send delay
|
|
||||||
lastUpdated, err := pmv.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 = pmv.updateKeys(ctx) |
|
||||||
if err != nil { |
|
||||||
pmv.mlog.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 (pmv *ManifestVerifier) updateKeys(ctx context.Context) error { |
|
||||||
pmv.lock.Lock() |
|
||||||
defer pmv.lock.Unlock() |
|
||||||
|
|
||||||
lastUpdated, err := pmv.kv.GetLastUpdated(ctx) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
if time.Since(*lastUpdated) < publicKeySyncInterval { |
|
||||||
// Cache is still valid
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
return pmv.downloadKeys(ctx) |
|
||||||
} |
|
||||||
|
|
||||||
const publicKeyID = "7e4d0c6a708866e7" |
|
||||||
const publicKeyText = `-----BEGIN PGP PUBLIC KEY BLOCK----- |
|
||||||
Version: OpenPGP.js v4.10.1 |
|
||||||
Comment: https://openpgpjs.org
|
|
||||||
|
|
||||||
xpMEXpTXXxMFK4EEACMEIwQBiOUQhvGbDLvndE0fEXaR0908wXzPGFpf0P0Z |
|
||||||
HJ06tsq+0higIYHp7WTNJVEZtcwoYLcPRGaa9OQqbUU63BEyZdgAkPTz3RFd |
|
||||||
5+TkDWZizDcaVFhzbDd500yTwexrpIrdInwC/jrgs7Zy/15h8KA59XXUkdmT |
|
||||||
YB6TR+OA9RKME+dCJozNGUdyYWZhbmEgPGVuZ0BncmFmYW5hLmNvbT7CvAQQ |
|
||||||
EwoAIAUCXpTXXwYLCQcIAwIEFQgKAgQWAgEAAhkBAhsDAh4BAAoJEH5NDGpw |
|
||||||
iGbnaWoCCQGQ3SQnCkRWrG6XrMkXOKfDTX2ow9fuoErN46BeKmLM4f1EkDZQ |
|
||||||
Tpq3SE8+My8B5BIH3SOcBeKzi3S57JHGBdFA+wIJAYWMrJNIvw8GeXne+oUo |
|
||||||
NzzACdvfqXAZEp/HFMQhCKfEoWGJE8d2YmwY2+3GufVRTI5lQnZOHLE8L/Vc |
|
||||||
1S5MXESjzpcEXpTXXxIFK4EEACMEIwQBtHX/SD5Qm3v4V92qpaIZQgtTX0sT |
|
||||||
cFPjYWAHqsQ1iENrYN/vg1wU3ADlYATvydOQYvkTyT/tbDvx2Fse8PL84MQA |
|
||||||
YKKQ6AJ3gLVvmeouZdU03YoV4MYaT8KbnJUkZQZkqdz2riOlySNI9CG3oYmv |
|
||||||
omjUAtzgAgnCcurfGLZkkMxlmY8DAQoJwqQEGBMKAAkFAl6U118CGwwACgkQ |
|
||||||
fk0ManCIZuc0jAIJAVw2xdLr4ZQqPUhubrUyFcqlWoW8dQoQagwO8s8ubmby |
|
||||||
KuLA9FWJkfuuRQr+O9gHkDVCez3aism7zmJBqIOi38aNAgjJ3bo6leSS2jR/ |
|
||||||
x5NqiKVi83tiXDPncDQYPymOnMhW0l7CVA7wj75HrFvvlRI/4MArlbsZ2tBn |
|
||||||
N1c5v9v/4h6qeA== |
|
||||||
=DNbR |
|
||||||
-----END PGP PUBLIC KEY BLOCK----- |
|
||||||
` |
|
||||||
|
|
||||||
// Retrieve the key from the API and store it in the database
|
|
||||||
func (pmv *ManifestVerifier) downloadKeys(ctx context.Context) error { |
|
||||||
var data struct { |
|
||||||
Items []ManifestKeys |
|
||||||
} |
|
||||||
|
|
||||||
url, err := url.JoinPath(pmv.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 := pmv.cli.Do(req) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
defer func() { |
|
||||||
err := resp.Body.Close() |
|
||||||
if err != nil { |
|
||||||
pmv.mlog.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 := pmv.kv.ListKeys(ctx) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
shouldKeep := make(map[string]bool) |
|
||||||
for _, key := range data.Items { |
|
||||||
err = pmv.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 = pmv.kv.Del(ctx, key) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Update the last updated timestamp
|
|
||||||
return pmv.kv.SetLastUpdated(ctx) |
|
||||||
} |
|
||||||
|
|
||||||
func (pmv *ManifestVerifier) ensureKeys(ctx context.Context) error { |
|
||||||
if pmv.hasKeys { |
|
||||||
return nil |
|
||||||
} |
|
||||||
keys, err := pmv.kv.ListKeys(ctx) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
if len(keys) == 0 { |
|
||||||
// Populate with the default key
|
|
||||||
err := pmv.kv.Set(ctx, publicKeyID, publicKeyText) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
pmv.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 (pmv *ManifestVerifier) getPublicKey(ctx context.Context, keyID string) (string, error) { |
|
||||||
if pmv.IsDisabled() { |
|
||||||
return publicKeyText, nil |
|
||||||
} |
|
||||||
|
|
||||||
pmv.lock.Lock() |
|
||||||
defer pmv.lock.Unlock() |
|
||||||
|
|
||||||
err := pmv.ensureKeys(ctx) |
|
||||||
if err != nil { |
|
||||||
return "", err |
|
||||||
} |
|
||||||
|
|
||||||
key, exist, err := pmv.kv.Get(ctx, keyID) |
|
||||||
if err != nil { |
|
||||||
return "", err |
|
||||||
} |
|
||||||
if exist { |
|
||||||
return key, nil |
|
||||||
} |
|
||||||
|
|
||||||
return "", fmt.Errorf("missing public key for %s", keyID) |
|
||||||
} |
|
||||||
|
|
||||||
func (pmv *ManifestVerifier) Verify(ctx context.Context, keyID string, block *clearsign.Block) error { |
|
||||||
publicKey, err := pmv.getPublicKey(ctx, keyID) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(publicKey)) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("%v: %w", "failed to parse public key", err) |
|
||||||
} |
|
||||||
|
|
||||||
if _, err = openpgp.CheckDetachedSignature(keyring, |
|
||||||
bytes.NewBuffer(block.Bytes), |
|
||||||
block.ArmoredSignature.Body, &packet.Config{}); err != nil { |
|
||||||
return fmt.Errorf("%v: %w", "failed to check signature", err) |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// 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, |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,56 @@ |
|||||||
|
package statickey |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/plugins" |
||||||
|
) |
||||||
|
|
||||||
|
const publicKeyID = "7e4d0c6a708866e7" |
||||||
|
const publicKeyText = `-----BEGIN PGP PUBLIC KEY BLOCK----- |
||||||
|
Version: OpenPGP.js v4.10.1 |
||||||
|
Comment: https://openpgpjs.org
|
||||||
|
|
||||||
|
xpMEXpTXXxMFK4EEACMEIwQBiOUQhvGbDLvndE0fEXaR0908wXzPGFpf0P0Z |
||||||
|
HJ06tsq+0higIYHp7WTNJVEZtcwoYLcPRGaa9OQqbUU63BEyZdgAkPTz3RFd |
||||||
|
5+TkDWZizDcaVFhzbDd500yTwexrpIrdInwC/jrgs7Zy/15h8KA59XXUkdmT |
||||||
|
YB6TR+OA9RKME+dCJozNGUdyYWZhbmEgPGVuZ0BncmFmYW5hLmNvbT7CvAQQ |
||||||
|
EwoAIAUCXpTXXwYLCQcIAwIEFQgKAgQWAgEAAhkBAhsDAh4BAAoJEH5NDGpw |
||||||
|
iGbnaWoCCQGQ3SQnCkRWrG6XrMkXOKfDTX2ow9fuoErN46BeKmLM4f1EkDZQ |
||||||
|
Tpq3SE8+My8B5BIH3SOcBeKzi3S57JHGBdFA+wIJAYWMrJNIvw8GeXne+oUo |
||||||
|
NzzACdvfqXAZEp/HFMQhCKfEoWGJE8d2YmwY2+3GufVRTI5lQnZOHLE8L/Vc |
||||||
|
1S5MXESjzpcEXpTXXxIFK4EEACMEIwQBtHX/SD5Qm3v4V92qpaIZQgtTX0sT |
||||||
|
cFPjYWAHqsQ1iENrYN/vg1wU3ADlYATvydOQYvkTyT/tbDvx2Fse8PL84MQA |
||||||
|
YKKQ6AJ3gLVvmeouZdU03YoV4MYaT8KbnJUkZQZkqdz2riOlySNI9CG3oYmv |
||||||
|
omjUAtzgAgnCcurfGLZkkMxlmY8DAQoJwqQEGBMKAAkFAl6U118CGwwACgkQ |
||||||
|
fk0ManCIZuc0jAIJAVw2xdLr4ZQqPUhubrUyFcqlWoW8dQoQagwO8s8ubmby |
||||||
|
KuLA9FWJkfuuRQr+O9gHkDVCez3aism7zmJBqIOi38aNAgjJ3bo6leSS2jR/ |
||||||
|
x5NqiKVi83tiXDPncDQYPymOnMhW0l7CVA7wj75HrFvvlRI/4MArlbsZ2tBn |
||||||
|
N1c5v9v/4h6qeA== |
||||||
|
=DNbR |
||||||
|
-----END PGP PUBLIC KEY BLOCK----- |
||||||
|
` |
||||||
|
|
||||||
|
type KeyRetriever struct{} |
||||||
|
|
||||||
|
var _ plugins.KeyRetriever = (*KeyRetriever)(nil) |
||||||
|
|
||||||
|
func New() *KeyRetriever { |
||||||
|
return &KeyRetriever{} |
||||||
|
} |
||||||
|
|
||||||
|
func (kr *KeyRetriever) GetPublicKey(ctx context.Context, keyID string) (string, error) { |
||||||
|
if keyID == publicKeyID { |
||||||
|
return publicKeyText, nil |
||||||
|
} |
||||||
|
return "", fmt.Errorf("missing public key for %s", keyID) |
||||||
|
} |
||||||
|
|
||||||
|
func GetDefaultKey() string { |
||||||
|
return publicKeyText |
||||||
|
} |
||||||
|
|
||||||
|
func GetDefaultKeyID() string { |
||||||
|
return publicKeyID |
||||||
|
} |
@ -0,0 +1,234 @@ |
|||||||
|
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/config" |
||||||
|
"github.com/grafana/grafana/pkg/plugins/log" |
||||||
|
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" |
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt" |
||||||
|
) |
||||||
|
|
||||||
|
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 *config.Cfg |
||||||
|
log log.Logger |
||||||
|
|
||||||
|
lock sync.Mutex |
||||||
|
cli http.Client |
||||||
|
kv plugins.KeyStore |
||||||
|
hasKeys bool |
||||||
|
} |
||||||
|
|
||||||
|
var _ plugins.KeyRetriever = (*KeyRetriever)(nil) |
||||||
|
|
||||||
|
func ProvideService(cfg *config.Cfg, kv plugins.KeyStore) *KeyRetriever { |
||||||
|
kr := &KeyRetriever{ |
||||||
|
cfg: cfg, |
||||||
|
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.cfg.Features.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 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, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
package keyretriever |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/plugins" |
||||||
|
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" |
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever/dynamic" |
||||||
|
) |
||||||
|
|
||||||
|
var _ plugins.KeyRetriever = (*Service)(nil) |
||||||
|
|
||||||
|
type Service struct { |
||||||
|
kr plugins.KeyRetriever |
||||||
|
} |
||||||
|
|
||||||
|
func ProvideService(dkr *dynamic.KeyRetriever) *Service { |
||||||
|
s := &Service{} |
||||||
|
if !dkr.IsDisabled() { |
||||||
|
s.kr = dkr |
||||||
|
} else { |
||||||
|
s.kr = statickey.New() |
||||||
|
} |
||||||
|
return s |
||||||
|
} |
||||||
|
|
||||||
|
func (kr *Service) GetPublicKey(ctx context.Context, keyID string) (string, error) { |
||||||
|
return kr.kr.GetPublicKey(ctx, keyID) |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
package keyretriever |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/kvstore" |
||||||
|
"github.com/grafana/grafana/pkg/plugins/config" |
||||||
|
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" |
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt" |
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever/dynamic" |
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/keystore" |
||||||
|
"github.com/stretchr/testify/require" |
||||||
|
) |
||||||
|
|
||||||
|
func Test_GetPublicKey(t *testing.T) { |
||||||
|
t.Run("it should return a static key", func(t *testing.T) { |
||||||
|
cfg := &config.Cfg{ |
||||||
|
Features: featuremgmt.WithFeatures(), |
||||||
|
} |
||||||
|
kr := ProvideService(dynamic.ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore()))) |
||||||
|
key, err := kr.GetPublicKey(context.Background(), statickey.GetDefaultKeyID()) |
||||||
|
require.NoError(t, err) |
||||||
|
require.Equal(t, statickey.GetDefaultKey(), key) |
||||||
|
}) |
||||||
|
} |
Loading…
Reference in new issue