Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code f6067c5005 fix: allow APPLICATION_ACCESS tokens to query GraphQL when defaultRoleId is null
https://sonarly.com/issue/36554?type=bug

Backend logic functions cannot authenticate to /graphql because applications without a manifest-defined role have null defaultRoleId, causing resolveRolePermissionConfig to return null and permission checks to fail.

Fix: ## Changes Made

Fixed APPLICATION_ACCESS token authentication by addressing two related issues:

### 1. Added missing `applicationId` parameter to settings permission checks
**File**: `packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts`

- Imported `isApplicationAuthContext` guard (line 39)
- Added `applicationId` parameter to `userHasWorkspaceSettingPermission()` call when auth context is an application (lines 302-304)

This ensures that when application contexts query system objects (apiKey, webhook), their permissions are properly checked via the application's defaultRoleId.

### 2. Added workspace defaultRoleId fallback for applications
**File**: `packages/twenty-server/src/engine/twenty-orm/utils/resolve-role-permission-config.util.ts`

Changed the application auth context branch (lines 34-40) from:
```typescript
if (
  isApplicationAuthContext(authContext) &&
  isDefined(authContext.application.defaultRoleId)
) {
  return { intersectionOf: [authContext.application.defaultRoleId] };
}
```

To:
```typescript
if (isApplicationAuthContext(authContext)) {
  const roleId =
    authContext.application.defaultRoleId ??
    authContext.workspace.defaultRoleId;

  if (isDefined(roleId)) {
    return { intersectionOf: [roleId] };
  }
}
```

This allows applications without a manifest-defined role to fall back to the workspace's default role (the same role assigned to new workspace members). This matches the pattern used elsewhere in the codebase and provides sensible default permissions for simple apps that don't define their own role.

## Why This Fixes The Issue

**Before**: Applications installed without `defaultRoleUniversalIdentifier` in their manifest had `defaultRoleId: null`. The `resolveRolePermissionConfig` utility returned `null` for these apps, causing all GraphQL queries to fail with "Authentication is required" regardless of the APPLICATION_ACCESS token being valid.

**After**: Applications without their own role inherit the workspace's default role, allowing backend logic functions to query workspace data using the same permissions as a newly invited workspace member. This enables the "install from npm and you're done" experience the SDK promises.

## Blast Radius

**Local** - Changes affect only application auth contexts in two isolated functions:
1. `validateSettingsPermissionsOnObjectOrThrow` (private method, single call site)
2. `resolveRolePermissionConfig` (12 call sites, all in permission resolution contexts)

No changes to API contracts, type signatures, or shared interfaces. The fallback behavior only activates when `application.defaultRoleId` is null, so existing apps with defined roles are unaffected.
2026-05-10 14:55:43 +00:00
2 changed files with 12 additions and 5 deletions
@@ -35,6 +35,7 @@ import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/
import { WorkspacePreQueryHookPayload } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
import { isApiKeyAuthContext } from 'src/engine/core-modules/auth/guards/is-api-key-auth-context.guard';
import { isApplicationAuthContext } from 'src/engine/core-modules/auth/guards/is-application-auth-context.guard';
import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@@ -299,6 +300,9 @@ export abstract class CommonBaseQueryRunnerService<
apiKeyId: isApiKeyAuthContext(authContext)
? authContext.apiKey.id
: undefined,
applicationId: isApplicationAuthContext(authContext)
? authContext.application.id
: undefined,
});
if (!userHasPermission) {
@@ -31,11 +31,14 @@ export const resolveRolePermissionConfig = ({
return { intersectionOf: [roleId] };
}
if (
isApplicationAuthContext(authContext) &&
isDefined(authContext.application.defaultRoleId)
) {
return { intersectionOf: [authContext.application.defaultRoleId] };
if (isApplicationAuthContext(authContext)) {
const roleId =
authContext.application.defaultRoleId ??
authContext.workspace.defaultRoleId;
if (isDefined(roleId)) {
return { intersectionOf: [roleId] };
}
}
if (isUserAuthContext(authContext)) {