UNPKG

powerplatform-mcp

Version:

PowerPlatform Model Context Protocol server

290 lines (289 loc) 13.9 kB
import { outputResult } from '../output.js'; const DEPTHS = ['Basic', 'Local', 'Deep', 'Global']; function parsePrivilegesArg(raw) { const trimmed = raw.trim(); if (!trimmed) return []; if (trimmed.startsWith('[')) { let parsed; try { parsed = JSON.parse(trimmed); } catch (err) { throw new Error(`--privileges JSON could not be parsed: ${err.message}`); } if (!Array.isArray(parsed)) { throw new Error('--privileges JSON must be an array'); } return parsed.map((item, idx) => { if (!item || typeof item !== 'object') { throw new Error(`--privileges[${idx}] must be an object`); } const obj = item; const privilegeId = typeof obj.privilegeId === 'string' ? obj.privilegeId : null; const depth = obj.depth; if (!privilegeId) throw new Error(`--privileges[${idx}].privilegeId is required`); if (!depth || !DEPTHS.includes(depth)) { throw new Error(`--privileges[${idx}].depth must be one of ${DEPTHS.join(', ')}`); } const businessUnitId = typeof obj.businessUnitId === 'string' ? obj.businessUnitId : undefined; return { privilegeId, depth, businessUnitId }; }); } // Shorthand: "<guid>:<Depth>,<guid>:<Depth>" return trimmed.split(',').map((token, idx) => { const [privilegeId, depth] = token.split(':').map((s) => s.trim()); if (!privilegeId) throw new Error(`--privileges token #${idx + 1} is missing a privilegeId`); if (!depth || !DEPTHS.includes(depth)) { throw new Error(`--privileges token #${idx + 1} depth must be one of ${DEPTHS.join(', ')}`); } return { privilegeId, depth: depth }; }); } export function registerSecurityRoleCommands(program, registry) { program .command('security-roles') .description('List security roles') .option('--solution <name>', 'Filter to roles in a specific solution') .option('--include-system', 'Include system roles (excluded by default)') .option('--include-privileges', 'Include privilege details for each role') .option('--max-records <n>', 'Maximum records to return', '100') .action(async (opts, command) => { const ctx = registry.getContext(command.optsWithGlobals().env); const service = ctx.getSecurityRoleService(); const result = await service.getSecurityRoles({ solutionUniqueName: opts.solution, excludeSystemRoles: !opts.includeSystem, maxRecords: parseInt(opts.maxRecords, 10), includePrivileges: opts.includePrivileges, }); const roles = result.value || []; const nameList = roles .slice(0, 10) .map((r) => `${r.name} (managed: ${r.ismanaged})`) .join('\n '); outputResult({ fileName: 'security-roles', data: result, summary: [ `Found ${roles.length} security roles:`, roles.length > 0 ? ` Roles:\n ${nameList}${roles.length > 10 ? '\n ...' : ''}` : '', ].filter(Boolean).join('\n'), }, ctx.environmentName); }); program .command('security-role-privileges <roleId>') .description('Get privileges for a security role') .option('--entity <name>', 'Filter by entity name') .option('--access-right <type>', 'Filter by access right (Read, Write, Create, Delete, etc.)') .action(async (roleId, opts, command) => { const ctx = registry.getContext(command.optsWithGlobals().env); const service = ctx.getSecurityRoleService(); const result = await service.getSecurityRolePrivileges(roleId, { entityFilter: opts.entity, accessRightFilter: opts.accessRight, }); const privileges = result.value || []; const nameList = privileges .slice(0, 15) .map((p) => String(p.name)) .join('\n '); outputResult({ fileName: `security-role-${roleId}-privileges`, data: result, summary: [ `Found ${privileges.length} privileges for role ${roleId}:`, privileges.length > 0 ? ` Privileges:\n ${nameList}${privileges.length > 15 ? '\n ...' : ''}` : '', ].filter(Boolean).join('\n'), }, ctx.environmentName); }); program .command('privileges') .description('List the system privilege catalog (use to discover privilegeId values)') .option('--entity <name>', 'Filter by privilege name contains (e.g. Account)') .option('--access-right <type>', 'Filter by access right (Read, Write, Create, Delete, Append, AppendTo, Assign, Share)') .option('--max-records <n>', 'Maximum records to return', '100') .action(async (opts, command) => { const ctx = registry.getContext(command.optsWithGlobals().env); const service = ctx.getSecurityRoleService(); const result = await service.listPrivileges({ entityFilter: opts.entity, accessRightFilter: opts.accessRight, maxRecords: parseInt(opts.maxRecords, 10), }); const privileges = result.value || []; const nameList = privileges .slice(0, 20) .map((p) => `${p.name} (${p.privilegeid})`) .join('\n '); outputResult({ fileName: 'privileges', data: result, summary: [ `Found ${privileges.length} privileges:`, privileges.length > 0 ? ` Sample:\n ${nameList}${privileges.length > 20 ? '\n ...' : ''}` : '', ].filter(Boolean).join('\n'), }, ctx.environmentName); }); program .command('create-security-role') .description('Create a new security role') .requiredOption('--name <name>', 'Role display name') .option('--bu <id>', 'Business unit ID (defaults to root BU)') .option('--description <desc>', 'Role description') .option('--solution <name>', 'Solution unique name (uses MSCRM.SolutionUniqueName so no separate add-solution-component is needed)') .action(async (opts, command) => { const ctx = registry.getContext(command.optsWithGlobals().env); const service = ctx.getSecurityRoleService(); const result = await service.createSecurityRole({ name: opts.name, businessUnitId: opts.bu, description: opts.description, solutionUniqueName: opts.solution, }); outputResult({ fileName: `create-security-role-${opts.name.replace(/\s+/g, '-').toLowerCase()}`, data: result, summary: [ `Created security role:`, ` Name: ${opts.name}`, opts.solution ? ` Solution: ${opts.solution}` : '', ` Role ID: ${result.roleId}`, ].filter(Boolean).join('\n'), }, ctx.environmentName); }); program .command('clone-security-role <sourceRoleId>') .description('Clone an existing security role (uses CloneAsRole; falls back to create-then-copy)') .option('--name <name>', 'Name for the new role') .option('--target-bu <id>', 'Target business unit ID (defaults to root BU)') .option('--solution <name>', 'Solution unique name to add the new role to') .action(async (sourceRoleId, opts, command) => { const ctx = registry.getContext(command.optsWithGlobals().env); const service = ctx.getSecurityRoleService(); const result = await service.cloneSecurityRole(sourceRoleId, { newName: opts.name, targetBusinessUnitId: opts.targetBu, solutionUniqueName: opts.solution, }); outputResult({ fileName: `clone-security-role-${result.roleId}`, data: result, summary: [ `Cloned security role:`, ` Source Role ID: ${sourceRoleId}`, ` New Role ID: ${result.roleId}`, opts.name ? ` Name: ${opts.name}` : '', opts.solution ? ` Solution: ${opts.solution}` : '', ].filter(Boolean).join('\n'), }, ctx.environmentName); }); program .command('update-security-role <roleId>') .description('Update a security role (name, description, business unit)') .option('--name <name>', 'New role name') .option('--description <desc>', 'New role description') .option('--bu <id>', 'New business unit ID') .option('--solution <name>', 'Solution unique name (MSCRM.SolutionUniqueName)') .action(async (roleId, opts, command) => { const ctx = registry.getContext(command.optsWithGlobals().env); const service = ctx.getSecurityRoleService(); await service.updateSecurityRole(roleId, { name: opts.name, description: opts.description, businessUnitId: opts.bu, solutionUniqueName: opts.solution, }); outputResult({ fileName: `update-security-role-${roleId}`, data: { roleId, ...opts }, summary: [ `Updated security role:`, ` Role ID: ${roleId}`, opts.name ? ` Name: ${opts.name}` : '', opts.description ? ` Description: ${opts.description}` : '', opts.bu ? ` Business Unit: ${opts.bu}` : '', ].filter(Boolean).join('\n'), }, ctx.environmentName); }); program .command('delete-security-role <roleId>') .description('Delete a security role (destructive — requires --yes)') .option('--yes', 'Confirm the deletion') .action(async (roleId, opts, command) => { if (!opts.yes) { console.error(`Refused to delete role ${roleId}: pass --yes to confirm.`); process.exitCode = 1; return; } const ctx = registry.getContext(command.optsWithGlobals().env); const service = ctx.getSecurityRoleService(); await service.deleteSecurityRole(roleId); console.log(`Deleted security role ${roleId}`); }); program .command('add-role-privileges <roleId>') .description("Add privileges to a role (additive). --privileges accepts JSON array or shorthand '<guid>:<Depth>,<guid>:<Depth>'.") .requiredOption('--privileges <spec>', 'JSON array [{privilegeId,depth,businessUnitId?}] or shorthand <guid>:<Basic|Local|Deep|Global>,...') .action(async (roleId, opts, command) => { const privileges = parsePrivilegesArg(opts.privileges); const ctx = registry.getContext(command.optsWithGlobals().env); const service = ctx.getSecurityRoleService(); await service.addRolePrivileges(roleId, privileges); outputResult({ fileName: `add-role-privileges-${roleId}`, data: { roleId, added: privileges }, summary: [ `Added ${privileges.length} privilege(s) to role ${roleId}:`, ` ${privileges.map((p) => `${p.privilegeId}:${p.depth}`).join('\n ')}`, ].join('\n'), }, ctx.environmentName); }); program .command('remove-role-privileges <roleId>') .description('Remove privileges from a role') .requiredOption('--privileges <ids>', 'Comma-separated privilegeId GUIDs to remove') .action(async (roleId, opts, command) => { const privilegeIds = opts.privileges.split(',').map((s) => s.trim()).filter(Boolean); if (privilegeIds.length === 0) { throw new Error('--privileges must contain at least one privilegeId'); } const ctx = registry.getContext(command.optsWithGlobals().env); const service = ctx.getSecurityRoleService(); await service.removeRolePrivileges(roleId, privilegeIds); outputResult({ fileName: `remove-role-privileges-${roleId}`, data: { roleId, removed: privilegeIds }, summary: [ `Removed ${privilegeIds.length} privilege(s) from role ${roleId}:`, ` ${privilegeIds.join('\n ')}`, ].join('\n'), }, ctx.environmentName); }); program .command('replace-role-privileges <roleId>') .description('Replace the full set of privileges on a role (destructive — requires --yes)') .requiredOption('--privileges <spec>', "JSON array [{privilegeId,depth,businessUnitId?}] or shorthand <guid>:<Basic|Local|Deep|Global>,... (use '[]' to clear)") .option('--yes', 'Confirm the destructive replace') .action(async (roleId, opts, command) => { if (!opts.yes) { console.error(`Refused to replace privileges on role ${roleId}: pass --yes to confirm.`); process.exitCode = 1; return; } const privileges = parsePrivilegesArg(opts.privileges); const ctx = registry.getContext(command.optsWithGlobals().env); const service = ctx.getSecurityRoleService(); await service.replaceRolePrivileges(roleId, privileges); outputResult({ fileName: `replace-role-privileges-${roleId}`, data: { roleId, applied: privileges }, summary: [ `Replaced privileges on role ${roleId} (${privileges.length} now applied):`, privileges.length > 0 ? ` ${privileges.map((p) => `${p.privilegeId}:${p.depth}`).join('\n ')}` : ' (none)', ].join('\n'), }, ctx.environmentName); }); }