mirror of
https://github.com/navidrome/navidrome.git
synced 2026-02-28 04:46:18 -05:00
Compare commits
2 Commits
master
...
rw-librari
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b545574c37 | ||
|
|
2c245e5446 |
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user