diff --git a/docker-compose.yml b/docker-compose.yml index 9b56e542d..72a136eb5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,6 @@ services: - NEXTAUTH_URL=http://localhost:3002 - NEXTAUTH_SECRET=gVRJMFKtjZmfNuLt3ZoxWTMujSwkig - IAM_GUEST_CONVERSION_REFERENCE_SECRET=odZhhAyEfv9iVi7IHKh4XsEo - - IAM_MCP_ACCESS_ENCRYPTION_SECRET=oruOYeLk8Ykqbjrf3J1qvVgD - SHARING_ENCRYPTION_SECRET=m2o0JBqYcDZxk7FUDrg0NBeq - PROCEED_PUBLIC_IAM_ACTIVE=true - PROCEED_PUBLIC_IAM_LOGIN_USER_PASSWORD_ACTIVE=true @@ -19,6 +18,7 @@ services: - PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE=true - PROCEED_PUBLIC_PROCESS_AUTOMATION_TASK_EDITOR_ACTIVE=true - PROCEED_PUBLIC_COMPETENCE_MATCHING_ACTIVE=true + - PROCEED_PUBLIC_MCP_ACTIVE=true ports: - '3002:33081' depends_on: diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/rolePermissions.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/rolePermissions.tsx index f9bbd0ec1..da04e509e 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/rolePermissions.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/rolePermissions.tsx @@ -181,8 +181,8 @@ const basePermissionOptions: PermissionCategory[] = [ { key: 'Manage Executions', title: 'Manage Executions', - description: 'Allows a user to to start, modify and delete process executions.', - permission: 'view', + description: 'Allows a user to start, modify and delete process executions.', + permission: 'manage', }, ], }, diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/spaces/environments-page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/spaces/environments-page.tsx index d91fc0137..97162c035 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/spaces/environments-page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/spaces/environments-page.tsx @@ -3,7 +3,7 @@ import Bar from '@/components/bar'; import { OrganizationEnvironment } from '@/lib/data/environment-schema'; import { App, Button, Space } from 'antd'; -import { FC } from 'react'; +import { FC, use } from 'react'; import useFuzySearch, { ReplaceKeysWithHighlighted } from '@/lib/useFuzySearch'; import ElementList from '@/components/item-list-view'; import Link from 'next/link'; @@ -14,6 +14,7 @@ import { useRouter } from 'next/navigation'; import { SettingOutlined } from '@ant-design/icons'; import { getPairingCode } from '@/lib/data/mcp-authorization'; import { isUserErrorResponse } from '@/lib/user-error'; +import { EnvVarsContext } from '@/components/env-vars-context'; const highlightedKeys = ['name', 'description'] as const; export type FilteredEnvironment = ReplaceKeysWithHighlighted< @@ -38,6 +39,8 @@ const EnvironmentsPage: FC<{ transformData: (results) => results.map((result) => result.item), }); + const env = use(EnvVarsContext); + const handleCreateAccessCode = async (environmentId: string) => { const code = await getPairingCode(environmentId); @@ -76,9 +79,11 @@ const EnvironmentsPage: FC<{ - + {env.PROCEED_PUBLIC_MCP_ACTIVE && ( + + )} {environment.isOrganization && ( { const { user } = await getCurrentUser(); @@ -21,6 +22,11 @@ const getUserData = async () => { export const getPairingCode = async (environmentId: string) => { try { + const msConfig = await getMSConfig(); + if (!msConfig.PROCEED_PUBLIC_MCP_ACTIVE) { + return userError('Not available.'); + } + const user = await getUserData(); if (isUserErrorResponse(user)) return user; @@ -51,6 +57,11 @@ export const getPairingCode = async (environmentId: string) => { export const getPairingInfo = async (code: string) => { try { + const msConfig = await getMSConfig(); + if (!msConfig.PROCEED_PUBLIC_MCP_ACTIVE) { + return userError('Not available.'); + } + const codeHash = await getTokenHash(code); const pairingInfo = await _getPairingCodeInfo(codeHash); @@ -69,6 +80,11 @@ export const getPairingInfo = async (code: string) => { }; export const revokePairingCodes = async () => { + const msConfig = await getMSConfig(); + if (!msConfig.PROCEED_PUBLIC_MCP_ACTIVE) { + return userError('Not available.'); + } + const user = await getUserData(); if (isUserErrorResponse(user)) return user; diff --git a/src/management-system-v2/lib/data/space-settings.ts b/src/management-system-v2/lib/data/space-settings.ts index 9bc02c68c..e1da2d73a 100644 --- a/src/management-system-v2/lib/data/space-settings.ts +++ b/src/management-system-v2/lib/data/space-settings.ts @@ -9,11 +9,15 @@ import { populateSpaceSettingsGroup as _populateSpaceSettingsGroup, updateSpaceSettings as _updateSpaceSettings, } from '@/lib/data/db/space-settings'; -import { UnauthorizedError } from '../ability/abilityHelper'; +import Ability, { UnauthorizedError } from '../ability/abilityHelper'; -export async function getSpaceSettingsValues(spaceId: string, searchKey: string) { +export async function getSpaceSettingsValues( + spaceId: string, + searchKey: string, + ability?: Ability, +) { try { - const { ability } = await getCurrentEnvironment(spaceId); + if (!ability) ({ ability } = await getCurrentEnvironment(spaceId)); return await _getSpaceSettingsValues(spaceId, searchKey, ability); } catch (e) { if (e instanceof UnauthorizedError) diff --git a/src/management-system-v2/lib/mcp-utils.ts b/src/management-system-v2/lib/mcp-utils.ts index 063181fee..dd6caed16 100644 --- a/src/management-system-v2/lib/mcp-utils.ts +++ b/src/management-system-v2/lib/mcp-utils.ts @@ -1,8 +1,11 @@ import { z } from 'zod'; -import { env } from './ms-config/env-vars'; import { getAbilityForUser } from './authorization/authorization'; import { getPairingInfo } from './data/mcp-authorization'; import { isUserErrorResponse } from './user-error'; +import { getSpaceSettingsValues } from './data/space-settings'; +import { ResourceActionType, ResourceType } from './ability/caslAbility'; +import { getMSConfig, getPublicMSConfig } from './ms-config/ms-config'; +import { PublicMSConfig } from './ms-config/config-schema'; export const authorizationInfoSchema = { userCode: z @@ -19,6 +22,11 @@ export function toAuthorizationSchema>( } export async function verifyCode(code: string) { + const msConfig = await getMSConfig(); + if (!msConfig.PROCEED_PUBLIC_MCP_ACTIVE) { + throw new Error('MCP feature is disabled.'); + } + if (!code) throw new Error('Invalid user code.'); const info = await getPairingInfo(code); @@ -31,3 +39,35 @@ export async function verifyCode(code: string) { return { userId, environmentId, ability }; } + +export async function isAccessible( + userId: string, + spaceId: string, + requiredEnvVars: (keyof PublicMSConfig)[] = [], + configValues: string[] = [], + permissions: [ResourceActionType, ResourceType][] = [], +) { + const msConfig = await getPublicMSConfig(); + if (requiredEnvVars.some((eV) => !msConfig[eV])) return false; + + const ability = await getAbilityForUser(userId, spaceId); + + for (const cV of configValues) { + // allow values that are defined with subpath (e.g. 'process-automation.tasklist') + const [settingName, ...path] = cV.split('.'); + let settings = await getSpaceSettingsValues(spaceId, settingName, ability); + if (isUserErrorResponse(settings)) return false; + if (settings?.active === false) return false; + let subSetting = settings; + for (let i = 0; i < path.length && !!subSetting; ++i) { + if (subSetting[path[i]]?.active === false) return false; + settings = subSetting[path[i]]; + } + } + + for (const [action, resource] of permissions) { + if (!ability.can(action, resource)) return false; + } + + return true; +} diff --git a/src/management-system-v2/lib/ms-config/config-schema.ts b/src/management-system-v2/lib/ms-config/config-schema.ts index b5aed16dc..98796c1b9 100644 --- a/src/management-system-v2/lib/ms-config/config-schema.ts +++ b/src/management-system-v2/lib/ms-config/config-schema.ts @@ -9,7 +9,6 @@ export const mSConfigEnvironmentOnlyKeys = [ 'NEXTAUTH_SECRET', 'IAM_ORG_USER_INVITATION_ENCRYPTION_SECRET', 'IAM_GUEST_CONVERSION_REFERENCE_SECRET', - 'IAM_MCP_ACCESS_ENCRYPTION_SECRET', 'SHARING_ENCRYPTION_SECRET', 'DATABASE_URL', @@ -72,6 +71,8 @@ export const msConfigSchema = { }), PROCEED_PUBLIC_CONFIG_SERVER_ACTIVE: z.string().default('FALSE').transform(boolParser), + PROCEED_PUBLIC_MCP_ACTIVE: z.string().default('FALSE').transform(boolParser), + NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), NEXTAUTH_URL: z @@ -91,7 +92,6 @@ export const msConfigSchema = { IAM_ORG_USER_INVITATION_ENCRYPTION_SECRET: z.string().default(''), SHARING_ENCRYPTION_SECRET: z.string().default(''), IAM_GUEST_CONVERSION_REFERENCE_SECRET: z.string().default(''), - IAM_MCP_ACCESS_ENCRYPTION_SECRET: z.string().optional().default(''), PROCEED_PUBLIC_STORAGE_DEPLOYMENT_ENV: z .enum(['cloud', 'local']) @@ -193,9 +193,6 @@ export const msConfigSchema = { IAM_GUEST_CONVERSION_REFERENCE_SECRET: z .string() .default('T8VB/r1dw0kJAXjanUvGXpDb+VRr4dV5y59BT9TBqiQ='), - IAM_MCP_ACCESS_ENCRYPTION_SECRET: z - .string() - .default('d0nb2+Jm1Ur1TQCAFrcH9M1FfRu6bJmL6LkuLslQUBE='), SCHEDULER_TOKEN: z.string().default('T8VB/r1dw0kJAXjanUvGXpDb+VRr4dV5y59BT9TBqiQ='), DATABASE_URL: z.string({ required_error: 'DATABASE_URL not in environment variables, try running `yarn dev-ms-db`', diff --git a/src/management-system-v2/prompts/test-prompt.ts b/src/management-system-v2/prompts/test-prompt.ts deleted file mode 100644 index fc4442b89..000000000 --- a/src/management-system-v2/prompts/test-prompt.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { z } from 'zod'; -import { type InferSchema, type PromptMetadata } from 'xmcp'; - -// Define the schema for prompt parameters -export const schema = { - code: z.string().describe('The code to review'), -}; - -// Define prompt metadata -export const metadata: PromptMetadata = { - name: 'analyze-script', - title: 'Review Script', - description: 'Review script for best practices and potential issues', - role: 'user', -}; - -export default function reviewScript({ code }: InferSchema) { - return `Please review this script for: - - Code quality and best practices - - Potential bugs or security issues - - Performance optimizations - - Readability and maintainability - - Code to review: - \`\`\` - ${code} - \`\`\` - `; -} diff --git a/src/management-system-v2/resources/(processes)/bpmn.ts b/src/management-system-v2/resources/(processes)/bpmn.ts deleted file mode 100644 index f3cc71ff6..000000000 --- a/src/management-system-v2/resources/(processes)/bpmn.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { type ResourceMetadata } from 'xmcp'; - -export const metadata: ResourceMetadata = { - name: 'process-bpmn', - title: 'Process BPMN', - description: 'Business Process Model and Notation', -}; - -export default function handler() { - return ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -`; -} diff --git a/src/management-system-v2/tools/getAccessibleTools.ts b/src/management-system-v2/tools/getAccessibleTools.ts new file mode 100644 index 000000000..cd9a73d94 --- /dev/null +++ b/src/management-system-v2/tools/getAccessibleTools.ts @@ -0,0 +1,81 @@ +import { type InferSchema } from 'xmcp'; +import { isAccessible, toAuthorizationSchema, verifyCode } from '@/lib/mcp-utils'; +import { isUserErrorResponse } from '@/lib/user-error'; + +// Define the schema for tool parameters +export const schema = toAuthorizationSchema({}); + +// Define tool metadata +export const metadata = { + name: 'get-accessible-tools', + description: + 'Get all of the available tools that the current user can access in the current space. Some of the tools that are exposed through MCP might be blocked due to the configuration of the space or the permissions of the user in the space.', + annotations: { + title: 'Get Available Tools', + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + }, +}; + +// Tool implementation +export default async function getAvailableTools({ userCode }: InferSchema) { + try { + const verification = await verifyCode(userCode); + + if (isUserErrorResponse(verification)) return `Error: ${verification.error.message}`; + + const { userId, environmentId } = verification; + + const canAccessProcesses = await isAccessible( + userId, + environmentId, + ['PROCEED_PUBLIC_PROCESS_DOCUMENTATION_ACTIVE'], + ['process-documentation'], + [['view', 'Process']], + ); + + let canAccessTasks = await isAccessible( + userId, + environmentId, + ['PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE'], + ['process-automation.tasklist'], + [['view', 'Task']], + ); + + let canAccessInstances = await isAccessible( + userId, + environmentId, + ['PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE'], + ['process-automation.executions'], + [['view', 'Execution']], + ); + + let canCreateInstances = await isAccessible( + userId, + environmentId, + ['PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE'], + ['process-automation.executions'], + [['create', 'Execution']], + ); + + const tools = { + 'get-processes': canAccessProcesses, + 'get-process-info': canAccessProcesses, + 'start-process': canCreateInstances, + 'get-organization-data': true, + 'get-user-data': true, + }; + + const result = Object.entries(tools) + .filter(([_, accessible]) => accessible) + .map(([name]) => name); + + return { + content: [{ type: 'text', text: JSON.stringify(result) }], + }; + } catch (err) { + if (err instanceof Error) return err.message; + else return 'Error: Something went wrong'; + } +} diff --git a/src/management-system-v2/tools/getAllProcesses.ts b/src/management-system-v2/tools/getAllProcesses.ts index 6cd39de9b..a699a11b0 100644 --- a/src/management-system-v2/tools/getAllProcesses.ts +++ b/src/management-system-v2/tools/getAllProcesses.ts @@ -1,6 +1,6 @@ import { type InferSchema } from 'xmcp'; import prisma from '@/lib/data/db'; -import { toAuthorizationSchema, verifyCode } from '@/lib/mcp-utils'; +import { isAccessible, toAuthorizationSchema, verifyCode } from '@/lib/mcp-utils'; import { isUserErrorResponse } from '@/lib/user-error'; import { getProcessLatestVersion } from '@/lib/data/db/process'; import { asyncMap } from '@/lib/helpers/javascriptHelpers'; @@ -27,7 +27,18 @@ export default async function getProcesses({ userCode }: InferSchema !!process.versions.length); if (!result) return `Error: No processes found.`; const processesWithLatestVersion = await asyncMap(result, async (process) => { - return getProcessLatestVersion(process.id, false); + const p: Omit>, 'processIds'> & { + processIds?: any[]; + } = await getProcessLatestVersion(process.id, false); + delete p.processIds; + return p; }); return { diff --git a/src/management-system-v2/tools/getProcessInfo.ts b/src/management-system-v2/tools/getProcessInfo.ts index 6ac0129f1..e616996a1 100644 --- a/src/management-system-v2/tools/getProcessInfo.ts +++ b/src/management-system-v2/tools/getProcessInfo.ts @@ -1,8 +1,9 @@ import { z } from 'zod'; import { type InferSchema } from 'xmcp'; -import { getProcessLatestVersion } from '@/lib/data/db/process'; -import { toAuthorizationSchema, verifyCode } from '@/lib/mcp-utils'; +import { getProcess, getProcessLatestVersion } from '@/lib/data/db/process'; +import { isAccessible, toAuthorizationSchema, verifyCode } from '@/lib/mcp-utils'; import { isUserErrorResponse } from '@/lib/user-error'; +import { toCaslResource } from '@/lib/ability/caslAbility'; // Define the schema for tool parameters export const schema = toAuthorizationSchema({ @@ -28,6 +29,26 @@ export default async function getProcessInfo({ processId, userCode }: InferSchem if (isUserErrorResponse(verification)) return `Error: ${verification.error.message}`; + const { userId, environmentId, ability } = verification; + + // check if the user can access processes in this space + let accessible = await isAccessible( + userId, + environmentId, + ['PROCEED_PUBLIC_PROCESS_DOCUMENTATION_ACTIVE'], + ['process-documentation'], + [['view', 'Process']], + ); + + // check if the user can access the specified process + // (the process might be in a directory that the user cannot access) + const internalProcess = await getProcess(processId); + accessible = accessible && ability.can('view', toCaslResource('Process', internalProcess)); + + if (!accessible) { + return 'Error: The user cannot access processes in this space. This might be due to a space wide setting or due to the user not having the permission to view processes or this specific process.'; + } + const process = await getProcessLatestVersion(processId, true); return process.bpmn; diff --git a/src/management-system-v2/tools/startProcess.ts b/src/management-system-v2/tools/startProcess.ts index a3f99246f..98453dacd 100644 --- a/src/management-system-v2/tools/startProcess.ts +++ b/src/management-system-v2/tools/startProcess.ts @@ -1,11 +1,12 @@ import { z } from 'zod'; import { type InferSchema } from 'xmcp'; -import { toAuthorizationSchema, verifyCode } from '@/lib/mcp-utils'; +import { isAccessible, toAuthorizationSchema, verifyCode } from '@/lib/mcp-utils'; import { isUserErrorResponse } from '@/lib/user-error'; import { getCorrectTargetEngines } from '@/lib/engines/server-actions'; import { deployProcess } from '@/lib/engines/deployment'; import { startInstanceOnMachine } from '@/lib/engines/instances'; -import { getProcessLatestVersion } from '@/lib/data/db/process'; +import { getProcess, getProcessLatestVersion } from '@/lib/data/db/process'; +import { toCaslResource } from '@/lib/ability/caslAbility'; // Define the schema for tool parameters export const schema = toAuthorizationSchema({ @@ -41,7 +42,27 @@ export default async function startProcess({ const verification = await verifyCode(userCode); if (isUserErrorResponse(verification)) return `Error: ${verification.error.message}`; - const { environmentId, ability } = verification; + const { userId, environmentId, ability } = verification; + + let accessible = await isAccessible( + userId, + environmentId, + ['PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE'], + ['process-automation.executions'], + [ + ['create', 'Execution'], + ['view', 'Machine'], + ], + ); + + // check if the user can access the specified process + // (the process might be in a directory that the user cannot access) + const internalProcess = await getProcess(processId); + accessible = accessible && ability.can('view', toCaslResource('Process', internalProcess)); + + if (!accessible) { + return 'Error: The user cannot start processes in this space. This might be due to a space wide setting or due to the user not having the permission to start processes.'; + } const process = await getProcessLatestVersion(processId, false); diff --git a/src/management-system-v2/xmcp.config.ts b/src/management-system-v2/xmcp.config.ts index 00c667390..b30486401 100644 --- a/src/management-system-v2/xmcp.config.ts +++ b/src/management-system-v2/xmcp.config.ts @@ -7,8 +7,8 @@ const config: XmcpConfig = { }, paths: { tools: 'tools', - prompts: 'prompts', - resources: 'resources', + prompts: false, + resources: false, }, };