Skip to content
This repository was archived by the owner on Apr 16, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions db/migrations/0003_cleanup_indexes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
CREATE INDEX IF NOT EXISTS idx_sessions_state_expires
ON sessions (state, expires_at);

CREATE INDEX IF NOT EXISTS idx_grants_session_id
ON grants (session_id);

CREATE INDEX IF NOT EXISTS idx_grants_state_expires
ON grants (state, expires_at);

CREATE INDEX IF NOT EXISTS idx_approvals_state_expires
ON approvals (state, expires_at);

CREATE INDEX IF NOT EXISTS idx_artifacts_state_expires
ON artifacts (state, expires_at);

CREATE INDEX IF NOT EXISTS idx_audit_events_created_at
ON audit_events (created_at);
6 changes: 5 additions & 1 deletion internal/store/postgres/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,16 @@ func (r *Repository) GetSession(ctx context.Context, sessionID string) (*core.Se
return &session, nil
}

const maxGrantsBySessionLookup = 10000

func (r *Repository) ListGrantsBySession(ctx context.Context, sessionID string) ([]*core.Grant, error) {
rows, err := r.db.Query(ctx, `
SELECT id, tenant_id, session_id, tool, capability, resource_ref, delivery_mode, connector_kind, approval_id, artifact_ref, state, requested_ttl_seconds, effective_ttl_seconds, expires_at, created_at, reason
FROM grants
WHERE session_id = $1
`, sessionID)
ORDER BY created_at ASC, id ASC
LIMIT $2
`, sessionID, maxGrantsBySessionLookup)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent truncation may leave grants unrevoked on session cleanup

Low Severity

ListGrantsBySession now silently caps results at 10,000, but both callers (expireSession and RevokeSession) assume they receive all grants for the session. In expireSession, the session is marked expired even when not all grants were processed, and since the session is no longer active, subsequent cleanup runs won't re-process it — leaving orphaned active grants. In RevokeSession, there's no retry mechanism at all. No error or warning is returned when the limit is reached.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9805c67. Configure here.

if err != nil {
return nil, err
}
Expand Down
38 changes: 38 additions & 0 deletions internal/store/postgres/repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,41 @@ func TestRepository_UseArtifactMarksSingleUse(t *testing.T) {
t.Fatalf("artifact state = %q, want %q", artifact.State, core.ArtifactStateUsed)
}
}

func TestRepository_ListGrantsBySessionIsBounded(t *testing.T) {
t.Parallel()

mock, err := pgxmock.NewPool()
if err != nil {
t.Fatalf("pgxmock.NewPool() error = %v", err)
}
defer mock.Close()

repo := postgres.NewRepository(mock)
createdAt := time.Date(2026, 4, 15, 18, 0, 0, 0, time.UTC)
expiresAt := createdAt.Add(30 * time.Minute)

mock.ExpectQuery("SELECT id, tenant_id, session_id, tool, capability, resource_ref, delivery_mode, connector_kind, approval_id, artifact_ref, state, requested_ttl_seconds, effective_ttl_seconds, expires_at, created_at, reason\\s+FROM grants\\s+WHERE session_id = \\$1\\s+ORDER BY created_at ASC, id ASC\\s+LIMIT \\$2").
WithArgs("sess_abc", pgxmock.AnyArg()).
WillReturnRows(pgxmock.NewRows([]string{
"id", "tenant_id", "session_id", "tool", "capability", "resource_ref", "delivery_mode", "connector_kind", "approval_id", "artifact_ref", "state", "requested_ttl_seconds", "effective_ttl_seconds", "expires_at", "created_at", "reason",
}).AddRow(
"gr_123", "t_acme", "sess_abc", "github", "repo.read", "repo:evalops/asb", "direct", "github", nil, nil,
string(core.GrantStateIssued), int32(300), int32(300), expiresAt, createdAt, "cleanup",
))

grants, err := repo.ListGrantsBySession(context.Background(), "sess_abc")
if err != nil {
t.Fatalf("ListGrantsBySession() error = %v", err)
}
if len(grants) != 1 {
t.Fatalf("len(grants) = %d, want 1", len(grants))
}
if grants[0].ID != "gr_123" {
t.Fatalf("grant id = %q, want %q", grants[0].ID, "gr_123")
}

if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("ExpectationsWereMet() error = %v", err)
}
}
Loading