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:
Deluan Quintão
2026-02-28 10:59:13 -05:00
committed by GitHub
parent d134de1061
commit d9a215e1e3
13 changed files with 229 additions and 75 deletions
+3 -1
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": {
+30
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,
}
+25 -2
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">