feat(plugins): allow mounting library directories as read-write (#5122)
* 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. * test: add tests to buildAllowedPaths Signed-off-by: Deluan <deluan@navidrome.org> * chore: improve allowed paths logging for library access Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
@@ -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())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user