diff --git a/server/backend/internal/httpapi/manage/override.go b/server/backend/internal/httpapi/manage/override.go index 067e5b0..0a2af48 100644 --- a/server/backend/internal/httpapi/manage/override.go +++ b/server/backend/internal/httpapi/manage/override.go @@ -112,7 +112,7 @@ func HandleSetScreenOverride(screens *store.ScreenStore, schedules *store.Screen http.Error(w, "screen not found", http.StatusNotFound) return } - if !requireScreenAccess(w, r, screen) { + if !requireScreenAccess(w, r, screen, screens) { return } diff --git a/server/backend/internal/httpapi/manage/schedule.go b/server/backend/internal/httpapi/manage/schedule.go index 15fcdd3..d145858 100644 --- a/server/backend/internal/httpapi/manage/schedule.go +++ b/server/backend/internal/httpapi/manage/schedule.go @@ -18,7 +18,7 @@ func HandleUpdateSchedule(screens *store.ScreenStore, schedules *store.ScreenSch http.Error(w, "screen not found", http.StatusNotFound) return } - if !requireScreenAccess(w, r, screen) { + if !requireScreenAccess(w, r, screen, screens) { return } diff --git a/server/backend/internal/httpapi/manage/ui.go b/server/backend/internal/httpapi/manage/ui.go index 8d706d7..9566ca1 100644 --- a/server/backend/internal/httpapi/manage/ui.go +++ b/server/backend/internal/httpapi/manage/ui.go @@ -43,9 +43,10 @@ func renderTemplate(w http.ResponseWriter, t *template.Template, data any) { } // requireScreenAccess prüft, ob der eingeloggte User Zugriff auf den Screen hat. -// Admins dürfen alles. Tenant-User dürfen nur Screens ihres eigenen Tenants bearbeiten. +// Admins dürfen alles. screen_user dürfen Screens ihres Tenants. restricted User +// benötigen zusätzlich einen Eintrag in user_screen_permissions. // Gibt true zurück wenn Zugriff erlaubt ist; schreibt 403 und gibt false zurück wenn nicht. -func requireScreenAccess(w http.ResponseWriter, r *http.Request, screen *store.Screen) bool { +func requireScreenAccess(w http.ResponseWriter, r *http.Request, screen *store.Screen, sc *store.ScreenStore) bool { u := reqcontext.UserFromContext(r.Context()) if u == nil { http.Error(w, "Forbidden", http.StatusForbidden) @@ -54,19 +55,18 @@ func requireScreenAccess(w http.ResponseWriter, r *http.Request, screen *store.S if u.Role == "admin" { return true } - // Tenant-User: Screen muss zum eigenen Tenant gehören. - // Wir vergleichen über TenantSlug→TenantID, aber der Screen hat TenantID. - // Da uns der Tenant-Slug des Users bekannt ist und wir keinen TenantStore - // hier haben, vergleichen wir TenantID des Screens mit dem user.TenantID-Feld. - // store.User hat TenantSlug aber nicht TenantID — deswegen muss der - // aufrufende Handler nach GetBySlug bereits die TenantID des Screens bekannt haben. - // Wir nutzen u.TenantSlug und vertrauen darauf dass der Screen bereits geladen ist. - // Den eigentlichen Vergleich machen wir via TenantID des Screens vs. - // dem TenantID-Feld im User (das über reqcontext gespeichert ist). if u.TenantID != "" && u.TenantID != screen.TenantID { http.Error(w, "Forbidden", http.StatusForbidden) return false } + // Restricted User: zusätzlich explizite Screen-Berechtigung prüfen. + if u.Role == "restricted" { + ok, err := sc.HasUserScreenAccess(r.Context(), u.ID, screen.ID) + if err != nil || !ok { + http.Error(w, "Forbidden", http.StatusForbidden) + return false + } + } return true } @@ -370,7 +370,7 @@ func HandleManageUI( notifier.RequestScreenshot(screen.Slug) // K2: Tenant-Isolation — nur eigener Tenant oder Admin. - if !requireScreenAccess(w, r, screen) { + if !requireScreenAccess(w, r, screen, screens) { return } @@ -607,7 +607,7 @@ func HandleUploadMediaUI(media *store.MediaStore, screens *store.ScreenStore, up } // K2: Tenant-Isolation. - if !requireScreenAccess(w, r, screen) { + if !requireScreenAccess(w, r, screen, screens) { return } @@ -694,7 +694,7 @@ func HandleAddItemUI(playlists *store.PlaylistStore, media *store.MediaStore, sc } // K2: Tenant-Isolation. - if !requireScreenAccess(w, r, screen) { + if !requireScreenAccess(w, r, screen, screens) { return } @@ -760,7 +760,7 @@ func HandleDeleteItemUI(playlists *store.PlaylistStore, screens *store.ScreenSto http.Error(w, "screen nicht gefunden", http.StatusNotFound) return } - if !requireScreenAccess(w, r, screen) { + if !requireScreenAccess(w, r, screen, screens) { return } @@ -783,7 +783,7 @@ func HandleReorderUI(playlists *store.PlaylistStore, screens *store.ScreenStore, return } // K2: Tenant-Isolation. - if !requireScreenAccess(w, r, screen) { + if !requireScreenAccess(w, r, screen, screens) { return } playlist, err := playlists.GetByScreen(r.Context(), screen.ID) @@ -822,7 +822,7 @@ func HandleUpdateItemUI(playlists *store.PlaylistStore, screens *store.ScreenSto http.Error(w, "screen nicht gefunden", http.StatusNotFound) return } - if !requireScreenAccess(w, r, screen) { + if !requireScreenAccess(w, r, screen, screens) { return } @@ -864,7 +864,7 @@ func HandleDeleteMediaUI(media *store.MediaStore, screens *store.ScreenStore, up http.Error(w, "screen nicht gefunden", http.StatusNotFound) return } - if !requireScreenAccess(w, r, screen) { + if !requireScreenAccess(w, r, screen, screens) { return }