Skip to content

refactor: clean up router validation#2836

Merged
Kitenite merged 11 commits into
mainfrom
kitenite/org-ownership-check
Mar 25, 2026
Merged

refactor: clean up router validation#2836
Kitenite merged 11 commits into
mainfrom
kitenite/org-ownership-check

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Mar 24, 2026

Summary

  • consolidate org-scoped router validation helpers
  • apply the shared validation path across related routers
  • add regression coverage for the touched routes

Testing

  • bun test packages/trpc/src/router/task/task.test.ts
  • bunx biome check packages/trpc/src/router/utils/active-organization.ts packages/trpc/src/router/utils/org-resource-access.ts packages/trpc/src/router/task/task.ts packages/trpc/src/router/task/task.test.ts packages/trpc/src/router/project/project.ts packages/trpc/src/router/workspace/workspace.ts packages/trpc/src/router/project/secrets/secrets.ts packages/trpc/src/router/v2-project/v2-project.ts packages/trpc/src/router/v2-workspace/v2-workspace.ts
  • bunx tsc -p packages/trpc/tsconfig.json --noEmit

Summary by cubic

Centralized org-scoped validation with shared helpers and enforced them across routers. Task reads are now protected and scoped to the active org; all write paths validate related resources belong to the same org and return clear errors.

  • Refactors

    • Added requireActiveOrgId, requireActiveOrgMembership, requireOrgScopedResource, requireOrgResourceAccess; replaced ad‑hoc checks in project, v2-project, workspace, v2-workspace, secrets, and task.
    • Converted task reads (all, byOrganization, byId, bySlug) to protected routes using the active org; validate statusId, assigneeId, githubRepositoryId, projectId, and deviceId before writes; reject empty update payloads; normalize deletes to org membership + resource scoping.
    • Removed outdated organization auth tests; added focused task auth/scoping tests; isolated session org state tests in @superset/auth; trimmed unstable desktop workspace tests.
  • Bug Fixes

    • Enforced membership on task reads and before updates/deletes; filter all by the active org; prevent updates/deletes on already-deleted tasks.
    • Blocked cross-org changes with clear 400/404 errors for related IDs and ensured internal assignment clears external assignee snapshots.

Written for commit b2b5efe. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Unified and tightened organization-scoped authorization across projects, workspaces, tasks, secrets, and repository linking to block cross-organization access; create/update/delete now validate and operate on resolved scoped resources and enforce non-deleted constraints.
    • Several flows now require membership checks instead of broader admin checks and return clearer bad-request/forbidden errors when scoped resources are missing or mismatched.
  • Tests

    • Added comprehensive org-scoping and tenant-isolation tests for task behaviors; removed an outdated organization authorization test suite.
  • Chores

    • Deferred database initialization to a lazy cached loader to improve startup behavior.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds org-scoped resource resolution and access helpers, applies them across project/v2-project, workspace/v2-workspace, project secrets, and task routers; converts task endpoints to protected procedures; adds a comprehensive task test suite; removes organization router tests; and lazily loads the DB in session-org resolver.

Changes

Cohort / File(s) Summary
Authorization utilities
packages/trpc/src/router/utils/active-org.ts, packages/trpc/src/router/utils/org-resource-access.ts
Add requireActiveOrgId, requireActiveOrgMembership, requireOrgScopedResource, requireOrgResourceAccess, and OrgScopedResource type to centralize active-org retrieval and org-scoped resource validation + access checks.
Project routers (classic & v2)
packages/trpc/src/router/project/project.ts, packages/trpc/src/router/v2-project/v2-project.ts
Introduce getProjectAccess, getScopedGithubRepository; validate and resolve GitHub repo and project scope on create/update/delete; persist validated resource IDs; replace inline org predicates/admin checks with helpers and remove and usage.
Project secrets
packages/trpc/src/router/project/secrets/secrets.ts
Add getScopedProject and getSecretAccess; upsert/delete/getDecrypted now resolve project/secret via scoped helpers and use resolved IDs and organizationId for queries.
Workspace routers (classic & v2)
packages/trpc/src/router/workspace/workspace.ts, packages/trpc/src/router/v2-workspace/v2-workspace.ts
Add scoped lookup helpers (getScopedProject, getScopedWorkspace, getWorkspaceAccess, getScopedDevice); replace inline membership/admin checks with shared utilities; create/update/delete use resolved IDs and drop compound (id AND organizationId) predicates.
Task router
packages/trpc/src/router/task/task.ts
Change several endpoints from publicProcedure to protectedProcedure; add getTaskAccess, getScopedStatusId, getScopedAssigneeId; enforce org-scoped, transaction-aware validations and constrain updates/deletes to non-deleted rows.
Task router tests
packages/trpc/src/router/task/task.test.ts
Add Bun test suite with extensive DB/tx mocks verifying org-scoped authorization, tenant isolation, transactional update behavior, sync integration, and expected returned txid.
Test removals
packages/trpc/src/router/organization/organization.test.ts
Remove the organization router authorization test suite.
Auth / session org resolver
packages/auth/src/lib/resolve-session-organization-state.ts
Replace static DB import with lazy cached dynamic import (dbPromise/getDb()); make default dependency closures async and await getDb() at call time.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant TRPC as TRPC Router
    participant Auth as OrgAuthUtils
    participant DB as Database

    Client->>TRPC: call create/update/delete (payload, resourceId)
    TRPC->>Auth: requireActiveOrgId / requireActiveOrgMembership
    Auth-->>TRPC: organizationId or throws
    TRPC->>Auth: requireOrgScopedResource / requireOrgResourceAccess(resolveResource)
    Auth->>DB: resolveResource query (by id)
    DB-->>Auth: resource (with organizationId) or null
    Auth-->>TRPC: validated resource or throws
    TRPC->>DB: perform insert/update/delete using validated resource.id
    DB-->>TRPC: result
    TRPC-->>Client: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I hopped through checks both near and far,
Scoped each repo, project, and workspace star,
Helpers held the garden neat and tight,
Tests queued up to dance through day and night,
A carrot cheer — scoped access done right! 🥕

🚥 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.
Title check ❓ Inconclusive The title is vague and overly generic, using a broad term 'clean up' without specifying what validation logic was consolidated or refactored. Consider a more specific title like 'refactor: consolidate org-scoped router validation' or 'refactor: centralize auth checks into shared helpers' to clearly convey the main refactoring effort.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description adequately covers the main objectives, changes, testing approach, and includes both author-provided and auto-generated summaries.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch kitenite/org-ownership-check

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.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 24, 2026

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) View Branch
Fly.io Electric (Fly.io) View App
Vercel API (Vercel) Open Preview
Vercel Web (Vercel) Open Preview
Vercel Marketing (Vercel) Open Preview
Vercel Admin (Vercel) Open Preview
Vercel Docs (Vercel) Open Preview

Preview updates automatically with new commits

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/trpc/src/router/workspace/workspace.ts">

<violation number="1" location="packages/trpc/src/router/workspace/workspace.ts:189">
P2: The delete flow checks admin access only after loading the workspace, which allows ID enumeration via `NOT_FOUND` vs `FORBIDDEN` responses.</violation>
</file>

<file name="packages/trpc/src/router/task/task.test.ts">

<violation number="1" location="packages/trpc/src/router/task/task.test.ts:199">
P2: `beforeEach` reassigns `dbState`, but the module mock keeps using the original `db` instance, so DB-call assertions can become false positives.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/trpc/src/router/workspace/workspace.ts Outdated
Comment thread packages/trpc/src/router/task/task.test.ts
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.

🧹 Nitpick comments (4)
packages/trpc/src/router/task/task.ts (2)

409-412: Same observation: consider using resolved taskAccess.id in delete WHERE clause.

Similar to the update mutation, the delete WHERE clause uses input directly rather than a resolved identifier from getTaskAccess. Since this is within a transaction where getTaskAccess was just called, the values are the same, but using the resolved ID would be more consistent.

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

In `@packages/trpc/src/router/task/task.ts` around lines 409 - 412, The delete
mutation uses the raw input value in the tx.update WHERE clause instead of the
resolved identifier returned by getTaskAccess; change the WHERE predicate to use
the resolved taskAccess.id (from getTaskAccess) and keep the
isNull(tasks.deletedAt) check so you update the same authorized record
consistently (refer to tx.update, tasks, getTaskAccess, and taskAccess.id).

385-389: Consider using taskAccess.id in the WHERE clause for consistency.

The WHERE clause uses eq(tasks.id, id) where id is the raw input, while getTaskAccess returns the resolved taskAccess.id. Although functionally equivalent (same value), using taskAccess.id would be more consistent with the pattern established in other routers (e.g., project.ts line 138, workspace.ts line 197).

🔧 Optional: Use resolved taskAccess.id for consistency
 				const [task] = await tx
 					.update(tasks)
 					.set(updateData)
-					.where(and(eq(tasks.id, id), isNull(tasks.deletedAt)))
+					.where(and(eq(tasks.id, taskAccess.id), isNull(tasks.deletedAt)))
 					.returning();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trpc/src/router/task/task.ts` around lines 385 - 389, The WHERE
clause in the tx.update for tasks uses the raw input id (eq(tasks.id, id)) but
should use the resolved taskAccess.id for consistency with other routers; update
the condition in the transaction where call (the
tx.update(tasks).set(updateData).where(...) expression) to use eq(tasks.id,
taskAccess.id) while keeping the isNull(tasks.deletedAt) check intact so the
update targets the resolved task record.
packages/trpc/src/router/v2-workspace/v2-workspace.ts (1)

71-77: Consider varying the error message based on organization context.

The getWorkspaceAccess helper in this file always returns "Workspace not found", while the equivalent in workspace.ts varies the message ("Workspace not found in this organization" vs "Workspace not found") based on whether organizationId is provided. Consider aligning the behavior for consistency.

🔧 Optional: Vary error message based on organizationId presence
 async function getWorkspaceAccess(
 	userId: string,
 	workspaceId: string,
 	options?: {
 		access?: "admin" | "member";
 		organizationId?: string;
 	},
 ) {
 	return requireOrgResourceAccess(
 		userId,
 		() =>
 			dbWs.query.v2Workspaces.findFirst({
 				columns: {
 					id: true,
 					organizationId: true,
 				},
 				where: eq(v2Workspaces.id, workspaceId),
 			}),
 		{
 			access: options?.access,
-			message: "Workspace not found",
+			message: options?.organizationId
+				? "Workspace not found in this organization"
+				: "Workspace not found",
 			organizationId: options?.organizationId,
 		},
 	);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trpc/src/router/v2-workspace/v2-workspace.ts` around lines 71 - 77,
The current getWorkspaceAccess helper always returns the message "Workspace not
found"; change it to vary the message based on whether options.organizationId is
present (match the pattern used in workspace.ts): use "Workspace not found in
this organization" when organizationId is provided, otherwise "Workspace not
found". Update the object returned/constructed in getWorkspaceAccess (the block
that sets access/message/organizationId) to compute the message accordingly so
behavior is consistent with workspace.ts.
packages/trpc/src/router/v2-project/v2-project.ts (1)

145-156: Reuse the validated repository ID in the update payload.

The repo is already looked up here, but the write still uses input.githubRepositoryId. Writing repo.id instead keeps validation and persistence tied to the same object.

♻️ Small cleanup
-			if (input.githubRepositoryId) {
-				await getScopedGithubRepository(
-					project.organizationId,
-					input.githubRepositoryId,
-				);
-			}
+			const repo = input.githubRepositoryId
+				? await getScopedGithubRepository(
+						project.organizationId,
+						input.githubRepositoryId,
+					)
+				: undefined;
 
 			const data = {
-				githubRepositoryId: input.githubRepositoryId,
+				githubRepositoryId: repo?.id,
 				name: input.name,
 				slug: input.slug,
 			};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trpc/src/router/v2-project/v2-project.ts` around lines 145 - 156,
The lookup result from getScopedGithubRepository is not being used; change the
call to capture the returned repository (e.g., const repo = await
getScopedGithubRepository(...)) and then set data.githubRepositoryId to that
repo.id instead of input.githubRepositoryId so the update payload uses the
validated repository ID; update references in the surrounding block
(getScopedGithubRepository and the data object) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/trpc/src/router/task/task.ts`:
- Around line 409-412: The delete mutation uses the raw input value in the
tx.update WHERE clause instead of the resolved identifier returned by
getTaskAccess; change the WHERE predicate to use the resolved taskAccess.id
(from getTaskAccess) and keep the isNull(tasks.deletedAt) check so you update
the same authorized record consistently (refer to tx.update, tasks,
getTaskAccess, and taskAccess.id).
- Around line 385-389: The WHERE clause in the tx.update for tasks uses the raw
input id (eq(tasks.id, id)) but should use the resolved taskAccess.id for
consistency with other routers; update the condition in the transaction where
call (the tx.update(tasks).set(updateData).where(...) expression) to use
eq(tasks.id, taskAccess.id) while keeping the isNull(tasks.deletedAt) check
intact so the update targets the resolved task record.

In `@packages/trpc/src/router/v2-project/v2-project.ts`:
- Around line 145-156: The lookup result from getScopedGithubRepository is not
being used; change the call to capture the returned repository (e.g., const repo
= await getScopedGithubRepository(...)) and then set data.githubRepositoryId to
that repo.id instead of input.githubRepositoryId so the update payload uses the
validated repository ID; update references in the surrounding block
(getScopedGithubRepository and the data object) accordingly.

In `@packages/trpc/src/router/v2-workspace/v2-workspace.ts`:
- Around line 71-77: The current getWorkspaceAccess helper always returns the
message "Workspace not found"; change it to vary the message based on whether
options.organizationId is present (match the pattern used in workspace.ts): use
"Workspace not found in this organization" when organizationId is provided,
otherwise "Workspace not found". Update the object returned/constructed in
getWorkspaceAccess (the block that sets access/message/organizationId) to
compute the message accordingly so behavior is consistent with workspace.ts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bd8d7890-7be5-4b9e-bfbc-41a103d21391

📥 Commits

Reviewing files that changed from the base of the PR and between 5b76635 and 1bf22d6.

📒 Files selected for processing (9)
  • packages/trpc/src/router/project/project.ts
  • packages/trpc/src/router/project/secrets/secrets.ts
  • packages/trpc/src/router/task/task.test.ts
  • packages/trpc/src/router/task/task.ts
  • packages/trpc/src/router/utils/active-org.ts
  • packages/trpc/src/router/utils/org-resource-access.ts
  • packages/trpc/src/router/v2-project/v2-project.ts
  • packages/trpc/src/router/v2-workspace/v2-workspace.ts
  • packages/trpc/src/router/workspace/workspace.ts

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.

🧹 Nitpick comments (1)
packages/auth/src/lib/resolve-session-organization-state.ts (1)

21-29: Consider clearing cached promise on import failure.

The lazy loading pattern is appropriate for deferring db module initialization. However, if the dynamic import fails, the rejected promise is cached permanently, causing all subsequent getDb() calls to fail even if the underlying issue becomes resolvable.

♻️ Optional: Clear cache on rejection
 async function getDb() {
 	if (!dbPromise) {
-		dbPromise = import("@superset/db/client").then(({ db }) => db);
+		dbPromise = import("@superset/db/client").then(({ db }) => db).catch((err) => {
+			dbPromise = undefined;
+			throw err;
+		});
 	}

 	return dbPromise;
 }

For dynamic imports, failures are typically deterministic (module resolution errors), so this may be acceptable as-is if you prefer the simpler approach.

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

In `@packages/auth/src/lib/resolve-session-organization-state.ts` around lines 21
- 29, The cached Promise dbPromise may remain permanently rejected if the
dynamic import in getDb() fails; update getDb() to clear dbPromise when the
import rejects so subsequent calls retry: when creating dbPromise via
import("@superset/db/client").then(...), attach a .catch handler that sets
dbPromise = undefined before rethrowing the error, so dbPromise and getDb remain
lazy-retryable after failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/auth/src/lib/resolve-session-organization-state.ts`:
- Around line 21-29: The cached Promise dbPromise may remain permanently
rejected if the dynamic import in getDb() fails; update getDb() to clear
dbPromise when the import rejects so subsequent calls retry: when creating
dbPromise via import("@superset/db/client").then(...), attach a .catch handler
that sets dbPromise = undefined before rethrowing the error, so dbPromise and
getDb remain lazy-retryable after failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2681c961-859c-4a17-bcbf-1f8c31ee44c3

📥 Commits

Reviewing files that changed from the base of the PR and between 47332a7 and d76ab15.

📒 Files selected for processing (1)
  • packages/auth/src/lib/resolve-session-organization-state.ts

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/auth/src/lib/resolve-session-organization-state.ts">

<violation number="1">
P2: This top-level DB import makes DB client initialization unconditional and bypasses the lazy/DI behavior in this resolver. It can trigger unnecessary startup work (and potential env/runtime failures) even when no DB call is needed.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@Kitenite Kitenite merged commit d1e1ee2 into main Mar 25, 2026
14 checks passed
@Kitenite Kitenite deleted the kitenite/org-ownership-check branch March 25, 2026 05:42
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