Skip to content

fix: support hashed API keys in RLS identity functions#1351

Merged
riderx merged 4 commits intomainfrom
riderx/apikey-hash-analysis
Jan 2, 2026
Merged

fix: support hashed API keys in RLS identity functions#1351
riderx merged 4 commits intomainfrom
riderx/apikey-hash-analysis

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Jan 2, 2026

Summary

Fixed a critical bug where hashed API keys weren't working with RLS policies. The get_identity functions only checked the plain 'key' column, failing when key is NULL and key_hash contains the SHA-256. Updated all identity functions to use find_apikey_by_value() for both plain and hashed key lookups.

Test plan

  • All 49 API key tests pass (28 existing + 21 new)
  • New hashed-apikey-rls.test.ts covers: identity function lookups, expiration, org/app restrictions, and SDK-based access
  • Tested with both plain and hashed keys via direct SQL and Supabase SDK

Checklist

  • My code follows the code style of this project
  • My change has adequate E2E test coverage (21 new tests)
  • I have tested my code manually

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Support for hashed API keys and unified API-key validation across identity checks.
    • More detailed authorization logging for access denials.
  • Bug Fixes / Behavior

    • Identity resolution now respects key expiry and org/app restrictions, reducing false positives.
  • Testing

    • Expanded tests covering hashed and plain API key scenarios via SDK and direct DB calls.
  • Other

    • Member payload validation extended to allow an is_tmp flag.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 2, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Refactors identity-resolution SQL functions to use find_apikey_by_value, add expiry checks and logging, and support hashed API keys; adds related integration and RLS tests and a minor API member schema field addition.

Changes

Cohort / File(s) Summary
Database Migration
supabase/migrations/20260102140000_fix_get_identity_hashed_apikeys.sql
Rewrites four public identity functions (get_identity, get_identity_apikey_only, get_identity_org_allowed, get_identity_org_appid) to centralize lookup via find_apikey_by_value, add is_apikey_expired checks, enforce mode/org/app restrictions, log denials, and grant execute on find_apikey_by_value to anon/authenticated.
RLS & SDK Tests
tests/hashed-apikey-rls.test.ts, tests/apikeys.test.ts
Adds comprehensive tests covering hashed and plain API keys: creation, expiry manipulation, SQL-level RPC/function checks, and SDK-based queries with key-in-header scenarios. Note: tests/apikeys.test.ts includes duplicated test blocks.
Function Schema Update
supabase/functions/_backend/public/organization/members/get.ts
Adds is_tmp: z.boolean() to memberSchema validation for the members response payload.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Client
    participant RLS_Fn as public.get_identity<br/>(and variants)
    participant Finder as find_apikey_by_value
    participant Expiry as is_apikey_expired
    participant DB as apikeys/users tables

    Client->>RLS_Fn: call RPC / invoke query (header capgkey)
    RLS_Fn->>RLS_Fn: extract API key from header
    RLS_Fn->>Finder: lookup key (plain or hashed)
    Finder->>DB: query apikeys table
    DB-->>Finder: apikey record or NULL
    Finder-->>RLS_Fn: apikey_id (or NULL)

    alt apikey found
        RLS_Fn->>Expiry: check expiration(apikey_id)
        Expiry-->>RLS_Fn: expired? boolean

        alt not expired & mode/org/app ok
            RLS_Fn->>DB: resolve user_id
            DB-->>RLS_Fn: user_id
            RLS_Fn-->>Client: return user_id
        else expired or restrictions fail
            RLS_Fn->>RLS_Fn: log denial
            RLS_Fn-->>Client: return NULL
        end
    else no apikey match
        RLS_Fn->>RLS_Fn: log denial
        RLS_Fn-->>Client: return NULL
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 A hop, a patch, a keyed-up song,
Hashed keys now find where they belong,
Expiry guards stand tall and bright,
Tests hop through day and night,
I nibble bugs and bound along. 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: adding support for hashed API keys in RLS identity functions.
Description check ✅ Passed The PR description covers all required template sections: summary, test plan, and checklist. Required sections are present with substantive content.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 SQLFluff (3.5.0)
supabase/migrations/20260102140000_fix_get_identity_hashed_apikeys.sql

User Error: No dialect was specified. You must configure a dialect or specify one on the command line using --dialect after the command. Available dialects:
ansi, athena, bigquery, clickhouse, databricks, db2, doris, duckdb, exasol, flink, greenplum, hive, impala, mariadb, materialize, mysql, oracle, postgres, redshift, snowflake, soql, sparksql, sqlite, starrocks, teradata, trino, tsql, vertica


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 and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +221 to +222
GRANT EXECUTE ON FUNCTION "public"."find_apikey_by_value"(text) TO "anon";
GRANT EXECUTE ON FUNCTION "public"."find_apikey_by_value"(text) TO "authenticated";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid granting find_apikey_by_value to anon/auth

Granting EXECUTE on find_apikey_by_value to anon/authenticated makes this SECURITY DEFINER function callable over RPC by any client with the anon key, and it returns the full apikeys row (including key for non‑hashed keys and metadata) while bypassing RLS. That means a client can probe API key values directly and read fields that are otherwise protected by the apikeys RLS policies. If the intent is only to enable RLS identity lookups, it’s safer to keep this function non‑public and expose a narrower wrapper (e.g., returning only user_id) for policies.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
supabase/migrations/20260102140000_fix_get_identity_hashed_apikeys.sql (1)

221-222: Security concern: granting find_apikey_by_value to anon/authenticated exposes API key metadata.

This has been flagged in a previous review. Since find_apikey_by_value is SECURITY DEFINER and returns the full apikeys row (including the plain key for non-hashed keys), granting EXECUTE to anon/authenticated makes it callable via RPC, allowing clients to probe API key values.

Consider one of:

  1. Not granting EXECUTE to anon/authenticated (RLS functions already run as SECURITY DEFINER)
  2. Creating a narrower internal function that only returns what's needed
  3. Prefixing with underscore to hide from PostgREST API while keeping grants
🧹 Nitpick comments (2)
tests/hashed-apikey-rls.test.ts (1)

17-28: Consider parameterized JSON for request.headers injection.

The current string interpolation is acceptable for test inputs but could be made more robust:

 async function execWithCapgkey(sql: string, capgkey: string): Promise<any> {
   const client = await pool.connect()
   try {
-    await client.query(`SET request.headers = '{"capgkey": "${capgkey}"}'`)
+    const headersJson = JSON.stringify({ capgkey })
+    await client.query(`SET request.headers = '${headersJson.replace(/'/g, "''")}'`)
     const result = await client.query(sql)
     return result.rows
   }

This would handle edge cases if a key value ever contained quotes. Since inputs are controlled in tests, this is optional.

supabase/migrations/20260102140000_fix_get_identity_hashed_apikeys.sql (1)

199-205: Inconsistent null/empty array handling between org and app restrictions.

The org check at line 192 uses COALESCE(array_length(...), 0) > 0 while the app check at line 200 uses IS DISTINCT FROM '{}'. While both work correctly, the different patterns make the code harder to reason about:

  • COALESCE(array_length(arr, 1), 0) > 0 explicitly handles NULL (returns FALSE)
  • arr IS DISTINCT FROM '{}' returns TRUE for NULL, then relies on ANY(NULL) returning NULL

For consistency with the org check pattern:

Suggested change
-    IF api_key.limited_to_apps IS DISTINCT FROM '{}' THEN
+    IF COALESCE(array_length(api_key.limited_to_apps, 1), 0) > 0 THEN
       IF NOT (app_id = ANY(api_key.limited_to_apps)) THEN
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 87f3ef9 and a0577e1.

📒 Files selected for processing (3)
  • supabase/migrations/20260102140000_fix_get_identity_hashed_apikeys.sql
  • tests/apikeys.test.ts
  • tests/hashed-apikey-rls.test.ts
🧰 Additional context used
📓 Path-based instructions (7)
supabase/migrations/**/*.sql

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Database migrations must be created with supabase migration new <feature_slug> and never modify previously committed migrations

Files:

  • supabase/migrations/20260102140000_fix_get_identity_hashed_apikeys.sql
**/{migrations,tests,__tests__}/**/*.{sql,ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

Always cover database changes with Postgres-level tests and complement them with end-to-end tests for affected user flows

Files:

  • supabase/migrations/20260102140000_fix_get_identity_hashed_apikeys.sql
  • tests/hashed-apikey-rls.test.ts
  • tests/apikeys.test.ts
supabase/migrations/*.sql

📄 CodeRabbit inference engine (AGENTS.md)

supabase/migrations/*.sql: When creating schema changes, use supabase migration new <feature_slug> to create a single migration file and keep editing that file until the feature ships; never edit previously committed migrations
A migration that introduces a new table may include seed inserts for that table, treating seeding as part of the current feature and not modifying previously committed migrations
Do not create new cron jobs; instead update the process_all_cron_tasks function in a new migration file to add your job if needed

Files:

  • supabase/migrations/20260102140000_fix_get_identity_hashed_apikeys.sql
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (CLAUDE.md)

Use single quotes and no semicolons per @antfu/eslint-config

Files:

  • tests/hashed-apikey-rls.test.ts
  • tests/apikeys.test.ts
tests/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Backend tests should be located in the tests/ directory and use Vitest test runner

Files:

  • tests/hashed-apikey-rls.test.ts
  • tests/apikeys.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use TypeScript strict mode with path aliases mapping ~/ to src/

Files:

  • tests/hashed-apikey-rls.test.ts
  • tests/apikeys.test.ts
tests/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Backend tests must use helpers from tests/test-utils.ts including getEndpointUrl(path) for correct worker routing and USE_CLOUDFLARE_WORKERS=true for CF Workers testing

Files:

  • tests/hashed-apikey-rls.test.ts
  • tests/apikeys.test.ts
🧠 Learnings (11)
📓 Common learnings
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Check `c.get('auth')?.authType` to determine authentication type ('apikey' vs 'jwt') in backend endpoints
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `middlewareAPISecret` for internal API endpoints and `middlewareKey` for external API keys; validate against `owner_org` in the `apikeys` table
📚 Learning: 2025-12-24T14:11:10.256Z
Learnt from: WcaleNieWolny
Repo: Cap-go/capgo PR: 1300
File: supabase/migrations/20251224103713_2fa_enforcement.sql:409-539
Timestamp: 2025-12-24T14:11:10.256Z
Learning: In supabase/migrations for get_orgs_v6 and get_orgs_v7: The inner functions with user_id parameter (get_orgs_v6(uuid) and get_orgs_v7(uuid)) should NOT be granted to anon/authenticated roles as this allows any user to query other users' organizations; only the no-argument wrapper functions should be public as they perform authentication checks.

Applied to files:

  • supabase/migrations/20260102140000_fix_get_identity_hashed_apikeys.sql
  • tests/hashed-apikey-rls.test.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `middlewareAPISecret` for internal API endpoints and `middlewareKey` for external API keys; validate against `owner_org` in the `apikeys` table

Applied to files:

  • supabase/migrations/20260102140000_fix_get_identity_hashed_apikeys.sql
  • tests/hashed-apikey-rls.test.ts
  • tests/apikeys.test.ts
📚 Learning: 2025-12-27T03:51:23.575Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-27T03:51:23.575Z
Learning: Applies to supabase/seed.sql : Update `supabase/seed.sql` to back new or evolved tests; keep fixtures focused on current behavior while leaving committed migrations unchanged

Applied to files:

  • supabase/migrations/20260102140000_fix_get_identity_hashed_apikeys.sql
  • tests/hashed-apikey-rls.test.ts
  • tests/apikeys.test.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Check `c.get('auth')?.authType` to determine authentication type ('apikey' vs 'jwt') in backend endpoints

Applied to files:

  • supabase/migrations/20260102140000_fix_get_identity_hashed_apikeys.sql
  • tests/hashed-apikey-rls.test.ts
  • tests/apikeys.test.ts
📚 Learning: 2025-12-25T11:22:13.039Z
Learnt from: WcaleNieWolny
Repo: Cap-go/capgo PR: 1300
File: supabase/migrations/20251224103713_2fa_enforcement.sql:85-96
Timestamp: 2025-12-25T11:22:13.039Z
Learning: In SQL migrations under the repository (e.g., supabase/migrations), enforce that when an org has enforcing_2fa=true, all users (including super_admins) must have 2FA enabled before accessing any org functions, including check_org_members_2fa_enabled. Do not grant admin exceptions to 2FA requirements. This ensures consistent security enforcement across all org-related operations; implement this rule within relevant migrations and associated stored procedures/tests.

Applied to files:

  • supabase/migrations/20260102140000_fix_get_identity_hashed_apikeys.sql
📚 Learning: 2025-12-27T03:51:23.575Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-27T03:51:23.575Z
Learning: Applies to **/{migrations,tests,__tests__}/**/*.{sql,ts,js} : Always cover database changes with Postgres-level tests and complement them with end-to-end tests for affected user flows

Applied to files:

  • tests/hashed-apikey-rls.test.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Backend tests modify local database; always reset database with `supabase db reset` before running tests to ensure clean state

Applied to files:

  • tests/apikeys.test.ts
📚 Learning: 2025-12-05T17:34:25.556Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-05T17:34:25.556Z
Learning: Backend tests that modify local database state require a running Supabase instance

Applied to files:

  • tests/apikeys.test.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to tests/**/*.{ts,js} : Backend tests must use helpers from `tests/test-utils.ts` including `getEndpointUrl(path)` for correct worker routing and `USE_CLOUDFLARE_WORKERS=true` for CF Workers testing

Applied to files:

  • tests/apikeys.test.ts
📚 Learning: 2025-12-05T17:34:25.556Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-05T17:34:25.556Z
Learning: Applies to tests/**/*.{test,spec}.{ts,tsx} : Backend tests should be located in the `tests/` directory and use Vitest test runner

Applied to files:

  • tests/apikeys.test.ts
🧬 Code graph analysis (2)
tests/hashed-apikey-rls.test.ts (1)
tests/test-utils.ts (5)
  • BASE_URL (19-19)
  • POSTGRES_URL (7-7)
  • USER_ID (45-45)
  • ORG_ID (42-42)
  • APP_NAME (57-57)
tests/apikeys.test.ts (2)
tests/test-utils.ts (1)
  • BASE_URL (19-19)
supabase/functions/_backend/utils/postgres_schema.ts (2)
  • apps (19-32)
  • orgs (94-103)
🪛 Gitleaks (8.30.0)
tests/hashed-apikey-rls.test.ts

[high] 445-445: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run tests
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (8)
tests/apikeys.test.ts (2)

1-4: LGTM! Import reorganization is appropriate.

Moving createClient import to the top level alongside other imports improves code organization and aligns with module-level import conventions.


548-640: Well-structured RLS integration tests for hashed API key support.

These tests effectively validate the core feature by simulating CLI usage patterns—creating API keys and using them directly with the Supabase SDK via the capgkey header. The coverage of both hashed and plain key scenarios, along with proper cleanup, ensures the identity resolution path works correctly for real-world usage.

tests/hashed-apikey-rls.test.ts (5)

1-11: Excellent documentation and imports for the test file.

The header comment clearly explains the purpose of these tests and their importance for CLI usage. The imports are well-organized and include all necessary dependencies.


88-175: Comprehensive test coverage for get_identity() with hashed API keys.

The test suite thoroughly covers key scenarios: plain keys, hashed keys, invalid keys, mode mismatches, expired keys, and non-expired keys. This validates the core identity resolution logic for both key types.


307-368: Good direct testing of find_apikey_by_value() function.

Testing the underlying lookup function directly ensures it correctly handles plain key matching, hashed key matching (by hashing the input and comparing), and non-existent keys. This complements the higher-level identity function tests.


439-458: Static analysis false positive: intentional test value for invalid key.

The 'invalid-key-12345' string flagged by Gitleaks is an intentionally invalid test value used to verify that RLS correctly blocks access when an invalid API key is provided. This is expected behavior for a negative test case.


370-458: SDK-based RLS tests validate end-to-end behavior.

These tests exercise the full path from Supabase client through PostgREST to RLS policies, ensuring hashed API keys work correctly in real-world CLI usage scenarios. Good coverage of table queries and RPC calls.

supabase/migrations/20260102140000_fix_get_identity_hashed_apikeys.sql (1)

16-54: Well-structured refactor of get_identity() for hashed key support.

The function correctly:

  1. Prioritizes JWT authentication via auth.uid()
  2. Falls back to API key lookup using find_apikey_by_value
  3. Validates mode membership and expiration
  4. Logs denials for observability

The use of SECURITY DEFINER with empty search_path follows security best practices.

riderx and others added 4 commits January 2, 2026 14:37
Fixed a critical bug where hashed API keys were not working with RLS
policies. The get_identity functions only checked the plain 'key' column,
but for hashed keys, the key is NULL and only key_hash is populated.

Updated get_identity(), get_identity_apikey_only(), get_identity_org_allowed(),
and get_identity_org_appid() to use find_apikey_by_value() which checks both
plain and hashed keys. This enables CLI usage with hashed API keys.

Added 21 comprehensive tests in hashed-apikey-rls.test.ts verifying:
- Plain and hashed key lookup in all identity functions
- Expiration checking with hashed keys
- Organization and app restriction enforcement
- RLS policy enforcement via Supabase SDK

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Move createClient import from dynamic imports inside test bodies to a
static import at the top of the file for cleaner code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The get_org_members RPC function returns is_tmp for pending invitations,
but the API schema was filtering it out. This fix allows the field to be
returned to clients.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@riderx riderx force-pushed the riderx/apikey-hash-analysis branch from 29a8662 to 96a066e Compare January 2, 2026 14:39
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/hashed-apikey-rls.test.ts (1)

58-64: Consider validating deletion response.

The function silently ignores failures, which could leave orphaned test keys in the database if the API endpoint has issues. For test reliability, consider logging or throwing on unexpected status codes.

🔎 Suggested improvement
 async function deleteApiKey(id: number): Promise<void> {
-  await fetch(`${BASE_URL}/apikey/${id}`, {
+  const response = await fetch(`${BASE_URL}/apikey/${id}`, {
     method: 'DELETE',
     headers,
   })
+  if (!response.ok) {
+    console.warn(`Failed to delete API key ${id}: ${response.status}`)
+  }
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a0577e1 and 96a066e.

📒 Files selected for processing (4)
  • supabase/functions/_backend/public/organization/members/get.ts
  • supabase/migrations/20260102140000_fix_get_identity_hashed_apikeys.sql
  • tests/apikeys.test.ts
  • tests/hashed-apikey-rls.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • supabase/migrations/20260102140000_fix_get_identity_hashed_apikeys.sql
  • tests/apikeys.test.ts
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (CLAUDE.md)

Use single quotes and no semicolons per @antfu/eslint-config

Files:

  • supabase/functions/_backend/public/organization/members/get.ts
  • tests/hashed-apikey-rls.test.ts
supabase/functions/_backend/**

📄 CodeRabbit inference engine (CLAUDE.md)

Backend logic should be organized in supabase/functions/_backend/ with subdirectories for plugins, private endpoints, public endpoints, triggers, and utilities

Files:

  • supabase/functions/_backend/public/organization/members/get.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use TypeScript strict mode with path aliases mapping ~/ to src/

Files:

  • supabase/functions/_backend/public/organization/members/get.ts
  • tests/hashed-apikey-rls.test.ts
supabase/functions/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Supabase Edge Functions use Deno runtime

Files:

  • supabase/functions/_backend/public/organization/members/get.ts
supabase/functions/_backend/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

supabase/functions/_backend/**/*.{ts,js}: Backend code must be placed in supabase/functions/_backend/ as shared code deployed to Cloudflare Workers (API/Plugin/Files workers), Supabase Edge Functions, and other platforms
Use createHono from utils/hono.ts for all Hono framework application initialization and routing
All database operations must use getPgClient() or getDrizzleClient() from utils/pg.ts for PostgreSQL access during active migration to Cloudflare D1
All Hono endpoint handlers must accept Context<MiddlewareKeyVariables> and use c.get('requestId'), c.get('apikey'), and c.get('auth') for request context
Use structured logging with cloudlog({ requestId: c.get('requestId'), message: '...' }) for all backend logging
Use middlewareAPISecret for internal API endpoints and middlewareKey for external API keys; validate against owner_org in the apikeys table
Check c.get('auth')?.authType to determine authentication type ('apikey' vs 'jwt') in backend endpoints
Use Drizzle ORM query patterns with schema from postgress_schema.ts for all database operations; use aliasV2() for self-joins or multiple table references

Files:

  • supabase/functions/_backend/public/organization/members/get.ts
supabase/functions/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Backend ESLint must pass before commit; run bun lint:backend for backend files

Files:

  • supabase/functions/_backend/public/organization/members/get.ts
tests/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Backend tests should be located in the tests/ directory and use Vitest test runner

Files:

  • tests/hashed-apikey-rls.test.ts
tests/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Backend tests must use helpers from tests/test-utils.ts including getEndpointUrl(path) for correct worker routing and USE_CLOUDFLARE_WORKERS=true for CF Workers testing

Files:

  • tests/hashed-apikey-rls.test.ts
**/{migrations,tests,__tests__}/**/*.{sql,ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

Always cover database changes with Postgres-level tests and complement them with end-to-end tests for affected user flows

Files:

  • tests/hashed-apikey-rls.test.ts
🧠 Learnings (6)
📓 Common learnings
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Check `c.get('auth')?.authType` to determine authentication type ('apikey' vs 'jwt') in backend endpoints
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `middlewareAPISecret` for internal API endpoints and `middlewareKey` for external API keys; validate against `owner_org` in the `apikeys` table
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `middlewareAPISecret` for internal API endpoints and `middlewareKey` for external API keys; validate against `owner_org` in the `apikeys` table

Applied to files:

  • tests/hashed-apikey-rls.test.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Check `c.get('auth')?.authType` to determine authentication type ('apikey' vs 'jwt') in backend endpoints

Applied to files:

  • tests/hashed-apikey-rls.test.ts
📚 Learning: 2025-12-27T03:51:23.575Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-27T03:51:23.575Z
Learning: Applies to **/{migrations,tests,__tests__}/**/*.{sql,ts,js} : Always cover database changes with Postgres-level tests and complement them with end-to-end tests for affected user flows

Applied to files:

  • tests/hashed-apikey-rls.test.ts
📚 Learning: 2025-12-27T03:51:23.575Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-27T03:51:23.575Z
Learning: Applies to supabase/seed.sql : Update `supabase/seed.sql` to back new or evolved tests; keep fixtures focused on current behavior while leaving committed migrations unchanged

Applied to files:

  • tests/hashed-apikey-rls.test.ts
📚 Learning: 2025-12-24T14:11:10.256Z
Learnt from: WcaleNieWolny
Repo: Cap-go/capgo PR: 1300
File: supabase/migrations/20251224103713_2fa_enforcement.sql:409-539
Timestamp: 2025-12-24T14:11:10.256Z
Learning: In supabase/migrations for get_orgs_v6 and get_orgs_v7: The inner functions with user_id parameter (get_orgs_v6(uuid) and get_orgs_v7(uuid)) should NOT be granted to anon/authenticated roles as this allows any user to query other users' organizations; only the no-argument wrapper functions should be public as they perform authentication checks.

Applied to files:

  • tests/hashed-apikey-rls.test.ts
🧬 Code graph analysis (1)
tests/hashed-apikey-rls.test.ts (1)
tests/test-utils.ts (5)
  • BASE_URL (19-19)
  • POSTGRES_URL (7-7)
  • USER_ID (45-45)
  • ORG_ID (42-42)
  • APP_NAME (57-57)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (7)
tests/hashed-apikey-rls.test.ts (6)

1-14: LGTM!

Clean imports and proper organization of dependencies for the test suite.


30-56: LGTM!

Helper functions properly validate response status and provide meaningful error messages on failure.


66-86: LGTM!

Proper resource lifecycle management with pool creation in beforeAll and cleanup in afterAll.


88-305: Well-structured test coverage for identity functions.

The tests comprehensively cover plain keys, hashed keys, invalid keys, mode restrictions, expiration handling, and org/app limitations. Each describe block properly manages its own test keys with appropriate setup and cleanup.


307-368: LGTM!

The find_apikey_by_value() tests properly validate lookup behavior for both plain keys (matching by key column) and hashed keys (hashing input and matching key_hash). Good verification of return values including the NULL key for hashed entries.


370-459: SDK integration tests provide good end-to-end coverage.

These tests validate real-world CLI usage patterns where the Supabase SDK is used directly with the capgkey header. The test at lines 439-458 correctly verifies that RLS blocks access gracefully (empty array) rather than throwing an error.

supabase/functions/_backend/public/organization/members/get.ts (1)

28-28: No issues found - the schema change is correct.

The get_org_members RPC function has been returning the is_tmp field since migration 20250613034031_tmp_users_table.sql. The schema change at line 28 simply synchronizes the TypeScript validation with the current RPC implementation. Since the field is guaranteed to be present (returned as false for existing members and true for pending invitations), marking it as required in the Zod schema is correct and will not cause safeParse failures.

Comment on lines +16 to +28
// Helper to execute SQL with capgkey header set
async function execWithCapgkey(sql: string, capgkey: string): Promise<any> {
const client = await pool.connect()
try {
// Set the capgkey header in request.headers (how Supabase passes it to RLS)
await client.query(`SET request.headers = '{"capgkey": "${capgkey}"}'`)
const result = await client.query(sql)
return result.rows
}
finally {
client.release()
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential SQL injection via unescaped capgkey value.

The capgkey is directly interpolated into the JSON string. If the key contains characters like " or \, it could break the JSON structure or allow injection. Use JSON.stringify to properly escape the value.

🔎 Proposed fix
 async function execWithCapgkey(sql: string, capgkey: string): Promise<any> {
   const client = await pool.connect()
   try {
     // Set the capgkey header in request.headers (how Supabase passes it to RLS)
-    await client.query(`SET request.headers = '{"capgkey": "${capgkey}"}'`)
+    await client.query(`SET request.headers = ${client.escapeLiteral(JSON.stringify({ capgkey }))}`)
     const result = await client.query(sql)
     return result.rows
   }
   finally {
     client.release()
   }
 }
🤖 Prompt for AI Agents
In tests/hashed-apikey-rls.test.ts around lines 16-28, the capgkey is
interpolated directly into the JSON string used in the SET request.headers call,
creating an injection risk; change the code to build a proper JSON string with
JSON.stringify(capgkey) (or build the full headers object and JSON.stringify it)
and pass that JSON via a parameterized query (e.g. SET request.headers = $1)
instead of string interpolation so special characters are escaped safely.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Jan 2, 2026

@riderx riderx merged commit 0768de5 into main Jan 2, 2026
11 checks passed
@riderx riderx deleted the riderx/apikey-hash-analysis branch January 2, 2026 14:48
Dalanir pushed a commit that referenced this pull request Jan 12, 2026
* fix: support hashed API keys in RLS identity functions

Fixed a critical bug where hashed API keys were not working with RLS
policies. The get_identity functions only checked the plain 'key' column,
but for hashed keys, the key is NULL and only key_hash is populated.

Updated get_identity(), get_identity_apikey_only(), get_identity_org_allowed(),
and get_identity_org_appid() to use find_apikey_by_value() which checks both
plain and hashed keys. This enables CLI usage with hashed API keys.

Added 21 comprehensive tests in hashed-apikey-rls.test.ts verifying:
- Plain and hashed key lookup in all identity functions
- Expiration checking with hashed keys
- Organization and app restriction enforcement
- RLS policy enforcement via Supabase SDK

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* refactor: move supabase-js import to top level in apikeys tests

Move createClient import from dynamic imports inside test bodies to a
static import at the top of the file for cleaner code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: trigger CI rebuild

* fix: add is_tmp field to organization members response schema

The get_org_members RPC function returns is_tmp for pending invitations,
but the API schema was filtering it out. This fix allows the field to be
returned to clients.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Co-authored-by: WcaleNieWolny <isupermichael007@gmail.com>
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.

2 participants