Skip to content

Fix build job ownership checks#1740

Closed
riderx wants to merge 9 commits intomainfrom
riderx/fix-attached-issue
Closed

Fix build job ownership checks#1740
riderx wants to merge 9 commits intomainfrom
riderx/fix-attached-issue

Conversation

@riderx
Copy link
Member

@riderx riderx commented Mar 3, 2026

Summary (AI generated)

Fixed cross-tenant authorization in build start and build cancel endpoints by resolving the build request from builder_job_id, enforcing app_id equality before permission checks, and running the app.build_native check against the resolved owning app before calling the builder service. The build actions now update using the resolved app_id, and related log messages were updated to avoid ambiguous context; bun.lock also includes the current configVersion metadata.

Motivation (AI generated)

This closes a path where a valid app permission could be paired with a victim job_id to start or cancel foreign builds.

Business Impact (AI generated)

This blocks cross-tenant build sabotage and unauthorized compute actions, reducing security risk without changing public API contracts.

Test Plan (AI generated)

  • bun lint:backend
  • Run tests/build-job-scope.test.ts assertions for cross-app start/cancel denial
  • Manually confirm start/cancel no longer proceeds when app_id does not match the resolved build request app

Summary by CodeRabbit

  • New Features

    • RBAC (Role-Based Access Control) is now enabled by default for new organizations.
  • Bug Fixes

    • Enhanced authorization validation for build operations to prevent cross-app access.

Dalanir and others added 4 commits March 3, 2026 11:42
New organizations now use the RBAC permission system (use_new_rbac = true)
by default instead of the legacy org_users.user_right system. Existing
organizations are unaffected. Tests are updated to explicitly mark legacy
orgs with use_new_rbac = false and add equivalent RBAC coverage.

- Migration: ALTER TABLE orgs ALTER COLUMN use_new_rbac SET DEFAULT true
- organization-api.test.ts: existing org pinned to legacy; new RBAC
  describe block covers GET/PUT org, GET/POST/DELETE members with
  role_bindings verification
- password-policy.test.ts: legacy org pinned; RBAC variant added
- private-error-cases.test.ts: test org pinned to legacy (error ordering
  differs between RBAC and legacy for non-existent apps)
- SQL tests 37 and 40: org INSERTs now explicit use_new_rbac = false
- seed.sql: seed orgs pinned to legacy
- Fix rbac_check_permission_direct: use v_effective_user_id in RBAC path
  (was using p_user_id which is NULL for API key auth)
- Fix invite_user_to_org_rbac: create role_binding directly after insert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- fix_rbac_check_effective_user.sql: replace raw `WHERE key = p_apikey`
  with `find_apikey_by_value(p_apikey)` in the apikey principal lookup so
  hashed keys are correctly resolved when checking RBAC permissions
  (regression from 20260228000300_fix_apikey_hashed_lookup.sql)

- password-policy.test.ts: assert insert errors for stripe_info, orgs,
  and org_users in the RBAC password-policy test so setup failures surface
  immediately rather than silently producing a false-positive result

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

📝 Walkthrough

Walkthrough

The PR refactors build endpoint authorization to validate app_id after fetching the build request, implements new RBAC permission check functions in the database, enables RBAC by default for new organizations, and adds comprehensive test coverage for both legacy and RBAC authentication paths.

Changes

Cohort / File(s) Summary
Build Endpoint Authorization
supabase/functions/_backend/public/build/cancel.ts, supabase/functions/_backend/public/build/start.ts
Moved authorization validation from pre-fetch to post-fetch: now fetches build_request first, validates its app_id matches the provided appId, and checks RBAC permissions against the actual app_id before proceeding with operations.
RBAC Migrations and Functions
supabase/migrations/20260302000000_rbac_default_for_new_orgs.sql, supabase/migrations/20260302185011_fix_rbac_check_effective_user.sql
Added two new SQL functions: rbac_check_permission_direct for centralized RBAC permission checking with API key resolution and 2FA/password policy enforcement, and invite_user_to_org_rbac for RBAC-mode org invitations. Sets RBAC as default for new organizations.
Schema and Seed Updates
supabase/schemas/prod.sql, supabase/seed.sql
Changed default value of public.orgs.use_new_rbac from false to true in schema; updated seed INSERT statements to explicitly include use_new_rbac column with values.
Legacy Path Test Setup
supabase/tests/37_test_check_min_rights_2fa_enforcement.sql, supabase/tests/40_test_password_policy_enforcement.sql
Added explicit use_new_rbac: false to org inserts to preserve legacy check_min_rights coverage in existing 2FA and password policy enforcement test scenarios.
Authorization and RBAC Test Coverage
tests/build-job-scope.test.ts, tests/organization-api.test.ts, tests/password-policy.test.ts, tests/private-error-cases.test.ts
Added fetchBuildRequestState helper to validate no state mutation on unauthorized build requests; expanded organization API tests with parallel RBAC-mode test suites; introduced RBAC path test blocks for password policy validation alongside legacy coverage; added use_new_rbac flags to test org creation payloads.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant API as Build Endpoint
    participant DB as Database
    participant RBAC as RBAC Engine
    participant Builder as External Builder
    
    Client->>API: POST /build/cancel/:jobId
    API->>DB: Fetch build_request by builder_job_id
    DB-->>API: Return buildRequest
    alt buildRequest.app_id mismatch
        API-->>Client: 401 Unauthorized (app_id mismatch)
    else app_id matches
        API->>RBAC: Check permission on buildRequest.app_id
        alt Permission denied
            RBAC-->>API: Permission denied
            API-->>Client: 401 Unauthorized
        else Permission granted
            RBAC-->>API: Permission granted
            API->>DB: Update build_requests status using buildRequest.app_id
            DB-->>API: Update confirmed
            API->>Builder: POST /cancel/:jobId
            Builder-->>API: Cancel result
            API-->>Client: Return result
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

💰 Rewarded

Poem

🐰 Authorization flows now dance with grace,
Post-fetch checks guard each sacred space,
App IDs matched, permissions blessed,
RBAC awakens, put to the test!
New orgs embrace the secure way,
Legacy and modern both here to stay!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description includes a summary section and test plan, but is AI-generated and lacks manual author context. The checklist is empty with no author verification of code style, documentation, test coverage, or manual testing. Add specific manual testing steps, confirm lint passes, verify E2E test coverage, and complete the provided checklist to demonstrate author review and validation.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix build job ownership checks' accurately and clearly describes the primary change: enforcing build job ownership validation in authorization checks for build start/cancel endpoints.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch riderx/fix-attached-issue

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/20260302185011_fix_rbac_check_effective_user.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

supabase/migrations/20260302000000_rbac_default_for_new_orgs.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

@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.

Reviewed commit: 8d38db8fce

ℹ️ 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 102 to 106
const { data: buildRequest, error: buildRequestError } = await supabase
.from('build_requests')
.select('app_id')
.eq('builder_job_id', jobId)
.eq('app_id', appId)
.maybeSingle()

Choose a reason for hiding this comment

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

P2 Badge Avoid assuming builder job IDs are globally unique

This lookup now uses maybeSingle() on builder_job_id alone, so if build_requests contains two rows with the same job ID (allowed today because there is no unique constraint on that column), Supabase returns a multi-row error and /build/start fails with internal_error before authorization logic can run; the same pattern was introduced in /build/cancel. This turns a recoverable duplicate-data condition into a hard failure for legitimate users, so the query should either include app_id disambiguation or handle multiple matches explicitly before deciding authorization.

Useful? React with 👍 / 👎.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 3, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
90.7% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/password-policy.test.ts (1)

514-590: ⚠️ Potential issue | 🟠 Major

Switch the new tests to it.concurrent() to match test-runner policy.

The newly added tests use it(...) inside a .test.ts file.

⚡ Proposed fix
-  it('check_min_rights respects password policy (legacy mode)', async () => {
+  it.concurrent('check_min_rights respects password policy (legacy mode)', async () => {

-  it('check_min_rights respects password policy (RBAC mode)', async () => {
+  it.concurrent('check_min_rights respects password policy (RBAC mode)', async () => {
As per coding guidelines: `ALL test files run in parallel; use it.concurrent() instead of it() to run tests within the same file in parallel for faster CI/CD`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/password-policy.test.ts` around lines 514 - 590, Two new tests
("check_min_rights respects password policy (legacy mode)" and "check_min_rights
respects password policy (RBAC mode)") use it(...) but must run concurrently per
test-runner policy; change both invocations from it(...) to it.concurrent(...).
Locate the two test cases by their exact titles and replace the test declaration
calls so they run in parallel (no other code changes needed).
supabase/seed.sql (1)

868-872: ⚠️ Potential issue | 🟡 Minor

Keep use_new_rbac deterministic in the upsert branch.

When public.reset_and_seed_app_data hits an existing org, use_new_rbac is not updated, so mode can drift between runs.

🔧 Proposed fix
   ON CONFLICT (id) DO UPDATE SET
     customer_id = EXCLUDED.customer_id,
     management_email = EXCLUDED.management_email,
     name = EXCLUDED.name,
+    use_new_rbac = EXCLUDED.use_new_rbac,
     updated_at = NOW();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/seed.sql` around lines 868 - 872, The upsert conflict branch fails
to update the use_new_rbac column, allowing its value to drift; modify the ON
CONFLICT ... DO UPDATE SET clause (the upsert for the org row used by
public.reset_and_seed_app_data) to explicitly set use_new_rbac =
EXCLUDED.use_new_rbac (or the intended deterministic expression) alongside
customer_id, management_email, name, and updated_at so the RBAC mode is always
updated on upsert.
🧹 Nitpick comments (2)
supabase/functions/_backend/public/build/start.ts (1)

130-157: Consider extracting this auth guard into a shared helper with cancelBuild.

The “resolve build_request → enforce app match → app-scoped checkPermission” sequence is duplicated in both public build actions. A shared helper would reduce future security drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/public/build/start.ts` around lines 130 - 157,
The auth checks for start builds duplicate the same "resolve build_request →
enforce app match → app-scoped checkPermission" logic found in cancelBuild;
extract that sequence into a shared helper (e.g., authorizeBuildAction or
ensureBuildPermission) which takes the context c, buildRequest (or jobId/appId)
and required permission string, performs the app_id match check (using
buildRequest.app_id vs appId), calls checkPermission(c, 'app.build_native', {
appId: buildRequest.app_id }), logs via cloudlogErr with the same fields
(requestId, job_id, app_id/requested_app_id, user_id) and throws simpleError on
failure, and replace the duplicated blocks in start.ts and the cancelBuild
implementation with calls to this helper, returning the resolved boundAppId for
downstream use.
tests/build-job-scope.test.ts (1)

191-209: ESLint: Test title should begin with lowercase.

The linter prefers test descriptions to start with a lowercase letter. Consider rephrasing or suppressing if HTTP method prefixes are a project convention.

🔧 Optional fix
-  it.concurrent('POST /build/start/:jobId denies cross-app jobId with allowed app_id', async () => {
+  it.concurrent('denies cross-app jobId with allowed app_id on POST /build/start/:jobId', async () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/build-job-scope.test.ts` around lines 191 - 209, Rename the test title
string used in the it.concurrent call so it begins with a lowercase letter
(e.g., change "POST /build/start/:jobId denies cross-app jobId with allowed
app_id" to "post /build/start/:jobId denies cross-app jobId with allowed
app_id") to satisfy the ESLint rule; locate the it.concurrent invocation and
update its first argument (the test description) accordingly (or if you prefer
to keep the exact casing, add an inline ESLint suppression immediately above the
it.concurrent statement).
🤖 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/20260302185011_fix_rbac_check_effective_user.sql`:
- Around line 261-272: Replace the vulnerable SELECT-then-INSERT logic with an
atomic upsert so concurrent invites won't conflict: perform INSERT INTO
public.org_users (user_id, org_id, user_right, rbac_role_name) VALUES
(invited_user.id, invite_user_to_org_rbac.org_id, invite_right,
invite_user_to_org_rbac.role_name) ON CONFLICT (user_id, org_id) DO NOTHING
RETURNING id INTO current_record (or use GET DIAGNOSTICS row_count to detect if
an insert happened); then if current_record IS NOT NULL (or row_count = 0)
return 'ALREADY_INVITED' else continue — reference the variables invited_user,
invite_user_to_org_rbac, current_record and the public.org_users table when
making the change.

In `@tests/build-job-scope.test.ts`:
- Around line 22-35: fetchBuildRequestState calls supabase.from(...).eq('id',
buildRequestId) but buildRequestId is typed string | null; add a guard at the
start of fetchBuildRequestState to check buildRequestId !== null (or throw a
descriptive error) before calling getSupabaseClient() and .eq, so .eq always
receives a string; reference fetchBuildRequestState and buildRequestId and
ensure the type-checker is satisfied (early return or throw) rather than using a
non-null assertion.

In `@tests/organization-api.test.ts`:
- Line 739: The describe title string in the test suite "RBAC mode -
organization member operations" violates the test/prefer-lowercase-title rule;
update the describe call (the describe(...) invocation that begins with the
title "RBAC mode - organization member operations") to use a lowercase title
(e.g., "rbac mode - organization member operations") so the title is fully
lowercase and satisfies ESLint.
- Around line 739-876: The tests use shared mutable fixtures (ORG_ID_RBAC,
beforeAll/afterAll) and plain it() which can race when tests run in parallel;
change each it(...) to it.concurrent(...) and refactor setup so each test
creates and cleans a dedicated org and related records (replace global
ORG_ID_RBAC usage with a per-test unique org id/name generated inside the test
or a per-test helper), move the org/stripe/org_users/role_bindings insertions
and deletions into per-test setup/teardown (beforeEach/afterEach or inline in
the test) and ensure cleanup removes role_bindings/org_users/orgs/stripe_info
created for that test (references: beforeAll, afterAll, ORG_ID_RBAC,
USER_ADMIN_EMAIL, the organization/members test cases and the DB queries to
role_bindings/org_users/stripe_info).

---

Outside diff comments:
In `@supabase/seed.sql`:
- Around line 868-872: The upsert conflict branch fails to update the
use_new_rbac column, allowing its value to drift; modify the ON CONFLICT ... DO
UPDATE SET clause (the upsert for the org row used by
public.reset_and_seed_app_data) to explicitly set use_new_rbac =
EXCLUDED.use_new_rbac (or the intended deterministic expression) alongside
customer_id, management_email, name, and updated_at so the RBAC mode is always
updated on upsert.

In `@tests/password-policy.test.ts`:
- Around line 514-590: Two new tests ("check_min_rights respects password policy
(legacy mode)" and "check_min_rights respects password policy (RBAC mode)") use
it(...) but must run concurrently per test-runner policy; change both
invocations from it(...) to it.concurrent(...). Locate the two test cases by
their exact titles and replace the test declaration calls so they run in
parallel (no other code changes needed).

---

Nitpick comments:
In `@supabase/functions/_backend/public/build/start.ts`:
- Around line 130-157: The auth checks for start builds duplicate the same
"resolve build_request → enforce app match → app-scoped checkPermission" logic
found in cancelBuild; extract that sequence into a shared helper (e.g.,
authorizeBuildAction or ensureBuildPermission) which takes the context c,
buildRequest (or jobId/appId) and required permission string, performs the
app_id match check (using buildRequest.app_id vs appId), calls
checkPermission(c, 'app.build_native', { appId: buildRequest.app_id }), logs via
cloudlogErr with the same fields (requestId, job_id, app_id/requested_app_id,
user_id) and throws simpleError on failure, and replace the duplicated blocks in
start.ts and the cancelBuild implementation with calls to this helper, returning
the resolved boundAppId for downstream use.

In `@tests/build-job-scope.test.ts`:
- Around line 191-209: Rename the test title string used in the it.concurrent
call so it begins with a lowercase letter (e.g., change "POST
/build/start/:jobId denies cross-app jobId with allowed app_id" to "post
/build/start/:jobId denies cross-app jobId with allowed app_id") to satisfy the
ESLint rule; locate the it.concurrent invocation and update its first argument
(the test description) accordingly (or if you prefer to keep the exact casing,
add an inline ESLint suppression immediately above the it.concurrent statement).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ffd4cb3 and 5103849.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • supabase/functions/_backend/public/build/cancel.ts
  • supabase/functions/_backend/public/build/start.ts
  • supabase/migrations/20260302000000_rbac_default_for_new_orgs.sql
  • supabase/migrations/20260302185011_fix_rbac_check_effective_user.sql
  • supabase/schemas/prod.sql
  • supabase/seed.sql
  • supabase/tests/37_test_check_min_rights_2fa_enforcement.sql
  • supabase/tests/40_test_password_policy_enforcement.sql
  • tests/build-job-scope.test.ts
  • tests/organization-api.test.ts
  • tests/password-policy.test.ts
  • tests/private-error-cases.test.ts

Comment on lines +261 to +272
IF invited_user IS NOT NULL THEN
SELECT public.org_users.id INTO current_record
FROM public.org_users
WHERE public.org_users.user_id = invited_user.id
AND public.org_users.org_id = invite_user_to_org_rbac.org_id;

IF current_record IS NOT NULL THEN
RETURN 'ALREADY_INVITED';
ELSE
INSERT INTO public.org_users (user_id, org_id, user_right, rbac_role_name)
VALUES (invited_user.id, invite_user_to_org_rbac.org_id, invite_right, invite_user_to_org_rbac.role_name);

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 | 🟠 Major

Make member invitation insert idempotent to avoid race failures.

This SELECT-then-INSERT sequence is vulnerable to concurrent invites for the same user/org, which can throw errors or create duplicate state depending on constraints.

🔒 Proposed fix
 DECLARE
   org record;
   invited_user record;
   current_record record;
   current_tmp_user record;
   role_id uuid;
   legacy_right public.user_min_right;
   invite_right public.user_min_right;
   api_key_text text;
   v_granted_by uuid;
+  v_rows_affected integer;
 BEGIN
@@
-      INSERT INTO public.org_users (user_id, org_id, user_right, rbac_role_name)
-      VALUES (invited_user.id, invite_user_to_org_rbac.org_id, invite_right, invite_user_to_org_rbac.role_name);
+      INSERT INTO public.org_users (user_id, org_id, user_right, rbac_role_name)
+      VALUES (invited_user.id, invite_user_to_org_rbac.org_id, invite_right, invite_user_to_org_rbac.role_name)
+      ON CONFLICT DO NOTHING;
+
+      GET DIAGNOSTICS v_rows_affected = ROW_COUNT;
+      IF v_rows_affected = 0 THEN
+        RETURN 'ALREADY_INVITED';
+      END IF;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/migrations/20260302185011_fix_rbac_check_effective_user.sql` around
lines 261 - 272, Replace the vulnerable SELECT-then-INSERT logic with an atomic
upsert so concurrent invites won't conflict: perform INSERT INTO
public.org_users (user_id, org_id, user_right, rbac_role_name) VALUES
(invited_user.id, invite_user_to_org_rbac.org_id, invite_right,
invite_user_to_org_rbac.role_name) ON CONFLICT (user_id, org_id) DO NOTHING
RETURNING id INTO current_record (or use GET DIAGNOSTICS row_count to detect if
an insert happened); then if current_record IS NOT NULL (or row_count = 0)
return 'ALREADY_INVITED' else continue — reference the variables invited_user,
invite_user_to_org_rbac, current_record and the public.org_users table when
making the change.

Comment on lines +22 to +35
const fetchBuildRequestState = async () => {
const supabase = getSupabaseClient()
const { data, error } = await supabase
.from('build_requests')
.select('status, app_id, last_error')
.eq('id', buildRequestId)
.single()

if (error) {
throw error
}

return data
}
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

Fix TypeScript error: buildRequestId may be null.

The pipeline fails because buildRequestId is typed as string | null but .eq() expects a string. Add a guard to satisfy the type checker.

🔧 Proposed fix
 const fetchBuildRequestState = async () => {
+   if (!buildRequestId) {
+     throw new Error('buildRequestId not initialized')
+   }
   const supabase = getSupabaseClient()
   const { data, error } = await supabase
     .from('build_requests')
     .select('status, app_id, last_error')
     .eq('id', buildRequestId)
     .single()
🧰 Tools
🪛 GitHub Actions: Run tests

[error] 27-27: TS2345: Argument of type 'string | null' is not assignable to parameter of type 'string'.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/build-job-scope.test.ts` around lines 22 - 35, fetchBuildRequestState
calls supabase.from(...).eq('id', buildRequestId) but buildRequestId is typed
string | null; add a guard at the start of fetchBuildRequestState to check
buildRequestId !== null (or throw a descriptive error) before calling
getSupabaseClient() and .eq, so .eq always receives a string; reference
fetchBuildRequestState and buildRequestId and ensure the type-checker is
satisfied (early return or throw) rather than using a non-null assertion.

const nameRbac = `RBAC Test Organization ${globalIdRbac}`
const customerIdRbac = `cus_test_rbac_${ORG_ID_RBAC}`

describe('RBAC mode - organization member operations', () => {
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

Lowercase the describe title to satisfy ESLint.

Line 739 currently violates test/prefer-lowercase-title.

🔧 Proposed fix
-describe('RBAC mode - organization member operations', () => {
+describe('rbac mode - organization member operations', () => {
📝 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
describe('RBAC mode - organization member operations', () => {
describe('rbac mode - organization member operations', () => {
🧰 Tools
🪛 ESLint

[error] 739-739: describes should begin with lowercase

(test/prefer-lowercase-title)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/organization-api.test.ts` at line 739, The describe title string in the
test suite "RBAC mode - organization member operations" violates the
test/prefer-lowercase-title rule; update the describe call (the describe(...)
invocation that begins with the title "RBAC mode - organization member
operations") to use a lowercase title (e.g., "rbac mode - organization member
operations") so the title is fully lowercase and satisfies ESLint.

Comment on lines +739 to +876
describe('RBAC mode - organization member operations', () => {
beforeAll(async () => {
const { error: stripeError } = await getSupabaseClient().from('stripe_info').insert({
customer_id: customerIdRbac,
status: 'succeeded',
product_id: 'prod_LQIregjtNduh4q',
subscription_id: `sub_${globalIdRbac}`,
trial_at: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000).toISOString(),
is_good_plan: true,
})
if (stripeError)
throw stripeError

const { error } = await getSupabaseClient().from('orgs').insert({
id: ORG_ID_RBAC,
name: nameRbac,
management_email: TEST_EMAIL,
created_by: USER_ID,
customer_id: customerIdRbac,
use_new_rbac: true, // Explicitly RBAC — tests the RBAC permission path
})
if (error)
throw error

// The generate_org_user_on_org_create trigger creates org_users(super_admin)
// and role_bindings(org_super_admin) for created_by automatically.
})

afterAll(async () => {
await getSupabaseClient().from('role_bindings').delete().eq('org_id', ORG_ID_RBAC)
await getSupabaseClient().from('org_users').delete().eq('org_id', ORG_ID_RBAC)
await getSupabaseClient().from('orgs').delete().eq('id', ORG_ID_RBAC)
await getSupabaseClient().from('stripe_info').delete().eq('customer_id', customerIdRbac)
})

it('[GET] /organization - get RBAC org by id', async () => {
const response = await fetch(`${BASE_URL}/organization?orgId=${ORG_ID_RBAC}`, {
headers,
})
expect(response.status).toBe(200)
const type = z.object({ id: z.string(), name: z.string() })
const safe = type.safeParse(await response.json())
expect(safe.success).toBe(true)
expect(safe.data).toEqual({ id: ORG_ID_RBAC, name: nameRbac })
})

it('[GET] /organization/members - returns members via role_bindings (RBAC path)', async () => {
const response = await fetch(`${BASE_URL}/organization/members?orgId=${ORG_ID_RBAC}`, {
headers,
})
expect(response.status).toBe(200)
const type = z.array(z.object({
uid: z.string(),
email: z.string(),
image_url: z.string(),
role: z.string(),
}))
const safe = type.safeParse(await response.json())
expect(safe.success).toBe(true)

const testUser = safe.data?.find(m => m.uid === USER_ID)
expect(testUser).toBeTruthy()
expect(testUser?.email).toBe(USER_EMAIL)
expect(testUser?.role).toBe('super_admin')
})

it('[PUT] /organization - update RBAC org name', async () => {
const updatedName = `RBAC Updated ${new Date().toISOString()}`
const response = await fetch(`${BASE_URL}/organization`, {
headers,
method: 'PUT',
body: JSON.stringify({ orgId: ORG_ID_RBAC, name: updatedName }),
})
expect(response.status).toBe(200)
const type = z.object({ id: z.uuid(), data: z.any() })
const safe = type.safeParse(await response.json())
expect(safe.success).toBe(true)
expect(safe.data?.id).toBe(ORG_ID_RBAC)
})

it('[POST] /organization/members - add member in RBAC mode (sync trigger creates role_binding)', async () => {
const response = await fetch(`${BASE_URL}/organization/members`, {
headers,
method: 'POST',
body: JSON.stringify({
orgId: ORG_ID_RBAC,
email: USER_ADMIN_EMAIL,
invite_type: 'read',
}),
})
expect(response.status).toBe(200)

const { data: userData } = await getSupabaseClient().from('users').select().eq('email', USER_ADMIN_EMAIL).single()
expect(userData).toBeTruthy()

// Verify org_users entry exists
const { data: orgUser } = await getSupabaseClient().from('org_users').select().eq('org_id', ORG_ID_RBAC).eq('user_id', userData!.id).single()
expect(orgUser).toBeTruthy()
expect(orgUser?.user_right).toBe('invite_read')

// Verify role_binding was created by sync trigger
const { data: binding } = await getSupabaseClient().from('role_bindings').select().eq('principal_type', 'user').eq('principal_id', userData!.id).eq('org_id', ORG_ID_RBAC)
expect(binding).toBeTruthy()
expect(binding!.length).toBeGreaterThan(0)

// Cleanup
await getSupabaseClient().from('org_users').delete().eq('org_id', ORG_ID_RBAC).eq('user_id', userData!.id)
})

it('[DELETE] /organization/members - remove member in RBAC mode cleans up role_bindings', async () => {
const { data: userData } = await getSupabaseClient().from('users').select().eq('email', USER_ADMIN_EMAIL).single()
expect(userData).toBeTruthy()

// Add member (sync trigger creates role_binding)
await getSupabaseClient().from('org_users').insert({
org_id: ORG_ID_RBAC,
user_id: userData!.id,
user_right: 'read',
})

const { data: bindingsBefore } = await getSupabaseClient().from('role_bindings').select().eq('principal_type', 'user').eq('principal_id', userData!.id).eq('org_id', ORG_ID_RBAC)
expect(bindingsBefore!.length).toBeGreaterThan(0)

const response = await fetch(`${BASE_URL}/organization/members?orgId=${ORG_ID_RBAC}&email=${USER_ADMIN_EMAIL}`, {
headers,
method: 'DELETE',
})
expect(response.status).toBe(200)

// org_users removed
const { data: orgUserAfter } = await getSupabaseClient().from('org_users').select().eq('org_id', ORG_ID_RBAC).eq('user_id', userData!.id)
expect(orgUserAfter).toHaveLength(0)

// role_bindings also cleaned up
const { data: bindingsAfter } = await getSupabaseClient().from('role_bindings').select().eq('principal_type', 'user').eq('principal_id', userData!.id).eq('org_id', ORG_ID_RBAC)
expect(bindingsAfter).toHaveLength(0)
})
})
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 | 🟠 Major

Use it.concurrent() for these new tests and isolate mutable fixtures per test.

This block adds new it(...) tests in a .test.ts file and mutates shared org/member state, which conflicts with the parallel-test guideline and introduces order coupling.

⚡ Suggested direction
-  it('[GET] /organization - get RBAC org by id', async () => {
+  it.concurrent('[GET] /organization - get RBAC org by id', async () => {

-  it('[POST] /organization/members - add member in RBAC mode (sync trigger creates role_binding)', async () => {
+  it.concurrent('[POST] /organization/members - add member in RBAC mode (sync trigger creates role_binding)', async () => {

Also prefer creating/cleaning a dedicated org per test case (or via a per-test helper) instead of sharing ORG_ID_RBAC across the whole block.

As per coding guidelines: `ALL test files run in parallel; use it.concurrent() instead of it()` and `When creating tests that interact with shared resources, create dedicated seed data with unique identifiers rather than modifying shared resources; use naming conventions like test file name or feature prefix`.
🧰 Tools
🪛 ESLint

[error] 739-739: describes should begin with lowercase

(test/prefer-lowercase-title)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/organization-api.test.ts` around lines 739 - 876, The tests use shared
mutable fixtures (ORG_ID_RBAC, beforeAll/afterAll) and plain it() which can race
when tests run in parallel; change each it(...) to it.concurrent(...) and
refactor setup so each test creates and cleans a dedicated org and related
records (replace global ORG_ID_RBAC usage with a per-test unique org id/name
generated inside the test or a per-test helper), move the
org/stripe/org_users/role_bindings insertions and deletions into per-test
setup/teardown (beforeEach/afterEach or inline in the test) and ensure cleanup
removes role_bindings/org_users/orgs/stripe_info created for that test
(references: beforeAll, afterAll, ORG_ID_RBAC, USER_ADMIN_EMAIL, the
organization/members test cases and the DB queries to
role_bindings/org_users/stripe_info).

@riderx riderx closed this Mar 3, 2026
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