grafana/pkg/plugins/pluginassets/modulehash/modulehash.go
Will Browne 6800746f35
Plugins: Move module hash logic into pkg/plugins (#116268)
* move module hash

* fix tests + lint

* more fixes

* tidy

* make workspace
2026-01-15 09:41:01 +00:00

144 lines
4.7 KiB
Go

package modulehash
import (
"context"
"encoding/base64"
"encoding/hex"
"fmt"
"path"
"path/filepath"
"sync"
"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/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
)
type Calculator struct {
reg registry.Service
cfg *config.PluginManagementCfg
cdn *pluginscdn.Service
signature *signature.Signature
log log.Logger
moduleHashCache sync.Map
}
func NewCalculator(cfg *config.PluginManagementCfg, reg registry.Service, cdn *pluginscdn.Service, signature *signature.Signature) *Calculator {
return &Calculator{
cfg: cfg,
reg: reg,
cdn: cdn,
signature: signature,
log: log.New("modulehash"),
}
}
// ModuleHash returns the module.js SHA256 hash for a plugin in the format expected by the browser for SRI checks.
// The module hash is read from the plugin's MANIFEST.txt file.
// The plugin can also be a nested plugin.
// If the plugin is unsigned, an empty string is returned.
// The results are cached to avoid repeated reads from the MANIFEST.txt file.
func (c *Calculator) ModuleHash(ctx context.Context, pluginID, pluginVersion string) string {
p, ok := c.reg.Plugin(ctx, pluginID, pluginVersion)
if !ok {
c.log.Error("Failed to calculate module hash as plugin is not registered", "pluginId", pluginID)
return ""
}
k := c.moduleHashCacheKey(pluginID, pluginVersion)
cachedValue, ok := c.moduleHashCache.Load(k)
if ok {
return cachedValue.(string)
}
mh, err := c.moduleHash(ctx, p, "")
if err != nil {
c.log.Error("Failed to calculate module hash", "pluginId", p.ID, "error", err)
}
c.moduleHashCache.Store(k, mh)
return mh
}
// moduleHash is the underlying function for ModuleHash. See its documentation for more information.
// If the plugin is not a CDN plugin, the function will return an empty string.
// It will read the module hash from the MANIFEST.txt in the [[plugins.FS]] of the provided plugin.
// If childFSBase is provided, the function will try to get the hash from MANIFEST.txt for the provided children's
// module.js file, rather than for the provided plugin.
func (c *Calculator) moduleHash(ctx context.Context, p *plugins.Plugin, childFSBase string) (r string, err error) {
if !c.cfg.Features.SriChecksEnabled {
return "", nil
}
// Ignore unsigned plugins
if !p.Signature.IsValid() {
return "", nil
}
if p.Parent != nil {
// The module hash is contained within the parent's MANIFEST.txt file.
// For example, the parent's MANIFEST.txt will contain an entry similar to this:
//
// ```
// "datasource/module.js": "1234567890abcdef..."
// ```
//
// Recursively call moduleHash with the parent plugin and with the children plugin folder path
// to get the correct module hash for the nested plugin.
if childFSBase == "" {
childFSBase = p.FS.Base()
}
return c.moduleHash(ctx, p.Parent, childFSBase)
}
// Only CDN plugins are supported for SRI checks.
// CDN plugins have the version as part of the URL, which acts as a cache-buster.
// Needed due to: https://github.com/grafana/plugin-tools/pull/1426
// FS plugins build before this change will have SRI mismatch issues.
if !c.cdnEnabled(p.ID, p.FS) {
return "", nil
}
manifest, err := c.signature.ReadPluginManifestFromFS(ctx, p.FS)
if err != nil {
return "", fmt.Errorf("read plugin manifest: %w", err)
}
if !manifest.IsV2() {
return "", nil
}
var childPath string
if childFSBase != "" {
// Calculate the relative path of the child plugin folder from the parent plugin folder.
childPath, err = p.FS.Rel(childFSBase)
if err != nil {
return "", fmt.Errorf("rel path: %w", err)
}
// MANIFETS.txt uses forward slashes as path separators.
childPath = filepath.ToSlash(childPath)
}
moduleHash, ok := manifest.Files[path.Join(childPath, "module.js")]
if !ok {
return "", nil
}
return convertHashForSRI(moduleHash)
}
func (c *Calculator) cdnEnabled(pluginID string, fs plugins.FS) bool {
return c.cdn.PluginSupported(pluginID) || fs.Type().CDN()
}
// convertHashForSRI takes a SHA256 hash string and returns it as expected by the browser for SRI checks.
func convertHashForSRI(h string) (string, error) {
hb, err := hex.DecodeString(h)
if err != nil {
return "", fmt.Errorf("hex decode string: %w", err)
}
return "sha256-" + base64.StdEncoding.EncodeToString(hb), nil
}
// moduleHashCacheKey returns a unique key for the module hash cache.
func (c *Calculator) moduleHashCacheKey(pluginId, pluginVersion string) string {
return pluginId + ":" + pluginVersion
}