Skip to content

enforce org authorization on metrics RPCs#1703

Open
riderx wants to merge 1 commit intomainfrom
riderx/fix-rpc-metrics-auth
Open

enforce org authorization on metrics RPCs#1703
riderx wants to merge 1 commit intomainfrom
riderx/fix-rpc-metrics-auth

Conversation

@riderx
Copy link
Member

@riderx riderx commented Feb 26, 2026

Summary (AI generated)

  • Added supabase/migrations/20260226100000_fix_metrics_rpcs_org_authorization.sql to enforce organization membership checks on metrics RPC endpoints and prevent anonymous cross-tenant usage telemetry disclosure.
  • Updated get_app_metrics, get_global_metrics, and get_total_metrics execution paths so requests now require valid read rights for the requested org and log denied attempts.

Test plan (AI generated)

  • bun lint:backend
  • Manual API validation: call each RPC with only anon key and verify unauthorized org IDs no longer return cross-tenant rows.

Screenshots (AI generated)

  • Not applicable (backend-only database migration).

Checklist (AI generated)

  • My code follows the code style of this project and passes bun run lint:backend && bun run lint.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • My change has adequate E2E test coverage.
  • I have tested my code manually, and I have provided steps how to reproduce my tests.

Summary by CodeRabbit

  • New Features
    • Added metrics endpoints to retrieve organization performance data by app and across the platform
    • Implemented secure access control with role-based authorization checks
    • Enabled performance optimization through intelligent data caching for faster queries

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

Introduces six new public PostgreSQL functions for retrieving organizational metrics with built-in authorization controls and caching. Functions compute per-app metrics, global aggregated metrics, and total metrics across specified date ranges, validating access permissions and leveraging cache mechanisms to optimize queries.

Changes

Cohort / File(s) Summary
Metrics RPC Functions
supabase/migrations/20260226100000_fix_metrics_rpcs_org_authorization.sql
Adds six overloaded public functions: get_app_metrics (2 versions), get_global_metrics (2 versions), and get_total_metrics (2 versions). Implements authorization checks (service_role bypass, permission validation), org existence verification, cache management (app_metrics_cache, org_metrics_cache with TTL logic), and conditional cache seeding. Includes request denial logging with JSONB context for audit purposes.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Auth as Authorization
    participant DB as Database
    participant Cache as Metrics Cache
    participant Compute as Computation

    Client->>Auth: Request metrics (org_id, date range)
    alt Service Role
        Auth-->>DB: Allow (bypass)
    else Regular User
        Auth->>DB: Check permissions & org
        DB-->>Auth: Validate rights
        alt Unauthorized
            Auth-->>Client: Deny (log event)
        else Authorized
            Auth-->>DB: Proceed
        end
    end

    DB->>Cache: Check cache validity
    alt Cache Valid & Recent
        Cache-->>Client: Return cached metrics
    else Cache Stale or Missing
        DB->>Compute: Seed metrics
        Compute->>Cache: Store results
        Cache-->>Client: Return metrics
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

💰 Rewarded

Poem

🐰 A rabbit hops through metrics bright,
With authorization checks held tight,
Caching bounds the queries fast,
Access controlled, permissions asked,
Metrics bloom in organized light!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'enforce org authorization on metrics RPCs' accurately describes the main change: adding authorization checks to metrics RPC endpoints.
Description check ✅ Passed The description covers the summary, test plan, and checklist sections from the template but most checklist items remain unchecked, indicating incomplete verification.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch riderx/fix-rpc-metrics-auth

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 (4.0.4)
supabase/migrations/20260226100000_fix_metrics_rpcs_org_authorization.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
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)
supabase/migrations/20260226100000_fix_metrics_rpcs_org_authorization.sql (1)

1-3: Consider adding explicit GRANT statements to document access intent.

The migration creates public functions without explicit GRANT or REVOKE statements. While PostgreSQL defaults grant EXECUTE to PUBLIC for new functions and the authorization is enforced internally, explicit grants improve clarity and make the security model auditable.

📝 Example: Explicit grants at end of migration
-- Explicitly grant to authenticated and anon for RPC access
GRANT EXECUTE ON FUNCTION public.get_app_metrics(uuid) TO authenticated, anon;
GRANT EXECUTE ON FUNCTION public.get_app_metrics(uuid, date, date) TO authenticated, anon;
GRANT EXECUTE ON FUNCTION public.get_global_metrics(uuid) TO authenticated, anon;
GRANT EXECUTE ON FUNCTION public.get_global_metrics(uuid, date, date) TO authenticated, anon;
GRANT EXECUTE ON FUNCTION public.get_total_metrics(uuid) TO authenticated, anon;
GRANT EXECUTE ON FUNCTION public.get_total_metrics(uuid, date, date) TO authenticated, anon;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/migrations/20260226100000_fix_metrics_rpcs_org_authorization.sql`
around lines 1 - 3, Add explicit GRANT/REVOKE statements for the created RPC
functions to document and lock down access intent: for each function created
(get_app_metrics(uuid), get_app_metrics(uuid, date, date),
get_global_metrics(uuid), get_global_metrics(uuid, date, date),
get_total_metrics(uuid), get_total_metrics(uuid, date, date)) add GRANT EXECUTE
ON FUNCTION ... TO authenticated, anon (or the appropriate roles) at the end of
the migration and consider REVOKE EXECUTE ON FUNCTION ... FROM PUBLIC if you
want to remove the default public execute privilege so the authorization model
is auditable and explicit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@supabase/migrations/20260226100000_fix_metrics_rpcs_org_authorization.sql`:
- Around line 173-208: Remove the redundant authorization block that duplicates
get_app_metrics' checks: delete the v_is_service_role/v_user_id check and the IF
NOT v_is_service_role ... END IF; block (including the calls to
public.get_identity, public.check_min_rights, public.pg_log and the early
RETURN) so this function simply calls public.get_app_metrics and returns its
aggregated results; keep the initial v_is_service_role assignment only if used
elsewhere, otherwise remove its declaration too.

---

Nitpick comments:
In `@supabase/migrations/20260226100000_fix_metrics_rpcs_org_authorization.sql`:
- Around line 1-3: Add explicit GRANT/REVOKE statements for the created RPC
functions to document and lock down access intent: for each function created
(get_app_metrics(uuid), get_app_metrics(uuid, date, date),
get_global_metrics(uuid), get_global_metrics(uuid, date, date),
get_total_metrics(uuid), get_total_metrics(uuid, date, date)) add GRANT EXECUTE
ON FUNCTION ... TO authenticated, anon (or the appropriate roles) at the end of
the migration and consider REVOKE EXECUTE ON FUNCTION ... FROM PUBLIC if you
want to remove the default public execute privilege so the authorization model
is auditable and explicit.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ab763b and cc2a1dd.

📒 Files selected for processing (1)
  • supabase/migrations/20260226100000_fix_metrics_rpcs_org_authorization.sql

Comment on lines +173 to +208
BEGIN
v_is_service_role := ((SELECT auth.jwt() ->> 'role') = 'service_role') OR ((SELECT session_user) IS NOT DISTINCT FROM 'postgres');

IF NOT v_is_service_role THEN
v_user_id := public.get_identity('{read,upload,write,all}'::public.key_mode[]);

IF v_user_id IS NULL OR NOT public.check_min_rights(
'read'::public.user_min_right,
v_user_id,
p_org_id,
NULL::character varying,
NULL::bigint
) THEN
PERFORM public.pg_log('deny: NO_RIGHTS', jsonb_build_object('org_id', p_org_id, 'uid', v_user_id, 'rpc', 'get_global_metrics'));
RETURN;
END IF;
END IF;

RETURN QUERY
SELECT
metrics.date,
SUM(metrics.mau)::bigint AS mau,
SUM(metrics.storage)::bigint AS storage,
SUM(metrics.bandwidth)::bigint AS bandwidth,
SUM(metrics.get)::bigint AS get,
SUM(metrics.fail)::bigint AS fail,
SUM(metrics.install)::bigint AS install,
SUM(metrics.uninstall)::bigint AS uninstall
FROM
public.get_app_metrics(p_org_id, p_start_date, p_end_date) AS metrics
GROUP BY
metrics.date
ORDER BY
metrics.date;
END;
$function$;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Redundant authorization check causes double denial logging.

This function performs authorization at lines 176-189, then calls get_app_metrics at line 202 which performs the same authorization check again. When access is denied, pg_log is invoked twice for a single request, creating confusing audit trails.

Consider removing the authorization block here since get_app_metrics already enforces it, or mark this function SECURITY DEFINER and use an internal helper that skips auth.

♻️ Suggested fix: Remove redundant auth check
 CREATE OR REPLACE FUNCTION public.get_global_metrics(
     p_org_id uuid,
     p_start_date date,
     p_end_date date
 ) RETURNS TABLE (
     date date,
     mau bigint,
     storage bigint,
     bandwidth bigint,
     get bigint,
     fail bigint,
     install bigint,
     uninstall bigint
 ) LANGUAGE plpgsql
 SET
 search_path = '' AS $function$
-DECLARE
-    v_user_id uuid;
-    v_is_service_role boolean;
 BEGIN
-    v_is_service_role := ((SELECT auth.jwt() ->> 'role') = 'service_role') OR ((SELECT session_user) IS NOT DISTINCT FROM 'postgres');
-
-    IF NOT v_is_service_role THEN
-        v_user_id := public.get_identity('{read,upload,write,all}'::public.key_mode[]);
-
-        IF v_user_id IS NULL OR NOT public.check_min_rights(
-            'read'::public.user_min_right,
-            v_user_id,
-            p_org_id,
-            NULL::character varying,
-            NULL::bigint
-        ) THEN
-            PERFORM public.pg_log('deny: NO_RIGHTS', jsonb_build_object('org_id', p_org_id, 'uid', v_user_id, 'rpc', 'get_global_metrics'));
-            RETURN;
-        END IF;
-    END IF;
-
     RETURN QUERY
     SELECT
         metrics.date,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
BEGIN
v_is_service_role := ((SELECT auth.jwt() ->> 'role') = 'service_role') OR ((SELECT session_user) IS NOT DISTINCT FROM 'postgres');
IF NOT v_is_service_role THEN
v_user_id := public.get_identity('{read,upload,write,all}'::public.key_mode[]);
IF v_user_id IS NULL OR NOT public.check_min_rights(
'read'::public.user_min_right,
v_user_id,
p_org_id,
NULL::character varying,
NULL::bigint
) THEN
PERFORM public.pg_log('deny: NO_RIGHTS', jsonb_build_object('org_id', p_org_id, 'uid', v_user_id, 'rpc', 'get_global_metrics'));
RETURN;
END IF;
END IF;
RETURN QUERY
SELECT
metrics.date,
SUM(metrics.mau)::bigint AS mau,
SUM(metrics.storage)::bigint AS storage,
SUM(metrics.bandwidth)::bigint AS bandwidth,
SUM(metrics.get)::bigint AS get,
SUM(metrics.fail)::bigint AS fail,
SUM(metrics.install)::bigint AS install,
SUM(metrics.uninstall)::bigint AS uninstall
FROM
public.get_app_metrics(p_org_id, p_start_date, p_end_date) AS metrics
GROUP BY
metrics.date
ORDER BY
metrics.date;
END;
$function$;
BEGIN
RETURN QUERY
SELECT
metrics.date,
SUM(metrics.mau)::bigint AS mau,
SUM(metrics.storage)::bigint AS storage,
SUM(metrics.bandwidth)::bigint AS bandwidth,
SUM(metrics.get)::bigint AS get,
SUM(metrics.fail)::bigint AS fail,
SUM(metrics.install)::bigint AS install,
SUM(metrics.uninstall)::bigint AS uninstall
FROM
public.get_app_metrics(p_org_id, p_start_date, p_end_date) AS metrics
GROUP BY
metrics.date
ORDER BY
metrics.date;
END;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/migrations/20260226100000_fix_metrics_rpcs_org_authorization.sql`
around lines 173 - 208, Remove the redundant authorization block that duplicates
get_app_metrics' checks: delete the v_is_service_role/v_user_id check and the IF
NOT v_is_service_role ... END IF; block (including the calls to
public.get_identity, public.check_min_rights, public.pg_log and the early
RETURN) so this function simply calls public.get_app_metrics and returns its
aggregated results; keep the initial v_is_service_role assignment only if used
elsewhere, otherwise remove its declaration too.

@sonarqubecloud
Copy link

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.

1 participant