From e5438552c63fecb6284e1b179dddae91ede869c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Wed, 21 May 2025 22:19:23 -0400 Subject: [PATCH] fix(transcoding): restrict transcoding operations to admin users (#4096) Signed-off-by: Deluan --- persistence/sql_base_repository.go | 5 ++ persistence/transcoding_repository.go | 12 +++ persistence/transcoding_repository_test.go | 96 ++++++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 persistence/transcoding_repository_test.go diff --git a/persistence/sql_base_repository.go b/persistence/sql_base_repository.go index f8edff0b8..7cc24b6c4 100644 --- a/persistence/sql_base_repository.go +++ b/persistence/sql_base_repository.go @@ -65,6 +65,11 @@ func loggedUser(ctx context.Context) *model.User { } } +func isAdmin(ctx context.Context) bool { + user := loggedUser(ctx) + return user.IsAdmin +} + func (r *sqlRepository) registerModel(instance any, filters map[string]filterFunc) { if r.tableName == "" { r.tableName = strings.TrimPrefix(reflect.TypeOf(instance).String(), "*model.") diff --git a/persistence/transcoding_repository.go b/persistence/transcoding_repository.go index 9f8998b80..bdcbe7262 100644 --- a/persistence/transcoding_repository.go +++ b/persistence/transcoding_repository.go @@ -41,6 +41,9 @@ func (r *transcodingRepository) FindByFormat(format string) (*model.Transcoding, } func (r *transcodingRepository) Put(t *model.Transcoding) error { + if !isAdmin(r.ctx) { + return rest.ErrPermissionDenied + } _, err := r.put(t.ID, t) return err } @@ -69,6 +72,9 @@ func (r *transcodingRepository) NewInstance() interface{} { } func (r *transcodingRepository) Save(entity interface{}) (string, error) { + if !isAdmin(r.ctx) { + return "", rest.ErrPermissionDenied + } t := entity.(*model.Transcoding) id, err := r.put(t.ID, t) if errors.Is(err, model.ErrNotFound) { @@ -78,6 +84,9 @@ func (r *transcodingRepository) Save(entity interface{}) (string, error) { } func (r *transcodingRepository) Update(id string, entity interface{}, cols ...string) error { + if !isAdmin(r.ctx) { + return rest.ErrPermissionDenied + } t := entity.(*model.Transcoding) t.ID = id _, err := r.put(id, t) @@ -88,6 +97,9 @@ func (r *transcodingRepository) Update(id string, entity interface{}, cols ...st } func (r *transcodingRepository) Delete(id string) error { + if !isAdmin(r.ctx) { + return rest.ErrPermissionDenied + } err := r.delete(Eq{"id": id}) if errors.Is(err, model.ErrNotFound) { return rest.ErrNotFound diff --git a/persistence/transcoding_repository_test.go b/persistence/transcoding_repository_test.go new file mode 100644 index 000000000..eddc5047a --- /dev/null +++ b/persistence/transcoding_repository_test.go @@ -0,0 +1,96 @@ +package persistence + +import ( + "github.com/deluan/rest" + "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/model/request" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("TranscodingRepository", func() { + var repo model.TranscodingRepository + var adminRepo model.TranscodingRepository + + BeforeEach(func() { + ctx := log.NewContext(GinkgoT().Context()) + ctx = request.WithUser(ctx, regularUser) + repo = NewTranscodingRepository(ctx, GetDBXBuilder()) + + adminCtx := log.NewContext(GinkgoT().Context()) + adminCtx = request.WithUser(adminCtx, adminUser) + adminRepo = NewTranscodingRepository(adminCtx, GetDBXBuilder()) + }) + + AfterEach(func() { + // Clean up any transcoding created during the tests + tc, err := adminRepo.FindByFormat("test_format") + if err == nil { + err = adminRepo.(*transcodingRepository).Delete(tc.ID) + Expect(err).ToNot(HaveOccurred()) + } + }) + + Describe("Admin User", func() { + It("creates a new transcoding", func() { + base, err := adminRepo.CountAll() + Expect(err).ToNot(HaveOccurred()) + + err = adminRepo.Put(&model.Transcoding{ID: "new", Name: "new", TargetFormat: "test_format", DefaultBitRate: 320, Command: "ffmpeg"}) + Expect(err).ToNot(HaveOccurred()) + + count, err := adminRepo.CountAll() + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(base + 1)) + }) + + It("updates an existing transcoding", func() { + tr := &model.Transcoding{ID: "upd", Name: "old", TargetFormat: "test_format", DefaultBitRate: 100, Command: "ffmpeg"} + Expect(adminRepo.Put(tr)).To(Succeed()) + tr.Name = "updated" + err := adminRepo.Put(tr) + Expect(err).ToNot(HaveOccurred()) + res, err := adminRepo.FindByFormat("test_format") + Expect(err).ToNot(HaveOccurred()) + Expect(res.Name).To(Equal("updated")) + }) + + It("deletes a transcoding", func() { + err := adminRepo.Put(&model.Transcoding{ID: "to-delete", Name: "temp", TargetFormat: "test_format", DefaultBitRate: 256, Command: "ffmpeg"}) + Expect(err).ToNot(HaveOccurred()) + err = adminRepo.(*transcodingRepository).Delete("to-delete") + Expect(err).ToNot(HaveOccurred()) + _, err = adminRepo.Get("to-delete") + Expect(err).To(MatchError(model.ErrNotFound)) + }) + }) + + Describe("Regular User", func() { + It("fails to create", func() { + err := repo.Put(&model.Transcoding{ID: "bad", Name: "bad", TargetFormat: "test_format", DefaultBitRate: 64, Command: "ffmpeg"}) + Expect(err).To(Equal(rest.ErrPermissionDenied)) + }) + + It("fails to update", func() { + tr := &model.Transcoding{ID: "updreg", Name: "old", TargetFormat: "test_format", DefaultBitRate: 64, Command: "ffmpeg"} + Expect(adminRepo.Put(tr)).To(Succeed()) + + tr.Name = "bad" + err := repo.Put(tr) + Expect(err).To(Equal(rest.ErrPermissionDenied)) + + //_ = adminRepo.(*transcodingRepository).Delete("updreg") + }) + + It("fails to delete", func() { + tr := &model.Transcoding{ID: "delreg", Name: "temp", TargetFormat: "test_format", DefaultBitRate: 64, Command: "ffmpeg"} + Expect(adminRepo.Put(tr)).To(Succeed()) + + err := repo.(*transcodingRepository).Delete("delreg") + Expect(err).To(Equal(rest.ErrPermissionDenied)) + + //_ = adminRepo.(*transcodingRepository).Delete("delreg") + }) + }) +})