Files
Marcus Efraimsson 333de57999 Plugins: Fix scanning plugins when permission for directories is lacking (#44587)
Fixes so that errors (directory not exists, no permission) when scanning plugins are logged as 
errors rather than with debug level. In addition, before the scanning would halt in case of 
referenced errors, but with these changes the scanning will continue. If any other error 
than the referenced error happens the scanning for specific directory would halt and return 
the error, e.g. stop Grafana from starting.

Fixes #43012
2022-01-31 14:07:10 +01:00

368 lines
11 KiB
Go

package loader
import (
"context"
"encoding/json"
"errors"
"net/url"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"github.com/gosimple/slug"
"github.com/grafana/grafana/pkg/infra/fs"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
"github.com/grafana/grafana/pkg/plugins/manager/loader/initializer"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
var (
ErrInvalidPluginJSON = errors.New("did not find valid type or id properties in plugin.json")
ErrInvalidPluginJSONFilePath = errors.New("invalid plugin.json filepath was provided")
)
var _ plugins.ErrorResolver = (*Loader)(nil)
type Loader struct {
cfg *plugins.Cfg
pluginFinder finder.Finder
pluginInitializer initializer.Initializer
signatureValidator signature.Validator
log log.Logger
errs map[string]*plugins.SignatureError
}
func ProvideService(cfg *setting.Cfg, license models.Licensing, authorizer plugins.PluginLoaderAuthorizer,
backendProvider plugins.BackendFactoryProvider) (*Loader, error) {
return New(plugins.FromGrafanaCfg(cfg), license, authorizer, backendProvider), nil
}
func New(cfg *plugins.Cfg, license models.Licensing, authorizer plugins.PluginLoaderAuthorizer,
backendProvider plugins.BackendFactoryProvider) *Loader {
return &Loader{
cfg: cfg,
pluginFinder: finder.New(),
pluginInitializer: initializer.New(cfg, backendProvider, license),
signatureValidator: signature.NewValidator(authorizer),
errs: make(map[string]*plugins.SignatureError),
log: log.New("plugin.loader"),
}
}
func (l *Loader) Load(ctx context.Context, class plugins.Class, paths []string, ignore map[string]struct{}) ([]*plugins.Plugin, error) {
pluginJSONPaths, err := l.pluginFinder.Find(paths)
if err != nil {
return nil, err
}
return l.loadPlugins(ctx, class, pluginJSONPaths, ignore)
}
func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSONPaths []string, existingPlugins map[string]struct{}) ([]*plugins.Plugin, error) {
var foundPlugins = foundPlugins{}
// load plugin.json files and map directory to JSON data
for _, pluginJSONPath := range pluginJSONPaths {
plugin, err := l.readPluginJSON(pluginJSONPath)
if err != nil {
l.log.Warn("Skipping plugin loading as it's plugin.json is invalid", "id", plugin.ID)
continue
}
pluginJSONAbsPath, err := filepath.Abs(pluginJSONPath)
if err != nil {
l.log.Warn("Skipping plugin loading as full plugin.json path could not be calculated", "id", plugin.ID)
continue
}
if _, dupe := foundPlugins[filepath.Dir(pluginJSONAbsPath)]; dupe {
l.log.Warn("Skipping plugin loading as it's a duplicate", "id", plugin.ID)
continue
}
foundPlugins[filepath.Dir(pluginJSONAbsPath)] = plugin
}
foundPlugins.stripDuplicates(existingPlugins, l.log)
// calculate initial signature state
loadedPlugins := make(map[string]*plugins.Plugin)
for pluginDir, pluginJSON := range foundPlugins {
plugin := createPluginBase(pluginJSON, class, pluginDir, l.log)
sig, err := signature.Calculate(l.log, plugin)
if err != nil {
l.log.Warn("Could not calculate plugin signature state", "pluginID", plugin.ID, "err", err)
continue
}
plugin.Signature = sig.Status
plugin.SignatureType = sig.Type
plugin.SignatureOrg = sig.SigningOrg
plugin.SignedFiles = sig.Files
loadedPlugins[plugin.PluginDir] = plugin
}
// wire up plugin dependencies
for _, plugin := range loadedPlugins {
ancestors := strings.Split(plugin.PluginDir, string(filepath.Separator))
ancestors = ancestors[0 : len(ancestors)-1]
pluginPath := ""
if runtime.GOOS != "windows" && filepath.IsAbs(plugin.PluginDir) {
pluginPath = "/"
}
for _, ancestor := range ancestors {
pluginPath = filepath.Join(pluginPath, ancestor)
if parentPlugin, ok := loadedPlugins[pluginPath]; ok {
plugin.Parent = parentPlugin
plugin.Parent.Children = append(plugin.Parent.Children, plugin)
break
}
}
}
// validate signatures
verifiedPlugins := make([]*plugins.Plugin, 0)
for _, plugin := range loadedPlugins {
signingError := l.signatureValidator.Validate(plugin)
if signingError != nil {
l.log.Warn("Skipping loading plugin due to problem with signature",
"pluginID", plugin.ID, "status", signingError.SignatureStatus)
plugin.SignatureError = signingError
l.errs[plugin.ID] = signingError
// skip plugin so it will not be loaded any further
continue
}
// clear plugin error if a pre-existing error has since been resolved
delete(l.errs, plugin.ID)
// verify module.js exists for SystemJS to load
if !plugin.IsRenderer() && !plugin.IsCorePlugin() {
module := filepath.Join(plugin.PluginDir, "module.js")
if exists, err := fs.Exists(module); err != nil {
return nil, err
} else if !exists {
l.log.Warn("Plugin missing module.js",
"pluginID", plugin.ID,
"warning", "Missing module.js, If you loaded this plugin from git, make sure to compile it.",
"path", module)
}
}
if plugin.IsApp() {
setDefaultNavURL(plugin, l.cfg.AppSubURL)
}
if plugin.Parent != nil && plugin.Parent.IsApp() {
configureAppChildOPlugin(plugin.Parent, plugin)
}
verifiedPlugins = append(verifiedPlugins, plugin)
}
for _, p := range verifiedPlugins {
err := l.pluginInitializer.Initialize(ctx, p)
if err != nil {
return nil, err
}
metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature))
}
return verifiedPlugins, nil
}
func (l *Loader) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error) {
l.log.Debug("Loading plugin", "path", pluginJSONPath)
if !strings.EqualFold(filepath.Ext(pluginJSONPath), ".json") {
return plugins.JSONData{}, ErrInvalidPluginJSONFilePath
}
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `currentPath` is based
// on plugin the folder structure on disk and not user input.
reader, err := os.Open(pluginJSONPath)
if err != nil {
return plugins.JSONData{}, err
}
plugin := plugins.JSONData{}
if err := json.NewDecoder(reader).Decode(&plugin); err != nil {
return plugins.JSONData{}, err
}
if err := reader.Close(); err != nil {
l.log.Warn("Failed to close JSON file", "path", pluginJSONPath, "err", err)
}
if err := validatePluginJSON(plugin); err != nil {
return plugins.JSONData{}, err
}
if plugin.ID == "grafana-piechart-panel" {
plugin.Name = "Pie Chart (old)"
}
if len(plugin.Dependencies.Plugins) == 0 {
plugin.Dependencies.Plugins = []plugins.Dependency{}
}
if plugin.Dependencies.GrafanaVersion == "" {
plugin.Dependencies.GrafanaVersion = "*"
}
for _, include := range plugin.Includes {
if include.Role == "" {
include.Role = models.ROLE_VIEWER
}
}
return plugin, nil
}
func createPluginBase(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string, logger log.Logger) *plugins.Plugin {
plugin := &plugins.Plugin{
JSONData: pluginJSON,
PluginDir: pluginDir,
BaseURL: baseURL(pluginJSON, class, pluginDir),
Module: module(pluginJSON, class, pluginDir),
Class: class,
}
plugin.SetLogger(logger.New("pluginID", plugin.ID))
setImages(plugin)
return plugin
}
func setImages(p *plugins.Plugin) {
p.Info.Logos.Small = pluginLogoURL(p.Type, p.Info.Logos.Small, p.BaseURL)
p.Info.Logos.Large = pluginLogoURL(p.Type, p.Info.Logos.Large, p.BaseURL)
for i := 0; i < len(p.Info.Screenshots); i++ {
p.Info.Screenshots[i].Path = evalRelativePluginURLPath(p.Info.Screenshots[i].Path, p.BaseURL, p.Type)
}
}
func setDefaultNavURL(p *plugins.Plugin, appSubURL string) {
// slugify pages
for _, include := range p.Includes {
if include.Slug == "" {
include.Slug = slug.Make(include.Name)
}
if include.Type == "page" && include.DefaultNav {
p.DefaultNavURL = appSubURL + "/plugins/" + p.ID + "/page/" + include.Slug
}
if include.Type == "dashboard" && include.DefaultNav {
p.DefaultNavURL = appSubURL + "/dashboard/db/" + include.Slug
}
}
}
func configureAppChildOPlugin(parent *plugins.Plugin, child *plugins.Plugin) {
if !parent.IsApp() {
return
}
appSubPath := strings.ReplaceAll(strings.Replace(child.PluginDir, parent.PluginDir, "", 1), "\\", "/")
child.IncludedInAppID = parent.ID
child.BaseURL = parent.BaseURL
if parent.IsCorePlugin() {
child.Module = util.JoinURLFragments("app/plugins/app/"+parent.ID, appSubPath) + "/module"
} else {
child.Module = util.JoinURLFragments("plugins/"+parent.ID, appSubPath) + "/module"
}
}
func pluginLogoURL(pluginType plugins.Type, path, baseURL string) string {
if path == "" {
return defaultLogoPath(pluginType)
}
return evalRelativePluginURLPath(path, baseURL, pluginType)
}
func defaultLogoPath(pluginType plugins.Type) string {
return "public/img/icn-" + string(pluginType) + ".svg"
}
func evalRelativePluginURLPath(pathStr, baseURL string, pluginType plugins.Type) string {
if pathStr == "" {
return ""
}
u, _ := url.Parse(pathStr)
if u.IsAbs() {
return pathStr
}
// is set as default or has already been prefixed with base path
if pathStr == defaultLogoPath(pluginType) || strings.HasPrefix(pathStr, baseURL) {
return pathStr
}
return path.Join(baseURL, pathStr)
}
func (l *Loader) PluginErrors() []*plugins.Error {
errs := make([]*plugins.Error, 0)
for _, err := range l.errs {
errs = append(errs, &plugins.Error{
PluginID: err.PluginID,
ErrorCode: err.AsErrorCode(),
})
}
return errs
}
func baseURL(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) string {
if class == plugins.Core {
return path.Join("public/app/plugins", string(pluginJSON.Type), filepath.Base(pluginDir))
}
return path.Join("public/plugins", pluginJSON.ID)
}
func module(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) string {
if class == plugins.Core {
return path.Join("app/plugins", string(pluginJSON.Type), filepath.Base(pluginDir), "module")
}
return path.Join("plugins", pluginJSON.ID, "module")
}
func validatePluginJSON(data plugins.JSONData) error {
if data.ID == "" || !data.Type.IsValid() {
return ErrInvalidPluginJSON
}
return nil
}
type foundPlugins map[string]plugins.JSONData
// stripDuplicates will strip duplicate plugins or plugins that already exist
func (f *foundPlugins) stripDuplicates(existingPlugins map[string]struct{}, log log.Logger) {
pluginsByID := make(map[string]struct{})
for k, scannedPlugin := range *f {
if _, existing := existingPlugins[scannedPlugin.ID]; existing {
log.Debug("Skipping plugin as it's already installed", "plugin", scannedPlugin.ID)
delete(*f, k)
continue
}
pluginsByID[scannedPlugin.ID] = struct{}{}
}
}