fix(auth): restricted User können nur zugewiesene Screens aufrufen

requireScreenAccess prüft jetzt für Rolle 'restricted' zusätzlich
ob ein Eintrag in user_screen_permissions existiert. Tenant-Match
allein reichte bisher nicht — restricted User konnten alle Screens
des Tenants aufrufen.
This commit is contained in:
Jesko Anschütz 2026-03-28 10:17:29 +01:00
parent 3ebeaa70e1
commit 3a0ac13faa
3 changed files with 20 additions and 20 deletions

View file

@ -112,7 +112,7 @@ func HandleSetScreenOverride(screens *store.ScreenStore, schedules *store.Screen
http.Error(w, "screen not found", http.StatusNotFound) http.Error(w, "screen not found", http.StatusNotFound)
return return
} }
if !requireScreenAccess(w, r, screen) { if !requireScreenAccess(w, r, screen, screens) {
return return
} }

View file

@ -18,7 +18,7 @@ func HandleUpdateSchedule(screens *store.ScreenStore, schedules *store.ScreenSch
http.Error(w, "screen not found", http.StatusNotFound) http.Error(w, "screen not found", http.StatusNotFound)
return return
} }
if !requireScreenAccess(w, r, screen) { if !requireScreenAccess(w, r, screen, screens) {
return return
} }

View file

@ -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. // 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. // 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()) u := reqcontext.UserFromContext(r.Context())
if u == nil { if u == nil {
http.Error(w, "Forbidden", http.StatusForbidden) 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" { if u.Role == "admin" {
return true 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 { if u.TenantID != "" && u.TenantID != screen.TenantID {
http.Error(w, "Forbidden", http.StatusForbidden) http.Error(w, "Forbidden", http.StatusForbidden)
return false 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 return true
} }
@ -370,7 +370,7 @@ func HandleManageUI(
notifier.RequestScreenshot(screen.Slug) notifier.RequestScreenshot(screen.Slug)
// K2: Tenant-Isolation — nur eigener Tenant oder Admin. // K2: Tenant-Isolation — nur eigener Tenant oder Admin.
if !requireScreenAccess(w, r, screen) { if !requireScreenAccess(w, r, screen, screens) {
return return
} }
@ -607,7 +607,7 @@ func HandleUploadMediaUI(media *store.MediaStore, screens *store.ScreenStore, up
} }
// K2: Tenant-Isolation. // K2: Tenant-Isolation.
if !requireScreenAccess(w, r, screen) { if !requireScreenAccess(w, r, screen, screens) {
return return
} }
@ -694,7 +694,7 @@ func HandleAddItemUI(playlists *store.PlaylistStore, media *store.MediaStore, sc
} }
// K2: Tenant-Isolation. // K2: Tenant-Isolation.
if !requireScreenAccess(w, r, screen) { if !requireScreenAccess(w, r, screen, screens) {
return return
} }
@ -760,7 +760,7 @@ func HandleDeleteItemUI(playlists *store.PlaylistStore, screens *store.ScreenSto
http.Error(w, "screen nicht gefunden", http.StatusNotFound) http.Error(w, "screen nicht gefunden", http.StatusNotFound)
return return
} }
if !requireScreenAccess(w, r, screen) { if !requireScreenAccess(w, r, screen, screens) {
return return
} }
@ -783,7 +783,7 @@ func HandleReorderUI(playlists *store.PlaylistStore, screens *store.ScreenStore,
return return
} }
// K2: Tenant-Isolation. // K2: Tenant-Isolation.
if !requireScreenAccess(w, r, screen) { if !requireScreenAccess(w, r, screen, screens) {
return return
} }
playlist, err := playlists.GetByScreen(r.Context(), screen.ID) 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) http.Error(w, "screen nicht gefunden", http.StatusNotFound)
return return
} }
if !requireScreenAccess(w, r, screen) { if !requireScreenAccess(w, r, screen, screens) {
return return
} }
@ -864,7 +864,7 @@ func HandleDeleteMediaUI(media *store.MediaStore, screens *store.ScreenStore, up
http.Error(w, "screen nicht gefunden", http.StatusNotFound) http.Error(w, "screen nicht gefunden", http.StatusNotFound)
return return
} }
if !requireScreenAccess(w, r, screen) { if !requireScreenAccess(w, r, screen, screens) {
return return
} }