summaryrefslogtreecommitdiff
path: root/internal/service/media.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-30 11:28:21 +0300
committerPaul Buetow <paul@buetow.org>2026-04-30 11:28:21 +0300
commit100a9bb282dd4226368b4e5fac398726c7acf653 (patch)
tree61730d7bc0d10cddd674ba24438dad2a381b88d1 /internal/service/media.go
parentadecf1fd2e55a0605d03f36952846f6336506d37 (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/service/media.go')
-rw-r--r--internal/service/media.go134
1 files changed, 127 insertions, 7 deletions
diff --git a/internal/service/media.go b/internal/service/media.go
index 5cb7772..f950db3 100644
--- a/internal/service/media.go
+++ b/internal/service/media.go
@@ -82,12 +82,9 @@ func (s *mediaService) ListSets(ctx context.Context, userID int64) ([]model.Set,
}
func (s *mediaService) GetMediaDetail(ctx context.Context, mediaID, userID int64) (*MediaDetail, error) {
- media, err := s.store.GetMediaByID(ctx, mediaID)
+ media, err := s.verifyAccess(ctx, mediaID, userID)
if err != nil {
- return nil, fmt.Errorf("get media: %w", err)
- }
- if media == nil {
- return nil, nil
+ return nil, err
}
tags, err := s.store.ListTagsByMedia(ctx, mediaID)
@@ -119,7 +116,26 @@ func (s *mediaService) GetMediaDetail(ctx context.Context, mediaID, userID int64
}, nil
}
-func (s *mediaService) ListMedia(ctx context.Context, filter repository.MediaFilter) ([]model.Media, error) {
+func (s *mediaService) ListMedia(ctx context.Context, userID int64, filter repository.MediaFilter) ([]model.Media, error) {
+ user, err := s.store.GetUserByID(ctx, userID)
+ if err != nil {
+ return nil, fmt.Errorf("get user: %w", err)
+ }
+
+ if user != nil && user.IsAdmin {
+ return s.store.ListMedia(ctx, filter)
+ }
+
+ perms, err := s.store.ListPermissionsByUser(ctx, userID)
+ if err != nil {
+ return nil, fmt.Errorf("list permissions: %w", err)
+ }
+
+ allowed := make([]int64, 0, len(perms))
+ for _, p := range perms {
+ allowed = append(allowed, p.SetID)
+ }
+ filter.AllowedSetIDs = allowed
return s.store.ListMedia(ctx, filter)
}
@@ -165,6 +181,77 @@ func (s *mediaService) verifyAccess(ctx context.Context, mediaID, userID int64)
return nil, ErrForbidden
}
+// verifyModifyAccess checks that the user has access to the media and is an owner or admin.
+func (s *mediaService) verifyModifyAccess(ctx context.Context, mediaID, userID int64) (*model.Media, error) {
+ media, err := s.verifyAccess(ctx, mediaID, userID)
+ if err != nil {
+ return nil, err
+ }
+
+ user, err := s.store.GetUserByID(ctx, userID)
+ if err != nil {
+ return nil, fmt.Errorf("get user: %w", err)
+ }
+ if user != nil && user.IsAdmin {
+ return media, nil
+ }
+
+ perm, err := s.store.GetPermission(ctx, media.SetID, userID)
+ if err != nil {
+ return nil, fmt.Errorf("get permission: %w", err)
+ }
+ if perm != nil && perm.Role == model.RoleOwner {
+ return media, nil
+ }
+
+ set, err := s.store.GetSetByID(ctx, media.SetID)
+ if err != nil {
+ return nil, fmt.Errorf("get set: %w", err)
+ }
+ if set != nil {
+ for _, p := range set.Permissions {
+ if p.UserID == userID && p.Role == model.RoleOwner {
+ return media, nil
+ }
+ }
+ }
+
+ return nil, ErrForbidden
+}
+
+// verifySetModifyAccess checks that the user is an owner or admin for a set.
+func (s *mediaService) verifySetModifyAccess(ctx context.Context, setID, userID int64) error {
+ user, err := s.store.GetUserByID(ctx, userID)
+ if err != nil {
+ return fmt.Errorf("get user: %w", err)
+ }
+ if user != nil && user.IsAdmin {
+ return nil
+ }
+
+ perm, err := s.store.GetPermission(ctx, setID, userID)
+ if err != nil {
+ return fmt.Errorf("get permission: %w", err)
+ }
+ if perm != nil && perm.Role == model.RoleOwner {
+ return nil
+ }
+
+ set, err := s.store.GetSetByID(ctx, setID)
+ if err != nil {
+ return fmt.Errorf("get set: %w", err)
+ }
+ if set != nil {
+ for _, p := range set.Permissions {
+ if p.UserID == userID && p.Role == model.RoleOwner {
+ return nil
+ }
+ }
+ }
+
+ return ErrForbidden
+}
+
func (s *mediaService) StreamMedia(ctx context.Context, mediaID, userID int64) (*FileResult, error) {
media, err := s.verifyAccess(ctx, mediaID, userID)
if err != nil {
@@ -201,18 +288,31 @@ func (s *mediaService) GetThumbnail(ctx context.Context, mediaID, userID int64)
}
func (s *mediaService) RegenerateThumbnail(ctx context.Context, mediaID, userID int64) error {
+ _, err := s.verifyModifyAccess(ctx, mediaID, userID)
+ if err != nil {
+ return err
+ }
return errors.New("not implemented")
}
func (s *mediaService) RegenerateSetCover(ctx context.Context, setID, userID int64) error {
+ if err := s.verifySetModifyAccess(ctx, setID, userID); err != nil {
+ return err
+ }
return errors.New("not implemented")
}
func (s *mediaService) ToggleFavorite(ctx context.Context, userID, mediaID int64) (bool, error) {
+ if _, err := s.verifyAccess(ctx, mediaID, userID); err != nil {
+ return false, err
+ }
return s.store.ToggleFavorite(ctx, userID, mediaID)
}
func (s *mediaService) AssignTag(ctx context.Context, mediaID, userID int64, tagName string) error {
+ if _, err := s.verifyAccess(ctx, mediaID, userID); err != nil {
+ return err
+ }
tag, err := s.store.GetTagByName(ctx, tagName)
if err != nil {
return fmt.Errorf("get tag: %w", err)
@@ -228,6 +328,9 @@ func (s *mediaService) AssignTag(ctx context.Context, mediaID, userID int64, tag
}
func (s *mediaService) RemoveTag(ctx context.Context, mediaID, userID int64, tagName string) error {
+ if _, err := s.verifyAccess(ctx, mediaID, userID); err != nil {
+ return err
+ }
tag, err := s.store.GetTagByName(ctx, tagName)
if err != nil {
return fmt.Errorf("get tag: %w", err)
@@ -239,7 +342,7 @@ func (s *mediaService) RemoveTag(ctx context.Context, mediaID, userID int64, tag
}
func (s *mediaService) SoftDeleteMedia(ctx context.Context, mediaID, userID int64) error {
- _, err := s.verifyAccess(ctx, mediaID, userID)
+ _, err := s.verifyModifyAccess(ctx, mediaID, userID)
if err != nil {
return err
}
@@ -247,6 +350,10 @@ func (s *mediaService) SoftDeleteMedia(ctx context.Context, mediaID, userID int6
}
func (s *mediaService) RestoreMedia(ctx context.Context, mediaID, userID int64) error {
+ _, err := s.verifyModifyAccess(ctx, mediaID, userID)
+ if err != nil {
+ return err
+ }
return s.store.RestoreMedia(ctx, mediaID)
}
@@ -280,6 +387,10 @@ func (s *mediaService) UploadMedia(ctx context.Context, setID, userID int64, fil
return nil, errors.New("set not found")
}
+ if err := s.verifySetModifyAccess(ctx, setID, userID); err != nil {
+ return nil, err
+ }
+
dir := filepath.Clean(filepath.Join(s.mediaRoot, set.RootPath))
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, fmt.Errorf("mkdir: %w", err)
@@ -441,14 +552,23 @@ func (s *mediaService) StreamSharedMedia(ctx context.Context, token string) (*Fi
}
func (s *mediaService) GetNote(ctx context.Context, mediaID, userID int64) (*model.Note, error) {
+ if _, err := s.verifyAccess(ctx, mediaID, userID); err != nil {
+ return nil, err
+ }
return s.store.GetNote(ctx, mediaID, userID)
}
func (s *mediaService) UpsertNote(ctx context.Context, note *model.Note) error {
+ if _, err := s.verifyAccess(ctx, note.MediaID, note.UserID); err != nil {
+ return err
+ }
note.UpdatedAt = s.clock.Now()
return s.store.UpsertNote(ctx, note)
}
func (s *mediaService) DeleteNote(ctx context.Context, mediaID, userID int64) error {
+ if _, err := s.verifyAccess(ctx, mediaID, userID); err != nil {
+ return err
+ }
return s.store.DeleteNote(ctx, mediaID, userID)
}