feat(plugins): allow Plugins to call the Subsonic API (#4260)

* chore: .gitignore any navidrome binary

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: implement internal authentication handling in middleware

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(manager): add SubsonicRouter to Manager for API routing

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(plugins): add SubsonicAPI Host service for plugins and an example plugin

Signed-off-by: Deluan <deluan@navidrome.org>

* fix lint

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(plugins): refactor path handling in SubsonicAPI to extract endpoint correctly

Signed-off-by: Deluan <deluan@navidrome.org>

* docs(plugins): add SubsonicAPI service documentation to README

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(plugins): implement permission checks for SubsonicAPI service

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(plugins): enhance SubsonicAPI service initialization with atomic router handling

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor(plugins): better encapsulated dependency injection

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor(plugins): rename parameter in WithInternalAuth for clarity

Signed-off-by: Deluan <deluan@navidrome.org>

* docs(plugins): update SubsonicAPI permissions section in README for clarity and detail

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(plugins): enhance SubsonicAPI permissions output with allowed usernames and admin flag

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(plugins): add schema reference to example plugins

Signed-off-by: Deluan <deluan@navidrome.org>

* remove import alias

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan Quintão
2025-06-25 14:18:32 -04:00
committed by GitHub
parent 024b50dc2b
commit 45c408a674
34 changed files with 1573 additions and 46 deletions

View File

@@ -157,6 +157,27 @@
"description": "Artwork service permissions"
}
]
},
"subsonicapi": {
"allOf": [
{ "$ref": "#/$defs/basePermission" },
{
"type": "object",
"description": "SubsonicAPI service permissions",
"properties": {
"allowedUsernames": {
"type": "array",
"description": "List of usernames the plugin can pass as u. Any user if empty",
"items": { "type": "string" }
},
"allowAdmins": {
"type": "boolean",
"description": "If false, reject calls where the u is an admin",
"default": false
}
}
}
]
}
}
}

View File

@@ -109,6 +109,9 @@ type PluginManifestPermissions struct {
// Scheduler corresponds to the JSON schema field "scheduler".
Scheduler *PluginManifestPermissionsScheduler `json:"scheduler,omitempty" yaml:"scheduler,omitempty" mapstructure:"scheduler,omitempty"`
// Subsonicapi corresponds to the JSON schema field "subsonicapi".
Subsonicapi *PluginManifestPermissionsSubsonicapi `json:"subsonicapi,omitempty" yaml:"subsonicapi,omitempty" mapstructure:"subsonicapi,omitempty"`
// Websocket corresponds to the JSON schema field "websocket".
Websocket *PluginManifestPermissionsWebsocket `json:"websocket,omitempty" yaml:"websocket,omitempty" mapstructure:"websocket,omitempty"`
@@ -305,6 +308,42 @@ func (j *PluginManifestPermissionsScheduler) UnmarshalJSON(value []byte) error {
return nil
}
// SubsonicAPI service permissions
type PluginManifestPermissionsSubsonicapi struct {
// If false, reject calls where the u is an admin
AllowAdmins bool `json:"allowAdmins,omitempty" yaml:"allowAdmins,omitempty" mapstructure:"allowAdmins,omitempty"`
// List of usernames the plugin can pass as u. Any user if empty
AllowedUsernames []string `json:"allowedUsernames,omitempty" yaml:"allowedUsernames,omitempty" mapstructure:"allowedUsernames,omitempty"`
// Explanation of why this permission is needed
Reason string `json:"reason" yaml:"reason" mapstructure:"reason"`
}
// UnmarshalJSON implements json.Unmarshaler.
func (j *PluginManifestPermissionsSubsonicapi) UnmarshalJSON(value []byte) error {
var raw map[string]interface{}
if err := json.Unmarshal(value, &raw); err != nil {
return err
}
if _, ok := raw["reason"]; raw != nil && !ok {
return fmt.Errorf("field reason in PluginManifestPermissionsSubsonicapi: required")
}
type Plain PluginManifestPermissionsSubsonicapi
var plain Plain
if err := json.Unmarshal(value, &plain); err != nil {
return err
}
if v, ok := raw["allowAdmins"]; !ok || v == nil {
plain.AllowAdmins = false
}
if len(plain.Reason) < 1 {
return fmt.Errorf("field %s length: must be >= %d", "reason", 1)
}
*j = PluginManifestPermissionsSubsonicapi(plain)
return nil
}
// WebSocket service permissions
type PluginManifestPermissionsWebsocket struct {
// Whether to allow connections to local/private network addresses