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}
/>