UNPKG

okta-mcp-server

Version:

Model Context Protocol (MCP) server for Okta API operations with support for bulk operations and caching

971 lines 32.6 kB
import { z } from 'zod'; import { logger } from '../../utils/logger.js'; import { ListPoliciesSchema, GetPolicySchema, CreatePasswordPolicySchema, CreateSignOnPolicySchema, CreateMfaPolicySchema, UpdatePolicySchema, DeletePolicySchema, ActivatePolicySchema, DeactivatePolicySchema, ListPolicyRulesSchema, GetPolicyRuleSchema, CreatePolicyRuleSchema, UpdatePolicyRuleSchema, DeletePolicyRuleSchema, ActivatePolicyRuleSchema, DeactivatePolicyRuleSchema, PolicyResponseSchema, PolicyRuleResponseSchema, } from './schemas.js'; /** * Extract pagination cursor from Okta Link header */ function extractCursor(linkHeader) { if (!linkHeader) return undefined; const match = linkHeader.match(/<[^>]+after=([^&>]+)[^>]*>;\s*rel="next"/); return match ? match[1] : undefined; } /** * List Okta policies with pagination and filtering */ export async function handleListPolicies(args, client) { try { const params = ListPoliciesSchema.parse(args); // Make API call const requestParams = { type: params.type, limit: params.limit, sortOrder: params.sortOrder, }; // Only add optional params if they have values if (params.after) requestParams.after = params.after; if (params.status) requestParams.status = params.status; if (params.sortBy) requestParams.sortBy = params.sortBy; if (params.filter) requestParams.filter = params.filter; const response = await client.listPolicies(requestParams); logger.debug('Raw Okta response:', { dataLength: response.data.length, headers: response.headers, }); // Extract next cursor from Link header if present const nextCursor = extractCursor(response.headers?.link); // Validate and return response // Handle null conditions from Okta API const policies = response.data.map((policy) => { // Convert null values to undefined for optional fields if (policy.conditions === null) { policy.conditions = undefined; } if (policy.settings === null) { policy.settings = undefined; } if (policy.description === null) { policy.description = undefined; } return PolicyResponseSchema.parse(policy); }); return { content: [ { type: 'text', text: JSON.stringify({ policies, _links: nextCursor ? { next: { href: `cursor:${nextCursor}` } } : undefined, }, null, 2), }, ], }; } catch (error) { if (error instanceof z.ZodError) { return { content: [ { type: 'text', text: `Validation error: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`, }, ], isError: true, }; } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; logger.error('Error listing policies:', error); return { content: [ { type: 'text', text: `Error listing policies: ${errorMessage}`, }, ], isError: true, }; } } /** * Get a single Okta policy by ID */ export async function handleGetPolicy(args, client) { try { const params = GetPolicySchema.parse(args); const policy = await client.getPolicy(params.policyId, params.expand); // Validate response const validatedPolicy = PolicyResponseSchema.parse(policy); return { content: [ { type: 'text', text: JSON.stringify(validatedPolicy, null, 2), }, ], }; } catch (error) { if (error instanceof z.ZodError) { return { content: [ { type: 'text', text: `Validation error: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`, }, ], isError: true, }; } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; logger.error('Error getting policy:', error); return { content: [ { type: 'text', text: `Error getting policy: ${errorMessage}`, }, ], isError: true, }; } } /** * Create a new password policy */ export async function handleCreatePasswordPolicy(args, client) { try { const params = CreatePasswordPolicySchema.parse(args); const policyData = { name: params.name, type: 'PASSWORD', description: params.description, priority: params.priority, status: params.status, conditions: params.conditions, settings: params.settings, }; const policy = await client.createPolicy(policyData); // Validate response const validatedPolicy = PolicyResponseSchema.parse(policy); // Log audit event logger.info('Password policy created', { policyId: validatedPolicy.id, policyName: validatedPolicy.name, status: validatedPolicy.status, }); return { content: [ { type: 'text', text: JSON.stringify(validatedPolicy, null, 2), }, ], }; } catch (error) { if (error instanceof z.ZodError) { return { content: [ { type: 'text', text: `Validation error: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`, }, ], isError: true, }; } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; logger.error('Error creating password policy:', error); return { content: [ { type: 'text', text: `Error creating password policy: ${errorMessage}`, }, ], isError: true, }; } } /** * Create a new sign-on policy */ export async function handleCreateSignOnPolicy(args, client) { try { const params = CreateSignOnPolicySchema.parse(args); // Ensure we have default settings for sign-on policy const defaultSettings = { signon: { access: 'ALLOW', requireFactor: false, factorMode: '1FA', factorPromptMode: 'ALWAYS', rememberDeviceByDefault: false, factorLifetime: 0, session: { usePersistentCookie: false, maxSessionIdleMinutes: 120, maxSessionLifetimeMinutes: 720, }, }, }; // Ensure we have minimal default conditions const defaultConditions = { people: { groups: { include: ['EVERYONE'], }, }, }; const policyData = { name: params.name, type: 'OKTA_SIGN_ON', description: params.description, priority: params.priority || 1, status: params.status || 'ACTIVE', conditions: params.conditions || defaultConditions, settings: params.settings || defaultSettings, }; const policy = await client.createPolicy(policyData); // Validate response const validatedPolicy = PolicyResponseSchema.parse(policy); // Log audit event logger.info('Sign-on policy created', { policyId: validatedPolicy.id, policyName: validatedPolicy.name, status: validatedPolicy.status, }); return { content: [ { type: 'text', text: JSON.stringify(validatedPolicy, null, 2), }, ], }; } catch (error) { if (error instanceof z.ZodError) { return { content: [ { type: 'text', text: `Validation error: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`, }, ], isError: true, }; } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; logger.error('Error creating sign-on policy:', error); return { content: [ { type: 'text', text: `Error creating sign-on policy: ${errorMessage}`, }, ], isError: true, }; } } /** * Create a new MFA policy */ export async function handleCreateMfaPolicy(args, client) { try { const params = CreateMfaPolicySchema.parse(args); // Ensure we have default settings for MFA policy // MFA_ENROLL policies use authenticators array structure const defaultSettings = { authenticators: [ { key: 'okta_email', enroll: { self: 'OPTIONAL', }, }, { key: 'okta_sms', enroll: { self: 'OPTIONAL', }, }, ], }; // Ensure we have minimal default conditions const defaultConditions = { people: { groups: { include: ['EVERYONE'], }, }, }; const policyData = { name: params.name, type: 'MFA_ENROLL', description: params.description, priority: params.priority || 1, status: params.status || 'ACTIVE', conditions: params.conditions || defaultConditions, settings: params.settings || defaultSettings, }; const policy = await client.createPolicy(policyData); // Validate response const validatedPolicy = PolicyResponseSchema.parse(policy); // Log audit event logger.info('MFA policy created', { policyId: validatedPolicy.id, policyName: validatedPolicy.name, status: validatedPolicy.status, }); return { content: [ { type: 'text', text: JSON.stringify(validatedPolicy, null, 2), }, ], }; } catch (error) { if (error instanceof z.ZodError) { return { content: [ { type: 'text', text: `Validation error: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`, }, ], isError: true, }; } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; logger.error('Error creating MFA policy:', error); return { content: [ { type: 'text', text: `Error creating MFA policy: ${errorMessage}`, }, ], isError: true, }; } } /** * Update an existing Okta policy */ export async function handleUpdatePolicy(args, client) { try { const params = UpdatePolicySchema.parse(args); // First, get the policy to check if it's a system policy const existingPolicy = await client.getPolicy(params.policyId); // Check if this is a system policy if (existingPolicy.system === true) { logger.info('Attempted to update system policy', { policyId: params.policyId, policyName: existingPolicy.name, isSystem: existingPolicy.system, }); return { content: [ { type: 'text', text: `Cannot update policy ${params.policyId}: System policies (like "Default Policy") cannot be updated through the API. Only custom policies can be modified.`, }, ], isError: true, }; } const updateData = {}; // Only include fields that are being updated if (params.name !== undefined) updateData.name = params.name; if (params.description !== undefined) updateData.description = params.description; if (params.priority !== undefined) updateData.priority = params.priority; if (params.status !== undefined) updateData.status = params.status; if (params.conditions !== undefined) updateData.conditions = params.conditions; if (params.settings !== undefined) updateData.settings = params.settings; const policy = await client.updatePolicy(params.policyId, updateData); // Validate response const validatedPolicy = PolicyResponseSchema.parse(policy); // Log audit event logger.info('Policy updated', { policyId: validatedPolicy.id, policyName: validatedPolicy.name, updates: Object.keys(updateData), }); return { content: [ { type: 'text', text: JSON.stringify(validatedPolicy, null, 2), }, ], }; } catch (error) { if (error instanceof z.ZodError) { return { content: [ { type: 'text', text: `Validation error: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`, }, ], isError: true, }; } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; logger.error('Error updating policy:', error); return { content: [ { type: 'text', text: `Error updating policy: ${errorMessage}`, }, ], isError: true, }; } } /** * Delete an Okta policy */ export async function handleDeletePolicy(args, client) { try { const params = DeletePolicySchema.parse(args); await client.deletePolicy(params.policyId); // Log audit event logger.info('Policy deleted', { policyId: params.policyId, }); return { content: [ { type: 'text', text: `Policy ${params.policyId} deleted successfully`, }, ], }; } catch (error) { if (error instanceof z.ZodError) { return { content: [ { type: 'text', text: `Validation error: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`, }, ], isError: true, }; } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; logger.error('Error deleting policy:', error); return { content: [ { type: 'text', text: `Error deleting policy: ${errorMessage}`, }, ], isError: true, }; } } /** * Activate an Okta policy */ export async function handleActivatePolicy(args, client) { try { const params = ActivatePolicySchema.parse(args); await client.activatePolicy(params.policyId); // Log audit event logger.info('Policy activated', { policyId: params.policyId, }); return { content: [ { type: 'text', text: `Policy ${params.policyId} activated successfully`, }, ], }; } catch (error) { if (error instanceof z.ZodError) { return { content: [ { type: 'text', text: `Validation error: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`, }, ], isError: true, }; } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; logger.error('Error activating policy:', error); return { content: [ { type: 'text', text: `Error activating policy: ${errorMessage}`, }, ], isError: true, }; } } /** * Deactivate an Okta policy */ export async function handleDeactivatePolicy(args, client) { try { const params = DeactivatePolicySchema.parse(args); await client.deactivatePolicy(params.policyId); // Log audit event logger.info('Policy deactivated', { policyId: params.policyId, }); return { content: [ { type: 'text', text: `Policy ${params.policyId} deactivated successfully`, }, ], }; } catch (error) { if (error instanceof z.ZodError) { return { content: [ { type: 'text', text: `Validation error: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`, }, ], isError: true, }; } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; logger.error('Error deactivating policy:', error); return { content: [ { type: 'text', text: `Error deactivating policy: ${errorMessage}`, }, ], isError: true, }; } } /** * List rules for a specific policy */ export async function handleListPolicyRules(args, client) { try { const params = ListPolicyRulesSchema.parse(args); const requestParams = { limit: params.limit, }; if (params.after) requestParams.after = params.after; const response = await client.listPolicyRules(params.policyId, requestParams); // Extract next cursor from Link header if present const nextCursor = extractCursor(response.headers?.link); // Validate and return response const rules = response.data.map((rule) => PolicyRuleResponseSchema.parse(rule)); return { content: [ { type: 'text', text: JSON.stringify({ rules, _links: nextCursor ? { next: { href: `cursor:${nextCursor}` } } : undefined, }, null, 2), }, ], }; } catch (error) { if (error instanceof z.ZodError) { return { content: [ { type: 'text', text: `Validation error: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`, }, ], isError: true, }; } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; logger.error('Error listing policy rules:', error); return { content: [ { type: 'text', text: `Error listing policy rules: ${errorMessage}`, }, ], isError: true, }; } } /** * Get a specific policy rule */ export async function handleGetPolicyRule(args, client) { try { const params = GetPolicyRuleSchema.parse(args); const rule = await client.getPolicyRule(params.policyId, params.ruleId); // Validate response const validatedRule = PolicyRuleResponseSchema.parse(rule); return { content: [ { type: 'text', text: JSON.stringify(validatedRule, null, 2), }, ], }; } catch (error) { if (error instanceof z.ZodError) { return { content: [ { type: 'text', text: `Validation error: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`, }, ], isError: true, }; } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; logger.error('Error getting policy rule:', error); return { content: [ { type: 'text', text: `Error getting policy rule: ${errorMessage}`, }, ], isError: true, }; } } /** * Create a new policy rule */ export async function handleCreatePolicyRule(args, client) { try { const params = CreatePolicyRuleSchema.parse(args); // Get the parent policy to determine the rule type const parentPolicy = await client.getPolicy(params.policyId); // Map policy type to rule type const getRuleTypeFromPolicyType = (policyType) => { switch (policyType) { case 'PASSWORD': return 'PASSWORD'; case 'OKTA_SIGN_ON': return 'SIGN_ON'; case 'MFA_ENROLL': return 'MFA_ENROLL'; case 'IDP_DISCOVERY': return 'IDP_DISCOVERY'; case 'PROFILE_ENROLLMENT': return 'PROFILE_ENROLLMENT'; default: return policyType; // Fallback to policy type } }; const ruleData = { name: params.name, priority: params.priority, status: params.status, type: getRuleTypeFromPolicyType(parentPolicy.type), conditions: params.conditions, actions: params.actions, }; const rule = await client.createPolicyRule(params.policyId, ruleData); // Validate response const validatedRule = PolicyRuleResponseSchema.parse(rule); // Log audit event logger.info('Policy rule created', { policyId: params.policyId, ruleId: validatedRule.id, ruleName: validatedRule.name, status: validatedRule.status, }); return { content: [ { type: 'text', text: JSON.stringify(validatedRule, null, 2), }, ], }; } catch (error) { if (error instanceof z.ZodError) { return { content: [ { type: 'text', text: `Validation error: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`, }, ], isError: true, }; } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; logger.error('Error creating policy rule:', error); return { content: [ { type: 'text', text: `Error creating policy rule: ${errorMessage}`, }, ], isError: true, }; } } /** * Update an existing policy rule */ export async function handleUpdatePolicyRule(args, client) { try { const params = UpdatePolicyRuleSchema.parse(args); const updateData = {}; // Only include fields that are being updated if (params.name !== undefined) updateData.name = params.name; if (params.priority !== undefined) updateData.priority = params.priority; if (params.status !== undefined) updateData.status = params.status; if (params.conditions !== undefined) updateData.conditions = params.conditions; if (params.actions !== undefined) updateData.actions = params.actions; const rule = await client.updatePolicyRule(params.policyId, params.ruleId, updateData); // Validate response const validatedRule = PolicyRuleResponseSchema.parse(rule); // Log audit event logger.info('Policy rule updated', { policyId: params.policyId, ruleId: validatedRule.id, ruleName: validatedRule.name, updates: Object.keys(updateData), }); return { content: [ { type: 'text', text: JSON.stringify(validatedRule, null, 2), }, ], }; } catch (error) { if (error instanceof z.ZodError) { return { content: [ { type: 'text', text: `Validation error: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`, }, ], isError: true, }; } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; logger.error('Error updating policy rule:', error); return { content: [ { type: 'text', text: `Error updating policy rule: ${errorMessage}`, }, ], isError: true, }; } } /** * Delete a policy rule */ export async function handleDeletePolicyRule(args, client) { try { const params = DeletePolicyRuleSchema.parse(args); // First, get the rule to check if it's a system rule const existingRule = await client.getPolicyRule(params.policyId, params.ruleId); // Check if this is a system rule if (existingRule.system === true) { logger.info('Attempted to delete system policy rule', { policyId: params.policyId, ruleId: params.ruleId, ruleName: existingRule.name, isSystem: existingRule.system, }); return { content: [ { type: 'text', text: `Cannot delete policy rule ${params.ruleId}: System rules (like "Default Rule") cannot be deleted through the API. Only custom rules can be deleted.`, }, ], isError: true, }; } await client.deletePolicyRule(params.policyId, params.ruleId); // Log audit event logger.info('Policy rule deleted', { policyId: params.policyId, ruleId: params.ruleId, ruleName: existingRule.name, }); return { content: [ { type: 'text', text: `Policy rule ${params.ruleId} deleted successfully from policy ${params.policyId}`, }, ], }; } catch (error) { if (error instanceof z.ZodError) { return { content: [ { type: 'text', text: `Validation error: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`, }, ], isError: true, }; } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; logger.error('Error deleting policy rule:', error); return { content: [ { type: 'text', text: `Error deleting policy rule: ${errorMessage}`, }, ], isError: true, }; } } /** * Activate a policy rule */ export async function handleActivatePolicyRule(args, client) { try { const params = ActivatePolicyRuleSchema.parse(args); await client.activatePolicyRule(params.policyId, params.ruleId); // Log audit event logger.info('Policy rule activated', { policyId: params.policyId, ruleId: params.ruleId, }); return { content: [ { type: 'text', text: `Policy rule ${params.ruleId} activated successfully in policy ${params.policyId}`, }, ], }; } catch (error) { if (error instanceof z.ZodError) { return { content: [ { type: 'text', text: `Validation error: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`, }, ], isError: true, }; } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; logger.error('Error activating policy rule:', error); return { content: [ { type: 'text', text: `Error activating policy rule: ${errorMessage}`, }, ], isError: true, }; } } /** * Deactivate a policy rule */ export async function handleDeactivatePolicyRule(args, client) { try { const params = DeactivatePolicyRuleSchema.parse(args); await client.deactivatePolicyRule(params.policyId, params.ruleId); // Log audit event logger.info('Policy rule deactivated', { policyId: params.policyId, ruleId: params.ruleId, }); return { content: [ { type: 'text', text: `Policy rule ${params.ruleId} deactivated successfully in policy ${params.policyId}`, }, ], }; } catch (error) { if (error instanceof z.ZodError) { return { content: [ { type: 'text', text: `Validation error: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`, }, ], isError: true, }; } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; logger.error('Error deactivating policy rule:', error); return { content: [ { type: 'text', text: `Error deactivating policy rule: ${errorMessage}`, }, ], isError: true, }; } } //# sourceMappingURL=handlers.js.map