package plugins import ( "context" "encoding/json" "errors" "fmt" "io/ioutil" "os" "path/filepath" "reflect" "runtime" "strings" "time" "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/plugins/backendplugin" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/errutil" ) var ( DataSources map[string]*DataSourcePlugin Panels map[string]*PanelPlugin StaticRoutes []*PluginStaticRoute Apps map[string]*AppPlugin Plugins map[string]*PluginBase PluginTypes map[string]interface{} Renderer *RendererPlugin GrafanaLatestVersion string GrafanaHasUpdate bool plog log.Logger pluginScanningErrors map[string]*PluginError ) type unsignedPluginConditionFunc = func(plugin *PluginBase) bool type PluginScanner struct { pluginPath string errors []error backendPluginManager backendplugin.Manager cfg *setting.Cfg requireSigned bool log log.Logger plugins map[string]*PluginBase allowUnsignedPluginsCondition unsignedPluginConditionFunc } type PluginManager struct { BackendPluginManager backendplugin.Manager `inject:""` Cfg *setting.Cfg `inject:""` log log.Logger scanningErrors []error // AllowUnsignedPluginsCondition changes the policy for allowing unsigned plugins. Signature validation only runs when plugins are starting // and running plugins will not be terminated if they violate the new policy. AllowUnsignedPluginsCondition unsignedPluginConditionFunc } func init() { registry.RegisterService(&PluginManager{}) } func (pm *PluginManager) Init() error { pm.log = log.New("plugins") plog = log.New("plugins") DataSources = map[string]*DataSourcePlugin{} StaticRoutes = []*PluginStaticRoute{} Panels = map[string]*PanelPlugin{} Apps = map[string]*AppPlugin{} Plugins = map[string]*PluginBase{} PluginTypes = map[string]interface{}{ "panel": PanelPlugin{}, "datasource": DataSourcePlugin{}, "app": AppPlugin{}, "renderer": RendererPlugin{}, } pluginScanningErrors = map[string]*PluginError{} pm.log.Info("Starting plugin search") plugDir := filepath.Join(setting.StaticRootPath, "app/plugins") pm.log.Debug("Scanning core plugin directory", "dir", plugDir) if err := pm.scan(plugDir, false); err != nil { return errutil.Wrapf(err, "failed to scan core plugin directory '%s'", plugDir) } plugDir = pm.Cfg.BundledPluginsPath pm.log.Debug("Scanning bundled plugins directory", "dir", plugDir) exists, err := fs.Exists(plugDir) if err != nil { return err } if exists { if err := pm.scan(plugDir, false); err != nil { return errutil.Wrapf(err, "failed to scan bundled plugins directory '%s'", plugDir) } } // check if plugins dir exists exists, err = fs.Exists(setting.PluginsPath) if err != nil { return err } if !exists { if err = os.MkdirAll(setting.PluginsPath, os.ModePerm); err != nil { pm.log.Error("failed to create external plugins directory", "dir", setting.PluginsPath, "error", err) } else { pm.log.Info("External plugins directory created", "directory", setting.PluginsPath) } } else { pm.log.Debug("Scanning external plugins directory", "dir", setting.PluginsPath) if err := pm.scan(setting.PluginsPath, true); err != nil { return errutil.Wrapf(err, "failed to scan external plugins directory '%s'", setting.PluginsPath) } } if err := pm.scanPluginPaths(); err != nil { return err } for _, panel := range Panels { panel.initFrontendPlugin() } for _, ds := range DataSources { ds.initFrontendPlugin() } for _, app := range Apps { app.initApp() } if Renderer != nil { Renderer.initFrontendPlugin() } for _, p := range Plugins { if p.IsCorePlugin { p.Signature = PluginSignatureInternal } else { metrics.SetPluginBuildInformation(p.Id, p.Type, p.Info.Version) } } return nil } func (pm *PluginManager) Run(ctx context.Context) error { pm.updateAppDashboards() pm.checkForUpdates() ticker := time.NewTicker(time.Minute * 10) run := true for run { select { case <-ticker.C: pm.checkForUpdates() case <-ctx.Done(): run = false } } return ctx.Err() } // scanPluginPaths scans configured plugin paths. func (pm *PluginManager) scanPluginPaths() error { for pluginID, settings := range pm.Cfg.PluginSettings { path, exists := settings["path"] if !exists || path == "" { continue } if err := pm.scan(path, true); err != nil { return errutil.Wrapf(err, "failed to scan directory configured for plugin '%s': '%s'", pluginID, path) } } return nil } // scan a directory for plugins. func (pm *PluginManager) scan(pluginDir string, requireSigned bool) error { scanner := &PluginScanner{ pluginPath: pluginDir, backendPluginManager: pm.BackendPluginManager, cfg: pm.Cfg, requireSigned: requireSigned, log: pm.log, plugins: map[string]*PluginBase{}, allowUnsignedPluginsCondition: pm.AllowUnsignedPluginsCondition, } // 1st pass: Scan plugins, also mapping plugins to their respective directories if err := util.Walk(pluginDir, true, true, scanner.walker); err != nil { if errors.Is(err, os.ErrNotExist) { pm.log.Debug("Couldn't scan directory since it doesn't exist", "pluginDir", pluginDir) return nil } if errors.Is(err, os.ErrPermission) { pm.log.Debug("Couldn't scan directory due to lack of permissions", "pluginDir", pluginDir) return nil } if pluginDir != "data/plugins" { pm.log.Warn("Could not scan dir", "pluginDir", pluginDir, "err", err) } return err } pm.log.Debug("Initial plugin loading done") // 2nd pass: Validate and register plugins for dpath, plugin := range scanner.plugins { // Try to find any root plugin ancestors := strings.Split(dpath, string(filepath.Separator)) ancestors = ancestors[0 : len(ancestors)-1] aPath := "" if runtime.GOOS != "windows" && filepath.IsAbs(dpath) { aPath = "/" } for _, a := range ancestors { aPath = filepath.Join(aPath, a) if root, ok := scanner.plugins[aPath]; ok { plugin.Root = root break } } pm.log.Debug("Found plugin", "id", plugin.Id, "signature", plugin.Signature, "hasRoot", plugin.Root != nil) signingError := scanner.validateSignature(plugin) if signingError != nil { pm.log.Debug("Failed to validate plugin signature. Will skip loading", "id", plugin.Id, "signature", plugin.Signature, "status", signingError.ErrorCode) pluginScanningErrors[plugin.Id] = signingError continue } pm.log.Debug("Attempting to add plugin", "id", plugin.Id) pluginGoType, exists := PluginTypes[plugin.Type] if !exists { return fmt.Errorf("unknown plugin type %q", plugin.Type) } jsonFPath := filepath.Join(plugin.PluginDir, "plugin.json") // External plugins need a module.js file for SystemJS to load if !strings.HasPrefix(jsonFPath, setting.StaticRootPath) && !scanner.IsBackendOnlyPlugin(plugin.Type) { module := filepath.Join(plugin.PluginDir, "module.js") exists, err := fs.Exists(module) if err != nil { return err } if !exists { scanner.log.Warn("Plugin missing module.js", "name", plugin.Name, "warning", "Missing module.js, If you loaded this plugin from git, make sure to compile it.", "path", module) } } // nolint:gosec // We can ignore the gosec G304 warning on this one because `jsonFPath` is based // on plugin the folder structure on disk and not user input. reader, err := os.Open(jsonFPath) if err != nil { return err } defer func() { if err := reader.Close(); err != nil { scanner.log.Warn("Failed to close JSON file", "path", jsonFPath, "err", err) } }() jsonParser := json.NewDecoder(reader) loader := reflect.New(reflect.TypeOf(pluginGoType)).Interface().(PluginLoader) // Load the full plugin, and add it to manager if err := loader.Load(jsonParser, plugin, scanner.backendPluginManager); err != nil { if errors.Is(err, duplicatePluginError{}) { pm.log.Warn("Plugin is duplicate", "error", err) scanner.errors = append(scanner.errors, err) continue } return err } pm.log.Debug("Successfully added plugin", "id", plugin.Id) } if len(scanner.errors) > 0 { pm.log.Warn("Some plugins failed to load", "errors", scanner.errors) pm.scanningErrors = scanner.errors } return nil } // GetDatasource returns a datasource based on passed pluginID if it exists // // This function fetches the datasource from the global variable DataSources in this package. // Rather then refactor all dependencies on the global variable we can use this as an transition. func (pm *PluginManager) GetDatasource(pluginID string) (*DataSourcePlugin, bool) { ds, exist := DataSources[pluginID] return ds, exist } func (s *PluginScanner) walker(currentPath string, f os.FileInfo, err error) error { // We scan all the subfolders for plugin.json (with some exceptions) so that we also load embedded plugins, for // example https://github.com/raintank/worldping-app/tree/master/dist/grafana-worldmap-panel worldmap panel plugin // is embedded in worldping app. if err != nil { return err } if f.Name() == "node_modules" || f.Name() == "Chromium.app" { return util.ErrWalkSkipDir } if f.IsDir() { return nil } if f.Name() != "plugin.json" { return nil } // 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. if err := s.loadPlugin(currentPath); err != nil { s.log.Error("Failed to load plugin", "error", err, "pluginPath", filepath.Dir(currentPath)) s.errors = append(s.errors, err) } return nil } func (s *PluginScanner) loadPlugin(pluginJSONFilePath string) error { s.log.Debug("Loading plugin", "path", pluginJSONFilePath) currentDir := filepath.Dir(pluginJSONFilePath) reader, err := os.Open(pluginJSONFilePath) if err != nil { return err } defer func() { if err := reader.Close(); err != nil { s.log.Warn("Failed to close JSON file", "path", pluginJSONFilePath, "err", err) } }() jsonParser := json.NewDecoder(reader) pluginCommon := PluginBase{} if err := jsonParser.Decode(&pluginCommon); err != nil { return err } if pluginCommon.Id == "" || pluginCommon.Type == "" { return errors.New("did not find type or id properties in plugin.json") } pluginCommon.PluginDir = filepath.Dir(pluginJSONFilePath) pluginCommon.Signature = getPluginSignatureState(s.log, &pluginCommon) s.plugins[currentDir] = &pluginCommon return nil } func (*PluginScanner) IsBackendOnlyPlugin(pluginType string) bool { return pluginType == "renderer" } // validateSignature validates a plugin's signature. func (s *PluginScanner) validateSignature(plugin *PluginBase) *PluginError { if plugin.Signature == PluginSignatureValid { s.log.Debug("Plugin has valid signature", "id", plugin.Id) return nil } if plugin.Root != nil { // If a descendant plugin with invalid signature, set signature to that of root if plugin.IsCorePlugin || plugin.Signature == PluginSignatureInternal { s.log.Debug("Not setting descendant plugin's signature to that of root since it's core or internal", "plugin", plugin.Id, "signature", plugin.Signature, "isCore", plugin.IsCorePlugin) } else { s.log.Debug("Setting descendant plugin's signature to that of root", "plugin", plugin.Id, "root", plugin.Root.Id, "signature", plugin.Signature, "rootSignature", plugin.Root.Signature) plugin.Signature = plugin.Root.Signature if plugin.Signature == PluginSignatureValid { s.log.Debug("Plugin has valid signature (inherited from root)", "id", plugin.Id) return nil } } } else { s.log.Debug("Non-valid plugin Signature", "pluginID", plugin.Id, "pluginDir", plugin.PluginDir, "state", plugin.Signature) } // For the time being, we choose to only require back-end plugins to be signed // NOTE: the state is calculated again when setting metadata on the object if !plugin.Backend || !s.requireSigned { return nil } switch plugin.Signature { case PluginSignatureUnsigned: if allowed := s.allowUnsigned(plugin); !allowed { s.log.Debug("Plugin is unsigned", "id", plugin.Id) s.errors = append(s.errors, fmt.Errorf("plugin %q is unsigned", plugin.Id)) return &PluginError{ ErrorCode: signatureMissing, } } s.log.Warn("Running an unsigned backend plugin", "pluginID", plugin.Id, "pluginDir", plugin.PluginDir) return nil case PluginSignatureInvalid: s.log.Debug("Plugin %q has an invalid signature", plugin.Id) s.errors = append(s.errors, fmt.Errorf("plugin %q has an invalid signature", plugin.Id)) return &PluginError{ ErrorCode: signatureInvalid, } case PluginSignatureModified: s.log.Debug("Plugin %q has a modified signature", plugin.Id) s.errors = append(s.errors, fmt.Errorf("plugin %q's signature has been modified", plugin.Id)) return &PluginError{ ErrorCode: signatureModified, } default: panic(fmt.Sprintf("Plugin %q has unrecognized plugin signature state %q", plugin.Id, plugin.Signature)) } } func (s *PluginScanner) allowUnsigned(plugin *PluginBase) bool { if s.allowUnsignedPluginsCondition != nil { return s.allowUnsignedPluginsCondition(plugin) } if setting.Env == setting.Dev { return true } for _, plug := range s.cfg.PluginsAllowUnsigned { if plug == plugin.Id { return true } } return false } func ScanningErrors() []PluginError { scanningErrs := make([]PluginError, 0) for id, e := range pluginScanningErrors { scanningErrs = append(scanningErrs, PluginError{ ErrorCode: e.ErrorCode, PluginID: id, }) } return scanningErrs } func GetPluginMarkdown(pluginId string, name string) ([]byte, error) { plug, exists := Plugins[pluginId] if !exists { return nil, PluginNotFoundError{pluginId} } // nolint:gosec // We can ignore the gosec G304 warning on this one because `plug.PluginDir` is based // on plugin the folder structure on disk and not user input. path := filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToUpper(name))) exists, err := fs.Exists(path) if err != nil { return nil, err } if !exists { path = filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToLower(name))) } exists, err = fs.Exists(path) if err != nil { return nil, err } if !exists { return make([]byte, 0), nil } // nolint:gosec // We can ignore the gosec G304 warning on this one because `plug.PluginDir` is based // on plugin the folder structure on disk and not user input. data, err := ioutil.ReadFile(path) if err != nil { return nil, err } return data, nil }