Fix AI permission gating: use Ask AI for chat UI, AI Settings for admin endpoints (#21030)
## Summary Closes #20662. Two AI permission flags exist: - **`AI`** (label "Ask AI") — user-facing: chat with AI agents, use AI features - **`AI_SETTINGS`** (label "AI") — admin: create and configure AI agents After auditing every use of these flags I found: ### Frontend — chat UI gated by the admin permission (user-facing bug from the issue) A user granted only `Ask AI` could not see chat tabs, the "new chat" button (desktop & mobile), or the chat content pane; thread initialization was also skipped, leaving the chat in a half-initialized state and producing intermittent `THREAD_NOT_FOUND` errors. Switched these to `AI`: - `MainNavigationDrawerTabsRow.tsx` - `MainNavigationDrawer.tsx` - `MobileNavigationBar.tsx` - `AgentChatThreadInitializationEffect.tsx` ### Backend — admin-only resolvers gated by the user permission (privilege escalation) Two resolvers had a class-level guard of `AI`, letting any user with the user-facing flag reach admin endpoints (skill CRUD, eval runs). Switched the class-level guards to `AI_SETTINGS`: - `SkillResolver` — create/update/delete/activate/deactivate skills - `AgentTurnResolver` — read turns, run/grade evaluations ### Left as-is (already correct) - `AgentResolver` — class-level `AI` for reads (workflow editors and admin pages both need them), mutation-level `AI_SETTINGS` overrides for writes - `AgentChatResolver` & `AgentChatSubscriptionResolver` — already `AI` - `AiGenerateTextController` — already `AI` - Workspace AI config fields in `workspace.service.ts` — already `AI_SETTINGS` ## Test plan - [ ] As a user with `Ask AI` only (no `AI_SETTINGS`): chat tabs, "new chat" button, and chat history pane are visible on desktop + mobile; sending a message works; no `THREAD_NOT_FOUND` errors - [ ] As a user with `AI_SETTINGS` but no `Ask AI`: chat UI is hidden - [ ] As a user with `Ask AI` only: calling `skills` / `createSkill` / `agentTurns` / `runEvaluationInput` via GraphQL returns permission denied - [ ] As an admin (`AI_SETTINGS`): skill settings and agent eval pages still work
This commit is contained in:
+7
-15
@@ -31,9 +31,7 @@ import {
|
||||
export const AgentChatThreadInitializationEffect = () => {
|
||||
const client = useApolloClient();
|
||||
const { replaceDraft, applyChanges } = useUpdateMetadataStoreDraft();
|
||||
const hasAiSettingsPermission = useHasPermissionFlag(
|
||||
PermissionFlagType.AI_SETTINGS,
|
||||
);
|
||||
const hasAiPermission = useHasPermissionFlag(PermissionFlagType.AI);
|
||||
|
||||
const currentAiChatThread = useAtomStateValue(currentAiChatThreadState);
|
||||
const setCurrentAiChatThread = useSetAtomState(currentAiChatThreadState);
|
||||
@@ -58,7 +56,7 @@ export const AgentChatThreadInitializationEffect = () => {
|
||||
useAtomState(hasInitializedAgentChatThreadsState);
|
||||
|
||||
useEffect(() => {
|
||||
if (storeEntry.status !== 'empty' || !hasAiSettingsPermission) {
|
||||
if (storeEntry.status !== 'empty' || !hasAiPermission) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,19 +73,13 @@ export const AgentChatThreadInitializationEffect = () => {
|
||||
replaceDraft('agentChatThreads', result.data.chatThreads);
|
||||
applyChanges();
|
||||
});
|
||||
}, [
|
||||
storeEntry.status,
|
||||
hasAiSettingsPermission,
|
||||
client,
|
||||
replaceDraft,
|
||||
applyChanges,
|
||||
]);
|
||||
}, [storeEntry.status, hasAiPermission, client, replaceDraft, applyChanges]);
|
||||
|
||||
useEffect(() => {
|
||||
setAgentChatThreadsLoading(
|
||||
storeEntry.status === 'empty' && hasAiSettingsPermission,
|
||||
storeEntry.status === 'empty' && hasAiPermission,
|
||||
);
|
||||
}, [storeEntry.status, hasAiSettingsPermission, setAgentChatThreadsLoading]);
|
||||
}, [storeEntry.status, hasAiPermission, setAgentChatThreadsLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -97,7 +89,7 @@ export const AgentChatThreadInitializationEffect = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (storeEntry.status === 'empty' && hasAiSettingsPermission) {
|
||||
if (storeEntry.status === 'empty' && hasAiPermission) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -152,7 +144,7 @@ export const AgentChatThreadInitializationEffect = () => {
|
||||
}, [
|
||||
agentChatVisibleThreads,
|
||||
currentAiChatThread,
|
||||
hasAiSettingsPermission,
|
||||
hasAiPermission,
|
||||
hasInitializedAgentChatThreads,
|
||||
setHasInitializedAgentChatThreads,
|
||||
storeEntry.status,
|
||||
|
||||
@@ -16,12 +16,10 @@ export const MainNavigationDrawer = ({ className }: { className?: string }) => {
|
||||
navigationDrawerActiveTabState,
|
||||
);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const hasAiSettingsPermission = useHasPermissionFlag(
|
||||
PermissionFlagType.AI_SETTINGS,
|
||||
);
|
||||
const hasAiPermission = useHasPermissionFlag(PermissionFlagType.AI);
|
||||
|
||||
const showAiChatContent =
|
||||
hasAiSettingsPermission &&
|
||||
hasAiPermission &&
|
||||
navigationDrawerActiveTab === NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY;
|
||||
|
||||
return (
|
||||
|
||||
+2
-4
@@ -144,13 +144,11 @@ export const MainNavigationDrawerTabsRow = () => {
|
||||
const setIsNavigationDrawerExpanded = useSetAtomState(
|
||||
isNavigationDrawerExpandedState,
|
||||
);
|
||||
const hasAiSettingsPermission = useHasPermissionFlag(
|
||||
PermissionFlagType.AI_SETTINGS,
|
||||
);
|
||||
const hasAiPermission = useHasPermissionFlag(PermissionFlagType.AI);
|
||||
|
||||
const isExpanded = isNavigationDrawerExpanded || isMobile;
|
||||
|
||||
if (!hasAiSettingsPermission) {
|
||||
if (!hasAiPermission) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,9 +41,7 @@ export const MobileNavigationBar = () => {
|
||||
const { switchToNewChat } = useSwitchToNewAiChat();
|
||||
const { alphaSortedActiveNonSystemObjectMetadataItems } =
|
||||
useFilteredObjectMetadataItems();
|
||||
const hasAiSettingsPermission = useHasPermissionFlag(
|
||||
PermissionFlagType.AI_SETTINGS,
|
||||
);
|
||||
const hasAiPermission = useHasPermissionFlag(PermissionFlagType.AI);
|
||||
|
||||
const setContextStoreCurrentObjectMetadataItemId = useSetAtomComponentState(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState,
|
||||
@@ -100,7 +98,7 @@ export const MobileNavigationBar = () => {
|
||||
openRecordsSearchPage();
|
||||
},
|
||||
},
|
||||
...(hasAiSettingsPermission
|
||||
...(hasAiPermission
|
||||
? [
|
||||
{
|
||||
name: 'newAiChat' as const,
|
||||
|
||||
+4
-1
@@ -24,7 +24,10 @@ import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.serv
|
||||
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity';
|
||||
import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator';
|
||||
import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository';
|
||||
@UseGuards(WorkspaceAuthGuard, SettingsPermissionGuard(PermissionFlagType.AI))
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
SettingsPermissionGuard(PermissionFlagType.AI_SETTINGS),
|
||||
)
|
||||
@MetadataResolver()
|
||||
export class AgentTurnResolver {
|
||||
private readonly logger = new Logger(AgentTurnResolver.name);
|
||||
|
||||
@@ -16,7 +16,10 @@ import { SkillGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules
|
||||
import { SkillService } from 'src/engine/metadata-modules/skill/skill.service';
|
||||
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, SettingsPermissionGuard(PermissionFlagType.AI))
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
SettingsPermissionGuard(PermissionFlagType.AI_SETTINGS),
|
||||
)
|
||||
@UseInterceptors(
|
||||
WorkspaceMigrationGraphqlApiExceptionInterceptor,
|
||||
SkillGraphqlApiExceptionInterceptor,
|
||||
|
||||
Reference in New Issue
Block a user