diff --git a/db/migrations/20260228020813_add_plugin_allow_write_access.sql b/db/migrations/20260228020813_add_plugin_allow_write_access.sql new file mode 100644 index 000000000..e17d874a5 --- /dev/null +++ b/db/migrations/20260228020813_add_plugin_allow_write_access.sql @@ -0,0 +1,5 @@ +-- +goose Up +ALTER TABLE plugin ADD COLUMN allow_write_access BOOL NOT NULL DEFAULT false; + +-- +goose Down +ALTER TABLE plugin DROP COLUMN allow_write_access; diff --git a/model/plugin.go b/model/plugin.go index d23103995..f4bad6783 100644 --- a/model/plugin.go +++ b/model/plugin.go @@ -3,19 +3,20 @@ 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"` - 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"` + 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"` + AllowWriteAccess bool `structs:"allow_write_access" json:"allowWriteAccess,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 diff --git a/persistence/plugin_repository.go b/persistence/plugin_repository.go index 4a98f148b..466abb40b 100644 --- a/persistence/plugin_repository.go +++ b/persistence/plugin_repository.go @@ -79,8 +79,8 @@ 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, 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}) + INSERT INTO plugin (id, path, manifest, config, users, all_users, libraries, all_libraries, allow_write_access, enabled, last_error, sha256, created_at, updated_at) + VALUES ({:id}, {:path}, {:manifest}, {:config}, {:users}, {:all_users}, {:libraries}, {:all_libraries}, {:allow_write_access}, {:enabled}, {:last_error}, {:sha256}, {:created_at}, {:updated_at}) ON CONFLICT(id) DO UPDATE SET path = excluded.path, manifest = excluded.manifest, @@ -89,24 +89,26 @@ func (r *pluginRepository) Put(plugin *model.Plugin) error { all_users = excluded.all_users, libraries = excluded.libraries, all_libraries = excluded.all_libraries, + allow_write_access = excluded.allow_write_access, 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, - "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, + "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, + "allow_write_access": plugin.AllowWriteAccess, + "enabled": plugin.Enabled, + "last_error": plugin.LastError, + "sha256": plugin.SHA256, + "created_at": time.Now(), + "updated_at": plugin.UpdatedAt, }).Execute() return err } diff --git a/plugins/manager.go b/plugins/manager.go index d8d4f28ef..f148a706c 100644 --- a/plugins/manager.go +++ b/plugins/manager.go @@ -428,10 +428,11 @@ func (m *Manager) UpdatePluginUsers(ctx context.Context, id, usersJSON string, a // 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 { +func (m *Manager) UpdatePluginLibraries(ctx context.Context, id, librariesJSON string, allLibraries, allowWriteAccess bool) error { return m.updatePluginSettings(ctx, id, func(p *model.Plugin) { p.Libraries = librariesJSON p.AllLibraries = allLibraries + p.AllowWriteAccess = allowWriteAccess }) } diff --git a/plugins/manager_loader.go b/plugins/manager_loader.go index c6355911f..688c4519c 100644 --- a/plugins/manager_loader.go +++ b/plugins/manager_loader.go @@ -226,6 +226,8 @@ func (m *Manager) loadEnabledPlugins(ctx context.Context) error { // loadPluginWithConfig loads a plugin with configuration from DB. // The p.Path should point to an .ndp package file. func (m *Manager) loadPluginWithConfig(p *model.Plugin) error { + ctx := log.NewContext(m.ctx, "plugin", p.ID) + if m.stopped.Load() { return fmt.Errorf("manager is stopped") } @@ -283,27 +285,13 @@ func (m *Manager) loadPluginWithConfig(p *model.Plugin) error { // Configure filesystem access for library permission if pkg.Manifest.Permissions != nil && pkg.Manifest.Permissions.Library != nil && pkg.Manifest.Permissions.Library.Filesystem { - adminCtx := adminContext(m.ctx) + adminCtx := adminContext(ctx) libraries, err := m.ds.Library(adminCtx).GetAll() if err != nil { 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 { - // 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)) - } - } + allowedPaths := buildAllowedPaths(ctx, libraries, allowedLibraries, p.AllLibraries, p.AllowWriteAccess) pluginManifest.AllowedPaths = allowedPaths } @@ -339,7 +327,7 @@ func (m *Manager) loadPluginWithConfig(p *model.Plugin) error { // Enable experimental threads if requested in manifest if pkg.Manifest.HasExperimentalThreads() { runtimeConfig = runtimeConfig.WithCoreFeatures(api.CoreFeaturesV2 | experimental.CoreFeaturesThreads) - log.Debug(m.ctx, "Enabling experimental threads support", "plugin", p.ID) + log.Debug(ctx, "Enabling experimental threads support") } extismConfig := extism.PluginConfig{ @@ -347,24 +335,24 @@ func (m *Manager) loadPluginWithConfig(p *model.Plugin) error { RuntimeConfig: runtimeConfig, EnableHttpResponseHeaders: true, } - compiled, err := extism.NewCompiledPlugin(m.ctx, pluginManifest, extismConfig, hostFunctions) + compiled, err := extism.NewCompiledPlugin(ctx, pluginManifest, extismConfig, hostFunctions) if err != nil { return fmt.Errorf("compiling plugin: %w", err) } // Create instance to detect capabilities - instance, err := compiled.Instance(m.ctx, extism.PluginInstanceConfig{}) + instance, err := compiled.Instance(ctx, extism.PluginInstanceConfig{}) if err != nil { - compiled.Close(m.ctx) + compiled.Close(ctx) return fmt.Errorf("creating instance: %w", err) } instance.SetLogger(extismLogger(p.ID)) capabilities := detectCapabilities(instance) - instance.Close(m.ctx) + instance.Close(ctx) // Validate manifest against detected capabilities if err := ValidateWithCapabilities(pkg.Manifest, capabilities); err != nil { - compiled.Close(m.ctx) + compiled.Close(ctx) return fmt.Errorf("manifest validation: %w", err) } @@ -383,7 +371,7 @@ func (m *Manager) loadPluginWithConfig(p *model.Plugin) error { m.mu.Unlock() // Call plugin init function - callPluginInit(m.ctx, m.plugins[p.ID]) + callPluginInit(ctx, m.plugins[p.ID]) return nil } @@ -414,3 +402,32 @@ func parsePluginConfig(configJSON string) (map[string]string, error) { } return pluginConfig, nil } + +// buildAllowedPaths constructs the extism AllowedPaths map for filesystem access. +// When allowWriteAccess is false (default), paths are prefixed with "ro:" for read-only. +// Only libraries that match the allowed set (or all libraries if allLibraries is true) are included. +func buildAllowedPaths(ctx context.Context, libraries model.Libraries, allowedLibraryIDs []int, allLibraries, allowWriteAccess bool) map[string]string { + allowedLibrarySet := make(map[int]struct{}, len(allowedLibraryIDs)) + for _, id := range allowedLibraryIDs { + allowedLibrarySet[id] = struct{}{} + } + allowedPaths := make(map[string]string) + for _, lib := range libraries { + _, allowed := allowedLibrarySet[lib.ID] + if allLibraries || allowed { + mountPoint := toPluginMountPoint(int32(lib.ID)) + hostPath := lib.Path + if !allowWriteAccess { + hostPath = "ro:" + hostPath + } + allowedPaths[hostPath] = mountPoint + log.Trace(ctx, "Added library to allowed paths", "libraryID", lib.ID, "mountPoint", mountPoint, "writeAccess", allowWriteAccess, "hostPath", hostPath) + } + } + if allowWriteAccess { + log.Info(ctx, "Granting read-write filesystem access to libraries", "libraryCount", len(allowedPaths), "allLibraries", allLibraries) + } else { + log.Debug(ctx, "Granting read-only filesystem access to libraries", "libraryCount", len(allowedPaths), "allLibraries", allLibraries) + } + return allowedPaths +} diff --git a/plugins/manager_loader_test.go b/plugins/manager_loader_test.go index 64bc5e810..3a00b07b7 100644 --- a/plugins/manager_loader_test.go +++ b/plugins/manager_loader_test.go @@ -3,6 +3,7 @@ package plugins import ( + "github.com/navidrome/navidrome/model" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -58,3 +59,66 @@ var _ = Describe("parsePluginConfig", func() { Expect(result).ToNot(BeNil()) }) }) + +var _ = Describe("buildAllowedPaths", func() { + var libraries model.Libraries + + BeforeEach(func() { + libraries = model.Libraries{ + {ID: 1, Path: "/music/library1"}, + {ID: 2, Path: "/music/library2"}, + {ID: 3, Path: "/music/library3"}, + } + }) + + Context("read-only (default)", func() { + It("mounts all libraries with ro: prefix when allLibraries is true", func() { + result := buildAllowedPaths(nil, libraries, nil, true, false) + Expect(result).To(HaveLen(3)) + Expect(result).To(HaveKeyWithValue("ro:/music/library1", "/libraries/1")) + Expect(result).To(HaveKeyWithValue("ro:/music/library2", "/libraries/2")) + Expect(result).To(HaveKeyWithValue("ro:/music/library3", "/libraries/3")) + }) + + It("mounts only selected libraries with ro: prefix", func() { + result := buildAllowedPaths(nil, libraries, []int{1, 3}, false, false) + Expect(result).To(HaveLen(2)) + Expect(result).To(HaveKeyWithValue("ro:/music/library1", "/libraries/1")) + Expect(result).To(HaveKeyWithValue("ro:/music/library3", "/libraries/3")) + Expect(result).ToNot(HaveKey("ro:/music/library2")) + }) + }) + + Context("read-write (allowWriteAccess=true)", func() { + It("mounts all libraries without ro: prefix when allLibraries is true", func() { + result := buildAllowedPaths(nil, libraries, nil, true, true) + Expect(result).To(HaveLen(3)) + Expect(result).To(HaveKeyWithValue("/music/library1", "/libraries/1")) + Expect(result).To(HaveKeyWithValue("/music/library2", "/libraries/2")) + Expect(result).To(HaveKeyWithValue("/music/library3", "/libraries/3")) + }) + + It("mounts only selected libraries without ro: prefix", func() { + result := buildAllowedPaths(nil, libraries, []int{2}, false, true) + Expect(result).To(HaveLen(1)) + Expect(result).To(HaveKeyWithValue("/music/library2", "/libraries/2")) + }) + }) + + Context("edge cases", func() { + It("returns empty map when no libraries match", func() { + result := buildAllowedPaths(nil, libraries, []int{99}, false, false) + Expect(result).To(BeEmpty()) + }) + + It("returns empty map when libraries list is empty", func() { + result := buildAllowedPaths(nil, nil, []int{1}, false, false) + Expect(result).To(BeEmpty()) + }) + + It("returns empty map when allLibraries is false and no IDs provided", func() { + result := buildAllowedPaths(nil, libraries, nil, false, false) + Expect(result).To(BeEmpty()) + }) + }) +}) diff --git a/resources/i18n/pt-br.json b/resources/i18n/pt-br.json index a844dccdb..1fc72bf30 100644 --- a/resources/i18n/pt-br.json +++ b/resources/i18n/pt-br.json @@ -353,7 +353,8 @@ "allUsers": "Permitir todos os usuários", "selectedUsers": "Usuários selecionados", "allLibraries": "Permitir todas as bibliotecas", - "selectedLibraries": "Bibliotecas selecionadas" + "selectedLibraries": "Bibliotecas selecionadas", + "allowWriteAccess": "Permitir acesso de escrita" }, "sections": { "status": "Status", @@ -396,6 +397,7 @@ "allLibrariesHelp": "Quando habilitado, o plugin terá acesso a todas as bibliotecas, incluindo as criadas no futuro.", "noLibraries": "Nenhuma biblioteca selecionada", "librariesRequired": "Este plugin requer acesso a informações de bibliotecas. Selecione quais bibliotecas o plugin pode acessar, ou habilite 'Permitir todas as bibliotecas'.", + "allowWriteAccessHelp": "Quando habilitado, o plugin pode modificar arquivos nos diretórios das bibliotecas. Por padrão, plugins têm acesso somente leitura.", "requiredHosts": "Hosts necessários", "configValidationError": "Falha na validação da configuração:", "schemaRenderError": "Não foi possível renderizar o formulário de configuração. O schema do plugin pode estar inválido." diff --git a/server/nativeapi/native_api.go b/server/nativeapi/native_api.go index 27b85a605..062cbf706 100644 --- a/server/nativeapi/native_api.go +++ b/server/nativeapi/native_api.go @@ -29,7 +29,7 @@ type PluginManager interface { ValidatePluginConfig(ctx context.Context, id, configJSON 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 + UpdatePluginLibraries(ctx context.Context, id, librariesJSON string, allLibraries, allowWriteAccess bool) error RescanPlugins(ctx context.Context) error UnloadDisabledPlugins(ctx context.Context) } diff --git a/server/nativeapi/plugin.go b/server/nativeapi/plugin.go index e733edc4b..a7d261681 100644 --- a/server/nativeapi/plugin.go +++ b/server/nativeapi/plugin.go @@ -56,12 +56,13 @@ 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"` - Libraries *string `json:"libraries,omitempty"` - AllLibraries *bool `json:"allLibraries,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"` + AllowWriteAccess *bool `json:"allowWriteAccess,omitempty"` } func (api *Router) updatePlugin(w http.ResponseWriter, r *http.Request) { @@ -109,7 +110,7 @@ func (api *Router) updatePlugin(w http.ResponseWriter, r *http.Request) { } // Handle libraries permission update (if provided) - if req.Libraries != nil || req.AllLibraries != nil { + if req.Libraries != nil || req.AllLibraries != nil || req.AllowWriteAccess != nil { if err := validateAndUpdateLibraries(ctx, api.pluginManager, repo, id, req, w); err != nil { log.Error(ctx, "Error updating plugin libraries", err) return @@ -245,6 +246,7 @@ func validateAndUpdateLibraries(ctx context.Context, pm PluginManager, repo mode librariesJSON := plugin.Libraries allLibraries := plugin.AllLibraries + allowWriteAccess := plugin.AllowWriteAccess if req.Libraries != nil { if *req.Libraries != "" && !isValidJSON(*req.Libraries) { @@ -256,8 +258,11 @@ func validateAndUpdateLibraries(ctx context.Context, pm PluginManager, repo mode if req.AllLibraries != nil { allLibraries = *req.AllLibraries } + if req.AllowWriteAccess != nil { + allowWriteAccess = *req.AllowWriteAccess + } - if err := pm.UpdatePluginLibraries(ctx, id, librariesJSON, allLibraries); err != nil { + if err := pm.UpdatePluginLibraries(ctx, id, librariesJSON, allLibraries, allowWriteAccess); 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 diff --git a/tests/mock_plugin_manager.go b/tests/mock_plugin_manager.go index 9691f7a38..05375f31c 100644 --- a/tests/mock_plugin_manager.go +++ b/tests/mock_plugin_manager.go @@ -18,7 +18,7 @@ type MockPluginManager struct { // 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 + UpdatePluginLibrariesFn func(ctx context.Context, id, librariesJSON string, allLibraries, allowWriteAccess bool) error // RescanPluginsFn is called when RescanPlugins is invoked. If nil, returns RescanError. RescanPluginsFn func(ctx context.Context) error @@ -48,9 +48,10 @@ type MockPluginManager struct { AllUsers bool } UpdatePluginLibrariesCalls []struct { - ID string - LibrariesJSON string - AllLibraries bool + ID string + LibrariesJSON string + AllLibraries bool + AllowWriteAccess bool } RescanPluginsCalls int } @@ -105,14 +106,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 { +func (m *MockPluginManager) UpdatePluginLibraries(ctx context.Context, id, librariesJSON string, allLibraries, allowWriteAccess bool) error { m.UpdatePluginLibrariesCalls = append(m.UpdatePluginLibrariesCalls, struct { - ID string - LibrariesJSON string - AllLibraries bool - }{ID: id, LibrariesJSON: librariesJSON, AllLibraries: allLibraries}) + ID string + LibrariesJSON string + AllLibraries bool + AllowWriteAccess bool + }{ID: id, LibrariesJSON: librariesJSON, AllLibraries: allLibraries, AllowWriteAccess: allowWriteAccess}) if m.UpdatePluginLibrariesFn != nil { - return m.UpdatePluginLibrariesFn(ctx, id, librariesJSON, allLibraries) + return m.UpdatePluginLibrariesFn(ctx, id, librariesJSON, allLibraries, allowWriteAccess) } return m.LibrariesError } diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json index 678abaabd..224b1c437 100644 --- a/ui/src/i18n/en.json +++ b/ui/src/i18n/en.json @@ -355,7 +355,8 @@ "allUsers": "Allow all users", "selectedUsers": "Selected users", "allLibraries": "Allow all libraries", - "selectedLibraries": "Selected libraries" + "selectedLibraries": "Selected libraries", + "allowWriteAccess": "Allow write access" }, "sections": { "status": "Status", @@ -400,6 +401,7 @@ "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'.", + "allowWriteAccessHelp": "When enabled, the plugin can modify files in the library directories. By default, plugins have read-only access.", "requiredHosts": "Required hosts" }, "placeholders": { diff --git a/ui/src/plugin/LibraryPermissionCard.jsx b/ui/src/plugin/LibraryPermissionCard.jsx index 885ac010b..d3c237279 100644 --- a/ui/src/plugin/LibraryPermissionCard.jsx +++ b/ui/src/plugin/LibraryPermissionCard.jsx @@ -23,8 +23,10 @@ export const LibraryPermissionCard = ({ classes, selectedLibraries, allLibraries, + allowWriteAccess, onSelectedLibrariesChange, onAllLibrariesChange, + onAllowWriteAccessChange, }) => { const translate = useTranslate() @@ -58,9 +60,17 @@ export const LibraryPermissionCard = ({ [onAllLibrariesChange], ) + const handleAllowWriteAccessToggle = React.useCallback( + (event) => { + onAllowWriteAccessChange(event.target.checked) + }, + [onAllowWriteAccessChange], + ) + // Get permission reason from manifest const libraryPermission = manifest?.permissions?.library const reason = libraryPermission?.reason + const hasFilesystem = libraryPermission?.filesystem === true // Check if permission is required but not configured const isConfigurationRequired = @@ -107,6 +117,24 @@ export const LibraryPermissionCard = ({ + {hasFilesystem && ( + + + } + label={translate('resources.plugin.fields.allowWriteAccess')} + /> + + {translate('resources.plugin.messages.allowWriteAccessHelp')} + + + )} + {!allLibraries && ( @@ -166,6 +194,8 @@ LibraryPermissionCard.propTypes = { classes: PropTypes.object.isRequired, selectedLibraries: PropTypes.array.isRequired, allLibraries: PropTypes.bool.isRequired, + allowWriteAccess: PropTypes.bool.isRequired, onSelectedLibrariesChange: PropTypes.func.isRequired, onAllLibrariesChange: PropTypes.func.isRequired, + onAllowWriteAccessChange: PropTypes.func.isRequired, } diff --git a/ui/src/plugin/PluginShow.jsx b/ui/src/plugin/PluginShow.jsx index 38e858af4..caea44a75 100644 --- a/ui/src/plugin/PluginShow.jsx +++ b/ui/src/plugin/PluginShow.jsx @@ -48,8 +48,11 @@ const PluginShowLayout = () => { // Libraries permission state const [selectedLibraries, setSelectedLibraries] = useState([]) const [allLibraries, setAllLibraries] = useState(false) + const [allowWriteAccess, setAllowWriteAccess] = useState(false) const [lastRecordLibraries, setLastRecordLibraries] = useState(null) const [lastRecordAllLibraries, setLastRecordAllLibraries] = useState(null) + const [lastRecordAllowWriteAccess, setLastRecordAllowWriteAccess] = + useState(null) // Parse JSON config to object const jsonToObject = useCallback((jsonString) => { @@ -99,10 +102,12 @@ const PluginShowLayout = () => { if (record && !isDirty) { const recordLibraries = record.libraries || '' const recordAllLibraries = record.allLibraries || false + const recordAllowWriteAccess = record.allowWriteAccess || false if ( recordLibraries !== lastRecordLibraries || - recordAllLibraries !== lastRecordAllLibraries + recordAllLibraries !== lastRecordAllLibraries || + recordAllowWriteAccess !== lastRecordAllowWriteAccess ) { try { setSelectedLibraries( @@ -112,11 +117,19 @@ const PluginShowLayout = () => { setSelectedLibraries([]) } setAllLibraries(recordAllLibraries) + setAllowWriteAccess(recordAllowWriteAccess) setLastRecordLibraries(recordLibraries) setLastRecordAllLibraries(recordAllLibraries) + setLastRecordAllowWriteAccess(recordAllowWriteAccess) } } - }, [record, lastRecordLibraries, lastRecordAllLibraries, isDirty]) + }, [ + record, + lastRecordLibraries, + lastRecordAllLibraries, + lastRecordAllowWriteAccess, + isDirty, + ]) const handleConfigDataChange = useCallback( (newData, errors) => { @@ -152,6 +165,11 @@ const PluginShowLayout = () => { setIsDirty(true) }, []) + const handleAllowWriteAccessChange = useCallback((newAllowWriteAccess) => { + setAllowWriteAccess(newAllowWriteAccess) + setIsDirty(true) + }, []) + const [updatePlugin, { loading }] = useUpdate( 'plugin', record?.id, @@ -167,6 +185,7 @@ const PluginShowLayout = () => { setLastRecordAllUsers(null) setLastRecordLibraries(null) setLastRecordAllLibraries(null) + setLastRecordAllowWriteAccess(null) notify('resources.plugin.notifications.updated', 'info') }, onFailure: (err) => { @@ -199,6 +218,7 @@ const PluginShowLayout = () => { if (parsedManifest?.permissions?.library) { data.libraries = JSON.stringify(selectedLibraries) data.allLibraries = allLibraries + data.allowWriteAccess = allowWriteAccess } updatePlugin('plugin', record.id, data, record) @@ -210,6 +230,7 @@ const PluginShowLayout = () => { allUsers, selectedLibraries, allLibraries, + allowWriteAccess, ]) // Parse manifest @@ -294,8 +315,10 @@ const PluginShowLayout = () => { classes={classes} selectedLibraries={selectedLibraries} allLibraries={allLibraries} + allowWriteAccess={allowWriteAccess} onSelectedLibrariesChange={handleSelectedLibrariesChange} onAllLibrariesChange={handleAllLibrariesChange} + onAllowWriteAccessChange={handleAllowWriteAccessChange} />