Commit 1bee9f41 by Will Browne Committed by GitHub

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 <arve.knudsen@gmail.com>

* 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 <arve.knudsen@gmail.com>
parent c8b7ccc6
......@@ -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))
......
......@@ -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,
}
}
......
......@@ -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)
......
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"`
}
......@@ -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 {
......
......@@ -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 {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment