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:
parent
52f503d462
commit
865c5e7ca8
2 changed files with 91 additions and 9 deletions
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
57
server/backend/internal/httpapi/manage/media_test.go
Normal file
57
server/backend/internal/httpapi/manage/media_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue