Compare commits

..

2 Commits

Author SHA1 Message Date
Deluan
b545574c37 test: add tests to buildAllowedPaths
Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-27 21:55:59 -05:00
Deluan
2c245e5446 feat(plugins): mount library directories as read-only by default
Add an AllowWriteAccess boolean to the plugin model, defaulting to
false. When off, library directories are mounted with the extism "ro:"
prefix (read-only). Admins can explicitly grant write access via a new
toggle in the Library Permission card.
2026-02-27 21:34:12 -05:00
13 changed files with 226 additions and 75 deletions

View File

@@ -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;

View File

@@ -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

View File

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

View File

@@ -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
})
}

View File

@@ -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,29 @@ 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))
if allowWriteAccess {
log.Info(ctx, "Granting read-write filesystem access to library", "libraryID", lib.ID, "mountPoint", mountPoint)
allowedPaths[lib.Path] = mountPoint
} else {
log.Debug(ctx, "Granting read-only filesystem access to library", "libraryID", lib.ID, "mountPoint", mountPoint)
allowedPaths["ro:"+lib.Path] = mountPoint
}
}
}
return allowedPaths
}

View File

@@ -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())
})
})
})

View File

@@ -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."

View File

@@ -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)
}

View File

@@ -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

View File

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

View File

@@ -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": {

View File

@@ -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 = ({
</Typography>
</Box>
{hasFilesystem && (
<Box mb={2}>
<FormControlLabel
control={
<Switch
checked={allowWriteAccess}
onChange={handleAllowWriteAccessToggle}
color="primary"
/>
}
label={translate('resources.plugin.fields.allowWriteAccess')}
/>
<Typography variant="body2" color="textSecondary">
{translate('resources.plugin.messages.allowWriteAccessHelp')}
</Typography>
</Box>
)}
{!allLibraries && (
<Box className={classes.usersList}>
<Typography variant="subtitle2" gutterBottom>
@@ -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,
}

View File

@@ -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}
/>
<Box display="flex" justifyContent="flex-end">