Skip to content

fix(store): a search value that is not a uuid reports bad input - #1952

Merged
AmanGIT07 merged 3 commits into
mainfrom
fix-search-invalid-uuid-bad-input
Sep 29, 2026
Merged

AmanGIT07 merged 3 commits into
mainfrom
fix-search-invalid-uuid-bad-input

Conversation

@AmanGIT07

@AmanGIT07 AmanGIT07 commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Follows #1942, which fixed this for the organization users search. This applies the same treatment to the rest.

Summary

Every aggregate search binds a caller supplied id to a uuid column. A value the database cannot read as a uuid reached the handler as a raw driver error, so the API answered internal and echoed the database text. It now answers invalid argument.

Changes

  • All eleven aggregate searches run their query error through checkPostgresError and return ErrBadInput for ErrInvalidTextRepresentation, matching the shape already used in the audit repositories. Searches that wrap their error keep their own wording in the default branch.
  • The organizations search handler gains the ErrBadInput branch. The other ten handlers already had it, because repositories already answer ErrBadInput for an unsupported filter or sort.
  • org_users_repository.go moves from the inline if added in fix(store): org users search reports a value that is not a uuid as bad input #1942 to the same switch, so all eleven read alike.
  • New search_invalid_uuid_pg_test.go: one case per search against a real postgres. New organization_billing_test.go: handler cases for the mapping above.

Technical Details

checkPostgresError is not automatic, it is a function each caller invokes. None of these searches called it, so the driver error travelled untouched to the handler and errors.Is(err, ErrInvalidTextRepresentation) was false everywhere.

The mapping sits after the transaction rather than inside its closure, so it does not depend on how the transaction wrapper reports a rollback.

The organizations search takes no id. Its bad value arrives through the id filter, which compares against the organizations primary key.

BillingInvoiceRepository.Search gets the same treatment. Its two errDB wraps used %s, which kept the cause out of the error chain, so nothing could have matched it; they now use %w. It returns invoice.ErrBadInput because that is the sentinel its own handler checks.

UserRepository.Search is left out. Its id filter is cast to text before comparison, so a malformed uuid never reaches a uuid column, and nothing else in that search binds a caller value to one.

SearchOrganizationInvoices and SearchOrganizationProjects answered CodeInvalidArgument with an internal server error message. Both now pass the reason through, like the other nine handlers.

Test Plan

  • go test ./internal/store/postgres/ ./internal/api/v1beta1connect/ passes
  • The eleven new cases fail on main for ten of the eleven searches; organization users already passed there because of fix(store): org users search reports a value that is not a uuid as bad input #1942
  • golangci-lint run on both packages reports no issues
  • End to end against a local server, calling each RPC with a malformed id, run against main and this branch on the same database with only the binary swapped
RPC main this branch
SearchOrganizationProjects 500 internal 400 invalid argument
SearchOrganizationServiceUserCredentials 500 internal 400 invalid argument
SearchProjectUsers 500 internal 400 invalid argument
SearchOrganizations 500 internal 400 invalid argument
SearchOrganizationUsers 400 400
SearchOrganizationPATs 400 400
SearchUserOrganizations 400 400
SearchUserProjects 400 400
SearchOrganizationServiceUsers 404 404
SearchOrganizationInvoices 404 404
SearchOrganizationTokens 404 404

Four RPCs change. Of the rest, organization PATs, user organizations and user projects validate the id as a uuid in proto so the request stops earlier, and organization service users, invoices and tokens resolve the organization before the search and answer not found. Their repositories are still covered, because the same path is reachable through a filter value.

SQL Safety

  • Values flow through ? placeholders, goqu.Ex{}, or goqu.Record{} — never fmt.Sprintf or + building a query that gets executed.
  • ToSQL() callers capture and forward params (query, params, err := stmt.ToSQL(); db.…Context(ctx, …, query, params...)). Never query, _, err := ….
  • No ? placeholders inside single-quoted SQL literals in goqu.L.
  • No new //nolint:forbidigo or // #nosec G20x annotations.

Every aggregate search runs its query error through checkPostgresError and
answers ErrBadInput when the database could not read a value as a uuid.
The organizations search handler gains the branch the others already had.
…d input

One case per search against postgres, plus handler cases for the
organizations search error mapping.
@vercel

vercel Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
frontier Error Error Sep 28, 2026 10:35am UTC

@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: raystack/frontier/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 780cd9fa-1a00-4b8e-a67d-d7a309f2e97e

📥 Commits

Reviewing files that changed from the base of the PR and between e4783a2 and 04fdedd.

📒 Files selected for processing (4)
  • internal/api/v1beta1connect/organization_invoices.go
  • internal/api/v1beta1connect/organization_projects.go
  • internal/store/postgres/billing_invoice_repository.go
  • internal/store/postgres/search_invalid_uuid_pg_test.go

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.


📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Invalid UUIDs in organization, project, user, billing, and related searches now return a clear invalid-input error instead of an internal server error. Other errors continue to be reported as internal errors.
  • Tests
    • Added coverage for invalid UUID handling and for successful and failed organization billing searches.

Walkthrough

Search repositories now map PostgreSQL invalid UUID errors to bad-input errors. Organization billing, invoice, and project Connect search endpoints return InvalidArgument for bad input. Tests cover repository searches and the billing endpoint.

Changes

Invalid UUID search handling

Layer / File(s) Summary
Map invalid UUID errors in searches
internal/store/postgres/*_repository.go, internal/store/postgres/search_invalid_uuid_pg_test.go
Search methods normalize PostgreSQL errors and map invalid text representations to the applicable bad-input sentinel. PostgreSQL-backed tests cover invalid UUID searches across organization, project, user, and billing repositories.
Map search errors to Connect responses
internal/api/v1beta1connect/organization_billing.go, internal/api/v1beta1connect/organization_billing_test.go, internal/api/v1beta1connect/organization_invoices.go, internal/api/v1beta1connect/organization_projects.go
The organization billing, invoice, and project search endpoints return InvalidArgument for bad-input errors. Billing endpoint tests cover bad input, other errors, and success.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Suggested reviewers: whoabhisheksah

Merge Risk: ⚪ Minimal · up to 04fde

The reviewed malformed-UUID search paths return invalid-argument responses; no actionable merge risk is established.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 04fde

Malformed UUID searches now receive a bad-input response instead of a database error. The reviewed paths use a generic validation message, with no identified expansion of data access. Some endpoint controls were not independently verified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The independently triggerable change is the response to malformed search input across the affected search endpoints, not a new route or a broader repository read. Organization-invoice tenant scoping was confirmed for the inspected path, but equivalent controls were not independently traced for every endpoint.

Trust Boundaries and Controls

  • observed — Caller-supplied search input reaches PostgreSQL after query transformation and validation. The inspected repository converts an invalid-text driver error to a generic bad-input error before the Connect handler exposes it; the changed handler branch runs after the existing service call.

Hardening Proposals

  • proposed — Consider masking unexpected database-error text at public internal-error responses as a separate hardening measure. The inspected billing error mapper still returns an unexpected error as the Internal response; this review did not establish that behavior as introduced by the PR.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coveralls

coveralls commented Sep 25, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 36410598822

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.6%) to 52.695%

Details

  • Coverage increased (+0.6%) from the base build.
  • Patch coverage: 19 uncovered changes across 10 files (58 of 77 lines covered, 75.32%).
  • 16 coverage regressions across 2 files.

Uncovered Changes

File Changed Covered %
internal/store/postgres/billing_invoice_repository.go 8 5 62.5%
internal/store/postgres/org_invoices_repository.go 6 4 66.67%
internal/store/postgres/org_pats_repository.go 6 4 66.67%
internal/store/postgres/org_serviceuser_credentials_repository.go 6 4 66.67%
internal/store/postgres/org_serviceuser_repository.go 6 4 66.67%
internal/store/postgres/org_tokens_repository.go 6 4 66.67%
internal/store/postgres/user_orgs_repository.go 6 4 66.67%
internal/store/postgres/user_projects_repository.go 6 4 66.67%
internal/api/v1beta1connect/organization_invoices.go 1 0 0.0%
internal/api/v1beta1connect/organization_projects.go 1 0 0.0%
Total (15 files) 77 58 75.32%

Coverage Regressions

16 previously-covered lines in 2 files lost coverage.

File Lines Losing Coverage Coverage
internal/store/postgres/org_invoices_repository.go 9 91.04%
internal/store/postgres/org_serviceuser_credentials_repository.go 7 91.3%

Coverage Stats

Coverage Status
Relevant Lines: 41228
Covered Lines: 21725
Line Coverage: 52.69%
Coverage Strength: 17.11 hits per line

💛 - Coveralls

@rohilsurana

Copy link
Copy Markdown
Member

Two more Search methods in this package are left out, and the description does not say why.

  • UserRepository.Search (internal/store/postgres/user_repository.go:607), which backs SearchUsers. Its handler at internal/api/v1beta1connect/user.go:648 already has the ErrBadInput branch, so that branch sits there with nothing that can reach it from a bad uuid.
  • BillingInvoiceRepository.Search (internal/store/postgres/billing_invoice_repository.go:344), which wraps every failure as errDB, so its handler still answers internal.

Either give them the same treatment, or add a line to the description saying why they are out of scope. Right now a reader cannot tell the difference between left out on purpose and missed.

Separate note, on a handler this PR now makes reachable. SearchOrganizationInvoices (internal/api/v1beta1connect/organization_invoices.go:33) answers the ErrBadInput branch with connect.NewError(connect.CodeInvalidArgument, ErrInternalServerError). The code says invalid argument but the message says internal server error. Before this PR the org invoices repository never returned ErrBadInput for a bad uuid, so that line was hard to hit. Now it is easy. It should pass err through the way the other nine handlers do.

The billing invoice search gains the same mapping as the others, and its
two wraps use %w so the cause stays in the chain. The org invoices and
org projects handlers pass the reason through instead of answering
invalid argument with an internal server error message.
@AmanGIT07

Copy link
Copy Markdown
Contributor Author

All three taken, and chasing the first two turned up a bug in each.

The invoices handler. Fixed in 04fdedd, and it was two handlers rather than one: SearchOrganizationInvoices and SearchOrganizationProjects both answered CodeInvalidArgument with ErrInternalServerError. Both now pass the reason through like the other nine. prospect.go:143 has the same line, but nothing in this PR makes it reachable, so I left it.

BillingInvoiceRepository.Search. Covered now. Two things had to change. The %s in fmt.Errorf("%w: %s", errDB, err) kept the cause out of the chain entirely, so no errors.Is could ever have matched it whatever the repository returned. That is now %w, and the same slip in List on the line above is fixed too. It also returns invoice.ErrBadInput rather than the postgres one, because that is the sentinel its handler checks.

While testing it I found its org_id filter is broken. addFilter builds billing_invoices.<filter name> for every field, but org_id lives on organizations, so filtering by it answers column billing_invoices.org_id does not exist. The id filter works and is the one the new test uses.

UserRepository.Search. Left out on purpose, and the description now says so. Its id filter is deliberately cast to text:

field = "CAST(" + TABLE_USERS + "." + filter.Name + " AS TEXT)"

so a malformed uuid compares as text and never reaches a uuid column. Nothing else in that search binds a caller value to one. I added the mapping first and then took it out again, because it was unreachable.

That cast is itself broken, though. The string is handed to goqu.I, which splits it on the dot and quotes the pieces, producing "CAST(users"."id AS TEXT)". So any id filter on SearchUsers answers missing FROM-clause entry for table "CAST(users" today, valid uuid or not. Separate from this PR; happy to raise it.

@rohilsurana rohilsurana left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All three points are handled. I checked each one rather than taking the reply at face value.

  • BillingInvoiceRepository.Search returning invoice.ErrBadInput is the right call, not the postgres one. billing/invoice/invoice.go:16 defines it and internal/api/v1beta1connect/billing_invoice.go:175 is the handler that matches it.
  • The %s to %w change is a fix, not a style edit. Lines 181, 205 and 245 in that file already used %w, so 262 and 355 were the two outliers. Callers in billing/invoice/service.go only pass the error through, so nothing downstream changes shape.
  • Leaving UserRepository.Search out is right. addFilter hands that CAST(...) string to goqu.I, which splits it on the dot, so the filter never reaches a uuid column. Worth its own issue, agreed.

Ran TestSearchInvalidUUID against a real postgres at 04fdeddc: all twelve cases pass. Also merged this with #1953 locally; no conflicts, and both docker suites pass together.

The duplicated seven-line block is still copied twelve times now. Not blocking, but a small searchQueryError helper would make the thirteenth search hard to get wrong.

@AmanGIT07

Copy link
Copy Markdown
Contributor Author

Raised both of these as their own tickets, so they do not ride along here.

The invoice filter one covers org_id, org_name and org_title. All three are declared filterable and all three answer column billing_invoices.org_id does not exist, because the filter builder assumes one table while the select aliases those columns out of the join.

The user id filter one covers every operator, since they all go through the same identifier string. A valid uuid fails exactly like a malformed one.

@AmanGIT07
AmanGIT07 merged commit 60ac2de into main Sep 29, 2026
7 of 8 checks passed
@AmanGIT07
AmanGIT07 deleted the fix-search-invalid-uuid-bad-input branch September 29, 2026 05:58

This branch had an error being deployed

1 failed deployment
Preview — 04fdeddc Deployed Sep 28, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants