From 1bee9f416890d608ab47f31b8c7dc652887f8fbf Mon Sep 17 00:00:00 2001 From: Will Browne Date: Fri, 23 Oct 2020 16:45:43 +0200 Subject: [PATCH] Plugins: Track plugin signing errors and expose them to the frontend (#28219) * first pass * return list * types and cleanup * add to plugin page and add styles * update comment * update comment * fix component path * simplify error component * simplify error struct * fix tests * don't export and fix string() * update naming * remove frontend * introduce phantom loader * track single error * remove error from base * remove unused struct * remove unnecessary filter * add errors endpoint * Update set log to use id field Co-authored-by: Arve Knudsen * skip adding BE plugins * remove errs from plugin + ds list * remove unnecessary fields * add signature state to panels * remove unused code * apply PR feedback * update comment * merge dto with model Co-authored-by: Arve Knudsen --- pkg/api/api.go | 1 + pkg/api/frontendsettings.go | 1 + pkg/api/plugins.go | 6 ++++- pkg/plugins/error.go | 14 +++++++++++ pkg/plugins/models.go | 2 +- pkg/plugins/plugins.go | 50 ++++++++++++++++++++++++++----------- 6 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 pkg/plugins/error.go diff --git a/pkg/api/api.go b/pkg/api/api.go index 6bca1c32d98..ebd5ff713ed 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -256,6 +256,7 @@ func (hs *HTTPServer) registerRoutes() { apiRoute.Get("/plugins/:pluginId/health", Wrap(hs.CheckHealth)) apiRoute.Any("/plugins/:pluginId/resources", hs.CallResource) apiRoute.Any("/plugins/:pluginId/resources/*", hs.CallResource) + apiRoute.Any("/plugins/errors", Wrap(hs.GetPluginErrorsList)) apiRoute.Group("/plugins", func(pluginRoute routing.RouteRegister) { pluginRoute.Get("/:pluginId/dashboards/", Wrap(GetPluginDashboards)) diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index f0266f78839..f7e3f674072 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -173,6 +173,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i "sort": getPanelSort(panel.Id), "skipDataQuery": panel.SkipDataQuery, "state": panel.State, + "signature": panel.Signature, } } diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index bdbbc373140..838243b099c 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -121,7 +121,7 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) Response { listItem.DefaultNavUrl = setting.AppSubUrl + "/plugins/" + listItem.Id + "/" } - // filter out disabled + // filter out disabled plugins if enabledFilter == "1" && !listItem.Enabled { continue } @@ -364,6 +364,10 @@ func (hs *HTTPServer) getCachedPluginSettings(pluginID string, user *models.Sign return query.Result, nil } +func (hs *HTTPServer) GetPluginErrorsList(c *models.ReqContext) Response { + return JSON(200, plugins.ScanningErrors()) +} + func translatePluginRequestErrorToAPIError(err error) Response { if errors.Is(err, backendplugin.ErrPluginNotRegistered) { return Error(404, "Plugin not found", err) diff --git a/pkg/plugins/error.go b/pkg/plugins/error.go new file mode 100644 index 00000000000..f2e58dc12c9 --- /dev/null +++ b/pkg/plugins/error.go @@ -0,0 +1,14 @@ +package plugins + +const ( + signatureMissing ErrorCode = "signatureMissing" + signatureModified ErrorCode = "signatureModified" + signatureInvalid ErrorCode = "signatureInvalid" +) + +type ErrorCode string + +type PluginError struct { + ErrorCode `json:"errorCode"` + PluginID string `json:"pluginId,omitempty"` +} diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 3a0d8056ff7..63a748b4d26 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -96,7 +96,7 @@ func (pb *PluginBase) registerPlugin(base *PluginBase) error { } if !strings.HasPrefix(base.PluginDir, setting.StaticRootPath) { - plog.Info("Registering plugin", "name", pb.Name) + plog.Info("Registering plugin", "id", pb.Id) } if len(pb.Dependencies.Plugins) == 0 { diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index f6f6f846f3c..0cffde11c10 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -36,6 +36,8 @@ var ( GrafanaLatestVersion string GrafanaHasUpdate bool plog log.Logger + + pluginScanningErrors map[string]*PluginError ) type PluginScanner struct { @@ -75,6 +77,7 @@ func (pm *PluginManager) Init() error { "renderer": RendererPlugin{}, "transform": TransformPlugin{}, } + pluginScanningErrors = map[string]*PluginError{} pm.log.Info("Starting plugin search") @@ -228,9 +231,11 @@ func (pm *PluginManager) scan(pluginDir string, requireSigned bool) error { } pm.log.Debug("Found plugin", "id", plugin.Id, "signature", plugin.Signature, "hasRoot", plugin.Root != nil) - if !scanner.validateSignature(plugin) { - pm.log.Debug("Not adding plugin since it lacks a valid signature", "id", plugin.Id, - "signature", plugin.Signature) + 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 } @@ -241,8 +246,6 @@ func (pm *PluginManager) scan(pluginDir string, requireSigned bool) error { return fmt.Errorf("unknown plugin type %q", plugin.Type) } - loader := reflect.New(reflect.TypeOf(pluginGoType)).Interface().(PluginLoader) - jsonFPath := filepath.Join(plugin.PluginDir, "plugin.json") // External plugins need a module.js file for SystemJS to load @@ -268,6 +271,8 @@ func (pm *PluginManager) scan(pluginDir string, requireSigned bool) error { 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{}) { @@ -275,10 +280,8 @@ func (pm *PluginManager) scan(pluginDir string, requireSigned bool) error { scanner.errors = append(scanner.errors, err) continue } - return err } - pm.log.Debug("Successfully added plugin", "id", plugin.Id) } @@ -369,16 +372,16 @@ func (scanner *PluginScanner) IsBackendOnlyPlugin(pluginType string) bool { } // validateSignature validates a plugin's signature. -func (s *PluginScanner) validateSignature(plugin *PluginBase) bool { +func (s *PluginScanner) validateSignature(plugin *PluginBase) *PluginError { // 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 true + return nil } if plugin.Signature == PluginSignatureValid { s.log.Debug("Plugin has valid signature", "id", plugin.Id) - return true + return nil } if plugin.Root != nil { @@ -392,7 +395,7 @@ func (s *PluginScanner) validateSignature(plugin *PluginBase) bool { plugin.Signature = plugin.Root.Signature if plugin.Signature == PluginSignatureValid { s.log.Debug("Plugin has valid signature (inherited from root)", "id", plugin.Id) - return true + return nil } } } else { @@ -412,25 +415,42 @@ func (s *PluginScanner) validateSignature(plugin *PluginBase) bool { if setting.Env != setting.Dev && !allowUnsigned { s.log.Debug("Plugin is unsigned", "id", plugin.Id) s.errors = append(s.errors, fmt.Errorf("plugin %q is unsigned", plugin.Id)) - return false + return &PluginError{ + ErrorCode: signatureMissing, + } } s.log.Warn("Running an unsigned backend plugin", "pluginID", plugin.Id, "pluginDir", plugin.PluginDir) - return true + 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 false + 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 false + return &PluginError{ + ErrorCode: signatureModified, + } default: panic(fmt.Sprintf("Plugin %q has unrecognized plugin signature state %q", plugin.Id, plugin.Signature)) } } +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 {