mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Batch Edit: Disable API endpoint if feature is disabled #271
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user