From 2e550c3d35a7e8eea442ce07dd84e99d32db71f8 Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Tue, 26 May 2026 19:34:16 -0400 Subject: [PATCH] fix(auth): tighten curator job response review fixes --- internal/api/handlers/admin_jobs.go | 1 + internal/api/handlers/admin_jobs_test.go | 6 +++++- internal/api/handlers/admin_test.go | 11 +++++++++++ internal/api/middleware/permissions.go | 3 +++ internal/api/router.go | 2 ++ 5 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 internal/api/handlers/admin_test.go diff --git a/internal/api/handlers/admin_jobs.go b/internal/api/handlers/admin_jobs.go index e1cffb19..20ae2ea6 100644 --- a/internal/api/handlers/admin_jobs.go +++ b/internal/api/handlers/admin_jobs.go @@ -171,6 +171,7 @@ func sanitizeAdminJobResponseForClaims(response *adminJobResponse, claims *auth. } response.RequestPayload = json.RawMessage(`{}`) response.ResultPayload = sanitizeNonAdminAdminJobResultPayload(response.JobType, response.ResultPayload) + response.ErrorMessage = "" response.PublicURL = "" response.DownloadURL = "" response.DownloadExpiresAt = nil diff --git a/internal/api/handlers/admin_jobs_test.go b/internal/api/handlers/admin_jobs_test.go index c64eda56..d7dd5ec1 100644 --- a/internal/api/handlers/admin_jobs_test.go +++ b/internal/api/handlers/admin_jobs_test.go @@ -53,7 +53,8 @@ func TestAdminJobToResponseForClaims_NonAdminSanitizesItemRefreshPayloads(t *tes ResultPayload: json.RawMessage( `{"requested_content_id":"item-1","detail_content_id":"item-2","scan_path":"/srv/media/private/movie","scan_result":{"New":1,"RootObservations":[{"RootPath":"/srv/media/private","SampleFilePath":"/srv/media/private/movie.mkv"}]}}`, ), - PublicURL: "https://example.test/public", + ErrorMessage: "scan scope: stat /srv/media/private/movie: permission denied", + PublicURL: "https://example.test/public", } resp := adminJobToResponseForClaims(nil, job, nil, claims) @@ -74,4 +75,7 @@ func TestAdminJobToResponseForClaims_NonAdminSanitizesItemRefreshPayloads(t *tes !bytes.Contains(resp.ResultPayload, []byte("detail_content_id")) { t.Fatalf("ResultPayload = %s, want safe item refresh summary fields", resp.ResultPayload) } + if resp.ErrorMessage != "" { + t.Fatalf("ErrorMessage = %q, want stripped for non-admin", resp.ErrorMessage) + } } diff --git a/internal/api/handlers/admin_test.go b/internal/api/handlers/admin_test.go new file mode 100644 index 00000000..57240e77 --- /dev/null +++ b/internal/api/handlers/admin_test.go @@ -0,0 +1,11 @@ +package handlers + +import "testing" + +func TestUpdateRequiresSessionRevocation_ForPermissions(t *testing.T) { + if !updateRequiresSessionRevocation(updateUserRequest{ + Permissions: updateStringSliceField{Set: true, Value: []string{"metadata_curation"}}, + }) { + t.Fatal("permission updates should revoke sessions") + } +} diff --git a/internal/api/middleware/permissions.go b/internal/api/middleware/permissions.go index 9ce18f1b..aa7059d6 100644 --- a/internal/api/middleware/permissions.go +++ b/internal/api/middleware/permissions.go @@ -30,6 +30,9 @@ func NewPermissionMiddleware(users PermissionUserLoader, libraries MetadataTarge return &PermissionMiddleware{users: users, libraries: libraries} } +// RequireMetadataCurationForItem allows admins or users with metadata_curation +// permission when every library containing the target item is within the user's +// assigned libraries. A nil user library list means unrestricted library access. func (m *PermissionMiddleware) RequireMetadataCurationForItem(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { claims := GetClaims(r.Context()) diff --git a/internal/api/router.go b/internal/api/router.go index fb36f2f2..75e38dfa 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -1678,6 +1678,8 @@ func NewRouter(deps Dependencies) chi.Router { }) if adminJobsHandler != nil { + // Curators must poll their own item-refresh jobs, so this stays outside + // the admin-only group. HandleGet enforces per-job authorization. r.Get("/jobs/{id}", adminJobsHandler.HandleGet) }