Batch Edit: Disable API endpoint if feature is disabled #271

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-11-19 14:30:02 +01:00
parent b67efd9cd3
commit ea6f98fc3e
4 changed files with 32 additions and 6 deletions

View File

@@ -11,6 +11,7 @@ import (
"github.com/photoprism/photoprism/internal/entity/query"
"github.com/photoprism/photoprism/internal/entity/search"
"github.com/photoprism/photoprism/internal/photoprism/batch"
"github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/i18n"
)
@@ -41,6 +42,12 @@ func BatchPhotosEdit(router *gin.RouterGroup) {
return
}
// Check feature flags.
if !get.Config().Settings().Features.BatchEdit {
AbortFeatureDisabled(c)
return
}
var frm batch.PhotosRequest
// Assign and validate request form values.

View File

@@ -17,6 +17,24 @@ import (
)
func TestBatchPhotosEdit(t *testing.T) {
t.Run("FeatureDisabled", func(t *testing.T) {
app, router, conf := NewApiTest()
settings := conf.Settings()
orig := settings.Features.BatchEdit
settings.Features.BatchEdit = false
t.Cleanup(func() {
settings.Features.BatchEdit = orig
})
BatchPhotosEdit(router)
photoUIDs := `{"photos": ["pqkm36fjqvset9uy"]}`
resp := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/edit", photoUIDs)
assert.Equal(t, http.StatusForbidden, resp.Code)
assert.Contains(t, resp.Body.String(), i18n.Msg(i18n.ErrFeatureDisabled))
})
t.Run("SuccessNoChange", func(t *testing.T) {
// Create new API test instance.
app, router, _ := NewApiTest()

View File

@@ -1,6 +1,7 @@
package api
import (
"errors"
"fmt"
"net/http"
"strings"
@@ -39,28 +40,27 @@ func findFileMarker(c *gin.Context) (file *entity.File, marker *entity.Marker, e
// Check authorization.
s := Auth(c, acl.ResourceFiles, acl.ActionUpdate)
if s.Invalid() {
AbortForbidden(c)
return nil, nil, fmt.Errorf("unauthorized")
if s.Abort(c) {
return nil, nil, errors.New("unauthorized")
}
// Check feature flags.
conf := get.Config()
if !conf.Settings().Features.People {
AbortFeatureDisabled(c)
return nil, nil, fmt.Errorf("feature disabled")
return nil, nil, errors.New("feature disabled")
}
// Find marker.
if uid := c.Param("marker_uid"); uid == "" {
AbortBadRequest(c)
return nil, nil, fmt.Errorf("bad request")
return nil, nil, errors.New("bad request")
} else if marker, err = query.MarkerByUID(uid); err != nil {
AbortEntityNotFound(c)
return nil, nil, fmt.Errorf("uid %s %s", uid, err)
} else if marker.FileUID == "" {
AbortEntityNotFound(c)
return nil, marker, fmt.Errorf("marker file missing")
return nil, marker, errors.New("marker file missing")
}
// Find file.

View File

@@ -132,6 +132,7 @@ The SPA consumes the endpoint through a dedicated REST model, dialog component,
#### Feature Flags & Permissions
- Batch edit is controlled via the `Features.BatchEdit` flag exposed in `customize.FeatureSettings`. The flag defaults to `true` alongside `Features.Edit`, but administrators can disable it in settings.
- The `/api/v1/batch/photos/edit` handler returns `ErrFeatureDisabled` (HTTP 403) whenever the flag is off, so automation cannot bypass the UI toggle.
- `Settings.ApplyACL` and `Settings.ApplyScope` only keep `BatchEdit` enabled when the current role can update photos **and** has `acl.AccessAll`; this prevents scoped API clients from invoking bulk edits outside their visibility window.
- The clipboard action (`component/photo/clipboard.vue`) checks the same flag and requires `photos/access_all` before publishing `dialog.batchedit`. If either requirement fails—or the selection only includes a single photo—the component falls back to the single-photo edit dialog so metadata edits remain available.
- Because the clipboard is our only UI entry point, disabling the flag hides the floating button, un-subscribes the dialog, and keeps backend enforcement consistent with the visible capabilities.