Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion apps/mesh/src/core/context-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,10 +473,22 @@ async function authenticateRequest(
try {
const meshJwtPayload = await verifyMeshToken(token);
if (meshJwtPayload) {
// Look up user's organization role for admin/owner bypass
let role: string | undefined;
if (meshJwtPayload.sub) {
const membership = await db
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 3, 2026

Choose a reason for hiding this comment

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

P1: Role lookup query doesn't filter by organization. A user with memberships in multiple organizations could get the wrong role (e.g., 'member' role from Org A instead of 'owner' from Org B), breaking the admin/owner bypass. Add .where("member.organizationId", "=", meshJwtPayload.metadata?.organizationId) to scope the query.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/core/context-factory.ts, line 478:

<comment>Role lookup query doesn&#39;t filter by organization. A user with memberships in multiple organizations could get the wrong role (e.g., &#39;member&#39; role from Org A instead of &#39;owner&#39; from Org B), breaking the admin/owner bypass. Add `.where(&quot;member.organizationId&quot;, &quot;=&quot;, meshJwtPayload.metadata?.organizationId)` to scope the query.</comment>

<file context>
@@ -472,10 +472,22 @@ async function authenticateRequest(
+        // Look up user&#39;s organization role for admin/owner bypass
+        let role: string | undefined;
+        if (meshJwtPayload.sub) {
+          const membership = await db
+            .selectFrom(&quot;member&quot;)
+            .select([&quot;member.role&quot;])
</file context>

✅ Addressed in de852d5

.selectFrom("member")
.select(["member.role"])
.where("member.userId", "=", meshJwtPayload.sub)
.executeTakeFirst();
role = membership?.role;
Copy link

Choose a reason for hiding this comment

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

DB error in role lookup silently fails valid authentication

The new database queries for role lookup are placed inside try blocks designed only for JWT/API key validation errors. If the database query throws (connection error, timeout, etc.), the exception is caught by the outer catch block, causing valid authentication to silently fail. For Mesh JWT, there's no logging at all - the code just proceeds to try API key authentication. For API keys, the error is logged with a misleading message. A transient database error would cause authenticated users to unexpectedly lose access.

Additional Locations (1)

Fix in Cursor Fix in Web

}

return {
user: {
id: meshJwtPayload.sub,
connectionId: meshJwtPayload.metadata?.connectionId,
role,
Copy link

Choose a reason for hiding this comment

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

Missing top-level role breaks bypass in two code paths

The PR adds role to the user object for Mesh JWT and API key authentication, but fails to also set the top-level role property in the return object. Session auth correctly returns both user.role AND a top-level role. However, the top-level role is what's used by createBoundAuthClient (line 674) and the main AccessControl instance (line 705) for the built-in role bypass. Since authResult.role will be undefined for Mesh JWT and API key auth, the admin/owner bypass won't work in boundAuth.hasPermission() or the default access instance—only the proxy-specific AccessControl instances that use ctx.auth.user?.role will correctly bypass.

Additional Locations (1)

Fix in Cursor Fix in Web

},
permissions: meshJwtPayload.permissions,
organization: meshJwtPayload.metadata?.organizationId
Expand Down Expand Up @@ -505,9 +517,20 @@ async function authenticateRequest(
// API keys have permissions stored directly on them
const permissions = result.key.permissions as Permission | undefined;

// Look up user's organization role for admin/owner bypass
let role: string | undefined;
if (result.key.userId) {
const membership = await db
.selectFrom("member")
.select(["member.role"])
.where("member.userId", "=", result.key.userId)
.executeTakeFirst();
role = membership?.role;
}

return {
apiKeyId: result.key.id,
user: { id: result.key.userId }, // Include userId from API key
user: { id: result.key.userId, role }, // Include userId and role from membership
permissions, // Store the API key's permissions
organization: orgMetadata
? {
Expand Down