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
JavaScript
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}.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