mirror of
https://github.com/navidrome/navidrome.git
synced 2025-12-23 23:18:05 -05:00
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:
@@ -214,6 +214,15 @@ func UsernameFromReverseProxyHeader(r *http.Request) string {
|
||||
return username
|
||||
}
|
||||
|
||||
func InternalAuth(r *http.Request) string {
|
||||
username, ok := request.InternalAuthFrom(r.Context())
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
log.Trace(r, "Found username in InternalAuth", "username", username)
|
||||
return username
|
||||
}
|
||||
|
||||
func UsernameFromConfig(*http.Request) string {
|
||||
return conf.Server.DevAutoLoginUsername
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/server"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
. "github.com/navidrome/navidrome/utils/gg"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
@@ -46,9 +47,9 @@ func postFormToQueryParams(next http.Handler) http.Handler {
|
||||
func checkRequiredParameters(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var requiredParameters []string
|
||||
var username string
|
||||
|
||||
if username = server.UsernameFromReverseProxyHeader(r); username != "" {
|
||||
username := cmp.Or(server.InternalAuth(r), server.UsernameFromReverseProxyHeader(r))
|
||||
if username != "" {
|
||||
requiredParameters = []string{"v", "c"}
|
||||
} else {
|
||||
requiredParameters = []string{"u", "v", "c"}
|
||||
@@ -87,16 +88,19 @@ func authenticate(ds model.DataStore) func(next http.Handler) http.Handler {
|
||||
var usr *model.User
|
||||
var err error
|
||||
|
||||
if username := server.UsernameFromReverseProxyHeader(r); username != "" {
|
||||
internalAuth := server.InternalAuth(r)
|
||||
proxyAuth := server.UsernameFromReverseProxyHeader(r)
|
||||
if username := cmp.Or(internalAuth, proxyAuth); username != "" {
|
||||
authType := If(internalAuth != "", "internal", "reverse-proxy")
|
||||
usr, err = ds.User(ctx).FindByUsername(username)
|
||||
if errors.Is(err, context.Canceled) {
|
||||
log.Debug(ctx, "API: Request canceled when authenticating", "auth", "reverse-proxy", "username", username, "remoteAddr", r.RemoteAddr, err)
|
||||
log.Debug(ctx, "API: Request canceled when authenticating", "auth", authType, "username", username, "remoteAddr", r.RemoteAddr, err)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
log.Warn(ctx, "API: Invalid login", "auth", "reverse-proxy", "username", username, "remoteAddr", r.RemoteAddr, err)
|
||||
log.Warn(ctx, "API: Invalid login", "auth", authType, "username", username, "remoteAddr", r.RemoteAddr, err)
|
||||
} else if err != nil {
|
||||
log.Error(ctx, "API: Error authenticating username", "auth", "reverse-proxy", "username", username, "remoteAddr", r.RemoteAddr, err)
|
||||
log.Error(ctx, "API: Error authenticating username", "auth", authType, "username", username, "remoteAddr", r.RemoteAddr, err)
|
||||
}
|
||||
} else {
|
||||
p := req.Params(r)
|
||||
|
||||
@@ -281,6 +281,31 @@ var _ = Describe("Middlewares", func() {
|
||||
Expect(next.called).To(BeFalse())
|
||||
})
|
||||
})
|
||||
|
||||
When("using internal authentication", func() {
|
||||
It("passes authentication with correct internal credentials", func() {
|
||||
// Simulate internal authentication by setting the context with WithInternalAuth
|
||||
r := newGetRequest()
|
||||
r = r.WithContext(request.WithInternalAuth(r.Context(), "admin"))
|
||||
cp := authenticate(ds)(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
Expect(next.called).To(BeTrue())
|
||||
user, _ := request.UserFrom(next.req.Context())
|
||||
Expect(user.UserName).To(Equal("admin"))
|
||||
})
|
||||
|
||||
It("fails authentication with missing internal context", func() {
|
||||
r := newGetRequest("u=admin")
|
||||
// Do not set the internal auth context
|
||||
cp := authenticate(ds)(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
// Internal auth requires the context, so this should fail
|
||||
Expect(w.Body.String()).To(ContainSubstring(`code="40"`))
|
||||
Expect(next.called).To(BeFalse())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetPlayer", func() {
|
||||
|
||||
Reference in New Issue
Block a user