feat(manage): canDeleteMedia + role-aware handlers für restricted users

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jesko Anschütz 2026-03-28 09:07:13 +01:00
parent 52f503d462
commit 865c5e7ca8
2 changed files with 91 additions and 9 deletions

View file

@ -15,6 +15,7 @@ import (
const maxUploadSize = 512 << 20 // 512 MB const maxUploadSize = 512 << 20 // 512 MB
// HandleListMedia returns all media assets for a tenant as JSON. // HandleListMedia returns all media assets for a tenant as JSON.
// restricted-Users sehen nur ihre eigenen Medien.
func HandleListMedia(tenants *store.TenantStore, media *store.MediaStore) http.HandlerFunc { func HandleListMedia(tenants *store.TenantStore, media *store.MediaStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
tenant, err := tenants.Get(r.Context(), r.PathValue("tenantSlug")) tenant, err := tenants.Get(r.Context(), r.PathValue("tenantSlug"))
@ -22,7 +23,12 @@ func HandleListMedia(tenants *store.TenantStore, media *store.MediaStore) http.H
http.Error(w, "tenant not found", http.StatusNotFound) http.Error(w, "tenant not found", http.StatusNotFound)
return return
} }
assets, err := media.List(r.Context(), tenant.ID) u := reqcontext.UserFromContext(r.Context())
ownerUserID := ""
if u != nil && u.Role == "restricted" {
ownerUserID = u.ID
}
assets, err := media.List(r.Context(), tenant.ID, ownerUserID)
if err != nil { if err != nil {
http.Error(w, "db error", http.StatusInternalServerError) http.Error(w, "db error", http.StatusInternalServerError)
return return
@ -45,6 +51,12 @@ func HandleUploadMedia(tenants *store.TenantStore, media *store.MediaStore, uplo
} }
tenantID := tenant.ID tenantID := tenant.ID
u := reqcontext.UserFromContext(r.Context())
createdByUserID := ""
if u != nil {
createdByUserID = u.ID
}
// W3: MaxBytesReader begrenzt den gesamten Request-Body auf maxUploadSize. // W3: MaxBytesReader begrenzt den gesamten Request-Body auf maxUploadSize.
r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize) r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
if err := r.ParseMultipartForm(maxUploadSize); err != nil { if err := r.ParseMultipartForm(maxUploadSize); err != nil {
@ -65,7 +77,7 @@ func HandleUploadMedia(tenants *store.TenantStore, media *store.MediaStore, uplo
if title == "" { if title == "" {
title = url title = url
} }
asset, err := media.Create(r.Context(), tenantID, title, "web", "", url, "", 0) asset, err := media.Create(r.Context(), tenantID, title, "web", "", url, "", createdByUserID, 0)
if err != nil { if err != nil {
http.Error(w, "db error", http.StatusInternalServerError) http.Error(w, "db error", http.StatusInternalServerError)
return return
@ -98,7 +110,7 @@ func HandleUploadMedia(tenants *store.TenantStore, media *store.MediaStore, uplo
return return
} }
asset, err := media.Create(r.Context(), tenantID, title, assetType, storagePath, "", mimeType, size) asset, err := media.Create(r.Context(), tenantID, title, assetType, storagePath, "", mimeType, createdByUserID, size)
if err != nil { if err != nil {
http.Error(w, "db error", http.StatusInternalServerError) http.Error(w, "db error", http.StatusInternalServerError)
return return
@ -123,13 +135,9 @@ func HandleDeleteMedia(media *store.MediaStore, uploadDir string) http.HandlerFu
return return
} }
// K3: Tenant-Check — nur der eigene Tenant oder Admin darf löschen. // K3: Rolle-bewusste Berechtigungsprüfung.
u := reqcontext.UserFromContext(r.Context()) u := reqcontext.UserFromContext(r.Context())
if u == nil { if u == nil || !canDeleteMedia(u, asset) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
if u.Role != "admin" && u.TenantID != asset.TenantID {
http.Error(w, "Forbidden", http.StatusForbidden) http.Error(w, "Forbidden", http.StatusForbidden)
return return
} }
@ -178,3 +186,20 @@ func sanitize(s string) string {
} }
return out return out
} }
// canDeleteMedia prüft ob User u das Medium asset löschen darf.
// admin: immer erlaubt
// screen_user: Tenant-Match reicht
// restricted: Tenant-Match UND Besitzer-Match
func canDeleteMedia(u *store.User, asset *store.MediaAsset) bool {
if u.Role == "admin" {
return true
}
if u.TenantID != asset.TenantID {
return false
}
if u.Role == "restricted" {
return asset.CreatedByUserID == u.ID
}
return true
}

View file

@ -0,0 +1,57 @@
package manage
import (
"testing"
"git.az-it.net/az/morz-infoboard/server/backend/internal/store"
)
func TestCanDeleteMedia(t *testing.T) {
asset := &store.MediaAsset{TenantID: "t1", CreatedByUserID: "u1"}
tests := []struct {
name string
user *store.User
allowed bool
}{
{
name: "admin darf immer",
user: &store.User{Role: "admin", TenantID: "t2", ID: "x"},
allowed: true,
},
{
name: "screen_user eigener Tenant",
user: &store.User{Role: "screen_user", TenantID: "t1", ID: "x"},
allowed: true,
},
{
name: "screen_user fremder Tenant",
user: &store.User{Role: "screen_user", TenantID: "t2", ID: "x"},
allowed: false,
},
{
name: "restricted eigenes Medium",
user: &store.User{Role: "restricted", TenantID: "t1", ID: "u1"},
allowed: true,
},
{
name: "restricted fremdes Medium gleicher Tenant",
user: &store.User{Role: "restricted", TenantID: "t1", ID: "u2"},
allowed: false,
},
{
name: "restricted fremder Tenant",
user: &store.User{Role: "restricted", TenantID: "t2", ID: "u1"},
allowed: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := canDeleteMedia(tt.user, asset)
if got != tt.allowed {
t.Errorf("canDeleteMedia() = %v, want %v", got, tt.allowed)
}
})
}
}