diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-30 11:28:21 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-30 11:28:21 +0300 |
| commit | 100a9bb282dd4226368b4e5fac398726c7acf653 (patch) | |
| tree | 61730d7bc0d10cddd674ba24438dad2a381b88d1 /internal/api | |
| parent | adecf1fd2e55a0605d03f36952846f6336506d37 (diff) | |
task da: enforce media access and owner/admin role permissions
Changes:
- MediaService.ListMedia now accepts userID and filters by allowed sets
for non-admins via AllowedSetIDs in repository.MediaFilter.
- Handlers pass userID into ListMedia; API returns 403 for forbidden.
- Added verifyModifyAccess and verifySetModifyAccess helpers so only
owners/admins can upload, soft-delete, restore, and regenerate
thumbnails/covers; viewers are blocked.
- GetMediaDetail, ToggleFavorite, AssignTag, RemoveTag, notes, and
shares now consistently verifyAccess before proceeding.
- Handlers handle ErrForbidden with 403 for soft-delete and restore.
- Added negative tests proving viewers cannot mutate and unauthorized
users cannot access/detail/tag/note/favorite/share inaccessible media.
Diffstat (limited to 'internal/api')
| -rw-r--r-- | internal/api/handlers.go | 18 | ||||
| -rw-r--r-- | internal/api/handlers_test.go | 42 |
2 files changed, 58 insertions, 2 deletions
diff --git a/internal/api/handlers.go b/internal/api/handlers.go index fc1d43d..af6a0a0 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -356,7 +356,7 @@ func (s *Server) handleListMedia(w http.ResponseWriter, r *http.Request) { return } filter := parseMediaListQuery(r.URL.Query()) - media, err := s.mediaSvc.ListMedia(r.Context(), filter) + media, err := s.mediaSvc.ListMedia(r.Context(), userIDFromContext(r), filter) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return @@ -452,6 +452,14 @@ func (s *Server) handleSoftDelete(w http.ResponseWriter, r *http.Request) { return } if err := s.mediaSvc.SoftDeleteMedia(r.Context(), id, userIDFromContext(r)); err != nil { + if errors.Is(err, service.ErrNotFound) { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"}) + return + } + if errors.Is(err, service.ErrForbidden) { + writeJSON(w, http.StatusForbidden, map[string]string{"error": "forbidden"}) + return + } writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } @@ -468,6 +476,14 @@ func (s *Server) handleRestore(w http.ResponseWriter, r *http.Request) { return } if err := s.mediaSvc.RestoreMedia(r.Context(), id, userIDFromContext(r)); err != nil { + if errors.Is(err, service.ErrNotFound) { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"}) + return + } + if errors.Is(err, service.ErrForbidden) { + writeJSON(w, http.StatusForbidden, map[string]string{"error": "forbidden"}) + return + } writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } diff --git a/internal/api/handlers_test.go b/internal/api/handlers_test.go index c853140..798ac81 100644 --- a/internal/api/handlers_test.go +++ b/internal/api/handlers_test.go @@ -587,7 +587,7 @@ func TestServer_MediaList(t *testing.T) { t.Run(tt.name, func(t *testing.T) { var gotFilter repository.MediaFilter ms := &service.MockMediaService{ - ListMediaFunc: func(ctx context.Context, filter repository.MediaFilter) ([]model.Media, error) { + ListMediaFunc: func(ctx context.Context, userID int64, filter repository.MediaFilter) ([]model.Media, error) { gotFilter = filter return tt.listResult, tt.listErr }, @@ -782,6 +782,46 @@ func TestServer_Restore(t *testing.T) { } } +func TestServer_SoftDelete_Forbidden(t *testing.T) { + ms := &service.MockMediaService{ + SoftDeleteMediaFunc: func(ctx context.Context, mediaID, userID int64) error { + return service.ErrForbidden + }, + } + store := buildSessionStore(1) + sm := auth.NewSessionManager(store, &clock.MockClock{T: time.Now()}, time.Hour) + cfg := &internal.Config{SessionTimeoutHours: 24} + srv := newTestServer(t, buildCountStore(1), nil, sm, cfg, ms, nil, nil, nil) + + req := httptest.NewRequest(http.MethodDelete, "/api/media/99", nil) + req.AddCookie(addSessionCookie(t, store, sm, 1)) + rr := httptest.NewRecorder() + srv.ServeHTTP(rr, req) + if rr.Code != http.StatusForbidden { + t.Fatalf("expected %d, got %d", http.StatusForbidden, rr.Code) + } +} + +func TestServer_Restore_Forbidden(t *testing.T) { + ms := &service.MockMediaService{ + RestoreMediaFunc: func(ctx context.Context, mediaID, userID int64) error { + return service.ErrForbidden + }, + } + store := buildSessionStore(1) + sm := auth.NewSessionManager(store, &clock.MockClock{T: time.Now()}, time.Hour) + cfg := &internal.Config{SessionTimeoutHours: 24} + srv := newTestServer(t, buildCountStore(1), nil, sm, cfg, ms, nil, nil, nil) + + req := httptest.NewRequest(http.MethodPost, "/api/media/99/restore", nil) + req.AddCookie(addSessionCookie(t, store, sm, 1)) + rr := httptest.NewRecorder() + srv.ServeHTTP(rr, req) + if rr.Code != http.StatusForbidden { + t.Fatalf("expected %d, got %d", http.StatusForbidden, rr.Code) + } +} + // ------------------------------------------------------------------ // Notes // ------------------------------------------------------------------ |
