diff --git a/server/backend/internal/httpapi/manage/media.go b/server/backend/internal/httpapi/manage/media.go index ccda860..d2ef292 100644 --- a/server/backend/internal/httpapi/manage/media.go +++ b/server/backend/internal/httpapi/manage/media.go @@ -15,6 +15,7 @@ import ( const maxUploadSize = 512 << 20 // 512 MB // 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 { return func(w http.ResponseWriter, r *http.Request) { 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) 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 { http.Error(w, "db error", http.StatusInternalServerError) return @@ -45,6 +51,12 @@ func HandleUploadMedia(tenants *store.TenantStore, media *store.MediaStore, uplo } tenantID := tenant.ID + u := reqcontext.UserFromContext(r.Context()) + createdByUserID := "" + if u != nil { + createdByUserID = u.ID + } + // W3: MaxBytesReader begrenzt den gesamten Request-Body auf maxUploadSize. r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize) if err := r.ParseMultipartForm(maxUploadSize); err != nil { @@ -65,7 +77,7 @@ func HandleUploadMedia(tenants *store.TenantStore, media *store.MediaStore, uplo if title == "" { 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 { http.Error(w, "db error", http.StatusInternalServerError) return @@ -98,7 +110,7 @@ func HandleUploadMedia(tenants *store.TenantStore, media *store.MediaStore, uplo 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 { http.Error(w, "db error", http.StatusInternalServerError) return @@ -123,13 +135,9 @@ func HandleDeleteMedia(media *store.MediaStore, uploadDir string) http.HandlerFu return } - // K3: Tenant-Check — nur der eigene Tenant oder Admin darf löschen. + // K3: Rolle-bewusste Berechtigungsprüfung. u := reqcontext.UserFromContext(r.Context()) - if u == nil { - http.Error(w, "Forbidden", http.StatusForbidden) - return - } - if u.Role != "admin" && u.TenantID != asset.TenantID { + if u == nil || !canDeleteMedia(u, asset) { http.Error(w, "Forbidden", http.StatusForbidden) return } @@ -178,3 +186,20 @@ func sanitize(s string) string { } 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 +} diff --git a/server/backend/internal/httpapi/manage/media_test.go b/server/backend/internal/httpapi/manage/media_test.go new file mode 100644 index 0000000..2387220 --- /dev/null +++ b/server/backend/internal/httpapi/manage/media_test.go @@ -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) + } + }) + } +}