mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
feat: add library permission management to plugin system
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
801466445d
commit
06a2a4d89c
@ -2,11 +2,13 @@
|
||||
CREATE TABLE IF NOT EXISTS plugin (
|
||||
id TEXT PRIMARY KEY,
|
||||
path TEXT NOT NULL,
|
||||
manifest TEXT NOT NULL,
|
||||
config TEXT,
|
||||
users TEXT,
|
||||
all_users INTEGER NOT NULL DEFAULT 0,
|
||||
enabled INTEGER NOT NULL DEFAULT 0,
|
||||
manifest JSONB NOT NULL,
|
||||
config JSONB,
|
||||
users JSONB,
|
||||
all_users BOOL NOT NULL DEFAULT false,
|
||||
libraries JSONB,
|
||||
all_libraries BOOL NOT NULL DEFAULT false,
|
||||
enabled BOOL NOT NULL DEFAULT false,
|
||||
last_error TEXT,
|
||||
sha256 TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
|
||||
@ -3,17 +3,19 @@ package model
|
||||
import "time"
|
||||
|
||||
type Plugin struct {
|
||||
ID string `structs:"id" json:"id"`
|
||||
Path string `structs:"path" json:"path"`
|
||||
Manifest string `structs:"manifest" json:"manifest"`
|
||||
Config string `structs:"config" json:"config,omitempty"`
|
||||
Users string `structs:"users" json:"users,omitempty"`
|
||||
AllUsers bool `structs:"all_users" json:"allUsers,omitempty"`
|
||||
Enabled bool `structs:"enabled" json:"enabled"`
|
||||
LastError string `structs:"last_error" json:"lastError,omitempty"`
|
||||
SHA256 string `structs:"sha256" json:"sha256"`
|
||||
CreatedAt time.Time `structs:"created_at" json:"createdAt"`
|
||||
UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"`
|
||||
ID string `structs:"id" json:"id"`
|
||||
Path string `structs:"path" json:"path"`
|
||||
Manifest string `structs:"manifest" json:"manifest"`
|
||||
Config string `structs:"config" json:"config,omitempty"`
|
||||
Users string `structs:"users" json:"users,omitempty"`
|
||||
AllUsers bool `structs:"all_users" json:"allUsers,omitempty"`
|
||||
Libraries string `structs:"libraries" json:"libraries,omitempty"`
|
||||
AllLibraries bool `structs:"all_libraries" json:"allLibraries,omitempty"`
|
||||
Enabled bool `structs:"enabled" json:"enabled"`
|
||||
LastError string `structs:"last_error" json:"lastError,omitempty"`
|
||||
SHA256 string `structs:"sha256" json:"sha256"`
|
||||
CreatedAt time.Time `structs:"created_at" json:"createdAt"`
|
||||
UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"`
|
||||
}
|
||||
|
||||
type Plugins []Plugin
|
||||
|
||||
@ -79,30 +79,34 @@ func (r *pluginRepository) Put(plugin *model.Plugin) error {
|
||||
|
||||
// Upsert using INSERT ... ON CONFLICT for atomic operation
|
||||
_, err := r.db.NewQuery(`
|
||||
INSERT INTO plugin (id, path, manifest, config, users, all_users, enabled, last_error, sha256, created_at, updated_at)
|
||||
VALUES ({:id}, {:path}, {:manifest}, {:config}, {:users}, {:all_users}, {:enabled}, {:last_error}, {:sha256}, {:created_at}, {:updated_at})
|
||||
INSERT INTO plugin (id, path, manifest, config, users, all_users, libraries, all_libraries, enabled, last_error, sha256, created_at, updated_at)
|
||||
VALUES ({:id}, {:path}, {:manifest}, {:config}, {:users}, {:all_users}, {:libraries}, {:all_libraries}, {:enabled}, {:last_error}, {:sha256}, {:created_at}, {:updated_at})
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
path = excluded.path,
|
||||
manifest = excluded.manifest,
|
||||
config = excluded.config,
|
||||
users = excluded.users,
|
||||
all_users = excluded.all_users,
|
||||
libraries = excluded.libraries,
|
||||
all_libraries = excluded.all_libraries,
|
||||
enabled = excluded.enabled,
|
||||
last_error = excluded.last_error,
|
||||
sha256 = excluded.sha256,
|
||||
updated_at = excluded.updated_at
|
||||
`).Bind(dbx.Params{
|
||||
"id": plugin.ID,
|
||||
"path": plugin.Path,
|
||||
"manifest": plugin.Manifest,
|
||||
"config": plugin.Config,
|
||||
"users": plugin.Users,
|
||||
"all_users": plugin.AllUsers,
|
||||
"enabled": plugin.Enabled,
|
||||
"last_error": plugin.LastError,
|
||||
"sha256": plugin.SHA256,
|
||||
"created_at": time.Now(),
|
||||
"updated_at": plugin.UpdatedAt,
|
||||
"id": plugin.ID,
|
||||
"path": plugin.Path,
|
||||
"manifest": plugin.Manifest,
|
||||
"config": plugin.Config,
|
||||
"users": plugin.Users,
|
||||
"all_users": plugin.AllUsers,
|
||||
"libraries": plugin.Libraries,
|
||||
"all_libraries": plugin.AllLibraries,
|
||||
"enabled": plugin.Enabled,
|
||||
"last_error": plugin.LastError,
|
||||
"sha256": plugin.SHA256,
|
||||
"created_at": time.Now(),
|
||||
"updated_at": plugin.UpdatedAt,
|
||||
}).Execute()
|
||||
return err
|
||||
}
|
||||
|
||||
@ -11,17 +11,32 @@ import (
|
||||
type libraryServiceImpl struct {
|
||||
ds model.DataStore
|
||||
hasFilesystemPerm bool
|
||||
allowedLibraryIDs []int
|
||||
allLibraries bool
|
||||
libraryIDMap map[int]struct{}
|
||||
}
|
||||
|
||||
func newLibraryService(ds model.DataStore, perm *LibraryPermission) host.LibraryService {
|
||||
func newLibraryService(ds model.DataStore, perm *LibraryPermission, allowedLibraryIDs []int, allLibraries bool) host.LibraryService {
|
||||
hasFS := perm != nil && perm.Filesystem
|
||||
libraryIDMap := make(map[int]struct{})
|
||||
for _, id := range allowedLibraryIDs {
|
||||
libraryIDMap[id] = struct{}{}
|
||||
}
|
||||
return &libraryServiceImpl{
|
||||
ds: ds,
|
||||
hasFilesystemPerm: hasFS,
|
||||
allowedLibraryIDs: allowedLibraryIDs,
|
||||
allLibraries: allLibraries,
|
||||
libraryIDMap: libraryIDMap,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *libraryServiceImpl) GetLibrary(ctx context.Context, id int32) (*host.Library, error) {
|
||||
// Check if the library is accessible
|
||||
if !s.isLibraryAccessible(int(id)) {
|
||||
return nil, fmt.Errorf("library not accessible: library ID %d is not in the allowed list", id)
|
||||
}
|
||||
|
||||
lib, err := s.ds.Library(ctx).Get(int(id))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("library not found: %w", err)
|
||||
@ -30,15 +45,27 @@ func (s *libraryServiceImpl) GetLibrary(ctx context.Context, id int32) (*host.Li
|
||||
return s.convertLibrary(lib), nil
|
||||
}
|
||||
|
||||
// isLibraryAccessible checks if a library ID is accessible to this plugin.
|
||||
func (s *libraryServiceImpl) isLibraryAccessible(id int) bool {
|
||||
if s.allLibraries {
|
||||
return true
|
||||
}
|
||||
_, ok := s.libraryIDMap[id]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s *libraryServiceImpl) GetAllLibraries(ctx context.Context) ([]host.Library, error) {
|
||||
libs, err := s.ds.Library(ctx).GetAll()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get libraries: %w", err)
|
||||
}
|
||||
|
||||
result := make([]host.Library, len(libs))
|
||||
for i, lib := range libs {
|
||||
result[i] = *s.convertLibrary(&lib)
|
||||
// Filter libraries based on allowed list
|
||||
var result []host.Library
|
||||
for _, lib := range libs {
|
||||
if s.isLibraryAccessible(lib.ID) {
|
||||
result = append(result, *s.convertLibrary(&lib))
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
@ -36,7 +36,7 @@ var _ = Describe("LibraryService", Ordered, func() {
|
||||
Describe("GetLibrary", func() {
|
||||
It("should return library metadata without filesystem permission", func() {
|
||||
reason := "test"
|
||||
service = newLibraryService(ds, &LibraryPermission{Reason: &reason, Filesystem: false}).(*libraryServiceImpl)
|
||||
service = newLibraryService(ds, &LibraryPermission{Reason: &reason, Filesystem: false}, nil, true).(*libraryServiceImpl)
|
||||
|
||||
lib := &model.Library{
|
||||
ID: 1,
|
||||
@ -68,7 +68,7 @@ var _ = Describe("LibraryService", Ordered, func() {
|
||||
|
||||
It("should return library metadata with filesystem permission", func() {
|
||||
reason := "test"
|
||||
service = newLibraryService(ds, &LibraryPermission{Reason: &reason, Filesystem: true}).(*libraryServiceImpl)
|
||||
service = newLibraryService(ds, &LibraryPermission{Reason: &reason, Filesystem: true}, nil, true).(*libraryServiceImpl)
|
||||
|
||||
lib := &model.Library{
|
||||
ID: 2,
|
||||
@ -94,7 +94,7 @@ var _ = Describe("LibraryService", Ordered, func() {
|
||||
|
||||
It("should return error for non-existent library", func() {
|
||||
reason := "test"
|
||||
service = newLibraryService(ds, &LibraryPermission{Reason: &reason}).(*libraryServiceImpl)
|
||||
service = newLibraryService(ds, &LibraryPermission{Reason: &reason}, nil, true).(*libraryServiceImpl)
|
||||
|
||||
mockLibRepo := ds.Library(ctx).(*tests.MockLibraryRepo)
|
||||
mockLibRepo.SetData(model.Libraries{})
|
||||
@ -108,7 +108,7 @@ var _ = Describe("LibraryService", Ordered, func() {
|
||||
Describe("GetAllLibraries", func() {
|
||||
It("should return all libraries without filesystem permission", func() {
|
||||
reason := "test"
|
||||
service = newLibraryService(ds, &LibraryPermission{Reason: &reason, Filesystem: false}).(*libraryServiceImpl)
|
||||
service = newLibraryService(ds, &LibraryPermission{Reason: &reason, Filesystem: false}, nil, true).(*libraryServiceImpl)
|
||||
|
||||
libs := model.Libraries{
|
||||
{ID: 1, Name: "Rock", Path: "/music/rock", TotalSongs: 100},
|
||||
@ -131,7 +131,7 @@ var _ = Describe("LibraryService", Ordered, func() {
|
||||
|
||||
It("should return all libraries with filesystem permission", func() {
|
||||
reason := "test"
|
||||
service = newLibraryService(ds, &LibraryPermission{Reason: &reason, Filesystem: true}).(*libraryServiceImpl)
|
||||
service = newLibraryService(ds, &LibraryPermission{Reason: &reason, Filesystem: true}, nil, true).(*libraryServiceImpl)
|
||||
|
||||
libs := model.Libraries{
|
||||
{ID: 1, Name: "Rock", Path: "/music/rock", TotalSongs: 100},
|
||||
@ -151,6 +151,103 @@ var _ = Describe("LibraryService", Ordered, func() {
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Library Access Filtering", func() {
|
||||
It("should only return libraries in the allowed list", func() {
|
||||
reason := "test"
|
||||
// Only allow library ID 2
|
||||
service = newLibraryService(ds, &LibraryPermission{Reason: &reason, Filesystem: false}, []int{2}, false).(*libraryServiceImpl)
|
||||
|
||||
libs := model.Libraries{
|
||||
{ID: 1, Name: "Rock", Path: "/music/rock", TotalSongs: 100},
|
||||
{ID: 2, Name: "Jazz", Path: "/music/jazz", TotalSongs: 50},
|
||||
{ID: 3, Name: "Classical", Path: "/music/classical", TotalSongs: 75},
|
||||
}
|
||||
|
||||
mockLibRepo := ds.Library(ctx).(*tests.MockLibraryRepo)
|
||||
mockLibRepo.SetData(libs)
|
||||
|
||||
results, err := service.GetAllLibraries(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(results).To(HaveLen(1))
|
||||
Expect(results[0].ID).To(Equal(int32(2)))
|
||||
Expect(results[0].Name).To(Equal("Jazz"))
|
||||
})
|
||||
|
||||
It("should return error when getting a library not in the allowed list", func() {
|
||||
reason := "test"
|
||||
// Only allow library ID 2
|
||||
service = newLibraryService(ds, &LibraryPermission{Reason: &reason, Filesystem: false}, []int{2}, false).(*libraryServiceImpl)
|
||||
|
||||
libs := model.Libraries{
|
||||
{ID: 1, Name: "Rock", Path: "/music/rock", TotalSongs: 100},
|
||||
{ID: 2, Name: "Jazz", Path: "/music/jazz", TotalSongs: 50},
|
||||
}
|
||||
|
||||
mockLibRepo := ds.Library(ctx).(*tests.MockLibraryRepo)
|
||||
mockLibRepo.SetData(libs)
|
||||
|
||||
// Requesting library 1 which is not in the allowed list
|
||||
_, err := service.GetLibrary(ctx, 1)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("not accessible"))
|
||||
})
|
||||
|
||||
It("should allow access to a library in the allowed list", func() {
|
||||
reason := "test"
|
||||
// Only allow library ID 2
|
||||
service = newLibraryService(ds, &LibraryPermission{Reason: &reason, Filesystem: false}, []int{2}, false).(*libraryServiceImpl)
|
||||
|
||||
libs := model.Libraries{
|
||||
{ID: 1, Name: "Rock", Path: "/music/rock", TotalSongs: 100},
|
||||
{ID: 2, Name: "Jazz", Path: "/music/jazz", TotalSongs: 50},
|
||||
}
|
||||
|
||||
mockLibRepo := ds.Library(ctx).(*tests.MockLibraryRepo)
|
||||
mockLibRepo.SetData(libs)
|
||||
|
||||
result, err := service.GetLibrary(ctx, 2)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(result.ID).To(Equal(int32(2)))
|
||||
Expect(result.Name).To(Equal("Jazz"))
|
||||
})
|
||||
|
||||
It("should return empty list when no libraries are allowed and allLibraries is false", func() {
|
||||
reason := "test"
|
||||
// No libraries allowed
|
||||
service = newLibraryService(ds, &LibraryPermission{Reason: &reason, Filesystem: false}, []int{}, false).(*libraryServiceImpl)
|
||||
|
||||
libs := model.Libraries{
|
||||
{ID: 1, Name: "Rock", Path: "/music/rock", TotalSongs: 100},
|
||||
{ID: 2, Name: "Jazz", Path: "/music/jazz", TotalSongs: 50},
|
||||
}
|
||||
|
||||
mockLibRepo := ds.Library(ctx).(*tests.MockLibraryRepo)
|
||||
mockLibRepo.SetData(libs)
|
||||
|
||||
results, err := service.GetAllLibraries(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(results).To(HaveLen(0))
|
||||
})
|
||||
|
||||
It("should return all libraries when allLibraries is true regardless of allowed list", func() {
|
||||
reason := "test"
|
||||
// allLibraries=true should ignore the allowed list
|
||||
service = newLibraryService(ds, &LibraryPermission{Reason: &reason, Filesystem: false}, []int{1}, true).(*libraryServiceImpl)
|
||||
|
||||
libs := model.Libraries{
|
||||
{ID: 1, Name: "Rock", Path: "/music/rock", TotalSongs: 100},
|
||||
{ID: 2, Name: "Jazz", Path: "/music/jazz", TotalSongs: 50},
|
||||
}
|
||||
|
||||
mockLibRepo := ds.Library(ctx).(*tests.MockLibraryRepo)
|
||||
mockLibRepo.SetData(libs)
|
||||
|
||||
results, err := service.GetAllLibraries(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(results).To(HaveLen(2))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Plugin Integration", func() {
|
||||
var (
|
||||
manager *Manager
|
||||
@ -269,10 +366,11 @@ var _ = Describe("LibraryService Integration", Ordered, func() {
|
||||
mockPluginRepo := tests.CreateMockPluginRepo()
|
||||
mockPluginRepo.Permitted = true
|
||||
mockPluginRepo.SetData(model.Plugins{{
|
||||
ID: "test-library",
|
||||
Path: destPath,
|
||||
SHA256: hashHex,
|
||||
Enabled: true,
|
||||
ID: "test-library",
|
||||
Path: destPath,
|
||||
SHA256: hashHex,
|
||||
Enabled: true,
|
||||
AllLibraries: true, // Grant access to all libraries for testing
|
||||
}})
|
||||
|
||||
mockLibraryRepo := &tests.MockLibraryRepo{}
|
||||
|
||||
@ -399,6 +399,17 @@ func (m *Manager) UpdatePluginUsers(ctx context.Context, id, usersJSON string, a
|
||||
})
|
||||
}
|
||||
|
||||
// UpdatePluginLibraries updates the libraries permission settings for a plugin.
|
||||
// If the plugin is enabled, it will be reloaded with the new settings.
|
||||
// If the plugin requires library permission and no libraries are configured (and allLibraries is false),
|
||||
// the plugin will be automatically disabled.
|
||||
func (m *Manager) UpdatePluginLibraries(ctx context.Context, id, librariesJSON string, allLibraries bool) error {
|
||||
return m.updatePluginSettings(ctx, id, func(p *model.Plugin) {
|
||||
p.Libraries = librariesJSON
|
||||
p.AllLibraries = allLibraries
|
||||
})
|
||||
}
|
||||
|
||||
// updatePluginSettings is a common implementation for updating plugin settings.
|
||||
// The updateFn is called to apply the specific field updates to the plugin.
|
||||
// If the plugin is enabled, it will be reloaded. If users permission is required
|
||||
@ -422,19 +433,25 @@ func (m *Manager) updatePluginSettings(ctx context.Context, id string, updateFn
|
||||
updateFn(plugin)
|
||||
plugin.UpdatedAt = time.Now()
|
||||
|
||||
// Check if plugin requires users permission and if it's still satisfied
|
||||
// Check if plugin requires permission and if it's still satisfied
|
||||
shouldDisable := false
|
||||
disableReason := ""
|
||||
if wasEnabled {
|
||||
manifest, err := readManifest(plugin.Path)
|
||||
if err == nil && manifest.Permissions != nil && manifest.Permissions.Users != nil {
|
||||
if !hasValidUsersConfig(plugin.Users, plugin.AllUsers) {
|
||||
if err == nil && manifest.Permissions != nil {
|
||||
if manifest.Permissions.Users != nil && !hasValidUsersConfig(plugin.Users, plugin.AllUsers) {
|
||||
shouldDisable = true
|
||||
disableReason = "users permission removal"
|
||||
}
|
||||
if manifest.Permissions.Library != nil && !hasValidLibrariesConfig(plugin.Libraries, plugin.AllLibraries) {
|
||||
shouldDisable = true
|
||||
disableReason = "library permission removal"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if shouldDisable {
|
||||
// Disable the plugin since users permission is no longer satisfied
|
||||
// Disable the plugin since permission is no longer satisfied
|
||||
if err := m.unloadPlugin(id); err != nil {
|
||||
log.Debug(ctx, "Plugin was not loaded", "plugin", id)
|
||||
}
|
||||
@ -442,7 +459,7 @@ func (m *Manager) updatePluginSettings(ctx context.Context, id string, updateFn
|
||||
if err := repo.Put(plugin); err != nil {
|
||||
return fmt.Errorf("updating plugin in DB: %w", err)
|
||||
}
|
||||
log.Info(ctx, "Disabled plugin due to users permission removal", "plugin", id)
|
||||
log.Info(ctx, "Disabled plugin due to "+disableReason, "plugin", id)
|
||||
m.sendPluginRefreshEvent(ctx, id)
|
||||
return nil
|
||||
}
|
||||
@ -519,6 +536,13 @@ func (m *Manager) checkPermissionGates(p *model.Plugin) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Check library permission gate
|
||||
if manifest.Permissions != nil && manifest.Permissions.Library != nil {
|
||||
if !hasValidLibrariesConfig(p.Libraries, p.AllLibraries) {
|
||||
return fmt.Errorf("library permission requires configuration: select libraries or enable 'all libraries' access")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -537,3 +561,19 @@ func hasValidUsersConfig(usersJSON string, allUsers bool) bool {
|
||||
}
|
||||
return len(users) > 0
|
||||
}
|
||||
|
||||
// hasValidLibrariesConfig checks if a plugin has valid libraries configuration.
|
||||
// Returns true if allLibraries is true, or if librariesJSON contains at least one library.
|
||||
func hasValidLibrariesConfig(librariesJSON string, allLibraries bool) bool {
|
||||
if allLibraries {
|
||||
return true
|
||||
}
|
||||
if librariesJSON == "" {
|
||||
return false
|
||||
}
|
||||
var libraries []int
|
||||
if err := json.Unmarshal([]byte(librariesJSON), &libraries); err != nil {
|
||||
return false
|
||||
}
|
||||
return len(libraries) > 0
|
||||
}
|
||||
|
||||
@ -20,12 +20,14 @@ import (
|
||||
|
||||
// serviceContext provides dependencies needed by host service factories.
|
||||
type serviceContext struct {
|
||||
pluginName string
|
||||
manager *Manager
|
||||
permissions *Permissions
|
||||
config map[string]string
|
||||
allowedUsers []string // User IDs this plugin can access
|
||||
allUsers bool // If true, plugin can access all users
|
||||
pluginName string
|
||||
manager *Manager
|
||||
permissions *Permissions
|
||||
config map[string]string
|
||||
allowedUsers []string // User IDs this plugin can access
|
||||
allUsers bool // If true, plugin can access all users
|
||||
allowedLibraries []int // Library IDs this plugin can access
|
||||
allLibraries bool // If true, plugin can access all libraries
|
||||
}
|
||||
|
||||
// hostServiceEntry defines a host service for table-driven registration.
|
||||
@ -92,7 +94,7 @@ var hostServices = []hostServiceEntry{
|
||||
hasPermission: func(p *Permissions) bool { return p != nil && p.Library != nil },
|
||||
create: func(ctx *serviceContext) ([]extism.HostFunction, io.Closer) {
|
||||
perm := ctx.permissions.Library
|
||||
service := newLibraryService(ctx.manager.ds, perm)
|
||||
service := newLibraryService(ctx.manager.ds, perm, ctx.allowedLibraries, ctx.allLibraries)
|
||||
return host.RegisterLibraryHostFunctions(service), nil
|
||||
},
|
||||
},
|
||||
@ -243,6 +245,14 @@ func (m *Manager) loadPluginWithConfig(p *model.Plugin) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse libraries from JSON
|
||||
var allowedLibraries []int
|
||||
if p.Libraries != "" {
|
||||
if err := json.Unmarshal([]byte(p.Libraries), &allowedLibraries); err != nil {
|
||||
return fmt.Errorf("parsing plugin libraries: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Open the .ndp package to get manifest and wasm bytes
|
||||
pkg, err := openPackage(p.Path)
|
||||
if err != nil {
|
||||
@ -270,9 +280,20 @@ func (m *Manager) loadPluginWithConfig(p *model.Plugin) error {
|
||||
return fmt.Errorf("failed to get libraries for filesystem access: %w", err)
|
||||
}
|
||||
|
||||
// Build a set of allowed library IDs for fast lookup
|
||||
allowedLibrarySet := make(map[int]struct{}, len(allowedLibraries))
|
||||
for _, id := range allowedLibraries {
|
||||
allowedLibrarySet[id] = struct{}{}
|
||||
}
|
||||
|
||||
allowedPaths := make(map[string]string)
|
||||
for _, lib := range libraries {
|
||||
allowedPaths[lib.Path] = toPluginMountPoint(int32(lib.ID))
|
||||
// Only mount if allLibraries is true or library is in the allowed list
|
||||
if p.AllLibraries {
|
||||
allowedPaths[lib.Path] = toPluginMountPoint(int32(lib.ID))
|
||||
} else if _, ok := allowedLibrarySet[lib.ID]; ok {
|
||||
allowedPaths[lib.Path] = toPluginMountPoint(int32(lib.ID))
|
||||
}
|
||||
}
|
||||
pluginManifest.AllowedPaths = allowedPaths
|
||||
}
|
||||
@ -282,12 +303,14 @@ func (m *Manager) loadPluginWithConfig(p *model.Plugin) error {
|
||||
var closers []io.Closer
|
||||
|
||||
svcCtx := &serviceContext{
|
||||
pluginName: p.ID,
|
||||
manager: m,
|
||||
permissions: pkg.Manifest.Permissions,
|
||||
config: pluginConfig,
|
||||
allowedUsers: allowedUsers,
|
||||
allUsers: p.AllUsers,
|
||||
pluginName: p.ID,
|
||||
manager: m,
|
||||
permissions: pkg.Manifest.Permissions,
|
||||
config: pluginConfig,
|
||||
allowedUsers: allowedUsers,
|
||||
allUsers: p.AllUsers,
|
||||
allowedLibraries: allowedLibraries,
|
||||
allLibraries: p.AllLibraries,
|
||||
}
|
||||
for _, entry := range hostServices {
|
||||
if entry.hasPermission(pkg.Manifest.Permissions) {
|
||||
|
||||
@ -27,6 +27,7 @@ type PluginManager interface {
|
||||
DisablePlugin(ctx context.Context, id string) error
|
||||
UpdatePluginConfig(ctx context.Context, id, configJSON string) error
|
||||
UpdatePluginUsers(ctx context.Context, id, usersJSON string, allUsers bool) error
|
||||
UpdatePluginLibraries(ctx context.Context, id, librariesJSON string, allLibraries bool) error
|
||||
}
|
||||
|
||||
type Router struct {
|
||||
|
||||
@ -43,10 +43,12 @@ func pluginsEnabledMiddleware(next http.Handler) http.Handler {
|
||||
|
||||
// PluginUpdateRequest represents the fields that can be updated via the API
|
||||
type PluginUpdateRequest struct {
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
Config *string `json:"config,omitempty"`
|
||||
Users *string `json:"users,omitempty"`
|
||||
AllUsers *bool `json:"allUsers,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
Config *string `json:"config,omitempty"`
|
||||
Users *string `json:"users,omitempty"`
|
||||
AllUsers *bool `json:"allUsers,omitempty"`
|
||||
Libraries *string `json:"libraries,omitempty"`
|
||||
AllLibraries *bool `json:"allLibraries,omitempty"`
|
||||
}
|
||||
|
||||
func (api *Router) updatePlugin(w http.ResponseWriter, r *http.Request) {
|
||||
@ -93,6 +95,14 @@ func (api *Router) updatePlugin(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle libraries permission update (if provided)
|
||||
if req.Libraries != nil || req.AllLibraries != nil {
|
||||
if err := validateAndUpdateLibraries(ctx, api.pluginManager, repo, id, req, w); err != nil {
|
||||
log.Error(ctx, "Error updating plugin libraries", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Handle enable/disable
|
||||
if req.Enabled != nil {
|
||||
if *req.Enabled {
|
||||
@ -195,3 +205,36 @@ func validateAndUpdateUsers(ctx context.Context, pm PluginManager, repo model.Pl
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAndUpdateLibraries validates the libraries JSON and updates the plugin.
|
||||
// Returns an error if validation or update fails (error response already written).
|
||||
func validateAndUpdateLibraries(ctx context.Context, pm PluginManager, repo model.PluginRepository, id string, req PluginUpdateRequest, w http.ResponseWriter) error {
|
||||
// Get current values if not provided in request
|
||||
plugin, err := repo.Get(id)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error getting plugin for libraries update", "id", id, err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
|
||||
librariesJSON := plugin.Libraries
|
||||
allLibraries := plugin.AllLibraries
|
||||
|
||||
if req.Libraries != nil {
|
||||
if *req.Libraries != "" && !isValidJSON(*req.Libraries) {
|
||||
http.Error(w, "Invalid JSON in libraries field", http.StatusBadRequest)
|
||||
return errors.New("invalid JSON")
|
||||
}
|
||||
librariesJSON = *req.Libraries
|
||||
}
|
||||
if req.AllLibraries != nil {
|
||||
allLibraries = *req.AllLibraries
|
||||
}
|
||||
|
||||
if err := pm.UpdatePluginLibraries(ctx, id, librariesJSON, allLibraries); err != nil {
|
||||
log.Error(ctx, "Error updating plugin libraries", "id", id, err)
|
||||
http.Error(w, "Error updating plugin libraries: "+err.Error(), http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import (
|
||||
)
|
||||
|
||||
// MockPluginManager is a mock implementation of plugins.PluginManager for testing.
|
||||
// It implements EnablePlugin, DisablePlugin, UpdatePluginConfig, and UpdatePluginUsers methods.
|
||||
// It implements EnablePlugin, DisablePlugin, UpdatePluginConfig, UpdatePluginUsers and UpdatePluginLibraries methods.
|
||||
type MockPluginManager struct {
|
||||
// EnablePluginFn is called when EnablePlugin is invoked. If nil, returns EnableError.
|
||||
EnablePluginFn func(ctx context.Context, id string) error
|
||||
@ -15,12 +15,15 @@ type MockPluginManager struct {
|
||||
UpdatePluginConfigFn func(ctx context.Context, id, configJSON string) error
|
||||
// UpdatePluginUsersFn is called when UpdatePluginUsers is invoked. If nil, returns UsersError.
|
||||
UpdatePluginUsersFn func(ctx context.Context, id, usersJSON string, allUsers bool) error
|
||||
// UpdatePluginLibrariesFn is called when UpdatePluginLibraries is invoked. If nil, returns LibrariesError.
|
||||
UpdatePluginLibrariesFn func(ctx context.Context, id, librariesJSON string, allLibraries bool) error
|
||||
|
||||
// Default errors to return when Fn callbacks are not set
|
||||
EnableError error
|
||||
DisableError error
|
||||
ConfigError error
|
||||
UsersError error
|
||||
EnableError error
|
||||
DisableError error
|
||||
ConfigError error
|
||||
UsersError error
|
||||
LibrariesError error
|
||||
|
||||
// Track calls for assertions
|
||||
EnablePluginCalls []string
|
||||
@ -34,6 +37,11 @@ type MockPluginManager struct {
|
||||
UsersJSON string
|
||||
AllUsers bool
|
||||
}
|
||||
UpdatePluginLibrariesCalls []struct {
|
||||
ID string
|
||||
LibrariesJSON string
|
||||
AllLibraries bool
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockPluginManager) EnablePlugin(ctx context.Context, id string) error {
|
||||
@ -74,3 +82,15 @@ func (m *MockPluginManager) UpdatePluginUsers(ctx context.Context, id, usersJSON
|
||||
}
|
||||
return m.UsersError
|
||||
}
|
||||
|
||||
func (m *MockPluginManager) UpdatePluginLibraries(ctx context.Context, id, librariesJSON string, allLibraries bool) error {
|
||||
m.UpdatePluginLibrariesCalls = append(m.UpdatePluginLibrariesCalls, struct {
|
||||
ID string
|
||||
LibrariesJSON string
|
||||
AllLibraries bool
|
||||
}{ID: id, LibrariesJSON: librariesJSON, AllLibraries: allLibraries})
|
||||
if m.UpdatePluginLibrariesFn != nil {
|
||||
return m.UpdatePluginLibrariesFn(ctx, id, librariesJSON, allLibraries)
|
||||
}
|
||||
return m.LibrariesError
|
||||
}
|
||||
|
||||
@ -351,14 +351,17 @@
|
||||
"configKey": "Key",
|
||||
"configValue": "Value",
|
||||
"allUsers": "Allow all users",
|
||||
"selectedUsers": "Selected users"
|
||||
"selectedUsers": "Selected users",
|
||||
"allLibraries": "Allow all libraries",
|
||||
"selectedLibraries": "Selected libraries"
|
||||
},
|
||||
"sections": {
|
||||
"status": "Status",
|
||||
"info": "Plugin Information",
|
||||
"configuration": "Configuration",
|
||||
"manifest": "Manifest",
|
||||
"usersPermission": "Users Permission"
|
||||
"usersPermission": "Users Permission",
|
||||
"libraryPermission": "Library Permission"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "Enabled",
|
||||
@ -369,6 +372,7 @@
|
||||
"disable": "Disable",
|
||||
"disabledDueToError": "Fix the error before enabling",
|
||||
"disabledUsersRequired": "Select users before enabling",
|
||||
"disabledLibrariesRequired": "Select libraries before enabling",
|
||||
"addConfig": "Add Configuration"
|
||||
},
|
||||
"notifications": {
|
||||
@ -387,7 +391,10 @@
|
||||
"allUsersHelp": "When enabled, the plugin will have access to all users, including those created in the future.",
|
||||
"noUsers": "No users selected",
|
||||
"permissionReason": "Reason",
|
||||
"usersRequired": "This plugin requires access to user information. Select which users the plugin can access, or enable 'Allow all users'."
|
||||
"usersRequired": "This plugin requires access to user information. Select which users the plugin can access, or enable 'Allow all users'.",
|
||||
"allLibrariesHelp": "When enabled, the plugin will have access to all libraries, including those created in the future.",
|
||||
"noLibraries": "No libraries selected",
|
||||
"librariesRequired": "This plugin requires access to library information. Select which libraries the plugin can access, or enable 'Allow all libraries'."
|
||||
},
|
||||
"placeholders": {
|
||||
"configKey": "key",
|
||||
|
||||
171
ui/src/plugin/LibraryPermissionCard.jsx
Normal file
171
ui/src/plugin/LibraryPermissionCard.jsx
Normal file
@ -0,0 +1,171 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Box,
|
||||
FormControlLabel,
|
||||
Switch,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Checkbox,
|
||||
} from '@material-ui/core'
|
||||
import CheckBoxIcon from '@material-ui/icons/CheckBox'
|
||||
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'
|
||||
import Alert from '@material-ui/lab/Alert'
|
||||
import { useGetList, useTranslate } from 'react-admin'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export const LibraryPermissionCard = ({
|
||||
manifest,
|
||||
classes,
|
||||
selectedLibraries,
|
||||
allLibraries,
|
||||
onSelectedLibrariesChange,
|
||||
onAllLibrariesChange,
|
||||
}) => {
|
||||
const translate = useTranslate()
|
||||
|
||||
// Fetch all libraries
|
||||
const { data: librariesData, loading: librariesLoading } = useGetList(
|
||||
'library',
|
||||
{
|
||||
pagination: { page: 1, perPage: 1000 },
|
||||
sort: { field: 'name', order: 'ASC' },
|
||||
},
|
||||
)
|
||||
|
||||
const libraries = React.useMemo(() => {
|
||||
return librariesData ? Object.values(librariesData) : []
|
||||
}, [librariesData])
|
||||
|
||||
const handleToggleLibrary = React.useCallback(
|
||||
(libraryId) => {
|
||||
const newSelected = selectedLibraries.includes(libraryId)
|
||||
? selectedLibraries.filter((id) => id !== libraryId)
|
||||
: [...selectedLibraries, libraryId]
|
||||
onSelectedLibrariesChange(newSelected)
|
||||
},
|
||||
[selectedLibraries, onSelectedLibrariesChange],
|
||||
)
|
||||
|
||||
const handleAllLibrariesToggle = React.useCallback(
|
||||
(event) => {
|
||||
onAllLibrariesChange(event.target.checked)
|
||||
},
|
||||
[onAllLibrariesChange],
|
||||
)
|
||||
|
||||
// Get permission reason from manifest
|
||||
const libraryPermission = manifest?.permissions?.library
|
||||
const reason = libraryPermission?.reason
|
||||
|
||||
// Check if permission is required but not configured
|
||||
const isConfigurationRequired =
|
||||
libraryPermission && !allLibraries && selectedLibraries.length === 0
|
||||
|
||||
if (!libraryPermission) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className={classes.section}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" className={classes.sectionTitle}>
|
||||
{translate('resources.plugin.sections.libraryPermission')}
|
||||
</Typography>
|
||||
|
||||
{reason && (
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
{translate('resources.plugin.messages.permissionReason')}: {reason}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{isConfigurationRequired && (
|
||||
<Box mb={2}>
|
||||
<Alert severity="warning">
|
||||
{translate('resources.plugin.messages.librariesRequired')}
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box mb={2}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={allLibraries}
|
||||
onChange={handleAllLibrariesToggle}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={translate('resources.plugin.fields.allLibraries')}
|
||||
/>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{translate('resources.plugin.messages.allLibrariesHelp')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{!allLibraries && (
|
||||
<Box className={classes.usersList}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
{translate('resources.plugin.fields.selectedLibraries')}
|
||||
</Typography>
|
||||
{librariesLoading ? (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{translate('ra.message.loading')}
|
||||
</Typography>
|
||||
) : libraries.length === 0 ? (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{translate('resources.plugin.messages.noLibraries')}
|
||||
</Typography>
|
||||
) : (
|
||||
<List
|
||||
dense
|
||||
style={{
|
||||
maxHeight: 200,
|
||||
overflow: 'auto',
|
||||
border: '1px solid rgba(0, 0, 0, 0.12)',
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
{libraries.map((library) => (
|
||||
<ListItem
|
||||
key={library.id}
|
||||
button
|
||||
onClick={() => handleToggleLibrary(library.id)}
|
||||
dense
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Checkbox
|
||||
icon={<CheckBoxOutlineBlankIcon fontSize="small" />}
|
||||
checkedIcon={<CheckBoxIcon fontSize="small" />}
|
||||
checked={selectedLibraries.includes(library.id)}
|
||||
tabIndex={-1}
|
||||
disableRipple
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={library.name}
|
||||
secondary={library.path}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
LibraryPermissionCard.propTypes = {
|
||||
manifest: PropTypes.object,
|
||||
classes: PropTypes.object.isRequired,
|
||||
selectedLibraries: PropTypes.array.isRequired,
|
||||
allLibraries: PropTypes.bool.isRequired,
|
||||
onSelectedLibrariesChange: PropTypes.func.isRequired,
|
||||
onAllLibrariesChange: PropTypes.func.isRequired,
|
||||
}
|
||||
@ -21,6 +21,7 @@ import { InfoCard } from './InfoCard'
|
||||
import { ManifestSection } from './ManifestSection'
|
||||
import { ConfigCard } from './ConfigCard'
|
||||
import { UsersPermissionCard } from './UsersPermissionCard'
|
||||
import { LibraryPermissionCard } from './LibraryPermissionCard'
|
||||
|
||||
// Main show layout component
|
||||
const PluginShowLayout = () => {
|
||||
@ -42,6 +43,12 @@ const PluginShowLayout = () => {
|
||||
const [lastRecordUsers, setLastRecordUsers] = useState(null)
|
||||
const [lastRecordAllUsers, setLastRecordAllUsers] = useState(null)
|
||||
|
||||
// Libraries permission state
|
||||
const [selectedLibraries, setSelectedLibraries] = useState([])
|
||||
const [allLibraries, setAllLibraries] = useState(false)
|
||||
const [lastRecordLibraries, setLastRecordLibraries] = useState(null)
|
||||
const [lastRecordAllLibraries, setLastRecordAllLibraries] = useState(null)
|
||||
|
||||
// Convert JSON config to key-value pairs
|
||||
const jsonToPairs = useCallback((jsonString) => {
|
||||
if (!jsonString || jsonString.trim() === '') return []
|
||||
@ -100,6 +107,30 @@ const PluginShowLayout = () => {
|
||||
}
|
||||
}, [record, lastRecordUsers, lastRecordAllUsers, isDirty])
|
||||
|
||||
// Initialize/update libraries permission state when record loads or changes
|
||||
React.useEffect(() => {
|
||||
if (record && !isDirty) {
|
||||
const recordLibraries = record.libraries || ''
|
||||
const recordAllLibraries = record.allLibraries || false
|
||||
|
||||
if (
|
||||
recordLibraries !== lastRecordLibraries ||
|
||||
recordAllLibraries !== lastRecordAllLibraries
|
||||
) {
|
||||
try {
|
||||
setSelectedLibraries(
|
||||
recordLibraries ? JSON.parse(recordLibraries) : [],
|
||||
)
|
||||
} catch {
|
||||
setSelectedLibraries([])
|
||||
}
|
||||
setAllLibraries(recordAllLibraries)
|
||||
setLastRecordLibraries(recordLibraries)
|
||||
setLastRecordAllLibraries(recordAllLibraries)
|
||||
}
|
||||
}
|
||||
}, [record, lastRecordLibraries, lastRecordAllLibraries, isDirty])
|
||||
|
||||
const handleConfigPairsChange = useCallback((newPairs) => {
|
||||
setConfigPairs(newPairs)
|
||||
setIsDirty(true)
|
||||
@ -115,6 +146,16 @@ const PluginShowLayout = () => {
|
||||
setIsDirty(true)
|
||||
}, [])
|
||||
|
||||
const handleSelectedLibrariesChange = useCallback((newSelectedLibraries) => {
|
||||
setSelectedLibraries(newSelectedLibraries)
|
||||
setIsDirty(true)
|
||||
}, [])
|
||||
|
||||
const handleAllLibrariesChange = useCallback((newAllLibraries) => {
|
||||
setAllLibraries(newAllLibraries)
|
||||
setIsDirty(true)
|
||||
}, [])
|
||||
|
||||
const [updatePlugin, { loading }] = useUpdate(
|
||||
'plugin',
|
||||
record?.id,
|
||||
@ -128,6 +169,8 @@ const PluginShowLayout = () => {
|
||||
setLastRecordConfig(null) // Reset to reinitialize from server
|
||||
setLastRecordUsers(null)
|
||||
setLastRecordAllUsers(null)
|
||||
setLastRecordLibraries(null)
|
||||
setLastRecordAllLibraries(null)
|
||||
notify('resources.plugin.notifications.updated', 'info')
|
||||
},
|
||||
onFailure: (err) => {
|
||||
@ -151,8 +194,23 @@ const PluginShowLayout = () => {
|
||||
data.allUsers = allUsers
|
||||
}
|
||||
|
||||
// Include libraries data if library permission is present
|
||||
if (manifest?.permissions?.library) {
|
||||
data.libraries = JSON.stringify(selectedLibraries)
|
||||
data.allLibraries = allLibraries
|
||||
}
|
||||
|
||||
updatePlugin('plugin', record.id, data, record)
|
||||
}, [updatePlugin, record, configPairs, pairsToJson, selectedUsers, allUsers])
|
||||
}, [
|
||||
updatePlugin,
|
||||
record,
|
||||
configPairs,
|
||||
pairsToJson,
|
||||
selectedUsers,
|
||||
allUsers,
|
||||
selectedLibraries,
|
||||
allLibraries,
|
||||
])
|
||||
|
||||
// Parse manifest
|
||||
const { manifest, manifestJson } = useMemo(() => {
|
||||
@ -230,6 +288,15 @@ const PluginShowLayout = () => {
|
||||
onAllUsersChange={handleAllUsersChange}
|
||||
/>
|
||||
|
||||
<LibraryPermissionCard
|
||||
manifest={manifest}
|
||||
classes={classes}
|
||||
selectedLibraries={selectedLibraries}
|
||||
allLibraries={allLibraries}
|
||||
onSelectedLibrariesChange={handleSelectedLibrariesChange}
|
||||
onAllLibrariesChange={handleAllLibrariesChange}
|
||||
/>
|
||||
|
||||
<Box display="flex" justifyContent="flex-end">
|
||||
<Button
|
||||
variant="contained"
|
||||
|
||||
@ -95,8 +95,24 @@ const ToggleEnabledSwitch = ({
|
||||
}
|
||||
}, [manifest, record?.allUsers, record?.users])
|
||||
|
||||
// Check if library permission is required but not configured
|
||||
const libraryPermissionRequired = useMemo(() => {
|
||||
if (!manifest?.permissions?.library) return false
|
||||
if (record?.allLibraries) return false
|
||||
// Check if libraries array is empty or not set
|
||||
if (!record?.libraries) return true
|
||||
try {
|
||||
const libraries = JSON.parse(record.libraries)
|
||||
return libraries.length === 0
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}, [manifest, record?.allLibraries, record?.libraries])
|
||||
|
||||
const permissionRequired =
|
||||
usersPermissionRequired || libraryPermissionRequired
|
||||
const isDisabled =
|
||||
loading || hasError || (usersPermissionRequired && !record?.enabled)
|
||||
loading || hasError || (permissionRequired && !record?.enabled)
|
||||
|
||||
const tooltipTitle = useMemo(() => {
|
||||
if (hasError) {
|
||||
@ -105,6 +121,9 @@ const ToggleEnabledSwitch = ({
|
||||
if (usersPermissionRequired && !record?.enabled) {
|
||||
return translate('resources.plugin.actions.disabledUsersRequired')
|
||||
}
|
||||
if (libraryPermissionRequired && !record?.enabled) {
|
||||
return translate('resources.plugin.actions.disabledLibrariesRequired')
|
||||
}
|
||||
if (!showLabel) {
|
||||
return translate(
|
||||
record?.enabled
|
||||
@ -113,7 +132,14 @@ const ToggleEnabledSwitch = ({
|
||||
)
|
||||
}
|
||||
return ''
|
||||
}, [hasError, usersPermissionRequired, showLabel, record?.enabled, translate])
|
||||
}, [
|
||||
hasError,
|
||||
usersPermissionRequired,
|
||||
libraryPermissionRequired,
|
||||
showLabel,
|
||||
record?.enabled,
|
||||
translate,
|
||||
])
|
||||
|
||||
const switchElement = (
|
||||
<Switch
|
||||
@ -127,11 +153,12 @@ const ToggleEnabledSwitch = ({
|
||||
)
|
||||
|
||||
if (showLabel) {
|
||||
const showTooltip = hasError || (permissionRequired && !record?.enabled)
|
||||
return (
|
||||
<Tooltip
|
||||
title={tooltipTitle}
|
||||
disableHoverListener={!hasError}
|
||||
disableFocusListener={!hasError}
|
||||
disableHoverListener={!showTooltip}
|
||||
disableFocusListener={!showTooltip}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={switchElement}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user