UNPKG

okta-mcp-server

Version:

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

684 lines 28.3 kB
import { UserGenerator } from './generators/user-generator.js'; import { GroupGenerator } from './generators/group-generator.js'; import { AppGenerator } from './generators/app-generator.js'; import { PolicyGenerator } from './generators/policy-generator.js'; import { PaginationHelper } from './middleware/pagination.js'; import { RateLimitSimulator } from './middleware/rate-limiter.js'; import { ErrorSimulator } from './middleware/error-simulator.js'; import { DelaySimulator } from './middleware/delay-simulator.js'; import { AuthSimulator } from './middleware/auth-simulator.js'; export class MockOktaClient { config; baseUrl = 'https://dev-12345.okta.com/api/v1'; // Data stores users = new Map(); groups = new Map(); apps = new Map(); policies = new Map(); userGroups = new Map(); // userId -> groupIds groupUsers = new Map(); // groupId -> userIds // Simulators auth; rateLimit; error; delay; // Generators userGenerator; groupGenerator; appGenerator; policyGenerator; constructor(config = {}) { this.config = { userCount: 1000, groupCount: 100, appCount: 50, policyCount: 20, enableAuth: true, enableRateLimit: true, enableErrors: true, enableDelays: true, errorRate: 0.02, networkConditions: 'normal', rateLimit: 600, rateLimitWindow: 60, ...config, }; // Initialize simulators this.auth = new AuthSimulator({ requireAuth: this.config.enableAuth }); this.rateLimit = new RateLimitSimulator({ limit: this.config.rateLimit, window: this.config.rateLimitWindow, }); this.error = new ErrorSimulator({ enabled: this.config.enableErrors, errorRate: this.config.errorRate, }); this.delay = new DelaySimulator({ enabled: this.config.enableDelays, baseDelay: 50, variability: 0.3, networkConditions: this.config.networkConditions, operationDelays: DelaySimulator.getRealisticDelays(), }); // Initialize generators with seed const generatorOptions = this.config.seed ? { seed: this.config.seed } : {}; this.userGenerator = new UserGenerator(generatorOptions); this.groupGenerator = new GroupGenerator(generatorOptions); this.appGenerator = new AppGenerator(generatorOptions); this.policyGenerator = new PolicyGenerator(generatorOptions); // Generate initial data this.generateInitialData(); } generateInitialData() { // Generate users for (let i = 0; i < this.config.userCount; i++) { const user = this.userGenerator.generate(); this.users.set(user.id, user); // Also index by login this.users.set(user.profile.login, user); } // Generate groups for (let i = 0; i < this.config.groupCount; i++) { const group = this.groupGenerator.generate(); this.groups.set(group.id, group); } // Generate apps for (let i = 0; i < this.config.appCount; i++) { const app = this.appGenerator.generate(); this.apps.set(app.id, app); } // Generate policies for (let i = 0; i < this.config.policyCount; i++) { const policy = this.policyGenerator.generate(); this.policies.set(policy.id, policy); } // Assign users to groups (simulate realistic distribution) this.assignUsersToGroups(); } assignUsersToGroups() { const userIds = Array.from(this.users.keys()).filter((key) => key.startsWith('00u')); const groupIds = Array.from(this.groups.keys()); // Each user belongs to 1-5 groups on average userIds.forEach((userId) => { const groupCount = Math.floor(Math.random() * 5) + 1; const userGroupSet = new Set(); for (let i = 0; i < groupCount && i < groupIds.length; i++) { const groupIndex = Math.floor(Math.random() * groupIds.length); const groupId = groupIds[groupIndex]; if (!groupId) continue; userGroupSet.add(groupId); // Update reverse mapping if (!this.groupUsers.has(groupId)) { this.groupUsers.set(groupId, new Set()); } this.groupUsers.get(groupId).add(userId); } this.userGroups.set(userId, userGroupSet); }); } // Simulate API request with middleware async simulateRequest(operation, handler, authHeader) { // Check authentication if (this.config.enableAuth && authHeader) { const authResult = this.auth.validateAuth(authHeader); if (!authResult.valid) { throw new Error(`Authentication failed: ${authResult.error}`); } } // Apply delay await this.delay.delay(operation); // Check rate limit const rateLimitCheck = this.rateLimit.check(); if (!rateLimitCheck.allowed) { const error = this.error.simulateAuthError(); error.code = 429; error.errorCode = 'E0000047'; error.errorSummary = 'API rate limit exceeded'; throw error; } // Simulate errors const error = this.error.generateError(); if (error) { throw error; } // Execute the handler const result = await handler(); // Check if result is already an ApiResponse if (result && typeof result === 'object' && 'data' in result) { const apiResponse = result; return { data: apiResponse.data, headers: { ...rateLimitCheck.headers, ...apiResponse.headers, }, }; } // Otherwise wrap in ApiResponse return { data: result, headers: rateLimitCheck.headers, }; } // User operations async listUsers(params) { return this.simulateRequest('listUsers', async () => { const allUsers = Array.from(this.users.values()).filter((user) => user.id.startsWith('00u')); // Filter out login-indexed entries let filteredUsers = [...allUsers]; // Apply filters if (params?.filter) { // Simple filter implementation filteredUsers = filteredUsers.filter((user) => { const filterLower = params.filter.toLowerCase(); return (user.profile.email.toLowerCase().includes(filterLower) || user.profile.firstName?.toLowerCase().includes(filterLower) || user.profile.lastName?.toLowerCase().includes(filterLower)); }); } if (params?.search) { // Simple search implementation filteredUsers = filteredUsers.filter((user) => { const searchLower = params.search.toLowerCase(); return JSON.stringify(user).toLowerCase().includes(searchLower); }); } if (params?.status) { // Filter by status filteredUsers = filteredUsers.filter((user) => user.status === params.status); } // Apply sorting if (params?.sortBy) { filteredUsers.sort((a, b) => { let aVal = a; let bVal = b; if (params.sortBy === 'lastUpdated') { aVal = new Date(a.lastUpdated).getTime(); bVal = new Date(b.lastUpdated).getTime(); } else if (params.sortBy === 'email') { aVal = a.profile.email; bVal = b.profile.email; } const comparison = aVal < bVal ? -1 : aVal > bVal ? 1 : 0; return params.sortOrder === 'desc' ? -comparison : comparison; }); } // Apply pagination const paginationOptions = {}; if (params?.limit !== undefined) paginationOptions.limit = params.limit; if (params?.after) paginationOptions.after = params.after; const paginated = PaginationHelper.paginate(filteredUsers, paginationOptions, `${this.baseUrl}/users`); return { data: paginated.data, headers: { ...paginated.headers, 'x-total-count': filteredUsers.length.toString(), }, }; }); } async getUser(userIdOrLogin) { return (await this.simulateRequest('getUser', () => { const user = this.users.get(userIdOrLogin); if (!user) { const error = this.error.generateError(); if (error) throw error; throw new Error(`User not found: ${userIdOrLogin}`); } return user; })).data; } async createUser(userData, queryParams) { return (await this.simulateRequest('createUser', () => { // Check if user already exists const existingUser = Array.from(this.users.values()).find((u) => u.profile.login === userData.profile['login'] || u.profile.email === userData.profile['email']); if (existingUser) { throw this.error.simulateConflictError('User'); } // Create new user const newUser = this.userGenerator.generate(); newUser.profile = { ...newUser.profile, ...userData.profile }; if (queryParams?.activate) { newUser.status = 'ACTIVE'; newUser.activated = new Date().toISOString(); } else { newUser.status = 'STAGED'; } this.users.set(newUser.id, newUser); this.users.set(newUser.profile.login, newUser); // Add to groups if specified if (userData.groupIds) { const userGroupSet = new Set(userData.groupIds); this.userGroups.set(newUser.id, userGroupSet); userData.groupIds.forEach((groupId) => { if (!this.groupUsers.has(groupId)) { this.groupUsers.set(groupId, new Set()); } this.groupUsers.get(groupId).add(newUser.id); }); } return newUser; })).data; } async updateUser(userId, userData) { return (await this.simulateRequest('updateUser', () => { const user = this.users.get(userId); if (!user) { throw new Error(`User not found: ${userId}`); } // Update user data if (userData.profile) { user.profile = { ...user.profile, ...userData.profile }; } user.lastUpdated = new Date().toISOString(); return user; })).data; } async deactivateUser(userId, _sendEmail = false) { await this.simulateRequest('deactivateUser', () => { const user = this.users.get(userId); if (!user) { throw new Error(`User not found: ${userId}`); } if (user.status !== 'ACTIVE') { throw new Error(`User is not active: ${userId}`); } user.status = 'DEPROVISIONED'; user.statusChanged = new Date().toISOString(); user.lastUpdated = new Date().toISOString(); }); } async deleteUser(userId) { await this.simulateRequest('deleteUser', () => { const user = this.users.get(userId); if (!user) { throw new Error(`User not found: ${userId}`); } if (user.status === 'ACTIVE') { throw new Error(`Cannot delete active user: ${userId}`); } // Remove user from all groups const userGroupSet = this.userGroups.get(userId); if (userGroupSet) { userGroupSet.forEach((groupId) => { const groupUserSet = this.groupUsers.get(groupId); if (groupUserSet) { groupUserSet.delete(userId); } }); this.userGroups.delete(userId); } // Remove user this.users.delete(userId); this.users.delete(user.profile.login); }); } async getUserGroups(userId, params) { return this.simulateRequest('getUserGroups', () => { const user = this.users.get(userId); if (!user) { throw new Error(`User not found: ${userId}`); } const userGroupIds = this.userGroups.get(userId) || new Set(); const userGroups = Array.from(userGroupIds) .map((groupId) => this.groups.get(groupId)) .filter((group) => group !== undefined); // Apply pagination const paginationOptions = {}; if (params?.limit !== undefined) paginationOptions.limit = params.limit; if (params?.after) paginationOptions.after = params.after; const paginated = PaginationHelper.paginate(userGroups, paginationOptions, `${this.baseUrl}/users/${userId}/groups`); return { data: paginated.data, headers: paginated.headers, }; }); } // Group operations async listGroups(params) { return this.simulateRequest('listGroups', () => { const allGroups = Array.from(this.groups.values()); let filteredGroups = [...allGroups]; // Apply filters if (params?.filter) { filteredGroups = filteredGroups.filter((group) => { const filterLower = params.filter.toLowerCase(); return (group.profile.name.toLowerCase().includes(filterLower) || group.profile.description?.toLowerCase().includes(filterLower)); }); } if (params?.search) { filteredGroups = filteredGroups.filter((group) => { const searchLower = params.search.toLowerCase(); return JSON.stringify(group).toLowerCase().includes(searchLower); }); } if (params?.type) { filteredGroups = filteredGroups.filter((group) => group.type === params.type); } // Apply sorting if (params?.sortBy) { filteredGroups.sort((a, b) => { let aVal = a; let bVal = b; if (params.sortBy === 'lastUpdated') { aVal = new Date(a.lastUpdated).getTime(); bVal = new Date(b.lastUpdated).getTime(); } else if (params.sortBy === 'name') { aVal = a.profile.name; bVal = b.profile.name; } const comparison = aVal < bVal ? -1 : aVal > bVal ? 1 : 0; return params.sortOrder === 'desc' ? -comparison : comparison; }); } // Apply pagination const paginationOptions = {}; if (params?.limit !== undefined) paginationOptions.limit = params.limit; if (params?.after) paginationOptions.after = params.after; const paginated = PaginationHelper.paginate(filteredGroups, paginationOptions, `${this.baseUrl}/groups`); return { data: paginated.data, headers: { ...paginated.headers, 'x-total-count': filteredGroups.length.toString(), }, }; }); } async getGroup(groupId) { return (await this.simulateRequest('getGroup', () => { const group = this.groups.get(groupId); if (!group) { const error = this.error.generateError(); if (error) throw error; throw new Error(`Group not found: ${groupId}`); } return group; })).data; } async createGroup(groupData) { return (await this.simulateRequest('createGroup', () => { // Check if group already exists const existingGroup = Array.from(this.groups.values()).find((g) => g.profile.name === groupData.profile.name); if (existingGroup) { throw this.error.simulateConflictError('Group'); } // Create new group const newGroup = this.groupGenerator.generate(); newGroup.profile = { ...newGroup.profile, ...groupData.profile }; this.groups.set(newGroup.id, newGroup); return newGroup; })).data; } async updateGroup(groupId, groupData) { return (await this.simulateRequest('updateGroup', () => { const group = this.groups.get(groupId); if (!group) { throw new Error(`Group not found: ${groupId}`); } // Update group data if (groupData.profile) { group.profile = { ...group.profile, ...groupData.profile }; } group.lastUpdated = new Date().toISOString(); return group; })).data; } async deleteGroup(groupId) { await this.simulateRequest('deleteGroup', () => { const group = this.groups.get(groupId); if (!group) { throw new Error(`Group not found: ${groupId}`); } // Remove all users from the group const groupUserSet = this.groupUsers.get(groupId); if (groupUserSet) { groupUserSet.forEach((userId) => { const userGroupSet = this.userGroups.get(userId); if (userGroupSet) { userGroupSet.delete(groupId); } }); this.groupUsers.delete(groupId); } // Remove group this.groups.delete(groupId); }); } async listGroupMembers(groupId, params) { return this.simulateRequest('listGroupMembers', () => { const group = this.groups.get(groupId); if (!group) { throw new Error(`Group not found: ${groupId}`); } const groupUserIds = this.groupUsers.get(groupId) || new Set(); const groupMembers = Array.from(groupUserIds) .map((userId) => this.users.get(userId)) .filter((user) => user !== undefined && user.id.startsWith('00u')); // Apply pagination const paginationOptions = {}; if (params?.limit !== undefined) paginationOptions.limit = params.limit; if (params?.after) paginationOptions.after = params.after; const paginated = PaginationHelper.paginate(groupMembers, paginationOptions, `${this.baseUrl}/groups/${groupId}/users`); return { data: paginated.data, headers: paginated.headers, }; }); } async addGroupMember(groupId, userId) { await this.simulateRequest('addGroupMember', () => { const group = this.groups.get(groupId); if (!group) { throw new Error(`Group not found: ${groupId}`); } const user = this.users.get(userId); if (!user) { throw new Error(`User not found: ${userId}`); } // Add user to group if (!this.groupUsers.has(groupId)) { this.groupUsers.set(groupId, new Set()); } this.groupUsers.get(groupId).add(userId); // Add group to user if (!this.userGroups.has(userId)) { this.userGroups.set(userId, new Set()); } this.userGroups.get(userId).add(groupId); // Update lastMembershipUpdated group.lastMembershipUpdated = new Date().toISOString(); }); } async removeGroupMember(groupId, userId) { await this.simulateRequest('removeGroupMember', () => { const group = this.groups.get(groupId); if (!group) { throw new Error(`Group not found: ${groupId}`); } const user = this.users.get(userId); if (!user) { throw new Error(`User not found: ${userId}`); } // Remove user from group const groupUserSet = this.groupUsers.get(groupId); if (groupUserSet) { groupUserSet.delete(userId); } // Remove group from user const userGroupSet = this.userGroups.get(userId); if (userGroupSet) { userGroupSet.delete(groupId); } // Update lastMembershipUpdated group.lastMembershipUpdated = new Date().toISOString(); }); } // Reset mock data reset() { this.users.clear(); this.groups.clear(); this.apps.clear(); this.policies.clear(); this.userGroups.clear(); this.groupUsers.clear(); this.userGenerator.reset(); this.groupGenerator.reset(); this.appGenerator.reset(); this.policyGenerator.reset(); this.generateInitialData(); } // Get statistics about mock data getStats() { return { users: this.users.size / 2, // Divide by 2 because we index by both id and login groups: this.groups.size, apps: this.apps.size, policies: this.policies.size, userGroupMappings: this.userGroups.size, }; } // Configure simulators setErrorRate(rate) { this.error = new ErrorSimulator({ enabled: this.config.enableErrors, errorRate: rate, }); } setNetworkConditions(conditions) { this.delay = new DelaySimulator({ enabled: this.config.enableDelays, baseDelay: 50, variability: 0.3, networkConditions: conditions, operationDelays: DelaySimulator.getRealisticDelays(), }); } simulateRateLimitScenario(scenario) { this.rateLimit.simulateScenario(scenario); } // System logs operations async getLogs(params) { return this.simulateRequest('getLogs', () => { // Generate mock log entries const logs = []; const logTypes = [ 'user.session.start', 'user.session.end', 'user.authentication.auth_via_mfa', 'user.authentication.auth_via_password', 'application.user_membership.add', 'application.user_membership.remove', 'group.user_membership.add', 'group.user_membership.remove', 'policy.evaluate', 'system.api_token.create', ]; const severities = ['INFO', 'WARN', 'ERROR']; const outcomes = ['SUCCESS', 'FAILURE', 'SKIPPED']; // Generate logs based on time range const since = params?.since ? new Date(params.since) : new Date(Date.now() - 24 * 60 * 60 * 1000); const until = params?.until ? new Date(params.until) : new Date(); const logCount = params?.limit || 100; for (let i = 0; i < logCount; i++) { const timestamp = new Date(since.getTime() + Math.random() * (until.getTime() - since.getTime())); const eventType = logTypes[Math.floor(Math.random() * logTypes.length)] || 'user.session.start'; const severity = severities[Math.floor(Math.random() * severities.length)] || 'INFO'; const outcome = outcomes[Math.floor(Math.random() * outcomes.length)] || 'SUCCESS'; const log = { uuid: `log_${i}_${Date.now()}`, published: timestamp.toISOString(), eventType, version: '0', severity, displayMessage: `${eventType} event`, actor: { id: `00u${Math.floor(Math.random() * 1000)}`, type: 'User', alternateId: `user${i}@example.com`, displayName: `User ${i}`, }, client: { ipAddress: `192.168.1.${Math.floor(Math.random() * 255)}`, userAgent: { rawUserAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', os: 'Windows', browser: 'Chrome', }, }, outcome: { result: outcome, }, target: eventType.includes('application') || eventType.includes('group') ? [ { id: `${eventType.includes('application') ? '0oa' : '00g'}${Math.floor(Math.random() * 1000)}`, type: eventType.includes('application') ? 'AppInstance' : 'UserGroup', alternateId: eventType.includes('application') ? `app${i}` : `group${i}`, displayName: eventType.includes('application') ? `Application ${i}` : `Group ${i}`, }, ] : undefined, }; logs.push(log); } // Sort logs if (params?.sortOrder === 'ASCENDING') { logs.sort((a, b) => new Date(a.published).getTime() - new Date(b.published).getTime()); } else { logs.sort((a, b) => new Date(b.published).getTime() - new Date(a.published).getTime()); } // Apply filters let filteredLogs = [...logs]; if (params?.filter) { // Simple filter implementation for eventType filteredLogs = filteredLogs.filter((log) => { return log.eventType.includes(params.filter); }); } if (params?.q) { // Simple search implementation filteredLogs = filteredLogs.filter((log) => { return JSON.stringify(log).toLowerCase().includes(params.q.toLowerCase()); }); } // Apply pagination const paginationOptions = {}; if (params?.limit !== undefined) paginationOptions.limit = params.limit; if (params?.after) paginationOptions.after = params.after; const paginated = PaginationHelper.paginate(filteredLogs, paginationOptions, `${this.baseUrl}/logs`); return { data: paginated.data, headers: paginated.headers, }; }); } // Close method for compatibility async close() { // Clean up any resources this.reset(); } } //# sourceMappingURL=mock-okta-client.js.map