You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by la...@apache.org on 2026/10/12 18:53:25 UTC

(airavata-custos) branch master updated: clamp list paging at the HTTP edge (#626)

This is an automated email from the ASF dual-hosted git repository.

lahirujayathilake pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git


The following commit(s) were added to refs/heads/master by this push:
     new 5cf28e3ff clamp list paging at the HTTP edge (#626)
5cf28e3ff is described below

commit 5cf28e3ff46338c3f5cec5ea99fbbad0b6d8cd8f
Author: Yasith Jayawardana <ya...@icloud.com>
AuthorDate: Mon Oct 12 14:53:19 2026 -0400

    clamp list paging at the HTTP edge (#626)
---
 .../ACCESS/AMIE-Processor/server/handlers.go       | 43 +++++-----------------
 .../ACCESS/AMIE-Processor/store/packet_store.go    | 10 +----
 internal/server/allocation.go                      |  5 ++-
 internal/server/allocation_change_request.go       |  3 +-
 internal/server/compute_cluster.go                 |  3 +-
 internal/server/list_endpoints_integration_test.go | 10 +++++
 internal/server/organization.go                    |  4 +-
 internal/server/project.go                         |  5 ++-
 internal/server/user.go                            |  4 +-
 .../compute_allocation_change_request_store.go     | 11 +-----
 internal/store/compute_allocation_store.go         | 13 +------
 internal/store/organization_store.go               |  9 -----
 internal/store/project_store.go                    | 13 +------
 internal/store/user_store.go                       |  9 -----
 pkg/common/support.go                              | 12 ++++++
 15 files changed, 51 insertions(+), 103 deletions(-)

diff --git a/connectors/ACCESS/AMIE-Processor/server/handlers.go b/connectors/ACCESS/AMIE-Processor/server/handlers.go
index 023dc062d..672114730 100644
--- a/connectors/ACCESS/AMIE-Processor/server/handlers.go
+++ b/connectors/ACCESS/AMIE-Processor/server/handlers.go
@@ -103,7 +103,7 @@ func (h *Handlers) listPacketAudits(w http.ResponseWriter, r *http.Request) {
 // @Router	/connectors/amie/packets [get]
 func (h *Handlers) listPackets(w http.ResponseWriter, r *http.Request) {
 	if h.packets == nil {
-		common.WriteJSON(w, http.StatusOK, emptyPacketPage(parseLimit(r), parseOffset(r)))
+		common.WriteJSON(w, http.StatusOK, emptyPacketPage(common.Paged(r.URL.Query())))
 		return
 	}
 	f, err := parsePacketFilter(r)
@@ -123,7 +123,7 @@ func (h *Handlers) listPackets(w http.ResponseWriter, r *http.Request) {
 	common.WriteJSON(w, http.StatusOK, PacketListResponse{
 		Packets: items,
 		Total:   total,
-		Limit:   effectiveLimit(f.Limit),
+		Limit:   f.Limit,
 		Offset:  f.Offset,
 	})
 }
@@ -218,11 +218,12 @@ type ReplyListResponse struct {
 // @Success	200	{object}	ReplyListResponse
 // @Router	/connectors/amie/replies [get]
 func (h *Handlers) listReplies(w http.ResponseWriter, r *http.Request) {
+	limit, offset := common.Paged(r.URL.Query())
 	common.WriteJSON(w, http.StatusOK, ReplyListResponse{
 		Replies: []any{},
 		Total:   0,
-		Limit:   effectiveLimit(parseLimit(r)),
-		Offset:  parseOffset(r),
+		Limit:   limit,
+		Offset:  offset,
 	})
 }
 
@@ -235,7 +236,7 @@ func (h *Handlers) listReplies(w http.ResponseWriter, r *http.Request) {
 // @Success	200	{object}	PacketListResponse
 // @Router	/connectors/amie/unmapped [get]
 func (h *Handlers) listUnmapped(w http.ResponseWriter, r *http.Request) {
-	common.WriteJSON(w, http.StatusOK, emptyPacketPage(parseLimit(r), parseOffset(r)))
+	common.WriteJSON(w, http.StatusOK, emptyPacketPage(common.Paged(r.URL.Query())))
 }
 
 // @Summary	Retry an AMIE packet (not yet implemented)
@@ -293,19 +294,20 @@ func emptyPacketPage(limit, offset int) PacketListResponse {
 	return PacketListResponse{
 		Packets: []PacketResponse{},
 		Total:   0,
-		Limit:   effectiveLimit(limit),
+		Limit:   limit,
 		Offset:  offset,
 	}
 }
 
 func parsePacketFilter(r *http.Request) (store.PacketListFilter, error) {
 	q := r.URL.Query()
+	limit, offset := common.Paged(q)
 	f := store.PacketListFilter{
 		Status: q.Get("status"),
 		Type:   q.Get("type"),
 		Query:  q.Get("q"),
-		Limit:  parseLimit(r),
-		Offset: parseOffset(r),
+		Limit:  limit,
+		Offset: offset,
 	}
 	if v := q.Get("from"); v != "" {
 		t, err := time.Parse(time.RFC3339Nano, v)
@@ -324,31 +326,6 @@ func parsePacketFilter(r *http.Request) (store.PacketListFilter, error) {
 	return f, nil
 }
 
-func parseLimit(r *http.Request) int {
-	if v := r.URL.Query().Get("limit"); v != "" {
-		if n, err := strconv.Atoi(v); err == nil && n >= 0 {
-			return n
-		}
-	}
-	return 0
-}
-
-func parseOffset(r *http.Request) int {
-	if v := r.URL.Query().Get("offset"); v != "" {
-		if n, err := strconv.Atoi(v); err == nil && n >= 0 {
-			return n
-		}
-	}
-	return 0
-}
-
-func effectiveLimit(l int) int {
-	if l <= 0 || l > 200 {
-		return 50
-	}
-	return l
-}
-
 func parseWindow(v string) time.Duration {
 	v = strings.TrimSpace(v)
 	if v == "" {
diff --git a/connectors/ACCESS/AMIE-Processor/store/packet_store.go b/connectors/ACCESS/AMIE-Processor/store/packet_store.go
index de579b64b..3bfa7a094 100644
--- a/connectors/ACCESS/AMIE-Processor/store/packet_store.go
+++ b/connectors/ACCESS/AMIE-Processor/store/packet_store.go
@@ -135,16 +135,8 @@ func (s *pgPacketStore) ListPackets(ctx context.Context, f PacketListFilter) ([]
 		return nil, 0, err
 	}
 
-	limit := f.Limit
-	if limit <= 0 || limit > 200 {
-		limit = 50
-	}
-	offset := f.Offset
-	if offset < 0 {
-		offset = 0
-	}
 	q := fmt.Sprintf(`SELECT %s FROM amie_packets%s ORDER BY received_at DESC LIMIT %d OFFSET %d`,
-		packetColumns, where, limit, offset)
+		packetColumns, where, f.Limit, f.Offset)
 
 	var rows []model.Packet
 	if err := s.db.SelectContext(ctx, &rows, s.db.Rebind(q), args...); err != nil {
diff --git a/internal/server/allocation.go b/internal/server/allocation.go
index 69c0053d9..e5e680ed0 100644
--- a/internal/server/allocation.go
+++ b/internal/server/allocation.go
@@ -190,12 +190,13 @@ func (s *Server) listComputeAllocations(w http.ResponseWriter, r *http.Request)
 		return
 	}
 	q := r.URL.Query()
+	limit, offset := common.Paged(q)
 	f := store.AllocationListFilter{
 		ProjectID: q.Get("project_id"),
 		Status:    q.Get("status"),
 		Query:     q.Get("q"),
-		Limit:     atoiOr(q.Get("limit"), 50),
-		Offset:    atoiOr(q.Get("offset"), 0),
+		Limit:     limit,
+		Offset:    offset,
 	}
 	rows, total, err := s.svc.ListComputeAllocations(r.Context(), f)
 	if err != nil {
diff --git a/internal/server/allocation_change_request.go b/internal/server/allocation_change_request.go
index 9267ffad4..0648b7490 100644
--- a/internal/server/allocation_change_request.go
+++ b/internal/server/allocation_change_request.go
@@ -231,9 +231,10 @@ func (s *Server) getLatestEventForChangeRequest(w http.ResponseWriter, r *http.R
 
 func (s *Server) listChangeRequests(w http.ResponseWriter, r *http.Request) {
 	q := r.URL.Query()
+	limit, _ := common.Paged(q)
 	f := store.ChangeRequestListFilter{
 		Status: q.Get("status"),
-		Limit:  atoiOr(q.Get("limit"), 50),
+		Limit:  limit,
 	}
 	rows, err := s.svc.ListChangeRequests(r.Context(), f)
 	if err != nil {
diff --git a/internal/server/compute_cluster.go b/internal/server/compute_cluster.go
index ffbe42dc2..b14b7d83e 100644
--- a/internal/server/compute_cluster.go
+++ b/internal/server/compute_cluster.go
@@ -202,7 +202,8 @@ func (s *Server) deleteComputeClusterUser(w http.ResponseWriter, r *http.Request
 func (s *Server) listClusterAccounts(w http.ResponseWriter, r *http.Request) {
 	q := r.URL.Query()
 	status := models.ClusterAccountApproval(q.Get("approval_status"))
-	rows, total, err := s.svc.ListComputeClusterUsersByApproval(r.Context(), status, atoiOr(q.Get("limit"), 50), atoiOr(q.Get("offset"), 0))
+	limit, offset := common.Paged(q)
+	rows, total, err := s.svc.ListComputeClusterUsersByApproval(r.Context(), status, limit, offset)
 	if err != nil {
 		common.WriteServiceError(w, err)
 		return
diff --git a/internal/server/list_endpoints_integration_test.go b/internal/server/list_endpoints_integration_test.go
index 55b5f5f31..f987b945d 100644
--- a/internal/server/list_endpoints_integration_test.go
+++ b/internal/server/list_endpoints_integration_test.go
@@ -252,3 +252,13 @@ func TestUpdateUser_RequiresPrivilege(t *testing.T) {
 		t.Fatalf("status: got %d, want 403", rr.Code)
 	}
 }
+
+func TestListClusterAccounts_NegativePagingClamps(t *testing.T) {
+	_, _, srv := setupTestStack(t)
+	rr := httptest.NewRecorder()
+	req := withTestCaller(httptest.NewRequest(http.MethodGet, "/compute-cluster-users?approval_status=PENDING&limit=-1&offset=-1", nil), "u-1", models.ClustersRead)
+	srv.ServeHTTP(rr, req)
+	if rr.Code != http.StatusOK {
+		t.Fatalf("status: got %d, want 200", rr.Code)
+	}
+}
diff --git a/internal/server/organization.go b/internal/server/organization.go
index 8a83c29c3..35070f678 100644
--- a/internal/server/organization.go
+++ b/internal/server/organization.go
@@ -56,8 +56,8 @@ func (s *Server) createOrganization(w http.ResponseWriter, r *http.Request) {
 // @Success	200	{object}	OrganizationListResponse
 // @Router	/organizations [get]
 func (s *Server) listOrganizations(w http.ResponseWriter, r *http.Request) {
-	q := r.URL.Query()
-	rows, total, err := s.svc.ListOrganizations(r.Context(), atoiOr(q.Get("limit"), 50), atoiOr(q.Get("offset"), 0))
+	limit, offset := common.Paged(r.URL.Query())
+	rows, total, err := s.svc.ListOrganizations(r.Context(), limit, offset)
 	if err != nil {
 		common.WriteServiceError(w, err)
 		return
diff --git a/internal/server/project.go b/internal/server/project.go
index 8d3be49c7..b3defe3c4 100644
--- a/internal/server/project.go
+++ b/internal/server/project.go
@@ -187,12 +187,13 @@ func (s *Server) listProjects(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	q := r.URL.Query()
+	limit, offset := common.Paged(q)
 	f := store.ProjectListFilter{
 		PIID:   q.Get("pi_id"),
 		Status: q.Get("status"),
 		Query:  q.Get("q"),
-		Limit:  atoiOr(q.Get("limit"), 50),
-		Offset: atoiOr(q.Get("offset"), 0),
+		Limit:  limit,
+		Offset: offset,
 	}
 	rows, total, err := s.svc.ListProjectsWithPI(r.Context(), f)
 	if err != nil {
diff --git a/internal/server/user.go b/internal/server/user.go
index b52afcb5b..a9238b00f 100644
--- a/internal/server/user.go
+++ b/internal/server/user.go
@@ -86,8 +86,8 @@ func (s *Server) createUser(w http.ResponseWriter, r *http.Request) {
 // @Success	200	{object}	UserListResponse
 // @Router	/users [get]
 func (s *Server) listUsers(w http.ResponseWriter, r *http.Request) {
-	q := r.URL.Query()
-	rows, total, err := s.svc.ListUsers(r.Context(), atoiOr(q.Get("limit"), 50), atoiOr(q.Get("offset"), 0))
+	limit, offset := common.Paged(r.URL.Query())
+	rows, total, err := s.svc.ListUsers(r.Context(), limit, offset)
 	if err != nil {
 		common.WriteServiceError(w, err)
 		return
diff --git a/internal/store/compute_allocation_change_request_store.go b/internal/store/compute_allocation_change_request_store.go
index d8cbbe876..da574dd50 100644
--- a/internal/store/compute_allocation_change_request_store.go
+++ b/internal/store/compute_allocation_change_request_store.go
@@ -109,25 +109,18 @@ func (s *pgComputeAllocationChangeRequestStore) Delete(ctx context.Context, tx *
 }
 
 func (s *pgComputeAllocationChangeRequestStore) List(ctx context.Context, f ChangeRequestListFilter) ([]models.ComputeAllocationChangeRequest, error) {
-	limit := f.Limit
-	if limit <= 0 {
-		limit = 50
-	}
-	if limit > 200 {
-		limit = 200
-	}
 	if f.Status != "" {
 		var rows []models.ComputeAllocationChangeRequest
 		err := s.db.SelectContext(ctx, &rows,
 			`SELECT `+computeAllocationChangeRequestColumns+
 				` FROM compute_allocation_change_requests WHERE change_status = $1`+
-				` ORDER BY timestamp DESC LIMIT $2`, f.Status, limit)
+				` ORDER BY timestamp DESC LIMIT $2`, f.Status, f.Limit)
 		return rows, err
 	}
 	var rows []models.ComputeAllocationChangeRequest
 	err := s.db.SelectContext(ctx, &rows,
 		`SELECT `+computeAllocationChangeRequestColumns+
 			` FROM compute_allocation_change_requests`+
-			` ORDER BY timestamp DESC LIMIT $1`, limit)
+			` ORDER BY timestamp DESC LIMIT $1`, f.Limit)
 	return rows, err
 }
diff --git a/internal/store/compute_allocation_store.go b/internal/store/compute_allocation_store.go
index 3f6c861d1..f2ee2ac7a 100644
--- a/internal/store/compute_allocation_store.go
+++ b/internal/store/compute_allocation_store.go
@@ -152,20 +152,9 @@ func (s *pgComputeAllocationStore) List(ctx context.Context, f AllocationListFil
 	if err := s.db.GetContext(ctx, &total, s.db.Rebind(`SELECT COUNT(*) FROM compute_allocations`+clause), args...); err != nil {
 		return nil, 0, err
 	}
-	limit := f.Limit
-	if limit <= 0 {
-		limit = 50
-	}
-	if limit > 200 {
-		limit = 200
-	}
-	offset := f.Offset
-	if offset < 0 {
-		offset = 0
-	}
 	query := `SELECT ` + computeAllocationColumns + ` FROM compute_allocations` + clause +
 		` ORDER BY start_time DESC LIMIT ? OFFSET ?`
-	args = append(args, limit, offset)
+	args = append(args, f.Limit, f.Offset)
 	var rows []models.ComputeAllocation
 	if err := s.db.SelectContext(ctx, &rows, s.db.Rebind(query), args...); err != nil {
 		return nil, 0, err
diff --git a/internal/store/organization_store.go b/internal/store/organization_store.go
index 753fd1825..e3166b74e 100644
--- a/internal/store/organization_store.go
+++ b/internal/store/organization_store.go
@@ -67,15 +67,6 @@ func (s *pgOrganizationStore) List(ctx context.Context, limit, offset int) ([]mo
 	if err := s.db.GetContext(ctx, &total, `SELECT COUNT(*) FROM organizations`); err != nil {
 		return nil, 0, err
 	}
-	if limit <= 0 {
-		limit = 50
-	}
-	if limit > 200 {
-		limit = 200
-	}
-	if offset < 0 {
-		offset = 0
-	}
 	var rows []models.Organization
 	if err := s.db.SelectContext(ctx, &rows,
 		`SELECT id, originated_id, name FROM organizations ORDER BY name LIMIT $1 OFFSET $2`, limit, offset); err != nil {
diff --git a/internal/store/project_store.go b/internal/store/project_store.go
index df83b43f3..56a1bd47e 100644
--- a/internal/store/project_store.go
+++ b/internal/store/project_store.go
@@ -173,19 +173,8 @@ func (s *pgProjectStore) ListWithPI(ctx context.Context, f ProjectListFilter) ([
 	if err := s.db.GetContext(ctx, &total, s.db.Rebind(`SELECT COUNT(*) FROM projects p`+clause), args...); err != nil {
 		return nil, 0, err
 	}
-	limit := f.Limit
-	if limit <= 0 {
-		limit = 50
-	}
-	if limit > 200 {
-		limit = 200
-	}
-	offset := f.Offset
-	if offset < 0 {
-		offset = 0
-	}
 	query := projectWithPISelect + clause + ` ORDER BY p.created_time DESC LIMIT ? OFFSET ?`
-	args = append(args, limit, offset)
+	args = append(args, f.Limit, f.Offset)
 	var rows []ProjectWithPI
 	if err := s.db.SelectContext(ctx, &rows, s.db.Rebind(query), args...); err != nil {
 		return nil, 0, err
diff --git a/internal/store/user_store.go b/internal/store/user_store.go
index 6d4710b42..566adf4a8 100644
--- a/internal/store/user_store.go
+++ b/internal/store/user_store.go
@@ -57,15 +57,6 @@ func (s *pgUserStore) List(ctx context.Context, limit, offset int) ([]models.Use
 	if err := s.db.GetContext(ctx, &total, `SELECT COUNT(*) FROM users`); err != nil {
 		return nil, 0, err
 	}
-	if limit <= 0 {
-		limit = 50
-	}
-	if limit > 200 {
-		limit = 200
-	}
-	if offset < 0 {
-		offset = 0
-	}
 	var rows []models.User
 	if err := s.db.SelectContext(ctx, &rows,
 		`SELECT `+userColumns+` FROM users ORDER BY email LIMIT $1 OFFSET $2`, limit, offset); err != nil {
diff --git a/pkg/common/support.go b/pkg/common/support.go
index a7404680a..b8f681793 100644
--- a/pkg/common/support.go
+++ b/pkg/common/support.go
@@ -22,7 +22,9 @@ import (
 	"errors"
 	"log/slog"
 	"net/http"
+	"net/url"
 	"reflect"
+	"strconv"
 	"strings"
 
 	"github.com/jackc/pgx/v5/pgconn"
@@ -30,6 +32,16 @@ import (
 	"github.com/apache/airavata-custos/pkg/service"
 )
 
+// Paged reads a page from q: a limit of 1..200 (50 when unset or invalid) and a non-negative offset.
+func Paged(q url.Values) (limit, offset int) {
+	limit, _ = strconv.Atoi(q.Get("limit"))
+	offset, _ = strconv.Atoi(q.Get("offset"))
+	if limit <= 0 {
+		limit = 50
+	}
+	return min(limit, 200), max(offset, 0)
+}
+
 func DecodeJSON(r *http.Request, dst any) error {
 	dec := json.NewDecoder(r.Body)
 	dec.DisallowUnknownFields()