feat(sidecar): enforce per-user BasePath on all filesystem mutations

Folder create/rename/delete/move, photo move, heap convert, and file
rename now reject paths outside the caller's BasePath (403). Sources
resolved via PhotoPrism UIDs are re-checked in movePhotoFiles. The
USER_BASEPATHS reconciler also sets upload_path so client-app uploads
land inside the user's subtree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 12:46:00 +02:00
parent 634abc2a95
commit 6cbabda86b
7 changed files with 122 additions and 8 deletions

View File

@@ -90,6 +90,9 @@ func handleHeapConvert(cfg *Config, pp *ppClient) gin.HandlerFunc {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid targetFolder"})
return
}
if !requireUserScope(c, cfg, targetAbs, false) {
return
}
// Pull the heap's membership via the q=album:UID query (count=1000
// covers every realistic heap). We only need the UID list here — the
// search's Files array is trimmed and drops videos, so we re-resolve
@@ -124,7 +127,7 @@ func handleHeapConvert(cfg *Config, pp *ppClient) gin.HandlerFunc {
return
}
moved, copied, errs, err := movePhotoFiles(cfg, pp, token, photos, targetAbs, subfolder, mode)
moved, copied, errs, err := movePhotoFiles(cfg, pp, token, photos, targetAbs, subfolder, mode, userScopeRoot(c, cfg))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
@@ -167,7 +170,10 @@ func handleHeapConvert(cfg *Config, pp *ppClient) gin.HandlerFunc {
// and handlePhotosMove (UID-list scoped); both resolve `photos` differently
// but move them identically. Returns per-photo errors in `errs`; the returned
// top-level error is only for a fatal precondition (subfolder mkdir failed).
func movePhotoFiles(cfg *Config, pp *ppClient, token string, photos []heapPhoto, targetAbs, subfolder, mode string) (moved, copied int, errs []heapErr, err error) {
// `scopeAbs` is the caller's userScopeRoot — source files outside it fail
// per-photo, so a UID that resolves outside the user's BasePath (however
// PhotoPrism came to return it) can't be used to pull files across users.
func movePhotoFiles(cfg *Config, pp *ppClient, token string, photos []heapPhoto, targetAbs, subfolder, mode, scopeAbs string) (moved, copied int, errs []heapErr, err error) {
destAbs := targetAbs
if subfolder != "" {
destAbs = filepath.Join(targetAbs, subfolder)
@@ -234,8 +240,8 @@ func movePhotoFiles(cfg *Config, pp *ppClient, token string, photos []heapPhoto,
for _, f := range group {
srcRel := f.Name
srcAbs := filepath.Join(cfg.OriginalsRoot, srcRel)
if !sameOrUnder(srcAbs, cfg.OriginalsRoot) {
failure = "path escapes originals"
if !sameOrUnder(srcAbs, scopeAbs) {
failure = "path outside your library"
break
}
st, statErr := os.Stat(srcAbs)