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()