fix: enforce user library ACL on mounted Emby libraries (#25)

Mounted Emby libraries were always appended to web/Emby library lists and
detail/play routes without checking allowed_library_ids, so restricted
users could still see and open them. Filter remotes with the same
visibility policy as local libraries across list/detail/series/stream and
Emby Views/Items/search/playback, and label mounts in the admin ACL UI.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: truewhile <truewhile@users.noreply.github.com>
This commit is contained in:
truewhile
2026-09-03 10:39:44 +08:00
committed by GitHub
parent 9ab32c10ca
commit 2b99f5f108
10 changed files with 172 additions and 18 deletions
+31 -5
View File
@@ -94,18 +94,26 @@ func listLibrariesHandler(svc *service.Container) gin.HandlerFunc {
out = append(out, webLibraryPayload{Library: l})
}
}
// 远程 Emby 挂载库追加在本地库之后。
// 远程 Emby 挂载库追加在本地库之后(非管理员视图仍受 allowed_library_ids 约束)。
if svc.EmbyRemote != nil {
if views, err := svc.EmbyRemote.RemoteLibraries(ctx); err == nil {
remotePayloads := make([]webLibraryPayload, len(views))
for i, v := range views {
visibility := mediaVisibilityForRequest(c, svc)
allowedViews := make([]service.RemoteLibraryView, 0, len(views))
for _, v := range views {
if !includeHidden && !service.LibraryVisibleForUser(ctx, svc.Repo, v.Library, visibility) {
continue
}
allowedViews = append(allowedViews, v)
}
remotePayloads := make([]webLibraryPayload, len(allowedViews))
for i, v := range allowedViews {
remotePayloads[i] = webLibraryPayload{Library: v.Library, IsRemoteEmby: true, RemoteSource: v.AccountName}
}
if withPreview && len(views) > 0 {
if withPreview && len(allowedViews) > 0 {
const maxRemotePreviewWorkers = 6
sem := make(chan struct{}, maxRemotePreviewWorkers)
var wg sync.WaitGroup
for i, v := range views {
for i, v := range allowedViews {
i, v := i, v
wg.Add(1)
go func() {
@@ -151,6 +159,12 @@ func getLibraryHandler(svc *service.Container) gin.HandlerFunc {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
role, _ := c.Get(middleware.CtxUserRole)
includeHidden := role == "admin" && (c.Query("include_hidden") == "1" || c.Query("include_hidden") == "true" || c.Query("all") == "1")
if !includeHidden && !service.LibraryVisibleForUser(ctx, svc.Repo, view.Library, mediaVisibilityForRequest(c, svc)) {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, webLibraryPayload{Library: view.Library, IsRemoteEmby: true, RemoteSource: view.AccountName})
return
}
@@ -330,6 +344,10 @@ func listMediaHandler(svc *service.Container) gin.HandlerFunc {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
if !service.EmbyMountLibraryAllowed(mediaVisibilityForRequest(c, svc), mount) {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
itemTypes := ""
if view, err := svc.EmbyRemote.RemoteLibraryByID(ctx, mountID, remoteID); err == nil && view != nil {
itemTypes = remoteLibraryItemTypes(view.CollectionType)
@@ -397,6 +415,10 @@ func getMediaHandler(svc *service.Container) gin.HandlerFunc {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
if !service.EmbyMountLibraryAllowed(mediaVisibilityForRequest(c, svc), mount) {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
m, err := svc.EmbyRemote.RemoteMediaDetail(ctx, mount, acct, remoteID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
@@ -579,6 +601,10 @@ func streamHandler(svc *service.Container) gin.HandlerFunc {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
if !service.EmbyMountLibraryAllowed(mediaVisibilityForRequest(c, svc), mount) {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
if mount.ProxyPlay {
if err := svc.Emby.ProxyRemoteVideoStream(ctx, c.Writer, c.Request, mountID, remoteID); err != nil {
if !c.Writer.Written() {
+12
View File
@@ -74,6 +74,10 @@ func listLibrarySeriesHandler(svc *service.Container) gin.HandlerFunc {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
if !service.EmbyMountLibraryAllowed(mediaVisibilityForRequest(c, svc), mount) {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
cards, err := svc.EmbyRemote.RemoteSeriesCards(ctx, mount, acct, remoteID)
if err != nil {
writeInternalOrCanceled(c, err)
@@ -165,6 +169,10 @@ func listLibrarySeriesEpisodesHandler(svc *service.Container) gin.HandlerFunc {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
if !service.EmbyMountLibraryAllowed(mediaVisibilityForRequest(c, svc), mount) {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
items, err := svc.EmbyRemote.RemoteEpisodes(ctx, mount, acct, remoteSeriesID)
if err != nil {
writeInternalOrCanceled(c, err)
@@ -207,6 +215,10 @@ func listMediaEpisodesHandler(svc *service.Container) gin.HandlerFunc {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
if !service.EmbyMountLibraryAllowed(mediaVisibilityForRequest(c, svc), mount) {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
items, err := svc.EmbyRemote.RemoteEpisodes(ctx, mount, acct, remoteID)
if err != nil {
writeInternalOrCanceled(c, err)