| 
									
										
										
										
											2023-03-20 21:35:49 +08:00
										 |  |  | package sources | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							| 
									
										
										
										
											2024-01-18 18:06:33 +08:00
										 |  |  | 	"errors" | 
					
						
							| 
									
										
										
										
											2025-06-19 17:28:23 +08:00
										 |  |  | 	"fmt" | 
					
						
							|  |  |  | 	"io" | 
					
						
							| 
									
										
										
										
											2024-01-18 18:06:33 +08:00
										 |  |  | 	"os" | 
					
						
							|  |  |  | 	"path/filepath" | 
					
						
							|  |  |  | 	"slices" | 
					
						
							| 
									
										
										
										
											2025-06-19 17:28:23 +08:00
										 |  |  | 	"strings" | 
					
						
							| 
									
										
										
										
											2023-03-20 21:35:49 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-19 17:28:23 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/infra/fs" | 
					
						
							| 
									
										
										
										
											2023-03-20 21:35:49 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/plugins" | 
					
						
							| 
									
										
										
										
											2025-06-19 17:28:23 +08:00
										 |  |  | 	"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") | 
					
						
							| 
									
										
										
										
											2023-03-20 21:35:49 +08:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type LocalSource struct { | 
					
						
							| 
									
										
										
										
											2025-06-19 17:28:23 +08:00
										 |  |  | 	paths      []string | 
					
						
							|  |  |  | 	class      plugins.Class | 
					
						
							|  |  |  | 	strictMode bool // If true, tracks files via a StaticFS
 | 
					
						
							|  |  |  | 	log        log.Logger | 
					
						
							| 
									
										
										
										
											2023-03-20 21:35:49 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-19 17:28:23 +08:00
										 |  |  | // NewLocalSource represents a plugin with a fixed set of files.
 | 
					
						
							| 
									
										
										
										
											2023-03-20 21:35:49 +08:00
										 |  |  | func NewLocalSource(class plugins.Class, paths []string) *LocalSource { | 
					
						
							|  |  |  | 	return &LocalSource{ | 
					
						
							| 
									
										
										
										
											2025-06-19 17:28:23 +08:00
										 |  |  | 		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"), | 
					
						
							| 
									
										
										
										
											2023-03-20 21:35:49 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (s *LocalSource) PluginClass(_ context.Context) plugins.Class { | 
					
						
							|  |  |  | 	return s.class | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-19 17:28:23 +08:00
										 |  |  | // 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 { | 
					
						
							| 
									
										
										
										
											2023-03-20 21:35:49 +08:00
										 |  |  | 	return s.paths | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-19 19:19:45 +08:00
										 |  |  | func (s *LocalSource) DefaultSignature(_ context.Context, _ string) (plugins.Signature, bool) { | 
					
						
							| 
									
										
										
										
											2023-03-20 21:35:49 +08:00
										 |  |  | 	switch s.class { | 
					
						
							| 
									
										
										
										
											2023-06-08 18:21:19 +08:00
										 |  |  | 	case plugins.ClassCore: | 
					
						
							| 
									
										
										
										
											2023-03-20 21:35:49 +08:00
										 |  |  | 		return plugins.Signature{ | 
					
						
							| 
									
										
										
										
											2023-06-08 18:21:19 +08:00
										 |  |  | 			Status: plugins.SignatureStatusInternal, | 
					
						
							| 
									
										
										
										
											2023-03-20 21:35:49 +08:00
										 |  |  | 		}, true | 
					
						
							|  |  |  | 	default: | 
					
						
							|  |  |  | 		return plugins.Signature{}, false | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2024-01-18 18:06:33 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-19 17:28:23 +08:00
										 |  |  | 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) { | 
					
						
							| 
									
										
										
										
											2024-01-18 18:06:33 +08:00
										 |  |  | 	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) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-15 02:16:36 +08:00
										 |  |  | 	sources := make([]*LocalSource, len(pluginDirs)) | 
					
						
							|  |  |  | 	for i, dir := range pluginDirs { | 
					
						
							| 
									
										
										
										
											2025-06-19 17:28:23 +08:00
										 |  |  | 		if cfg.DevMode { | 
					
						
							|  |  |  | 			sources[i] = NewUnsafeLocalSource(class, []string{dir}) | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			sources[i] = NewLocalSource(class, []string{dir}) | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-01-18 18:06:33 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-06-15 02:16:36 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-18 18:06:33 +08:00
										 |  |  | 	return sources, nil | 
					
						
							|  |  |  | } |