mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			289 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			289 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Go
		
	
	
	
| package sources
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"slices"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/grafana/grafana/pkg/infra/fs"
 | |
| 	"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/util"
 | |
| )
 | |
| 
 | |
| var walk = util.Walk
 | |
| 
 | |
| var (
 | |
| 	ErrInvalidPluginJSONFilePath = errors.New("invalid plugin.json filepath was provided")
 | |
| )
 | |
| 
 | |
| type LocalSource struct {
 | |
| 	paths      []string
 | |
| 	class      plugins.Class
 | |
| 	strictMode bool // If true, tracks files via a StaticFS
 | |
| 	log        log.Logger
 | |
| }
 | |
| 
 | |
| // NewLocalSource represents a plugin with a fixed set of files.
 | |
| func NewLocalSource(class plugins.Class, paths []string) *LocalSource {
 | |
| 	return &LocalSource{
 | |
| 		paths:      paths,
 | |
| 		class:      class,
 | |
| 		strictMode: true,
 | |
| 		log:        log.New("local.source"),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // NewUnsafeLocalSource represents a plugin that has an unbounded set of files. This useful when running in
 | |
| // dev mode whilst developing a plugin.
 | |
| func NewUnsafeLocalSource(class plugins.Class, paths []string) *LocalSource {
 | |
| 	return &LocalSource{
 | |
| 		paths:      paths,
 | |
| 		class:      class,
 | |
| 		strictMode: false,
 | |
| 		log:        log.New("local.source"),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (s *LocalSource) PluginClass(_ context.Context) plugins.Class {
 | |
| 	return s.class
 | |
| }
 | |
| 
 | |
| // Paths returns the file system paths that this source will search for plugins.
 | |
| // This method is primarily intended for testing purposes.
 | |
| func (s *LocalSource) Paths() []string {
 | |
| 	return s.paths
 | |
| }
 | |
| 
 | |
| func (s *LocalSource) DefaultSignature(_ context.Context, _ string) (plugins.Signature, bool) {
 | |
| 	switch s.class {
 | |
| 	case plugins.ClassCore:
 | |
| 		return plugins.Signature{
 | |
| 			Status: plugins.SignatureStatusInternal,
 | |
| 		}, true
 | |
| 	default:
 | |
| 		return plugins.Signature{}, false
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (s *LocalSource) Discover(_ context.Context) ([]*plugins.FoundBundle, error) {
 | |
| 	if len(s.paths) == 0 {
 | |
| 		return []*plugins.FoundBundle{}, nil
 | |
| 	}
 | |
| 
 | |
| 	pluginJSONPaths := make([]string, 0, len(s.paths))
 | |
| 	for _, path := range s.paths {
 | |
| 		exists, err := fs.Exists(path)
 | |
| 		if err != nil {
 | |
| 			s.log.Warn("Skipping finding plugins as an error occurred", "path", path, "error", err)
 | |
| 			continue
 | |
| 		}
 | |
| 		if !exists {
 | |
| 			s.log.Warn("Skipping finding plugins as directory does not exist", "path", path)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		paths, err := s.getAbsPluginJSONPaths(path)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		pluginJSONPaths = append(pluginJSONPaths, paths...)
 | |
| 	}
 | |
| 
 | |
| 	// load plugin.json files and map directory to JSON data
 | |
| 	foundPlugins := make(map[string]plugins.JSONData)
 | |
| 	for _, pluginJSONPath := range pluginJSONPaths {
 | |
| 		plugin, err := s.readPluginJSON(pluginJSONPath)
 | |
| 		if err != nil {
 | |
| 			s.log.Warn("Skipping plugin loading as its plugin.json could not be read", "path", pluginJSONPath, "error", err)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		pluginJSONAbsPath, err := filepath.Abs(pluginJSONPath)
 | |
| 		if err != nil {
 | |
| 			s.log.Warn("Skipping plugin loading as absolute plugin.json path could not be calculated", "pluginId", plugin.ID, "error", err)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		foundPlugins[filepath.Dir(pluginJSONAbsPath)] = plugin
 | |
| 	}
 | |
| 
 | |
| 	res := make(map[string]*plugins.FoundBundle)
 | |
| 	for pluginDir, data := range foundPlugins {
 | |
| 		var pluginFs plugins.FS
 | |
| 		pluginFs = plugins.NewLocalFS(pluginDir)
 | |
| 		if s.strictMode {
 | |
| 			// Tighten up security by allowing access only to the files present up to this point.
 | |
| 			// Any new file "sneaked in" won't be allowed and will act as if the file does not exist.
 | |
| 			var err error
 | |
| 			pluginFs, err = plugins.NewStaticFS(pluginFs)
 | |
| 			if err != nil {
 | |
| 				return nil, err
 | |
| 			}
 | |
| 		}
 | |
| 		res[pluginDir] = &plugins.FoundBundle{
 | |
| 			Primary: plugins.FoundPlugin{
 | |
| 				JSONData: data,
 | |
| 				FS:       pluginFs,
 | |
| 			},
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Track child plugins and add them to their parent.
 | |
| 	childPlugins := make(map[string]struct{})
 | |
| 	for dir, p := range res {
 | |
| 		// Check if this plugin is the parent of another plugin.
 | |
| 		for dir2, p2 := range res {
 | |
| 			if dir == dir2 {
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			relPath, err := filepath.Rel(dir, dir2)
 | |
| 			if err != nil {
 | |
| 				s.log.Error("Cannot calculate relative path. Skipping", "pluginId", p2.Primary.JSONData.ID, "err", err)
 | |
| 				continue
 | |
| 			}
 | |
| 			if !strings.Contains(relPath, "..") {
 | |
| 				child := p2.Primary
 | |
| 				s.log.Debug("Adding child", "parent", p.Primary.JSONData.ID, "child", child.JSONData.ID, "relPath", relPath)
 | |
| 				p.Children = append(p.Children, &child)
 | |
| 				childPlugins[dir2] = struct{}{}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Remove child plugins from the result (they are already tracked via their parent).
 | |
| 	result := make([]*plugins.FoundBundle, 0, len(res))
 | |
| 	for k := range res {
 | |
| 		if _, ok := childPlugins[k]; !ok {
 | |
| 			result = append(result, res[k])
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| func (s *LocalSource) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error) {
 | |
| 	reader, err := s.readFile(pluginJSONPath)
 | |
| 	defer func() {
 | |
| 		if reader == nil {
 | |
| 			return
 | |
| 		}
 | |
| 		if err = reader.Close(); err != nil {
 | |
| 			s.log.Warn("Failed to close plugin JSON file", "path", pluginJSONPath, "error", err)
 | |
| 		}
 | |
| 	}()
 | |
| 	if err != nil {
 | |
| 		s.log.Warn("Skipping plugin loading as its plugin.json could not be read", "path", pluginJSONPath, "error", err)
 | |
| 		return plugins.JSONData{}, err
 | |
| 	}
 | |
| 	plugin, err := plugins.ReadPluginJSON(reader)
 | |
| 	if err != nil {
 | |
| 		s.log.Warn("Skipping plugin loading as its plugin.json could not be read", "path", pluginJSONPath, "error", err)
 | |
| 		return plugins.JSONData{}, err
 | |
| 	}
 | |
| 
 | |
| 	return plugin, nil
 | |
| }
 | |
| 
 | |
| func (s *LocalSource) getAbsPluginJSONPaths(path string) ([]string, error) {
 | |
| 	var pluginJSONPaths []string
 | |
| 
 | |
| 	var err error
 | |
| 	path, err = filepath.Abs(path)
 | |
| 	if err != nil {
 | |
| 		return []string{}, err
 | |
| 	}
 | |
| 
 | |
| 	if err = walk(path, true, true,
 | |
| 		func(currentPath string, fi os.FileInfo, err error) error {
 | |
| 			if err != nil {
 | |
| 				if errors.Is(err, os.ErrNotExist) {
 | |
| 					s.log.Error("Couldn't scan directory since it doesn't exist", "pluginDir", path, "error", err)
 | |
| 					return nil
 | |
| 				}
 | |
| 				if errors.Is(err, os.ErrPermission) {
 | |
| 					s.log.Error("Couldn't scan directory due to lack of permissions", "pluginDir", path, "error", err)
 | |
| 					return nil
 | |
| 				}
 | |
| 
 | |
| 				return fmt.Errorf("filepath.Walk reported an error for %q: %w", currentPath, err)
 | |
| 			}
 | |
| 
 | |
| 			if fi.Name() == "node_modules" {
 | |
| 				return util.ErrWalkSkipDir
 | |
| 			}
 | |
| 
 | |
| 			if fi.IsDir() {
 | |
| 				return nil
 | |
| 			}
 | |
| 
 | |
| 			if fi.Name() != "plugin.json" {
 | |
| 				return nil
 | |
| 			}
 | |
| 
 | |
| 			pluginJSONPaths = append(pluginJSONPaths, currentPath)
 | |
| 			return nil
 | |
| 		}); err != nil {
 | |
| 		return []string{}, err
 | |
| 	}
 | |
| 
 | |
| 	return pluginJSONPaths, nil
 | |
| }
 | |
| 
 | |
| func (s *LocalSource) readFile(pluginJSONPath string) (io.ReadCloser, error) {
 | |
| 	s.log.Debug("Loading plugin", "path", pluginJSONPath)
 | |
| 
 | |
| 	if !strings.EqualFold(filepath.Ext(pluginJSONPath), ".json") {
 | |
| 		return nil, ErrInvalidPluginJSONFilePath
 | |
| 	}
 | |
| 
 | |
| 	absPluginJSONPath, err := filepath.Abs(pluginJSONPath)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// Wrapping in filepath.Clean to properly handle
 | |
| 	// gosec G304 Potential file inclusion via variable rule.
 | |
| 	return os.Open(filepath.Clean(absPluginJSONPath))
 | |
| }
 | |
| 
 | |
| func DirAsLocalSources(cfg *config.PluginManagementCfg, pluginsPath string, class plugins.Class) ([]*LocalSource, error) {
 | |
| 	if pluginsPath == "" {
 | |
| 		return []*LocalSource{}, errors.New("plugins path not configured")
 | |
| 	}
 | |
| 
 | |
| 	// It's safe to ignore gosec warning G304 since the variable part of the file path comes from a configuration
 | |
| 	// variable.
 | |
| 	// nolint:gosec
 | |
| 	d, err := os.ReadDir(pluginsPath)
 | |
| 	if err != nil {
 | |
| 		return []*LocalSource{}, errors.New("failed to open plugins path")
 | |
| 	}
 | |
| 
 | |
| 	var pluginDirs []string
 | |
| 	for _, dir := range d {
 | |
| 		if dir.IsDir() || dir.Type()&os.ModeSymlink == os.ModeSymlink {
 | |
| 			pluginDirs = append(pluginDirs, filepath.Join(pluginsPath, dir.Name()))
 | |
| 		}
 | |
| 	}
 | |
| 	slices.Sort(pluginDirs)
 | |
| 
 | |
| 	sources := make([]*LocalSource, len(pluginDirs))
 | |
| 	for i, dir := range pluginDirs {
 | |
| 		if cfg.DevMode {
 | |
| 			sources[i] = NewUnsafeLocalSource(class, []string{dir})
 | |
| 		} else {
 | |
| 			sources[i] = NewLocalSource(class, []string{dir})
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return sources, nil
 | |
| }
 |