From 3704fcab4916fb2b2bb89167a07e17c133ee9342 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 19 Mar 2026 14:42:14 -0700 Subject: [PATCH 1/7] feat(okta): add complete Okta identity management integration Add 18 Okta Management API tools covering user lifecycle (list, get, create, update, activate, deactivate, suspend, unsuspend, reset password, delete) and group management (list, get, create, update, delete, add/remove members, list members). Includes block with conditional UI, icon, registry entries, and generated docs. Co-Authored-By: Claude Opus 4.6 --- apps/docs/components/icons.tsx | 41 ++ apps/docs/components/ui/icon-mapping.ts | 4 + apps/docs/content/docs/en/tools/ashby.mdx | 388 ++++++++++++-- apps/docs/content/docs/en/tools/meta.json | 2 + apps/docs/content/docs/en/tools/okta.mdx | 497 ++++++++++++++++++ apps/docs/content/docs/en/tools/workday.mdx | 262 +++++++++ .../integrations/data/icon-mapping.ts | 8 + .../integrations/data/integrations.json | 336 +++++++++++- apps/sim/blocks/blocks/okta.ts | 380 +++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 13 + apps/sim/tools/okta/activate_user.ts | 108 ++++ apps/sim/tools/okta/add_user_to_group.ts | 89 ++++ apps/sim/tools/okta/create_group.ts | 102 ++++ apps/sim/tools/okta/create_user.ts | 160 ++++++ apps/sim/tools/okta/deactivate_user.ts | 92 ++++ apps/sim/tools/okta/delete_group.ts | 79 +++ apps/sim/tools/okta/delete_user.ts | 85 +++ apps/sim/tools/okta/get_group.ts | 91 ++++ apps/sim/tools/okta/get_user.ts | 119 +++++ apps/sim/tools/okta/index.ts | 19 + apps/sim/tools/okta/list_group_members.ts | 134 +++++ apps/sim/tools/okta/list_groups.ts | 127 +++++ apps/sim/tools/okta/list_users.ts | 140 +++++ apps/sim/tools/okta/remove_user_from_group.ts | 89 ++++ apps/sim/tools/okta/reset_password.ts | 99 ++++ apps/sim/tools/okta/suspend_user.ts | 79 +++ apps/sim/tools/okta/types.ts | 446 ++++++++++++++++ apps/sim/tools/okta/unsuspend_user.ts | 80 +++ apps/sim/tools/okta/update_group.ts | 109 ++++ apps/sim/tools/okta/update_user.ts | 144 +++++ apps/sim/tools/registry.ts | 38 ++ 32 files changed, 4320 insertions(+), 42 deletions(-) create mode 100644 apps/docs/content/docs/en/tools/okta.mdx create mode 100644 apps/docs/content/docs/en/tools/workday.mdx create mode 100644 apps/sim/blocks/blocks/okta.ts create mode 100644 apps/sim/tools/okta/activate_user.ts create mode 100644 apps/sim/tools/okta/add_user_to_group.ts create mode 100644 apps/sim/tools/okta/create_group.ts create mode 100644 apps/sim/tools/okta/create_user.ts create mode 100644 apps/sim/tools/okta/deactivate_user.ts create mode 100644 apps/sim/tools/okta/delete_group.ts create mode 100644 apps/sim/tools/okta/delete_user.ts create mode 100644 apps/sim/tools/okta/get_group.ts create mode 100644 apps/sim/tools/okta/get_user.ts create mode 100644 apps/sim/tools/okta/index.ts create mode 100644 apps/sim/tools/okta/list_group_members.ts create mode 100644 apps/sim/tools/okta/list_groups.ts create mode 100644 apps/sim/tools/okta/list_users.ts create mode 100644 apps/sim/tools/okta/remove_user_from_group.ts create mode 100644 apps/sim/tools/okta/reset_password.ts create mode 100644 apps/sim/tools/okta/suspend_user.ts create mode 100644 apps/sim/tools/okta/types.ts create mode 100644 apps/sim/tools/okta/unsuspend_user.ts create mode 100644 apps/sim/tools/okta/update_group.ts create mode 100644 apps/sim/tools/okta/update_user.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index f8ba78c4ba7..978a1e93d91 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -124,6 +124,34 @@ export function NoteIcon(props: SVGProps) { ) } +export function WorkdayIcon(props: SVGProps) { + const id = useId() + const clipId = `workday_clip_${id}` + return ( + + + + + + + + + + + + ) +} + export function WorkflowIcon(props: SVGProps) { return ( ) { ) } +export function OktaIcon(props: SVGProps) { + return ( + + + + ) +} + export function OnePasswordIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 8d2be1cae95..45ee9e41b78 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -109,6 +109,7 @@ import { Neo4jIcon, NotionIcon, ObsidianIcon, + OktaIcon, OnePasswordIcon, OpenAIIcon, OutlookIcon, @@ -164,6 +165,7 @@ import { WhatsAppIcon, WikipediaIcon, WordpressIcon, + WorkdayIcon, xIcon, YouTubeIcon, ZendeskIcon, @@ -277,6 +279,7 @@ export const blockTypeToIconMap: Record = { neo4j: Neo4jIcon, notion_v2: NotionIcon, obsidian: ObsidianIcon, + okta: OktaIcon, onedrive: MicrosoftOneDriveIcon, onepassword: OnePasswordIcon, openai: OpenAIIcon, @@ -335,6 +338,7 @@ export const blockTypeToIconMap: Record = { whatsapp: WhatsAppIcon, wikipedia: WikipediaIcon, wordpress: WordpressIcon, + workday: WorkdayIcon, x: xIcon, youtube: YouTubeIcon, zendesk: ZendeskIcon, diff --git a/apps/docs/content/docs/en/tools/ashby.mdx b/apps/docs/content/docs/en/tools/ashby.mdx index ac9c39d19a8..138368e36c6 100644 --- a/apps/docs/content/docs/en/tools/ashby.mdx +++ b/apps/docs/content/docs/en/tools/ashby.mdx @@ -30,12 +30,50 @@ In Sim, the Ashby integration enables your agents to programmatically manage you ## Usage Instructions -Integrate Ashby into the workflow. Can list, search, create, and update candidates, list and get job details, create notes, list notes, list and get applications, create applications, and list offers. +Integrate Ashby into the workflow. Manage candidates (list, get, create, update, search, tag), applications (list, get, create, change stage), jobs (list, get), job postings (list, get), offers (list, get), notes (list, create), interviews (list), and reference data (sources, tags, archive reasons, custom fields, departments, locations, openings, users). ## Tools +### `ashby_add_candidate_tag` + +Adds a tag to a candidate in Ashby. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Ashby API Key | +| `candidateId` | string | Yes | The UUID of the candidate to add the tag to | +| `tagId` | string | Yes | The UUID of the tag to add | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the tag was successfully added | + +### `ashby_change_application_stage` + +Moves an application to a different interview stage. Requires an archive reason when moving to an Archived stage. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Ashby API Key | +| `applicationId` | string | Yes | The UUID of the application to update the stage of | +| `interviewStageId` | string | Yes | The UUID of the interview stage to move the application to | +| `archiveReasonId` | string | No | Archive reason UUID. Required when moving to an Archived stage, ignored otherwise | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `applicationId` | string | Application UUID | +| `stageId` | string | New interview stage UUID | + ### `ashby_create_application` Creates a new application for a candidate on a job. Optionally specify interview plan, stage, source, and credited user. @@ -57,23 +95,7 @@ Creates a new application for a candidate on a job. Optionally specify interview | Parameter | Type | Description | | --------- | ---- | ----------- | -| `id` | string | Created application UUID | -| `status` | string | Application status \(Active, Hired, Archived, Lead\) | -| `candidate` | object | Associated candidate | -| ↳ `id` | string | Candidate UUID | -| ↳ `name` | string | Candidate name | -| `job` | object | Associated job | -| ↳ `id` | string | Job UUID | -| ↳ `title` | string | Job title | -| `currentInterviewStage` | object | Current interview stage | -| ↳ `id` | string | Stage UUID | -| ↳ `title` | string | Stage title | -| ↳ `type` | string | Stage type | -| `source` | object | Application source | -| ↳ `id` | string | Source UUID | -| ↳ `title` | string | Source title | -| `createdAt` | string | ISO 8601 creation timestamp | -| `updatedAt` | string | ISO 8601 last update timestamp | +| `applicationId` | string | Created application UUID | ### `ashby_create_candidate` @@ -85,10 +107,8 @@ Creates a new candidate record in Ashby. | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Ashby API Key | | `name` | string | Yes | The candidate full name | -| `email` | string | No | Primary email address for the candidate | -| `emailType` | string | No | Email address type: Personal, Work, or Other \(default Work\) | +| `email` | string | Yes | Primary email address for the candidate | | `phoneNumber` | string | No | Primary phone number for the candidate | -| `phoneType` | string | No | Phone number type: Personal, Work, or Other \(default Work\) | | `linkedInUrl` | string | No | LinkedIn profile URL | | `githubUrl` | string | No | GitHub profile URL | | `sourceId` | string | No | UUID of the source to attribute the candidate to | @@ -127,14 +147,7 @@ Creates a note on a candidate in Ashby. Supports plain text and HTML content (bo | Parameter | Type | Description | | --------- | ---- | ----------- | -| `id` | string | Created note UUID | -| `content` | string | Note content as stored | -| `author` | object | Note author | -| ↳ `id` | string | Author user UUID | -| ↳ `firstName` | string | First name | -| ↳ `lastName` | string | Last name | -| ↳ `email` | string | Email address | -| `createdAt` | string | ISO 8601 creation timestamp | +| `noteId` | string | Created note UUID | ### `ashby_get_application` @@ -228,7 +241,7 @@ Retrieves full details about a single job by its ID. | --------- | ---- | ----------- | | `id` | string | Job UUID | | `title` | string | Job title | -| `status` | string | Job status \(Open, Closed, Draft, Archived, On Hold\) | +| `status` | string | Job status \(Open, Closed, Draft, Archived\) | | `employmentType` | string | Employment type \(FullTime, PartTime, Intern, Contract, Temporary\) | | `departmentId` | string | Department UUID | | `locationId` | string | Location UUID | @@ -237,6 +250,58 @@ Retrieves full details about a single job by its ID. | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 last update timestamp | +### `ashby_get_job_posting` + +Retrieves full details about a single job posting by its ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Ashby API Key | +| `jobPostingId` | string | Yes | The UUID of the job posting to fetch | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Job posting UUID | +| `title` | string | Job posting title | +| `jobId` | string | Associated job UUID | +| `locationName` | string | Location name | +| `departmentName` | string | Department name | +| `employmentType` | string | Employment type \(e.g. FullTime, PartTime, Contract\) | +| `descriptionPlain` | string | Job posting description in plain text | +| `isListed` | boolean | Whether the posting is publicly listed | +| `publishedDate` | string | ISO 8601 published date | +| `externalLink` | string | External link to the job posting | + +### `ashby_get_offer` + +Retrieves full details about a single offer by its ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Ashby API Key | +| `offerId` | string | Yes | The UUID of the offer to fetch | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Offer UUID | +| `offerStatus` | string | Offer status \(e.g. WaitingOnCandidateResponse, CandidateAccepted\) | +| `acceptanceStatus` | string | Acceptance status \(e.g. Accepted, Declined, Pending\) | +| `applicationId` | string | Associated application UUID | +| `startDate` | string | Offer start date | +| `salary` | object | Salary details | +| ↳ `currencyCode` | string | ISO 4217 currency code | +| ↳ `value` | number | Salary amount | +| `openingId` | string | Associated opening UUID | +| `createdAt` | string | ISO 8601 creation timestamp \(from latest version\) | + ### `ashby_list_applications` Lists all applications in an Ashby organization with pagination and optional filters for status, job, candidate, and creation date. @@ -278,6 +343,45 @@ Lists all applications in an Ashby organization with pagination and optional fil | `moreDataAvailable` | boolean | Whether more pages of results exist | | `nextCursor` | string | Opaque cursor for fetching the next page | +### `ashby_list_archive_reasons` + +Lists all archive reasons configured in Ashby. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Ashby API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `archiveReasons` | array | List of archive reasons | +| ↳ `id` | string | Archive reason UUID | +| ↳ `text` | string | Archive reason text | +| ↳ `reasonType` | string | Reason type | +| ↳ `isArchived` | boolean | Whether the reason is archived | + +### `ashby_list_candidate_tags` + +Lists all candidate tags configured in Ashby. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Ashby API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tags` | array | List of candidate tags | +| ↳ `id` | string | Tag UUID | +| ↳ `title` | string | Tag title | +| ↳ `isArchived` | boolean | Whether the tag is archived | + ### `ashby_list_candidates` Lists all candidates in an Ashby organization with cursor-based pagination. @@ -310,6 +414,98 @@ Lists all candidates in an Ashby organization with cursor-based pagination. | `moreDataAvailable` | boolean | Whether more pages of results exist | | `nextCursor` | string | Opaque cursor for fetching the next page | +### `ashby_list_custom_fields` + +Lists all custom field definitions configured in Ashby. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Ashby API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `customFields` | array | List of custom field definitions | +| ↳ `id` | string | Custom field UUID | +| ↳ `title` | string | Custom field title | +| ↳ `fieldType` | string | Field type \(e.g. String, Number, Boolean\) | +| ↳ `objectType` | string | Object type the field applies to \(e.g. Candidate, Application, Job\) | +| ↳ `isArchived` | boolean | Whether the custom field is archived | + +### `ashby_list_departments` + +Lists all departments in Ashby. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Ashby API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `departments` | array | List of departments | +| ↳ `id` | string | Department UUID | +| ↳ `name` | string | Department name | +| ↳ `isArchived` | boolean | Whether the department is archived | +| ↳ `parentId` | string | Parent department UUID | + +### `ashby_list_interviews` + +Lists interview schedules in Ashby, optionally filtered by application or interview stage. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Ashby API Key | +| `applicationId` | string | No | The UUID of the application to list interview schedules for | +| `interviewStageId` | string | No | The UUID of the interview stage to list interview schedules for | +| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | +| `perPage` | number | No | Number of results per page \(default 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `interviewSchedules` | array | List of interview schedules | +| ↳ `id` | string | Interview schedule UUID | +| ↳ `applicationId` | string | Associated application UUID | +| ↳ `interviewStageId` | string | Interview stage UUID | +| ↳ `status` | string | Schedule status | +| ↳ `createdAt` | string | ISO 8601 creation timestamp | +| `moreDataAvailable` | boolean | Whether more pages of results exist | +| `nextCursor` | string | Opaque cursor for fetching the next page | + +### `ashby_list_job_postings` + +Lists all job postings in Ashby. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Ashby API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `jobPostings` | array | List of job postings | +| ↳ `id` | string | Job posting UUID | +| ↳ `title` | string | Job posting title | +| ↳ `jobId` | string | Associated job UUID | +| ↳ `locationName` | string | Location name | +| ↳ `departmentName` | string | Department name | +| ↳ `employmentType` | string | Employment type \(e.g. FullTime, PartTime, Contract\) | +| ↳ `isListed` | boolean | Whether the posting is publicly listed | +| ↳ `publishedDate` | string | ISO 8601 published date | + ### `ashby_list_jobs` Lists all jobs in an Ashby organization. By default returns Open, Closed, and Archived jobs. Specify status to filter. @@ -339,6 +535,30 @@ Lists all jobs in an Ashby organization. By default returns Open, Closed, and Ar | `moreDataAvailable` | boolean | Whether more pages of results exist | | `nextCursor` | string | Opaque cursor for fetching the next page | +### `ashby_list_locations` + +Lists all locations configured in Ashby. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Ashby API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `locations` | array | List of locations | +| ↳ `id` | string | Location UUID | +| ↳ `name` | string | Location name | +| ↳ `isArchived` | boolean | Whether the location is archived | +| ↳ `isRemote` | boolean | Whether this is a remote location | +| ↳ `address` | object | Location address | +| ↳ `city` | string | City | +| ↳ `region` | string | State or region | +| ↳ `country` | string | Country | + ### `ashby_list_notes` Lists all notes on a candidate with pagination support. @@ -386,18 +606,106 @@ Lists all offers with their latest version in an Ashby organization. | --------- | ---- | ----------- | | `offers` | array | List of offers | | ↳ `id` | string | Offer UUID | -| ↳ `status` | string | Offer status | -| ↳ `candidate` | object | Associated candidate | -| ↳ `id` | string | Candidate UUID | -| ↳ `name` | string | Candidate name | -| ↳ `job` | object | Associated job | -| ↳ `id` | string | Job UUID | -| ↳ `title` | string | Job title | +| ↳ `offerStatus` | string | Offer status | +| ↳ `acceptanceStatus` | string | Acceptance status | +| ↳ `applicationId` | string | Associated application UUID | +| ↳ `startDate` | string | Offer start date | +| ↳ `salary` | object | Salary details | +| ↳ `currencyCode` | string | ISO 4217 currency code | +| ↳ `value` | number | Salary amount | +| ↳ `openingId` | string | Associated opening UUID | | ↳ `createdAt` | string | ISO 8601 creation timestamp | -| ↳ `updatedAt` | string | ISO 8601 last update timestamp | | `moreDataAvailable` | boolean | Whether more pages of results exist | | `nextCursor` | string | Opaque cursor for fetching the next page | +### `ashby_list_openings` + +Lists all openings in Ashby with pagination. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Ashby API Key | +| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | +| `perPage` | number | No | Number of results per page \(default 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `openings` | array | List of openings | +| ↳ `id` | string | Opening UUID | +| ↳ `openingState` | string | Opening state \(Approved, Closed, Draft, Filled, Open\) | +| ↳ `isArchived` | boolean | Whether the opening is archived | +| ↳ `openedAt` | string | ISO 8601 opened timestamp | +| ↳ `closedAt` | string | ISO 8601 closed timestamp | +| `moreDataAvailable` | boolean | Whether more pages of results exist | +| `nextCursor` | string | Opaque cursor for fetching the next page | + +### `ashby_list_sources` + +Lists all candidate sources configured in Ashby. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Ashby API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `sources` | array | List of sources | +| ↳ `id` | string | Source UUID | +| ↳ `title` | string | Source title | +| ↳ `isArchived` | boolean | Whether the source is archived | + +### `ashby_list_users` + +Lists all users in Ashby with pagination. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Ashby API Key | +| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | +| `perPage` | number | No | Number of results per page \(default 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `users` | array | List of users | +| ↳ `id` | string | User UUID | +| ↳ `firstName` | string | First name | +| ↳ `lastName` | string | Last name | +| ↳ `email` | string | Email address | +| ↳ `isEnabled` | boolean | Whether the user account is enabled | +| ↳ `globalRole` | string | User role \(Organization Admin, Elevated Access, Limited Access, External Recruiter\) | +| `moreDataAvailable` | boolean | Whether more pages of results exist | +| `nextCursor` | string | Opaque cursor for fetching the next page | + +### `ashby_remove_candidate_tag` + +Removes a tag from a candidate in Ashby. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Ashby API Key | +| `candidateId` | string | Yes | The UUID of the candidate to remove the tag from | +| `tagId` | string | Yes | The UUID of the tag to remove | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the tag was successfully removed | + ### `ashby_search_candidates` Searches for candidates by name and/or email with AND logic. Results are limited to 100 matches. Use candidate.list for full pagination. @@ -425,6 +733,8 @@ Searches for candidates by name and/or email with AND logic. Results are limited | ↳ `value` | string | Phone number | | ↳ `type` | string | Contact type \(Personal, Work, Other\) | | ↳ `isPrimary` | boolean | Whether this is the primary phone | +| ↳ `createdAt` | string | ISO 8601 creation timestamp | +| ↳ `updatedAt` | string | ISO 8601 last update timestamp | ### `ashby_update_candidate` @@ -438,9 +748,7 @@ Updates an existing candidate record in Ashby. Only provided fields are changed. | `candidateId` | string | Yes | The UUID of the candidate to update | | `name` | string | No | Updated full name | | `email` | string | No | Updated primary email address | -| `emailType` | string | No | Email address type: Personal, Work, or Other \(default Work\) | | `phoneNumber` | string | No | Updated primary phone number | -| `phoneType` | string | No | Phone number type: Personal, Work, or Other \(default Work\) | | `linkedInUrl` | string | No | LinkedIn profile URL | | `githubUrl` | string | No | GitHub profile URL | | `websiteUrl` | string | No | Personal website URL | diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index b8df33eb269..9729feb3f8a 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -104,6 +104,7 @@ "neo4j", "notion", "obsidian", + "okta", "onedrive", "onepassword", "openai", @@ -163,6 +164,7 @@ "whatsapp", "wikipedia", "wordpress", + "workday", "x", "youtube", "zendesk", diff --git a/apps/docs/content/docs/en/tools/okta.mdx b/apps/docs/content/docs/en/tools/okta.mdx new file mode 100644 index 00000000000..62b12d8e49e --- /dev/null +++ b/apps/docs/content/docs/en/tools/okta.mdx @@ -0,0 +1,497 @@ +--- +title: Okta +description: Manage users and groups in Okta +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Integrate Okta identity management into your workflow. List, create, update, activate, suspend, and delete users. Reset passwords. Manage groups and group membership. + + + +## Tools + +### `okta_list_users` + +List all users in your Okta organization with optional search and filtering + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Okta API token for authentication | +| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | +| `search` | string | No | Okta search expression \(e.g., profile.firstName eq "John" or profile.email co "example.com"\) | +| `filter` | string | No | Okta filter expression \(e.g., status eq "ACTIVE"\) | +| `limit` | number | No | Maximum number of users to return \(default: 200, max: 200\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `users` | array | Array of Okta user objects | +| ↳ `id` | string | User ID | +| ↳ `status` | string | User status \(ACTIVE, STAGED, PROVISIONED, etc.\) | +| ↳ `firstName` | string | First name | +| ↳ `lastName` | string | Last name | +| ↳ `email` | string | Email address | +| ↳ `login` | string | Login \(usually email\) | +| ↳ `mobilePhone` | string | Mobile phone | +| ↳ `title` | string | Job title | +| ↳ `department` | string | Department | +| ↳ `created` | string | Creation timestamp | +| ↳ `lastLogin` | string | Last login timestamp | +| ↳ `lastUpdated` | string | Last update timestamp | +| ↳ `activated` | string | Activation timestamp | +| ↳ `statusChanged` | string | Status change timestamp | +| `count` | number | Number of users returned | +| `success` | boolean | Operation success status | + +### `okta_get_user` + +Get a specific user by ID or login from your Okta organization + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Okta API token for authentication | +| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | +| `userId` | string | Yes | User ID or login \(email\) to look up | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | User ID | +| `status` | string | User status | +| `firstName` | string | First name | +| `lastName` | string | Last name | +| `email` | string | Email address | +| `login` | string | Login \(usually email\) | +| `mobilePhone` | string | Mobile phone | +| `secondEmail` | string | Secondary email | +| `displayName` | string | Display name | +| `title` | string | Job title | +| `department` | string | Department | +| `organization` | string | Organization | +| `manager` | string | Manager name | +| `managerId` | string | Manager ID | +| `division` | string | Division | +| `employeeNumber` | string | Employee number | +| `userType` | string | User type | +| `created` | string | Creation timestamp | +| `activated` | string | Activation timestamp | +| `lastLogin` | string | Last login timestamp | +| `lastUpdated` | string | Last update timestamp | +| `statusChanged` | string | Status change timestamp | +| `passwordChanged` | string | Password change timestamp | +| `success` | boolean | Operation success status | + +### `okta_create_user` + +Create a new user in your Okta organization + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Okta API token for authentication | +| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | +| `firstName` | string | Yes | First name of the user | +| `lastName` | string | Yes | Last name of the user | +| `email` | string | Yes | Email address of the user | +| `login` | string | No | Login for the user \(defaults to email if not provided\) | +| `password` | string | No | Password for the user \(if not set, user will be emailed to set password\) | +| `mobilePhone` | string | No | Mobile phone number | +| `title` | string | No | Job title | +| `department` | string | No | Department | +| `activate` | boolean | No | Whether to activate the user immediately \(default: true\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Created user ID | +| `status` | string | User status | +| `firstName` | string | First name | +| `lastName` | string | Last name | +| `email` | string | Email address | +| `login` | string | Login | +| `created` | string | Creation timestamp | +| `lastUpdated` | string | Last update timestamp | +| `success` | boolean | Operation success status | + +### `okta_update_user` + +Update a user profile in your Okta organization + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Okta API token for authentication | +| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | +| `userId` | string | Yes | User ID or login to update | +| `firstName` | string | No | Updated first name | +| `lastName` | string | No | Updated last name | +| `email` | string | No | Updated email address | +| `login` | string | No | Updated login | +| `mobilePhone` | string | No | Updated mobile phone number | +| `title` | string | No | Updated job title | +| `department` | string | No | Updated department | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | User ID | +| `status` | string | User status | +| `firstName` | string | First name | +| `lastName` | string | Last name | +| `email` | string | Email address | +| `login` | string | Login | +| `created` | string | Creation timestamp | +| `lastUpdated` | string | Last update timestamp | +| `success` | boolean | Operation success status | + +### `okta_activate_user` + +Activate a user in your Okta organization. Can only be performed on users with STAGED or DEPROVISIONED status. Optionally sends an activation email. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Okta API token for authentication | +| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | +| `userId` | string | Yes | User ID or login to activate | +| `sendEmail` | boolean | No | Send activation email to the user \(default: true\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `userId` | string | Activated user ID | +| `activated` | boolean | Whether the user was activated | +| `activationUrl` | string | Activation URL \(only returned when sendEmail is false\) | +| `activationToken` | string | Activation token \(only returned when sendEmail is false\) | +| `success` | boolean | Operation success status | + +### `okta_deactivate_user` + +Deactivate a user in your Okta organization. This transitions the user to DEPROVISIONED status. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Okta API token for authentication | +| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | +| `userId` | string | Yes | User ID or login to deactivate | +| `sendEmail` | boolean | No | Send deactivation email to admin \(default: false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `userId` | string | Deactivated user ID | +| `deactivated` | boolean | Whether the user was deactivated | +| `success` | boolean | Operation success status | + +### `okta_suspend_user` + +Suspend a user in your Okta organization. Only users with ACTIVE status can be suspended. Suspended users cannot log in but retain group and app assignments. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Okta API token for authentication | +| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | +| `userId` | string | Yes | User ID or login to suspend | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `userId` | string | Suspended user ID | +| `suspended` | boolean | Whether the user was suspended | +| `success` | boolean | Operation success status | + +### `okta_unsuspend_user` + +Unsuspend a previously suspended user in your Okta organization. Returns the user to ACTIVE status. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Okta API token for authentication | +| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | +| `userId` | string | Yes | User ID or login to unsuspend | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `userId` | string | Unsuspended user ID | +| `unsuspended` | boolean | Whether the user was unsuspended | +| `success` | boolean | Operation success status | + +### `okta_reset_password` + +Generate a one-time token to reset a user password. Can email the reset link to the user or return it directly. Transitions the user to RECOVERY status. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Okta API token for authentication | +| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | +| `userId` | string | Yes | User ID or login to reset password for | +| `sendEmail` | boolean | No | Send password reset email to the user \(default: true\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `userId` | string | User ID | +| `resetPasswordUrl` | string | Password reset URL \(only returned when sendEmail is false\) | +| `success` | boolean | Operation success status | + +### `okta_delete_user` + +Permanently delete a user from your Okta organization. Can only be performed on DEPROVISIONED users. If the user is active, this will first deactivate them and a second call is needed to delete. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Okta API token for authentication | +| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | +| `userId` | string | Yes | User ID to delete | +| `sendEmail` | boolean | No | Send deactivation email to admin \(default: false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `userId` | string | Deleted user ID | +| `deleted` | boolean | Whether the user was deleted | +| `success` | boolean | Operation success status | + +### `okta_list_groups` + +List all groups in your Okta organization with optional search and filtering + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Okta API token for authentication | +| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | +| `search` | string | No | Okta search expression for groups \(e.g., profile.name sw "Engineering" or type eq "OKTA_GROUP"\) | +| `filter` | string | No | Okta filter expression \(e.g., type eq "OKTA_GROUP"\) | +| `limit` | number | No | Maximum number of groups to return \(default: 10000, max: 10000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `groups` | array | Array of Okta group objects | +| ↳ `id` | string | Group ID | +| ↳ `name` | string | Group name | +| ↳ `description` | string | Group description | +| ↳ `type` | string | Group type \(OKTA_GROUP, APP_GROUP, BUILT_IN\) | +| ↳ `created` | string | Creation timestamp | +| ↳ `lastUpdated` | string | Last update timestamp | +| ↳ `lastMembershipUpdated` | string | Last membership change timestamp | +| `count` | number | Number of groups returned | +| `success` | boolean | Operation success status | + +### `okta_get_group` + +Get a specific group by ID from your Okta organization + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Okta API token for authentication | +| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | +| `groupId` | string | Yes | Group ID to look up | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Group ID | +| `name` | string | Group name | +| `description` | string | Group description | +| `type` | string | Group type | +| `created` | string | Creation timestamp | +| `lastUpdated` | string | Last update timestamp | +| `lastMembershipUpdated` | string | Last membership change timestamp | +| `success` | boolean | Operation success status | + +### `okta_create_group` + +Create a new group in your Okta organization + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Okta API token for authentication | +| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | +| `name` | string | Yes | Name of the group | +| `description` | string | No | Description of the group | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Created group ID | +| `name` | string | Group name | +| `description` | string | Group description | +| `type` | string | Group type | +| `created` | string | Creation timestamp | +| `lastUpdated` | string | Last update timestamp | +| `lastMembershipUpdated` | string | Last membership change timestamp | +| `success` | boolean | Operation success status | + +### `okta_update_group` + +Update a group profile in your Okta organization. Only groups of OKTA_GROUP type can be updated. All profile properties must be specified (full replacement). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Okta API token for authentication | +| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | +| `groupId` | string | Yes | Group ID to update | +| `name` | string | Yes | Updated group name | +| `description` | string | No | Updated group description | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Group ID | +| `name` | string | Group name | +| `description` | string | Group description | +| `type` | string | Group type | +| `created` | string | Creation timestamp | +| `lastUpdated` | string | Last update timestamp | +| `lastMembershipUpdated` | string | Last membership change timestamp | +| `success` | boolean | Operation success status | + +### `okta_delete_group` + +Delete a group from your Okta organization. Groups of OKTA_GROUP or APP_GROUP type can be removed. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Okta API token for authentication | +| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | +| `groupId` | string | Yes | Group ID to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `groupId` | string | Deleted group ID | +| `deleted` | boolean | Whether the group was deleted | +| `success` | boolean | Operation success status | + +### `okta_add_user_to_group` + +Add a user to a group in your Okta organization + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Okta API token for authentication | +| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | +| `groupId` | string | Yes | Group ID to add the user to | +| `userId` | string | Yes | User ID to add to the group | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `groupId` | string | Group ID | +| `userId` | string | User ID added to the group | +| `added` | boolean | Whether the user was added | +| `success` | boolean | Operation success status | + +### `okta_remove_user_from_group` + +Remove a user from a group in your Okta organization + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Okta API token for authentication | +| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | +| `groupId` | string | Yes | Group ID to remove the user from | +| `userId` | string | Yes | User ID to remove from the group | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `groupId` | string | Group ID | +| `userId` | string | User ID removed from the group | +| `removed` | boolean | Whether the user was removed | +| `success` | boolean | Operation success status | + +### `okta_list_group_members` + +List all members of a specific group in your Okta organization + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Okta API token for authentication | +| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) | +| `groupId` | string | Yes | Group ID to list members for | +| `limit` | number | No | Maximum number of members to return \(default: 1000, max: 1000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `members` | array | Array of group member user objects | +| ↳ `id` | string | User ID | +| ↳ `status` | string | User status | +| ↳ `firstName` | string | First name | +| ↳ `lastName` | string | Last name | +| ↳ `email` | string | Email address | +| ↳ `login` | string | Login | +| ↳ `mobilePhone` | string | Mobile phone | +| ↳ `title` | string | Job title | +| ↳ `department` | string | Department | +| ↳ `created` | string | Creation timestamp | +| ↳ `lastLogin` | string | Last login timestamp | +| ↳ `lastUpdated` | string | Last update timestamp | +| ↳ `activated` | string | Activation timestamp | +| ↳ `statusChanged` | string | Status change timestamp | +| `count` | number | Number of members returned | +| `success` | boolean | Operation success status | + + diff --git a/apps/docs/content/docs/en/tools/workday.mdx b/apps/docs/content/docs/en/tools/workday.mdx new file mode 100644 index 00000000000..14feff6d9a7 --- /dev/null +++ b/apps/docs/content/docs/en/tools/workday.mdx @@ -0,0 +1,262 @@ +--- +title: Workday +description: Manage workers, hiring, onboarding, and HR operations in Workday +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Integrate Workday HRIS into your workflow. Create pre-hires, hire employees, manage worker profiles, assign onboarding plans, handle job changes, retrieve compensation data, and process terminations. + + + +## Tools + +### `workday_get_worker` + +Retrieve a specific worker profile including personal, employment, and organization data. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tenantUrl` | string | Yes | Workday instance URL \(e.g., https://wd5-impl-services1.workday.com\) | +| `tenant` | string | Yes | Workday tenant name | +| `username` | string | Yes | Integration System User username | +| `password` | string | Yes | Integration System User password | +| `workerId` | string | Yes | Worker ID to retrieve \(e.g., 3aa5550b7fe348b98d7b5741afc65534\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `worker` | json | Worker profile with personal, employment, and organization data | + +### `workday_list_workers` + +List or search workers with optional filtering and pagination. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tenantUrl` | string | Yes | Workday instance URL \(e.g., https://wd5-impl-services1.workday.com\) | +| `tenant` | string | Yes | Workday tenant name | +| `username` | string | Yes | Integration System User username | +| `password` | string | Yes | Integration System User password | +| `limit` | number | No | Maximum number of workers to return \(default: 20\) | +| `offset` | number | No | Number of records to skip for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `workers` | array | Array of worker profiles | +| `total` | number | Total number of matching workers | + +### `workday_create_prehire` + +Create a new pre-hire (applicant) record in Workday. This is typically the first step before hiring an employee. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tenantUrl` | string | Yes | Workday instance URL \(e.g., https://wd5-impl-services1.workday.com\) | +| `tenant` | string | Yes | Workday tenant name | +| `username` | string | Yes | Integration System User username | +| `password` | string | Yes | Integration System User password | +| `legalName` | string | Yes | Full legal name of the pre-hire \(e.g., "Jane Doe"\) | +| `email` | string | No | Email address of the pre-hire | +| `phoneNumber` | string | No | Phone number of the pre-hire | +| `address` | string | No | Address of the pre-hire | +| `countryCode` | string | No | ISO 3166-1 Alpha-2 country code \(defaults to US\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `preHireId` | string | ID of the created pre-hire record | +| `descriptor` | string | Display name of the pre-hire | + +### `workday_hire_employee` + +Hire a pre-hire into an employee position. Converts an applicant into an active employee record with position, start date, and manager assignment. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tenantUrl` | string | Yes | Workday instance URL \(e.g., https://wd5-impl-services1.workday.com\) | +| `tenant` | string | Yes | Workday tenant name | +| `username` | string | Yes | Integration System User username | +| `password` | string | Yes | Integration System User password | +| `preHireId` | string | Yes | Pre-hire \(applicant\) ID to convert into an employee | +| `positionId` | string | Yes | Position ID to assign the new hire to | +| `hireDate` | string | Yes | Hire date in ISO 8601 format \(e.g., 2025-06-01\) | +| `employeeType` | string | No | Employee type \(e.g., Regular, Temporary, Contractor\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `workerId` | string | Worker ID of the newly hired employee | +| `employeeId` | string | Employee ID assigned to the new hire | +| `eventId` | string | Event ID of the hire business process | +| `hireDate` | string | Effective hire date | + +### `workday_update_worker` + +Update fields on an existing worker record in Workday. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tenantUrl` | string | Yes | Workday instance URL \(e.g., https://wd5-impl-services1.workday.com\) | +| `tenant` | string | Yes | Workday tenant name | +| `username` | string | Yes | Integration System User username | +| `password` | string | Yes | Integration System User password | +| `workerId` | string | Yes | Worker ID to update | +| `fields` | json | Yes | Fields to update as JSON \(e.g., \{"businessTitle": "Senior Engineer", "primaryWorkEmail": "new@company.com"\}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Event ID of the change personal information business process | +| `workerId` | string | Worker ID that was updated | + +### `workday_assign_onboarding` + +Create or update an onboarding plan assignment for a worker. Sets up onboarding stages and manages the assignment lifecycle. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tenantUrl` | string | Yes | Workday instance URL \(e.g., https://wd5-impl-services1.workday.com\) | +| `tenant` | string | Yes | Workday tenant name | +| `username` | string | Yes | Integration System User username | +| `password` | string | Yes | Integration System User password | +| `workerId` | string | Yes | Worker ID to assign the onboarding plan to | +| `onboardingPlanId` | string | Yes | Onboarding plan ID to assign | +| `actionEventId` | string | Yes | Action event ID that enables the onboarding plan \(e.g., the hiring event ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `assignmentId` | string | Onboarding plan assignment ID | +| `workerId` | string | Worker ID the plan was assigned to | +| `planId` | string | Onboarding plan ID that was assigned | + +### `workday_get_organizations` + +Retrieve organizations, departments, and cost centers from Workday. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tenantUrl` | string | Yes | Workday instance URL \(e.g., https://wd5-impl-services1.workday.com\) | +| `tenant` | string | Yes | Workday tenant name | +| `username` | string | Yes | Integration System User username | +| `password` | string | Yes | Integration System User password | +| `type` | string | No | Organization type filter \(e.g., Supervisory, Cost_Center, Company, Region\) | +| `limit` | number | No | Maximum number of organizations to return \(default: 20\) | +| `offset` | number | No | Number of records to skip for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `organizations` | array | Array of organization records | +| `total` | number | Total number of matching organizations | + +### `workday_change_job` + +Perform a job change for a worker including transfers, promotions, demotions, and lateral moves. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tenantUrl` | string | Yes | Workday instance URL \(e.g., https://wd5-impl-services1.workday.com\) | +| `tenant` | string | Yes | Workday tenant name | +| `username` | string | Yes | Integration System User username | +| `password` | string | Yes | Integration System User password | +| `workerId` | string | Yes | Worker ID for the job change | +| `effectiveDate` | string | Yes | Effective date for the job change in ISO 8601 format \(e.g., 2025-06-01\) | +| `newPositionId` | string | No | New position ID \(for transfers\) | +| `newJobProfileId` | string | No | New job profile ID \(for role changes\) | +| `newLocationId` | string | No | New work location ID \(for relocations\) | +| `newSupervisoryOrgId` | string | No | Target supervisory organization ID \(for org transfers\) | +| `reason` | string | Yes | Reason for the job change \(e.g., Promotion, Transfer, Reorganization\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Job change event ID | +| `workerId` | string | Worker ID the job change was applied to | +| `effectiveDate` | string | Effective date of the job change | + +### `workday_get_compensation` + +Retrieve compensation plan details for a specific worker. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tenantUrl` | string | Yes | Workday instance URL \(e.g., https://wd5-impl-services1.workday.com\) | +| `tenant` | string | Yes | Workday tenant name | +| `username` | string | Yes | Integration System User username | +| `password` | string | Yes | Integration System User password | +| `workerId` | string | Yes | Worker ID to retrieve compensation data for | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `compensationPlans` | array | Array of compensation plan details | +| ↳ `id` | string | Compensation plan ID | +| ↳ `planName` | string | Name of the compensation plan | +| ↳ `amount` | number | Compensation amount | +| ↳ `currency` | string | Currency code | +| ↳ `frequency` | string | Pay frequency | + +### `workday_terminate_worker` + +Initiate a worker termination in Workday. Triggers the Terminate Employee business process. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tenantUrl` | string | Yes | Workday instance URL \(e.g., https://wd5-impl-services1.workday.com\) | +| `tenant` | string | Yes | Workday tenant name | +| `username` | string | Yes | Integration System User username | +| `password` | string | Yes | Integration System User password | +| `workerId` | string | Yes | Worker ID to terminate | +| `terminationDate` | string | Yes | Termination date in ISO 8601 format \(e.g., 2025-06-01\) | +| `reason` | string | Yes | Termination reason \(e.g., Resignation, End_of_Contract, Retirement\) | +| `notificationDate` | string | No | Date the termination was communicated in ISO 8601 format | +| `lastDayOfWork` | string | No | Last day of work in ISO 8601 format \(defaults to termination date\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Termination event ID | +| `workerId` | string | Worker ID that was terminated | +| `terminationDate` | string | Effective termination date | + + diff --git a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts index b7bb61a5936..68518b87b03 100644 --- a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts +++ b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts @@ -16,6 +16,7 @@ import { AsanaIcon, AshbyIcon, AttioIcon, + BoxCompanyIcon, BrainIcon, BrandfetchIcon, BrowserUseIcon, @@ -32,6 +33,7 @@ import { DevinIcon, DiscordIcon, DocumentIcon, + DocuSignIcon, DropboxIcon, DsPyIcon, DubIcon, @@ -107,6 +109,7 @@ import { Neo4jIcon, NotionIcon, ObsidianIcon, + OktaIcon, OnePasswordIcon, OpenAIIcon, OutlookIcon, @@ -162,6 +165,7 @@ import { WhatsAppIcon, WikipediaIcon, WordpressIcon, + WorkdayIcon, xIcon, YouTubeIcon, ZendeskIcon, @@ -184,6 +188,7 @@ export const blockTypeToIconMap: Record = { asana: AsanaIcon, ashby: AshbyIcon, attio: AttioIcon, + box: BoxCompanyIcon, brandfetch: BrandfetchIcon, browser_use: BrowserUseIcon, calcom: CalComIcon, @@ -198,6 +203,7 @@ export const blockTypeToIconMap: Record = { datadog: DatadogIcon, devin: DevinIcon, discord: DiscordIcon, + docusign: DocuSignIcon, dropbox: DropboxIcon, dspy: DsPyIcon, dub: DubIcon, @@ -273,6 +279,7 @@ export const blockTypeToIconMap: Record = { neo4j: Neo4jIcon, notion_v2: NotionIcon, obsidian: ObsidianIcon, + okta: OktaIcon, onedrive: MicrosoftOneDriveIcon, onepassword: OnePasswordIcon, openai: OpenAIIcon, @@ -331,6 +338,7 @@ export const blockTypeToIconMap: Record = { whatsapp: WhatsAppIcon, wikipedia: WikipediaIcon, wordpress: WordpressIcon, + workday: WorkdayIcon, x: xIcon, youtube: YouTubeIcon, zendesk: ZendeskIcon, diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 1aeed01608b..cf510fd1be7 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -683,7 +683,7 @@ "slug": "ashby", "name": "Ashby", "description": "Manage candidates, jobs, and applications in Ashby", - "longDescription": "Integrate Ashby into the workflow. Can list, search, create, and update candidates, list and get job details, create notes, list notes, list and get applications, create applications, and list offers.", + "longDescription": "Integrate Ashby into the workflow. Manage candidates (list, get, create, update, search, tag), applications (list, get, create, change stage), jobs (list, get), job postings (list, get), offers (list, get), notes (list, create), interviews (list), and reference data (sources, tags, archive reasons, custom fields, departments, locations, openings, users).", "bgColor": "#5D4ED6", "iconName": "AshbyIcon", "docsUrl": "https://docs.sim.ai/tools/ashby", @@ -739,9 +739,69 @@ { "name": "List Offers", "description": "Lists all offers with their latest version in an Ashby organization." + }, + { + "name": "Change Application Stage", + "description": "Moves an application to a different interview stage. Requires an archive reason when moving to an Archived stage." + }, + { + "name": "Add Candidate Tag", + "description": "Adds a tag to a candidate in Ashby." + }, + { + "name": "Remove Candidate Tag", + "description": "Removes a tag from a candidate in Ashby." + }, + { + "name": "Get Offer", + "description": "Retrieves full details about a single offer by its ID." + }, + { + "name": "List Sources", + "description": "Lists all candidate sources configured in Ashby." + }, + { + "name": "List Candidate Tags", + "description": "Lists all candidate tags configured in Ashby." + }, + { + "name": "List Archive Reasons", + "description": "Lists all archive reasons configured in Ashby." + }, + { + "name": "List Custom Fields", + "description": "Lists all custom field definitions configured in Ashby." + }, + { + "name": "List Departments", + "description": "Lists all departments in Ashby." + }, + { + "name": "List Locations", + "description": "Lists all locations configured in Ashby." + }, + { + "name": "List Job Postings", + "description": "Lists all job postings in Ashby." + }, + { + "name": "Get Job Posting", + "description": "Retrieves full details about a single job posting by its ID." + }, + { + "name": "List Openings", + "description": "Lists all openings in Ashby with pagination." + }, + { + "name": "List Users", + "description": "Lists all users in Ashby with pagination." + }, + { + "name": "List Interviews", + "description": "Lists interview schedules in Ashby, optionally filtered by application or interview stage." } ], - "operationCount": 13, + "operationCount": 28, "triggers": [ { "id": "ashby_application_submit", @@ -1062,6 +1122,83 @@ "authType": "none", "category": "tools" }, + { + "type": "box", + "slug": "box", + "name": "Box", + "description": "Manage files, folders, and e-signatures with Box", + "longDescription": "Integrate Box into your workflow to manage files, folders, and e-signatures. Upload and download files, search content, create folders, send documents for e-signature, track signing status, and more.", + "bgColor": "#FFFFFF", + "iconName": "BoxCompanyIcon", + "docsUrl": "https://docs.sim.ai/tools/box", + "operations": [ + { + "name": "Upload File", + "description": "Upload a file to a Box folder" + }, + { + "name": "Download File", + "description": "Download a file from Box" + }, + { + "name": "Get File Info", + "description": "Get detailed information about a file in Box" + }, + { + "name": "List Folder Items", + "description": "List files and folders in a Box folder" + }, + { + "name": "Create Folder", + "description": "Create a new folder in Box" + }, + { + "name": "Delete File", + "description": "Delete a file from Box" + }, + { + "name": "Delete Folder", + "description": "Delete a folder from Box" + }, + { + "name": "Copy File", + "description": "Copy a file to another folder in Box" + }, + { + "name": "Search", + "description": "Search for files and folders in Box" + }, + { + "name": "Update File", + "description": "Update file info in Box (rename, move, change description, add tags)" + }, + { + "name": "Create Sign Request", + "description": "Create a new Box Sign request to send documents for e-signature" + }, + { + "name": "Get Sign Request", + "description": "Get the details and status of a Box Sign request" + }, + { + "name": "List Sign Requests", + "description": "List all Box Sign requests" + }, + { + "name": "Cancel Sign Request", + "description": "Cancel a pending Box Sign request" + }, + { + "name": "Resend Sign Request", + "description": "Resend a Box Sign request to signers who have not yet signed" + } + ], + "operationCount": 15, + "triggers": [], + "triggerCount": 0, + "authType": "oauth", + "category": "tools" + }, { "type": "brandfetch", "slug": "brandfetch", @@ -2117,6 +2254,55 @@ "authType": "none", "category": "tools" }, + { + "type": "docusign", + "slug": "docusign", + "name": "DocuSign", + "description": "Send documents for e-signature via DocuSign", + "longDescription": "Create and send envelopes for e-signature, use templates, check signing status, download signed documents, and manage recipients with DocuSign.", + "bgColor": "#FFFFFF", + "iconName": "DocuSignIcon", + "docsUrl": "https://docs.sim.ai/tools/docusign", + "operations": [ + { + "name": "Send Envelope", + "description": "Create and send a DocuSign envelope with a document for e-signature" + }, + { + "name": "Send from Template", + "description": "Create and send a DocuSign envelope using a pre-built template" + }, + { + "name": "Get Envelope", + "description": "Get the details and status of a DocuSign envelope" + }, + { + "name": "List Envelopes", + "description": "List envelopes from your DocuSign account with optional filters" + }, + { + "name": "Void Envelope", + "description": "Void (cancel) a sent DocuSign envelope that has not yet been completed" + }, + { + "name": "Download Document", + "description": "Download a signed document from a completed DocuSign envelope" + }, + { + "name": "List Templates", + "description": "List available templates in your DocuSign account" + }, + { + "name": "List Recipients", + "description": "Get the recipient status details for a DocuSign envelope" + } + ], + "operationCount": 8, + "triggers": [], + "triggerCount": 0, + "authType": "oauth", + "category": "tools" + }, { "type": "dropbox", "slug": "dropbox", @@ -7208,6 +7394,95 @@ "authType": "api-key", "category": "tools" }, + { + "type": "okta", + "slug": "okta", + "name": "Okta", + "description": "Manage users and groups in Okta", + "longDescription": "Integrate Okta identity management into your workflow. List, create, update, activate, suspend, and delete users. Reset passwords. Manage groups and group membership.", + "bgColor": "#191919", + "iconName": "OktaIcon", + "docsUrl": "https://docs.sim.ai/tools/okta", + "operations": [ + { + "name": "List Users", + "description": "List all users in your Okta organization with optional search and filtering" + }, + { + "name": "Get User", + "description": "Get a specific user by ID or login from your Okta organization" + }, + { + "name": "Create User", + "description": "Create a new user in your Okta organization" + }, + { + "name": "Update User", + "description": "Update a user profile in your Okta organization" + }, + { + "name": "Activate User", + "description": "Activate a user in your Okta organization. Can only be performed on users with STAGED or DEPROVISIONED status. Optionally sends an activation email." + }, + { + "name": "Deactivate User", + "description": "Deactivate a user in your Okta organization. This transitions the user to DEPROVISIONED status." + }, + { + "name": "Suspend User", + "description": "Suspend a user in your Okta organization. Only users with ACTIVE status can be suspended. Suspended users cannot log in but retain group and app assignments." + }, + { + "name": "Unsuspend User", + "description": "Unsuspend a previously suspended user in your Okta organization. Returns the user to ACTIVE status." + }, + { + "name": "Reset Password", + "description": "Generate a one-time token to reset a user password. Can email the reset link to the user or return it directly. Transitions the user to RECOVERY status." + }, + { + "name": "Delete User", + "description": "Permanently delete a user from your Okta organization. Can only be performed on DEPROVISIONED users. If the user is active, this will first deactivate them and a second call is needed to delete." + }, + { + "name": "List Groups", + "description": "List all groups in your Okta organization with optional search and filtering" + }, + { + "name": "Get Group", + "description": "Get a specific group by ID from your Okta organization" + }, + { + "name": "Create Group", + "description": "Create a new group in your Okta organization" + }, + { + "name": "Update Group", + "description": "Update a group profile in your Okta organization. Only groups of OKTA_GROUP type can be updated. All profile properties must be specified (full replacement)." + }, + { + "name": "Delete Group", + "description": "Delete a group from your Okta organization. Groups of OKTA_GROUP or APP_GROUP type can be removed." + }, + { + "name": "Add User to Group", + "description": "Add a user to a group in your Okta organization" + }, + { + "name": "Remove User from Group", + "description": "Remove a user from a group in your Okta organization" + }, + { + "name": "List Group Members", + "description": "List all members of a specific group in your Okta organization" + } + ], + "operationCount": 18, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools" + }, { "type": "onedrive", "slug": "onedrive", @@ -10302,6 +10577,63 @@ "authType": "oauth", "category": "tools" }, + { + "type": "workday", + "slug": "workday", + "name": "Workday", + "description": "Manage workers, hiring, onboarding, and HR operations in Workday", + "longDescription": "Integrate Workday HRIS into your workflow. Create pre-hires, hire employees, manage worker profiles, assign onboarding plans, handle job changes, retrieve compensation data, and process terminations.", + "bgColor": "#F5F0EB", + "iconName": "WorkdayIcon", + "docsUrl": "https://docs.sim.ai/tools/workday", + "operations": [ + { + "name": "Get Worker", + "description": "Retrieve a specific worker profile including personal, employment, and organization data." + }, + { + "name": "List Workers", + "description": "List or search workers with optional filtering and pagination." + }, + { + "name": "Create Pre-Hire", + "description": "Create a new pre-hire (applicant) record in Workday. This is typically the first step before hiring an employee." + }, + { + "name": "Hire Employee", + "description": "Hire a pre-hire into an employee position. Converts an applicant into an active employee record with position, start date, and manager assignment." + }, + { + "name": "Update Worker", + "description": "Update fields on an existing worker record in Workday." + }, + { + "name": "Assign Onboarding Plan", + "description": "Create or update an onboarding plan assignment for a worker. Sets up onboarding stages and manages the assignment lifecycle." + }, + { + "name": "Get Organizations", + "description": "Retrieve organizations, departments, and cost centers from Workday." + }, + { + "name": "Change Job", + "description": "Perform a job change for a worker including transfers, promotions, demotions, and lateral moves." + }, + { + "name": "Get Compensation", + "description": "Retrieve compensation plan details for a specific worker." + }, + { + "name": "Terminate Worker", + "description": "Initiate a worker termination in Workday. Triggers the Terminate Employee business process." + } + ], + "operationCount": 10, + "triggers": [], + "triggerCount": 0, + "authType": "none", + "category": "tools" + }, { "type": "x", "slug": "x", diff --git a/apps/sim/blocks/blocks/okta.ts b/apps/sim/blocks/blocks/okta.ts new file mode 100644 index 00000000000..92f1b3047ab --- /dev/null +++ b/apps/sim/blocks/blocks/okta.ts @@ -0,0 +1,380 @@ +import { OktaIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import type { OktaResponse } from '@/tools/okta/types' + +export const OktaBlock: BlockConfig = { + type: 'okta', + name: 'Okta', + description: 'Manage users and groups in Okta', + longDescription: + 'Integrate Okta identity management into your workflow. List, create, update, activate, suspend, and delete users. Reset passwords. Manage groups and group membership.', + docsLink: 'https://docs.sim.ai/tools/okta', + category: 'tools', + bgColor: '#191919', + icon: OktaIcon, + + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'List Users', id: 'okta_list_users' }, + { label: 'Get User', id: 'okta_get_user' }, + { label: 'Create User', id: 'okta_create_user' }, + { label: 'Update User', id: 'okta_update_user' }, + { label: 'Activate User', id: 'okta_activate_user' }, + { label: 'Deactivate User', id: 'okta_deactivate_user' }, + { label: 'Suspend User', id: 'okta_suspend_user' }, + { label: 'Unsuspend User', id: 'okta_unsuspend_user' }, + { label: 'Reset Password', id: 'okta_reset_password' }, + { label: 'Delete User', id: 'okta_delete_user' }, + { label: 'List Groups', id: 'okta_list_groups' }, + { label: 'Get Group', id: 'okta_get_group' }, + { label: 'Create Group', id: 'okta_create_group' }, + { label: 'Update Group', id: 'okta_update_group' }, + { label: 'Delete Group', id: 'okta_delete_group' }, + { label: 'Add User to Group', id: 'okta_add_user_to_group' }, + { label: 'Remove User from Group', id: 'okta_remove_user_from_group' }, + { label: 'List Group Members', id: 'okta_list_group_members' }, + ], + value: () => 'okta_list_users', + }, + { + id: 'apiKey', + title: 'API Token', + type: 'short-input', + password: true, + placeholder: 'Enter your Okta API token', + required: true, + }, + { + id: 'domain', + title: 'Okta Domain', + type: 'short-input', + placeholder: 'dev-123456.okta.com', + required: true, + }, + // Search/Filter params (list operations) + { + id: 'search', + title: 'Search', + type: 'short-input', + placeholder: 'profile.firstName eq "John"', + condition: { field: 'operation', value: ['okta_list_users', 'okta_list_groups'] }, + }, + { + id: 'filter', + title: 'Filter', + type: 'short-input', + placeholder: 'status eq "ACTIVE"', + condition: { field: 'operation', value: ['okta_list_users', 'okta_list_groups'] }, + mode: 'advanced', + }, + // User ID (shared across user operations that need it) + { + id: 'userId', + title: 'User ID', + type: 'short-input', + placeholder: 'User ID or login (email)', + condition: { + field: 'operation', + value: [ + 'okta_get_user', + 'okta_update_user', + 'okta_activate_user', + 'okta_deactivate_user', + 'okta_suspend_user', + 'okta_unsuspend_user', + 'okta_reset_password', + 'okta_delete_user', + 'okta_add_user_to_group', + 'okta_remove_user_from_group', + ], + }, + required: { + field: 'operation', + value: [ + 'okta_get_user', + 'okta_update_user', + 'okta_activate_user', + 'okta_deactivate_user', + 'okta_suspend_user', + 'okta_unsuspend_user', + 'okta_reset_password', + 'okta_delete_user', + 'okta_add_user_to_group', + 'okta_remove_user_from_group', + ], + }, + }, + // Group ID (shared across group operations that need it) + { + id: 'groupId', + title: 'Group ID', + type: 'short-input', + placeholder: 'Okta group ID', + condition: { + field: 'operation', + value: [ + 'okta_get_group', + 'okta_update_group', + 'okta_delete_group', + 'okta_add_user_to_group', + 'okta_remove_user_from_group', + 'okta_list_group_members', + ], + }, + required: { + field: 'operation', + value: [ + 'okta_get_group', + 'okta_update_group', + 'okta_delete_group', + 'okta_add_user_to_group', + 'okta_remove_user_from_group', + 'okta_list_group_members', + ], + }, + }, + // Create/Update User profile params + { + id: 'firstName', + title: 'First Name', + type: 'short-input', + placeholder: 'John', + condition: { field: 'operation', value: ['okta_create_user', 'okta_update_user'] }, + required: { field: 'operation', value: 'okta_create_user' }, + }, + { + id: 'lastName', + title: 'Last Name', + type: 'short-input', + placeholder: 'Doe', + condition: { field: 'operation', value: ['okta_create_user', 'okta_update_user'] }, + required: { field: 'operation', value: 'okta_create_user' }, + }, + { + id: 'email', + title: 'Email', + type: 'short-input', + placeholder: 'john.doe@example.com', + condition: { field: 'operation', value: ['okta_create_user', 'okta_update_user'] }, + required: { field: 'operation', value: 'okta_create_user' }, + }, + { + id: 'login', + title: 'Login', + type: 'short-input', + placeholder: 'john.doe@example.com (defaults to email)', + condition: { field: 'operation', value: ['okta_create_user', 'okta_update_user'] }, + mode: 'advanced', + }, + { + id: 'password', + title: 'Password', + type: 'short-input', + password: true, + placeholder: 'Set user password', + condition: { field: 'operation', value: 'okta_create_user' }, + mode: 'advanced', + }, + { + id: 'mobilePhone', + title: 'Mobile Phone', + type: 'short-input', + placeholder: '+1234567890', + condition: { field: 'operation', value: ['okta_create_user', 'okta_update_user'] }, + mode: 'advanced', + }, + { + id: 'title', + title: 'Job Title', + type: 'short-input', + placeholder: 'Software Engineer', + condition: { field: 'operation', value: ['okta_create_user', 'okta_update_user'] }, + mode: 'advanced', + }, + { + id: 'department', + title: 'Department', + type: 'short-input', + placeholder: 'Engineering', + condition: { field: 'operation', value: ['okta_create_user', 'okta_update_user'] }, + mode: 'advanced', + }, + { + id: 'activate', + title: 'Activate Immediately', + type: 'switch', + condition: { field: 'operation', value: 'okta_create_user' }, + mode: 'advanced', + }, + // Group name (for create/update group) + { + id: 'groupName', + title: 'Group Name', + type: 'short-input', + placeholder: 'Engineering Team', + condition: { field: 'operation', value: ['okta_create_group', 'okta_update_group'] }, + required: { field: 'operation', value: ['okta_create_group', 'okta_update_group'] }, + }, + { + id: 'groupDescription', + title: 'Group Description', + type: 'short-input', + placeholder: 'Description for the group', + condition: { field: 'operation', value: ['okta_create_group', 'okta_update_group'] }, + }, + // Send email option (activate, reset password, delete) + { + id: 'sendEmail', + title: 'Send Email', + type: 'switch', + condition: { + field: 'operation', + value: [ + 'okta_activate_user', + 'okta_deactivate_user', + 'okta_reset_password', + 'okta_delete_user', + ], + }, + mode: 'advanced', + }, + // Pagination + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: 'Max results to return', + condition: { + field: 'operation', + value: ['okta_list_users', 'okta_list_groups', 'okta_list_group_members'], + }, + mode: 'advanced', + }, + ], + + tools: { + access: [ + 'okta_list_users', + 'okta_get_user', + 'okta_create_user', + 'okta_update_user', + 'okta_activate_user', + 'okta_deactivate_user', + 'okta_suspend_user', + 'okta_unsuspend_user', + 'okta_reset_password', + 'okta_delete_user', + 'okta_list_groups', + 'okta_get_group', + 'okta_create_group', + 'okta_update_group', + 'okta_delete_group', + 'okta_add_user_to_group', + 'okta_remove_user_from_group', + 'okta_list_group_members', + ], + config: { + tool: (params) => params.operation as string, + params: (params) => { + const result: Record = { + apiKey: params.apiKey, + domain: params.domain, + } + + if (params.limit) result.limit = Number(params.limit) + + // Map group-specific UI fields to tool param names + if (params.groupName) result.name = params.groupName + if (params.groupDescription) result.description = params.groupDescription + + // Pass through all other non-empty params + const skipKeys = new Set([ + 'operation', + 'apiKey', + 'domain', + 'limit', + 'groupName', + 'groupDescription', + ]) + for (const [key, value] of Object.entries(params)) { + if (!skipKeys.has(key) && value !== undefined && value !== null && value !== '') { + result[key] = value + } + } + + return result + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Okta API token' }, + domain: { type: 'string', description: 'Okta domain' }, + userId: { type: 'string', description: 'User ID or login' }, + groupId: { type: 'string', description: 'Group ID' }, + search: { type: 'string', description: 'Search expression' }, + filter: { type: 'string', description: 'Filter expression' }, + limit: { type: 'number', description: 'Max results to return' }, + firstName: { type: 'string', description: 'First name' }, + lastName: { type: 'string', description: 'Last name' }, + email: { type: 'string', description: 'Email address' }, + login: { type: 'string', description: 'Login (defaults to email)' }, + password: { type: 'string', description: 'User password' }, + mobilePhone: { type: 'string', description: 'Mobile phone number' }, + title: { type: 'string', description: 'Job title' }, + department: { type: 'string', description: 'Department' }, + activate: { type: 'boolean', description: 'Activate user immediately on creation' }, + groupName: { type: 'string', description: 'Group name' }, + groupDescription: { type: 'string', description: 'Group description' }, + sendEmail: { type: 'boolean', description: 'Whether to send email notification' }, + }, + + outputs: { + users: { + type: 'json', + description: + 'Array of user objects (id, status, firstName, lastName, email, login, mobilePhone, title, department, created, lastLogin, lastUpdated)', + }, + members: { + type: 'json', + description: + 'Array of group member user objects (id, status, firstName, lastName, email, login, mobilePhone, title, department, created, lastLogin, lastUpdated)', + }, + groups: { + type: 'json', + description: + 'Array of group objects (id, name, description, type, created, lastUpdated, lastMembershipUpdated)', + }, + id: { type: 'string', description: 'Resource ID' }, + status: { type: 'string', description: 'User status' }, + firstName: { type: 'string', description: 'First name' }, + lastName: { type: 'string', description: 'Last name' }, + email: { type: 'string', description: 'Email address' }, + login: { type: 'string', description: 'Login' }, + name: { type: 'string', description: 'Group name' }, + description: { type: 'string', description: 'Group description' }, + type: { type: 'string', description: 'Group type' }, + count: { type: 'number', description: 'Number of results' }, + added: { type: 'boolean', description: 'Whether user was added to group' }, + removed: { type: 'boolean', description: 'Whether user was removed from group' }, + deactivated: { type: 'boolean', description: 'Whether user was deactivated' }, + suspended: { type: 'boolean', description: 'Whether user was suspended' }, + unsuspended: { type: 'boolean', description: 'Whether user was unsuspended' }, + activated: { type: 'boolean', description: 'Whether user was activated' }, + deleted: { type: 'boolean', description: 'Whether resource was deleted' }, + activationUrl: { type: 'string', description: 'Activation URL (when sendEmail is false)' }, + activationToken: { type: 'string', description: 'Activation token (when sendEmail is false)' }, + resetPasswordUrl: { + type: 'string', + description: 'Password reset URL (when sendEmail is false)', + }, + created: { type: 'string', description: 'Creation timestamp' }, + lastUpdated: { type: 'string', description: 'Last update timestamp' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 627e4494115..5830c9d57ff 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -120,6 +120,7 @@ import { Neo4jBlock } from '@/blocks/blocks/neo4j' import { NoteBlock } from '@/blocks/blocks/note' import { NotionBlock, NotionV2Block } from '@/blocks/blocks/notion' import { ObsidianBlock } from '@/blocks/blocks/obsidian' +import { OktaBlock } from '@/blocks/blocks/okta' import { OneDriveBlock } from '@/blocks/blocks/onedrive' import { OnePasswordBlock } from '@/blocks/blocks/onepassword' import { OpenAIBlock } from '@/blocks/blocks/openai' @@ -336,6 +337,7 @@ export const registry: Record = { notion: NotionBlock, notion_v2: NotionV2Block, obsidian: ObsidianBlock, + okta: OktaBlock, onepassword: OnePasswordBlock, onedrive: OneDriveBlock, openai: OpenAIBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 16cf00d4a42..978a1e93d91 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -6096,6 +6096,19 @@ export function AgentSkillsIcon(props: SVGProps) { ) } +export function OktaIcon(props: SVGProps) { + return ( + + + + ) +} + export function OnePasswordIcon(props: SVGProps) { return ( diff --git a/apps/sim/tools/okta/activate_user.ts b/apps/sim/tools/okta/activate_user.ts new file mode 100644 index 00000000000..8375f6f9484 --- /dev/null +++ b/apps/sim/tools/okta/activate_user.ts @@ -0,0 +1,108 @@ +import { createLogger } from '@sim/logger' +import type { + OktaActivateUserParams, + OktaActivateUserResponse, + OktaApiError, +} from '@/tools/okta/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OktaActivateUser') + +export const oktaActivateUserTool: ToolConfig = { + id: 'okta_activate_user', + name: 'Activate User in Okta', + description: + 'Activate a user in your Okta organization. Can only be performed on users with STAGED or DEPROVISIONED status. Optionally sends an activation email.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta API token for authentication', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta domain (e.g., dev-123456.okta.com)', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'User ID or login to activate', + }, + sendEmail: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Send activation email to the user (default: true)', + }, + }, + + request: { + url: (params) => { + const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const sendEmail = params.sendEmail !== false + return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}/lifecycle/activate?sendEmail=${sendEmail}` + }, + method: 'POST', + headers: (params) => ({ + Authorization: `SSWS ${params.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params) => { + if (!response.ok) { + let error: OktaApiError = {} + try { + error = await response.json() + } catch { + // empty response body + } + logger.error('Okta API request failed', { data: error, status: response.status }) + throw new Error(error.errorSummary || 'Failed to activate user in Okta') + } + + let activationUrl: string | null = null + let activationToken: string | null = null + try { + const data = await response.json() + activationUrl = data.activationUrl ?? null + activationToken = data.activationToken ?? null + } catch { + // empty body when sendEmail=true + } + + return { + success: true, + output: { + userId: params?.userId ?? '', + activated: true, + activationUrl, + activationToken, + success: true, + }, + } + }, + + outputs: { + userId: { type: 'string', description: 'Activated user ID' }, + activated: { type: 'boolean', description: 'Whether the user was activated' }, + activationUrl: { + type: 'string', + description: 'Activation URL (only returned when sendEmail is false)', + optional: true, + }, + activationToken: { + type: 'string', + description: 'Activation token (only returned when sendEmail is false)', + optional: true, + }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/okta/add_user_to_group.ts b/apps/sim/tools/okta/add_user_to_group.ts new file mode 100644 index 00000000000..d4529540a4e --- /dev/null +++ b/apps/sim/tools/okta/add_user_to_group.ts @@ -0,0 +1,89 @@ +import { createLogger } from '@sim/logger' +import type { + OktaAddUserToGroupParams, + OktaAddUserToGroupResponse, + OktaApiError, +} from '@/tools/okta/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OktaAddUserToGroup') + +export const oktaAddUserToGroupTool: ToolConfig< + OktaAddUserToGroupParams, + OktaAddUserToGroupResponse +> = { + id: 'okta_add_user_to_group', + name: 'Add User to Group in Okta', + description: 'Add a user to a group in your Okta organization', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta API token for authentication', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta domain (e.g., dev-123456.okta.com)', + }, + groupId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Group ID to add the user to', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'User ID to add to the group', + }, + }, + + request: { + url: (params) => { + const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId)}/users/${encodeURIComponent(params.userId)}` + }, + method: 'PUT', + headers: (params) => ({ + Authorization: `SSWS ${params.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params) => { + if (!response.ok) { + let error: OktaApiError = {} + try { + error = await response.json() + } catch { + // empty response body + } + logger.error('Okta API request failed', { data: error, status: response.status }) + throw new Error(error.errorSummary || 'Failed to add user to group in Okta') + } + + return { + success: true, + output: { + groupId: params?.groupId ?? '', + userId: params?.userId ?? '', + added: true, + success: true, + }, + } + }, + + outputs: { + groupId: { type: 'string', description: 'Group ID' }, + userId: { type: 'string', description: 'User ID added to the group' }, + added: { type: 'boolean', description: 'Whether the user was added' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/okta/create_group.ts b/apps/sim/tools/okta/create_group.ts new file mode 100644 index 00000000000..288cfea0d99 --- /dev/null +++ b/apps/sim/tools/okta/create_group.ts @@ -0,0 +1,102 @@ +import { createLogger } from '@sim/logger' +import type { + OktaApiError, + OktaCreateGroupParams, + OktaCreateGroupResponse, + OktaGroup, +} from '@/tools/okta/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OktaCreateGroup') + +export const oktaCreateGroupTool: ToolConfig = { + id: 'okta_create_group', + name: 'Create Group in Okta', + description: 'Create a new group in your Okta organization', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta API token for authentication', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta domain (e.g., dev-123456.okta.com)', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the group', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Description of the group', + }, + }, + + request: { + url: (params) => { + const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + return `https://${domain}/api/v1/groups` + }, + method: 'POST', + headers: (params) => ({ + Authorization: `SSWS ${params.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + body: (params) => { + const profile: Record = { name: params.name } + if (params.description) profile.description = params.description + return { profile } + }, + }, + + transformResponse: async (response: Response) => { + const data: OktaGroup | OktaApiError = await response.json() + + if (!response.ok) { + const error = data as OktaApiError + logger.error('Okta API request failed', { data: error, status: response.status }) + throw new Error(error.errorSummary || 'Failed to create group in Okta') + } + + const group = data as OktaGroup + return { + success: true, + output: { + id: group.id, + name: group.profile?.name ?? '', + description: group.profile?.description ?? null, + type: group.type, + created: group.created, + lastUpdated: group.lastUpdated, + lastMembershipUpdated: group.lastMembershipUpdated ?? null, + success: true, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Created group ID' }, + name: { type: 'string', description: 'Group name' }, + description: { type: 'string', description: 'Group description', optional: true }, + type: { type: 'string', description: 'Group type' }, + created: { type: 'string', description: 'Creation timestamp' }, + lastUpdated: { type: 'string', description: 'Last update timestamp' }, + lastMembershipUpdated: { + type: 'string', + description: 'Last membership change timestamp', + optional: true, + }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/okta/create_user.ts b/apps/sim/tools/okta/create_user.ts new file mode 100644 index 00000000000..f26e8a72df8 --- /dev/null +++ b/apps/sim/tools/okta/create_user.ts @@ -0,0 +1,160 @@ +import { createLogger } from '@sim/logger' +import type { + OktaApiError, + OktaCreateUserParams, + OktaCreateUserResponse, + OktaUser, +} from '@/tools/okta/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OktaCreateUser') + +export const oktaCreateUserTool: ToolConfig = { + id: 'okta_create_user', + name: 'Create User in Okta', + description: 'Create a new user in your Okta organization', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta API token for authentication', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta domain (e.g., dev-123456.okta.com)', + }, + firstName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'First name of the user', + }, + lastName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Last name of the user', + }, + email: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Email address of the user', + }, + login: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Login for the user (defaults to email if not provided)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for the user (if not set, user will be emailed to set password)', + }, + mobilePhone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Mobile phone number', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Job title', + }, + department: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Department', + }, + activate: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to activate the user immediately (default: true)', + }, + }, + + request: { + url: (params) => { + const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const activate = params.activate !== false + return `https://${domain}/api/v1/users?activate=${activate}` + }, + method: 'POST', + headers: (params) => ({ + Authorization: `SSWS ${params.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + body: (params) => { + const profile: Record = { + firstName: params.firstName, + lastName: params.lastName, + email: params.email, + login: params.login || params.email, + } + + if (params.mobilePhone) profile.mobilePhone = params.mobilePhone + if (params.title) profile.title = params.title + if (params.department) profile.department = params.department + + const body: Record = { profile } + + if (params.password) { + body.credentials = { + password: { value: params.password }, + } + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data: OktaUser | OktaApiError = await response.json() + + if (!response.ok) { + const error = data as OktaApiError + logger.error('Okta API request failed', { data: error, status: response.status }) + throw new Error(error.errorSummary || 'Failed to create user in Okta') + } + + const user = data as OktaUser + return { + success: true, + output: { + id: user.id, + status: user.status, + firstName: user.profile?.firstName ?? null, + lastName: user.profile?.lastName ?? null, + email: user.profile?.email ?? null, + login: user.profile?.login ?? null, + created: user.created, + lastUpdated: user.lastUpdated, + success: true, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Created user ID' }, + status: { type: 'string', description: 'User status' }, + firstName: { type: 'string', description: 'First name', optional: true }, + lastName: { type: 'string', description: 'Last name', optional: true }, + email: { type: 'string', description: 'Email address', optional: true }, + login: { type: 'string', description: 'Login', optional: true }, + created: { type: 'string', description: 'Creation timestamp' }, + lastUpdated: { type: 'string', description: 'Last update timestamp' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/okta/deactivate_user.ts b/apps/sim/tools/okta/deactivate_user.ts new file mode 100644 index 00000000000..f2cde3d78c6 --- /dev/null +++ b/apps/sim/tools/okta/deactivate_user.ts @@ -0,0 +1,92 @@ +import { createLogger } from '@sim/logger' +import type { + OktaApiError, + OktaDeactivateUserParams, + OktaDeactivateUserResponse, +} from '@/tools/okta/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OktaDeactivateUser') + +export const oktaDeactivateUserTool: ToolConfig< + OktaDeactivateUserParams, + OktaDeactivateUserResponse +> = { + id: 'okta_deactivate_user', + name: 'Deactivate User in Okta', + description: + 'Deactivate a user in your Okta organization. This transitions the user to DEPROVISIONED status.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta API token for authentication', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta domain (e.g., dev-123456.okta.com)', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'User ID or login to deactivate', + }, + sendEmail: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Send deactivation email to admin (default: false)', + }, + }, + + request: { + url: (params) => { + const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const queryParams = new URLSearchParams() + if (params.sendEmail) queryParams.append('sendEmail', 'true') + const queryString = queryParams.toString() + const base = `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}/lifecycle/deactivate` + return queryString ? `${base}?${queryString}` : base + }, + method: 'POST', + headers: (params) => ({ + Authorization: `SSWS ${params.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params) => { + if (!response.ok) { + let error: OktaApiError = {} + try { + error = await response.json() + } catch { + // empty response body on some error codes + } + logger.error('Okta API request failed', { data: error, status: response.status }) + throw new Error(error.errorSummary || 'Failed to deactivate user in Okta') + } + + return { + success: true, + output: { + userId: params?.userId ?? '', + deactivated: true, + success: true, + }, + } + }, + + outputs: { + userId: { type: 'string', description: 'Deactivated user ID' }, + deactivated: { type: 'boolean', description: 'Whether the user was deactivated' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/okta/delete_group.ts b/apps/sim/tools/okta/delete_group.ts new file mode 100644 index 00000000000..997632b8f4b --- /dev/null +++ b/apps/sim/tools/okta/delete_group.ts @@ -0,0 +1,79 @@ +import { createLogger } from '@sim/logger' +import type { + OktaApiError, + OktaDeleteGroupParams, + OktaDeleteGroupResponse, +} from '@/tools/okta/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OktaDeleteGroup') + +export const oktaDeleteGroupTool: ToolConfig = { + id: 'okta_delete_group', + name: 'Delete Group from Okta', + description: + 'Delete a group from your Okta organization. Groups of OKTA_GROUP or APP_GROUP type can be removed.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta API token for authentication', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta domain (e.g., dev-123456.okta.com)', + }, + groupId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Group ID to delete', + }, + }, + + request: { + url: (params) => { + const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId)}` + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `SSWS ${params.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params) => { + if (!response.ok) { + let error: OktaApiError = {} + try { + error = await response.json() + } catch { + // empty response body + } + logger.error('Okta API request failed', { data: error, status: response.status }) + throw new Error(error.errorSummary || 'Failed to delete group from Okta') + } + + return { + success: true, + output: { + groupId: params?.groupId ?? '', + deleted: true, + success: true, + }, + } + }, + + outputs: { + groupId: { type: 'string', description: 'Deleted group ID' }, + deleted: { type: 'boolean', description: 'Whether the group was deleted' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/okta/delete_user.ts b/apps/sim/tools/okta/delete_user.ts new file mode 100644 index 00000000000..9d8ebb4373b --- /dev/null +++ b/apps/sim/tools/okta/delete_user.ts @@ -0,0 +1,85 @@ +import { createLogger } from '@sim/logger' +import type { OktaApiError, OktaDeleteUserParams, OktaDeleteUserResponse } from '@/tools/okta/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OktaDeleteUser') + +export const oktaDeleteUserTool: ToolConfig = { + id: 'okta_delete_user', + name: 'Delete User from Okta', + description: + 'Permanently delete a user from your Okta organization. Can only be performed on DEPROVISIONED users. If the user is active, this will first deactivate them and a second call is needed to delete.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta API token for authentication', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta domain (e.g., dev-123456.okta.com)', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'User ID to delete', + }, + sendEmail: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Send deactivation email to admin (default: false)', + }, + }, + + request: { + url: (params) => { + const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const queryParams = new URLSearchParams() + if (params.sendEmail) queryParams.append('sendEmail', 'true') + const queryString = queryParams.toString() + const base = `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}` + return queryString ? `${base}?${queryString}` : base + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `SSWS ${params.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params) => { + if (!response.ok) { + let error: OktaApiError = {} + try { + error = await response.json() + } catch { + // empty response body + } + logger.error('Okta API request failed', { data: error, status: response.status }) + throw new Error(error.errorSummary || 'Failed to delete user from Okta') + } + + return { + success: true, + output: { + userId: params?.userId ?? '', + deleted: true, + success: true, + }, + } + }, + + outputs: { + userId: { type: 'string', description: 'Deleted user ID' }, + deleted: { type: 'boolean', description: 'Whether the user was deleted' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/okta/get_group.ts b/apps/sim/tools/okta/get_group.ts new file mode 100644 index 00000000000..56d085a8ad6 --- /dev/null +++ b/apps/sim/tools/okta/get_group.ts @@ -0,0 +1,91 @@ +import { createLogger } from '@sim/logger' +import type { + OktaApiError, + OktaGetGroupParams, + OktaGetGroupResponse, + OktaGroup, +} from '@/tools/okta/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OktaGetGroup') + +export const oktaGetGroupTool: ToolConfig = { + id: 'okta_get_group', + name: 'Get Group from Okta', + description: 'Get a specific group by ID from your Okta organization', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta API token for authentication', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta domain (e.g., dev-123456.okta.com)', + }, + groupId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Group ID to look up', + }, + }, + + request: { + url: (params) => { + const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId)}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `SSWS ${params.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data: OktaGroup | OktaApiError = await response.json() + + if (!response.ok) { + const error = data as OktaApiError + logger.error('Okta API request failed', { data: error, status: response.status }) + throw new Error(error.errorSummary || 'Failed to get group from Okta') + } + + const group = data as OktaGroup + return { + success: true, + output: { + id: group.id, + name: group.profile?.name ?? '', + description: group.profile?.description ?? null, + type: group.type, + created: group.created, + lastUpdated: group.lastUpdated, + lastMembershipUpdated: group.lastMembershipUpdated ?? null, + success: true, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Group ID' }, + name: { type: 'string', description: 'Group name' }, + description: { type: 'string', description: 'Group description', optional: true }, + type: { type: 'string', description: 'Group type' }, + created: { type: 'string', description: 'Creation timestamp' }, + lastUpdated: { type: 'string', description: 'Last update timestamp' }, + lastMembershipUpdated: { + type: 'string', + description: 'Last membership change timestamp', + optional: true, + }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/okta/get_user.ts b/apps/sim/tools/okta/get_user.ts new file mode 100644 index 00000000000..d03cf78a697 --- /dev/null +++ b/apps/sim/tools/okta/get_user.ts @@ -0,0 +1,119 @@ +import { createLogger } from '@sim/logger' +import type { + OktaApiError, + OktaGetUserParams, + OktaGetUserResponse, + OktaUser, +} from '@/tools/okta/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OktaGetUser') + +export const oktaGetUserTool: ToolConfig = { + id: 'okta_get_user', + name: 'Get User from Okta', + description: 'Get a specific user by ID or login from your Okta organization', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta API token for authentication', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta domain (e.g., dev-123456.okta.com)', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'User ID or login (email) to look up', + }, + }, + + request: { + url: (params) => { + const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `SSWS ${params.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data: OktaUser | OktaApiError = await response.json() + + if (!response.ok) { + const error = data as OktaApiError + logger.error('Okta API request failed', { data: error, status: response.status }) + throw new Error(error.errorSummary || 'Failed to get user from Okta') + } + + const user = data as OktaUser + return { + success: true, + output: { + id: user.id, + status: user.status, + firstName: user.profile?.firstName ?? null, + lastName: user.profile?.lastName ?? null, + email: user.profile?.email ?? null, + login: user.profile?.login ?? null, + mobilePhone: user.profile?.mobilePhone ?? null, + secondEmail: user.profile?.secondEmail ?? null, + displayName: user.profile?.displayName ?? null, + title: user.profile?.title ?? null, + department: user.profile?.department ?? null, + organization: user.profile?.organization ?? null, + manager: user.profile?.manager ?? null, + managerId: user.profile?.managerId ?? null, + division: user.profile?.division ?? null, + employeeNumber: user.profile?.employeeNumber ?? null, + userType: user.profile?.userType ?? null, + created: user.created, + activated: user.activated ?? null, + lastLogin: user.lastLogin ?? null, + lastUpdated: user.lastUpdated, + statusChanged: user.statusChanged ?? null, + passwordChanged: user.passwordChanged ?? null, + success: true, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'User ID' }, + status: { type: 'string', description: 'User status' }, + firstName: { type: 'string', description: 'First name', optional: true }, + lastName: { type: 'string', description: 'Last name', optional: true }, + email: { type: 'string', description: 'Email address', optional: true }, + login: { type: 'string', description: 'Login (usually email)', optional: true }, + mobilePhone: { type: 'string', description: 'Mobile phone', optional: true }, + secondEmail: { type: 'string', description: 'Secondary email', optional: true }, + displayName: { type: 'string', description: 'Display name', optional: true }, + title: { type: 'string', description: 'Job title', optional: true }, + department: { type: 'string', description: 'Department', optional: true }, + organization: { type: 'string', description: 'Organization', optional: true }, + manager: { type: 'string', description: 'Manager name', optional: true }, + managerId: { type: 'string', description: 'Manager ID', optional: true }, + division: { type: 'string', description: 'Division', optional: true }, + employeeNumber: { type: 'string', description: 'Employee number', optional: true }, + userType: { type: 'string', description: 'User type', optional: true }, + created: { type: 'string', description: 'Creation timestamp' }, + activated: { type: 'string', description: 'Activation timestamp', optional: true }, + lastLogin: { type: 'string', description: 'Last login timestamp', optional: true }, + lastUpdated: { type: 'string', description: 'Last update timestamp' }, + statusChanged: { type: 'string', description: 'Status change timestamp', optional: true }, + passwordChanged: { type: 'string', description: 'Password change timestamp', optional: true }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/okta/index.ts b/apps/sim/tools/okta/index.ts new file mode 100644 index 00000000000..874c50d8cbe --- /dev/null +++ b/apps/sim/tools/okta/index.ts @@ -0,0 +1,19 @@ +export { oktaActivateUserTool } from './activate_user' +export { oktaAddUserToGroupTool } from './add_user_to_group' +export { oktaCreateGroupTool } from './create_group' +export { oktaCreateUserTool } from './create_user' +export { oktaDeactivateUserTool } from './deactivate_user' +export { oktaDeleteGroupTool } from './delete_group' +export { oktaDeleteUserTool } from './delete_user' +export { oktaGetGroupTool } from './get_group' +export { oktaGetUserTool } from './get_user' +export { oktaListGroupMembersTool } from './list_group_members' +export { oktaListGroupsTool } from './list_groups' +export { oktaListUsersTool } from './list_users' +export { oktaRemoveUserFromGroupTool } from './remove_user_from_group' +export { oktaResetPasswordTool } from './reset_password' +export { oktaSuspendUserTool } from './suspend_user' +export * from './types' +export { oktaUnsuspendUserTool } from './unsuspend_user' +export { oktaUpdateGroupTool } from './update_group' +export { oktaUpdateUserTool } from './update_user' diff --git a/apps/sim/tools/okta/list_group_members.ts b/apps/sim/tools/okta/list_group_members.ts new file mode 100644 index 00000000000..e696bf4ce5f --- /dev/null +++ b/apps/sim/tools/okta/list_group_members.ts @@ -0,0 +1,134 @@ +import { createLogger } from '@sim/logger' +import type { + OktaApiError, + OktaListGroupMembersParams, + OktaListGroupMembersResponse, + OktaUser, +} from '@/tools/okta/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OktaListGroupMembers') + +export const oktaListGroupMembersTool: ToolConfig< + OktaListGroupMembersParams, + OktaListGroupMembersResponse +> = { + id: 'okta_list_group_members', + name: 'List Group Members from Okta', + description: 'List all members of a specific group in your Okta organization', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta API token for authentication', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta domain (e.g., dev-123456.okta.com)', + }, + groupId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Group ID to list members for', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of members to return (default: 1000, max: 1000)', + }, + }, + + request: { + url: (params) => { + const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const queryParams = new URLSearchParams() + + if (params.limit) queryParams.append('limit', params.limit.toString()) + + const queryString = queryParams.toString() + const base = `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId)}/users` + return queryString ? `${base}?${queryString}` : base + }, + method: 'GET', + headers: (params) => ({ + Authorization: `SSWS ${params.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data: OktaUser[] | OktaApiError = await response.json() + + if (!response.ok) { + const error = data as OktaApiError + logger.error('Okta API request failed', { data: error, status: response.status }) + throw new Error(error.errorSummary || 'Failed to list group members from Okta') + } + + const members = (data as OktaUser[]).map((user) => ({ + id: user.id, + status: user.status, + firstName: user.profile?.firstName ?? null, + lastName: user.profile?.lastName ?? null, + email: user.profile?.email ?? null, + login: user.profile?.login ?? null, + mobilePhone: user.profile?.mobilePhone ?? null, + title: user.profile?.title ?? null, + department: user.profile?.department ?? null, + created: user.created, + lastLogin: user.lastLogin ?? null, + lastUpdated: user.lastUpdated, + activated: user.activated ?? null, + statusChanged: user.statusChanged ?? null, + })) + + return { + success: true, + output: { + members, + count: members.length, + success: true, + }, + } + }, + + outputs: { + members: { + type: 'array', + description: 'Array of group member user objects', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'User ID' }, + status: { type: 'string', description: 'User status' }, + firstName: { type: 'string', description: 'First name', optional: true }, + lastName: { type: 'string', description: 'Last name', optional: true }, + email: { type: 'string', description: 'Email address', optional: true }, + login: { type: 'string', description: 'Login', optional: true }, + mobilePhone: { type: 'string', description: 'Mobile phone', optional: true }, + title: { type: 'string', description: 'Job title', optional: true }, + department: { type: 'string', description: 'Department', optional: true }, + created: { type: 'string', description: 'Creation timestamp' }, + lastLogin: { type: 'string', description: 'Last login timestamp', optional: true }, + lastUpdated: { type: 'string', description: 'Last update timestamp' }, + activated: { type: 'string', description: 'Activation timestamp', optional: true }, + statusChanged: { + type: 'string', + description: 'Status change timestamp', + optional: true, + }, + }, + }, + }, + count: { type: 'number', description: 'Number of members returned' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/okta/list_groups.ts b/apps/sim/tools/okta/list_groups.ts new file mode 100644 index 00000000000..24e7583b7c4 --- /dev/null +++ b/apps/sim/tools/okta/list_groups.ts @@ -0,0 +1,127 @@ +import { createLogger } from '@sim/logger' +import type { + OktaApiError, + OktaGroup, + OktaListGroupsParams, + OktaListGroupsResponse, +} from '@/tools/okta/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OktaListGroups') + +export const oktaListGroupsTool: ToolConfig = { + id: 'okta_list_groups', + name: 'List Groups from Okta', + description: 'List all groups in your Okta organization with optional search and filtering', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta API token for authentication', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta domain (e.g., dev-123456.okta.com)', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Okta search expression for groups (e.g., profile.name sw "Engineering" or type eq "OKTA_GROUP")', + }, + filter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Okta filter expression (e.g., type eq "OKTA_GROUP")', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of groups to return (default: 10000, max: 10000)', + }, + }, + + request: { + url: (params) => { + const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const queryParams = new URLSearchParams() + + if (params.search) queryParams.append('search', params.search) + if (params.filter) queryParams.append('filter', params.filter) + if (params.limit) queryParams.append('limit', params.limit.toString()) + + const queryString = queryParams.toString() + return queryString + ? `https://${domain}/api/v1/groups?${queryString}` + : `https://${domain}/api/v1/groups` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `SSWS ${params.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data: OktaGroup[] | OktaApiError = await response.json() + + if (!response.ok) { + const error = data as OktaApiError + logger.error('Okta API request failed', { data: error, status: response.status }) + throw new Error(error.errorSummary || 'Failed to list groups from Okta') + } + + const groups = (data as OktaGroup[]).map((group) => ({ + id: group.id, + name: group.profile?.name ?? '', + description: group.profile?.description ?? null, + type: group.type, + created: group.created, + lastUpdated: group.lastUpdated, + lastMembershipUpdated: group.lastMembershipUpdated ?? null, + })) + + return { + success: true, + output: { + groups, + count: groups.length, + success: true, + }, + } + }, + + outputs: { + groups: { + type: 'array', + description: 'Array of Okta group objects', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Group ID' }, + name: { type: 'string', description: 'Group name' }, + description: { type: 'string', description: 'Group description', optional: true }, + type: { type: 'string', description: 'Group type (OKTA_GROUP, APP_GROUP, BUILT_IN)' }, + created: { type: 'string', description: 'Creation timestamp' }, + lastUpdated: { type: 'string', description: 'Last update timestamp' }, + lastMembershipUpdated: { + type: 'string', + description: 'Last membership change timestamp', + optional: true, + }, + }, + }, + }, + count: { type: 'number', description: 'Number of groups returned' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/okta/list_users.ts b/apps/sim/tools/okta/list_users.ts new file mode 100644 index 00000000000..43679f4c28e --- /dev/null +++ b/apps/sim/tools/okta/list_users.ts @@ -0,0 +1,140 @@ +import { createLogger } from '@sim/logger' +import type { + OktaApiError, + OktaListUsersParams, + OktaListUsersResponse, + OktaUser, +} from '@/tools/okta/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OktaListUsers') + +export const oktaListUsersTool: ToolConfig = { + id: 'okta_list_users', + name: 'List Users from Okta', + description: 'List all users in your Okta organization with optional search and filtering', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta API token for authentication', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta domain (e.g., dev-123456.okta.com)', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Okta search expression (e.g., profile.firstName eq "John" or profile.email co "example.com")', + }, + filter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Okta filter expression (e.g., status eq "ACTIVE")', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of users to return (default: 200, max: 200)', + }, + }, + + request: { + url: (params) => { + const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const queryParams = new URLSearchParams() + + if (params.search) queryParams.append('search', params.search) + if (params.filter) queryParams.append('filter', params.filter) + if (params.limit) queryParams.append('limit', params.limit.toString()) + + const queryString = queryParams.toString() + return queryString + ? `https://${domain}/api/v1/users?${queryString}` + : `https://${domain}/api/v1/users` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `SSWS ${params.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data: OktaUser[] | OktaApiError = await response.json() + + if (!response.ok) { + const error = data as OktaApiError + logger.error('Okta API request failed', { data: error, status: response.status }) + throw new Error(error.errorSummary || 'Failed to list users from Okta') + } + + const users = (data as OktaUser[]).map((user) => ({ + id: user.id, + status: user.status, + firstName: user.profile?.firstName ?? null, + lastName: user.profile?.lastName ?? null, + email: user.profile?.email ?? null, + login: user.profile?.login ?? null, + mobilePhone: user.profile?.mobilePhone ?? null, + title: user.profile?.title ?? null, + department: user.profile?.department ?? null, + created: user.created, + lastLogin: user.lastLogin ?? null, + lastUpdated: user.lastUpdated, + activated: user.activated ?? null, + statusChanged: user.statusChanged ?? null, + })) + + return { + success: true, + output: { + users, + count: users.length, + success: true, + }, + } + }, + + outputs: { + users: { + type: 'array', + description: 'Array of Okta user objects', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'User ID' }, + status: { + type: 'string', + description: 'User status (ACTIVE, STAGED, PROVISIONED, etc.)', + }, + firstName: { type: 'string', description: 'First name', optional: true }, + lastName: { type: 'string', description: 'Last name', optional: true }, + email: { type: 'string', description: 'Email address', optional: true }, + login: { type: 'string', description: 'Login (usually email)', optional: true }, + mobilePhone: { type: 'string', description: 'Mobile phone', optional: true }, + title: { type: 'string', description: 'Job title', optional: true }, + department: { type: 'string', description: 'Department', optional: true }, + created: { type: 'string', description: 'Creation timestamp' }, + lastLogin: { type: 'string', description: 'Last login timestamp', optional: true }, + lastUpdated: { type: 'string', description: 'Last update timestamp' }, + activated: { type: 'string', description: 'Activation timestamp', optional: true }, + statusChanged: { type: 'string', description: 'Status change timestamp', optional: true }, + }, + }, + }, + count: { type: 'number', description: 'Number of users returned' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/okta/remove_user_from_group.ts b/apps/sim/tools/okta/remove_user_from_group.ts new file mode 100644 index 00000000000..0db9e2d56fc --- /dev/null +++ b/apps/sim/tools/okta/remove_user_from_group.ts @@ -0,0 +1,89 @@ +import { createLogger } from '@sim/logger' +import type { + OktaApiError, + OktaRemoveUserFromGroupParams, + OktaRemoveUserFromGroupResponse, +} from '@/tools/okta/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OktaRemoveUserFromGroup') + +export const oktaRemoveUserFromGroupTool: ToolConfig< + OktaRemoveUserFromGroupParams, + OktaRemoveUserFromGroupResponse +> = { + id: 'okta_remove_user_from_group', + name: 'Remove User from Group in Okta', + description: 'Remove a user from a group in your Okta organization', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta API token for authentication', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta domain (e.g., dev-123456.okta.com)', + }, + groupId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Group ID to remove the user from', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'User ID to remove from the group', + }, + }, + + request: { + url: (params) => { + const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId)}/users/${encodeURIComponent(params.userId)}` + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `SSWS ${params.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params) => { + if (!response.ok) { + let error: OktaApiError = {} + try { + error = await response.json() + } catch { + // empty response body + } + logger.error('Okta API request failed', { data: error, status: response.status }) + throw new Error(error.errorSummary || 'Failed to remove user from group in Okta') + } + + return { + success: true, + output: { + groupId: params?.groupId ?? '', + userId: params?.userId ?? '', + removed: true, + success: true, + }, + } + }, + + outputs: { + groupId: { type: 'string', description: 'Group ID' }, + userId: { type: 'string', description: 'User ID removed from the group' }, + removed: { type: 'boolean', description: 'Whether the user was removed' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/okta/reset_password.ts b/apps/sim/tools/okta/reset_password.ts new file mode 100644 index 00000000000..5c23fcda0b9 --- /dev/null +++ b/apps/sim/tools/okta/reset_password.ts @@ -0,0 +1,99 @@ +import { createLogger } from '@sim/logger' +import type { + OktaApiError, + OktaResetPasswordParams, + OktaResetPasswordResponse, +} from '@/tools/okta/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OktaResetPassword') + +export const oktaResetPasswordTool: ToolConfig = + { + id: 'okta_reset_password', + name: 'Reset Password in Okta', + description: + 'Generate a one-time token to reset a user password. Can email the reset link to the user or return it directly. Transitions the user to RECOVERY status.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta API token for authentication', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta domain (e.g., dev-123456.okta.com)', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'User ID or login to reset password for', + }, + sendEmail: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Send password reset email to the user (default: true)', + }, + }, + + request: { + url: (params) => { + const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const sendEmail = params.sendEmail !== false + return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}/lifecycle/reset_password?sendEmail=${sendEmail}` + }, + method: 'POST', + headers: (params) => ({ + Authorization: `SSWS ${params.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params) => { + if (!response.ok) { + let error: OktaApiError = {} + try { + error = await response.json() + } catch { + // empty response body + } + logger.error('Okta API request failed', { data: error, status: response.status }) + throw new Error(error.errorSummary || 'Failed to reset password in Okta') + } + + let resetPasswordUrl: string | null = null + try { + const data = await response.json() + resetPasswordUrl = data.resetPasswordUrl ?? null + } catch { + // empty body when sendEmail=true + } + + return { + success: true, + output: { + userId: params?.userId ?? '', + resetPasswordUrl, + success: true, + }, + } + }, + + outputs: { + userId: { type: 'string', description: 'User ID' }, + resetPasswordUrl: { + type: 'string', + description: 'Password reset URL (only returned when sendEmail is false)', + optional: true, + }, + success: { type: 'boolean', description: 'Operation success status' }, + }, + } diff --git a/apps/sim/tools/okta/suspend_user.ts b/apps/sim/tools/okta/suspend_user.ts new file mode 100644 index 00000000000..a866e81599c --- /dev/null +++ b/apps/sim/tools/okta/suspend_user.ts @@ -0,0 +1,79 @@ +import { createLogger } from '@sim/logger' +import type { + OktaApiError, + OktaSuspendUserParams, + OktaSuspendUserResponse, +} from '@/tools/okta/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OktaSuspendUser') + +export const oktaSuspendUserTool: ToolConfig = { + id: 'okta_suspend_user', + name: 'Suspend User in Okta', + description: + 'Suspend a user in your Okta organization. Only users with ACTIVE status can be suspended. Suspended users cannot log in but retain group and app assignments.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta API token for authentication', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta domain (e.g., dev-123456.okta.com)', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'User ID or login to suspend', + }, + }, + + request: { + url: (params) => { + const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}/lifecycle/suspend` + }, + method: 'POST', + headers: (params) => ({ + Authorization: `SSWS ${params.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params) => { + if (!response.ok) { + let error: OktaApiError = {} + try { + error = await response.json() + } catch { + // empty response body + } + logger.error('Okta API request failed', { data: error, status: response.status }) + throw new Error(error.errorSummary || 'Failed to suspend user in Okta') + } + + return { + success: true, + output: { + userId: params?.userId ?? '', + suspended: true, + success: true, + }, + } + }, + + outputs: { + userId: { type: 'string', description: 'Suspended user ID' }, + suspended: { type: 'boolean', description: 'Whether the user was suspended' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/okta/types.ts b/apps/sim/tools/okta/types.ts new file mode 100644 index 00000000000..ac3081a92e9 --- /dev/null +++ b/apps/sim/tools/okta/types.ts @@ -0,0 +1,446 @@ +import type { ToolResponse } from '@/tools/types' + +/** + * Okta API error response + */ +export interface OktaApiError { + errorCode?: string + errorSummary?: string + errorCauses?: { errorSummary: string }[] +} + +/** + * Common params for all Okta tools + */ +export interface OktaBaseParams { + apiKey: string + domain: string +} + +/** + * Okta User profile object from the API + */ +export interface OktaUserProfile { + firstName?: string | null + lastName?: string | null + email?: string | null + login?: string | null + mobilePhone?: string | null + secondEmail?: string | null + displayName?: string | null + nickName?: string | null + title?: string | null + department?: string | null + organization?: string | null + manager?: string | null + managerId?: string | null + division?: string | null + costCenter?: string | null + employeeNumber?: string | null + userType?: string | null +} + +/** + * Okta User object from the API + */ +export interface OktaUser { + id: string + status: string + created: string + activated: string | null + statusChanged: string | null + lastLogin: string | null + lastUpdated: string + passwordChanged: string | null + type: { id: string } + profile: OktaUserProfile +} + +/** + * Okta Group profile from the API + */ +export interface OktaGroupProfile { + name: string + description?: string | null +} + +/** + * Okta Group object from the API + */ +export interface OktaGroup { + id: string + created: string + lastUpdated: string + lastMembershipUpdated: string | null + type: string + profile: OktaGroupProfile +} + +/** + * Transformed user output + */ +export interface OktaUserOutput { + id: string + status: string + firstName: string | null + lastName: string | null + email: string | null + login: string | null + mobilePhone: string | null + title: string | null + department: string | null + created: string + lastLogin: string | null + lastUpdated: string + activated: string | null + statusChanged: string | null +} + +/** + * Transformed group output + */ +export interface OktaGroupOutput { + id: string + name: string + description: string | null + type: string + created: string + lastUpdated: string + lastMembershipUpdated: string | null +} + +// List Users +export interface OktaListUsersParams extends OktaBaseParams { + search?: string + filter?: string + limit?: number +} + +export interface OktaListUsersResponse extends ToolResponse { + output: { + users: OktaUserOutput[] + count: number + success: boolean + } +} + +// Get User +export interface OktaGetUserParams extends OktaBaseParams { + userId: string +} + +export interface OktaGetUserResponse extends ToolResponse { + output: { + id: string + status: string + firstName: string | null + lastName: string | null + email: string | null + login: string | null + mobilePhone: string | null + secondEmail: string | null + displayName: string | null + title: string | null + department: string | null + organization: string | null + manager: string | null + managerId: string | null + division: string | null + employeeNumber: string | null + userType: string | null + created: string + activated: string | null + lastLogin: string | null + lastUpdated: string + statusChanged: string | null + passwordChanged: string | null + success: boolean + } +} + +// Create User +export interface OktaCreateUserParams extends OktaBaseParams { + firstName: string + lastName: string + email: string + login?: string + password?: string + mobilePhone?: string + title?: string + department?: string + activate?: boolean +} + +export interface OktaCreateUserResponse extends ToolResponse { + output: { + id: string + status: string + firstName: string | null + lastName: string | null + email: string | null + login: string | null + created: string + lastUpdated: string + success: boolean + } +} + +// Update User +export interface OktaUpdateUserParams extends OktaBaseParams { + userId: string + firstName?: string + lastName?: string + email?: string + login?: string + mobilePhone?: string + title?: string + department?: string +} + +export interface OktaUpdateUserResponse extends ToolResponse { + output: { + id: string + status: string + firstName: string | null + lastName: string | null + email: string | null + login: string | null + created: string + lastUpdated: string + success: boolean + } +} + +// Deactivate User +export interface OktaDeactivateUserParams extends OktaBaseParams { + userId: string + sendEmail?: boolean +} + +export interface OktaDeactivateUserResponse extends ToolResponse { + output: { + userId: string + deactivated: boolean + success: boolean + } +} + +// List Groups +export interface OktaListGroupsParams extends OktaBaseParams { + search?: string + filter?: string + limit?: number +} + +export interface OktaListGroupsResponse extends ToolResponse { + output: { + groups: OktaGroupOutput[] + count: number + success: boolean + } +} + +// Get Group +export interface OktaGetGroupParams extends OktaBaseParams { + groupId: string +} + +export interface OktaGetGroupResponse extends ToolResponse { + output: { + id: string + name: string + description: string | null + type: string + created: string + lastUpdated: string + lastMembershipUpdated: string | null + success: boolean + } +} + +// Add User to Group +export interface OktaAddUserToGroupParams extends OktaBaseParams { + groupId: string + userId: string +} + +export interface OktaAddUserToGroupResponse extends ToolResponse { + output: { + groupId: string + userId: string + added: boolean + success: boolean + } +} + +// Remove User from Group +export interface OktaRemoveUserFromGroupParams extends OktaBaseParams { + groupId: string + userId: string +} + +export interface OktaRemoveUserFromGroupResponse extends ToolResponse { + output: { + groupId: string + userId: string + removed: boolean + success: boolean + } +} + +// List Group Members +export interface OktaListGroupMembersParams extends OktaBaseParams { + groupId: string + limit?: number +} + +export interface OktaListGroupMembersResponse extends ToolResponse { + output: { + members: OktaUserOutput[] + count: number + success: boolean + } +} + +// Suspend User +export interface OktaSuspendUserParams extends OktaBaseParams { + userId: string +} + +export interface OktaSuspendUserResponse extends ToolResponse { + output: { + userId: string + suspended: boolean + success: boolean + } +} + +// Unsuspend User +export interface OktaUnsuspendUserParams extends OktaBaseParams { + userId: string +} + +export interface OktaUnsuspendUserResponse extends ToolResponse { + output: { + userId: string + unsuspended: boolean + success: boolean + } +} + +// Activate User +export interface OktaActivateUserParams extends OktaBaseParams { + userId: string + sendEmail?: boolean +} + +export interface OktaActivateUserResponse extends ToolResponse { + output: { + userId: string + activated: boolean + activationUrl: string | null + activationToken: string | null + success: boolean + } +} + +// Reset Password +export interface OktaResetPasswordParams extends OktaBaseParams { + userId: string + sendEmail?: boolean +} + +export interface OktaResetPasswordResponse extends ToolResponse { + output: { + userId: string + resetPasswordUrl: string | null + success: boolean + } +} + +// Delete User +export interface OktaDeleteUserParams extends OktaBaseParams { + userId: string + sendEmail?: boolean +} + +export interface OktaDeleteUserResponse extends ToolResponse { + output: { + userId: string + deleted: boolean + success: boolean + } +} + +// Create Group +export interface OktaCreateGroupParams extends OktaBaseParams { + name: string + description?: string +} + +export interface OktaCreateGroupResponse extends ToolResponse { + output: { + id: string + name: string + description: string | null + type: string + created: string + lastUpdated: string + lastMembershipUpdated: string | null + success: boolean + } +} + +// Update Group +export interface OktaUpdateGroupParams extends OktaBaseParams { + groupId: string + name: string + description?: string +} + +export interface OktaUpdateGroupResponse extends ToolResponse { + output: { + id: string + name: string + description: string | null + type: string + created: string + lastUpdated: string + lastMembershipUpdated: string | null + success: boolean + } +} + +// Delete Group +export interface OktaDeleteGroupParams extends OktaBaseParams { + groupId: string +} + +export interface OktaDeleteGroupResponse extends ToolResponse { + output: { + groupId: string + deleted: boolean + success: boolean + } +} + +// Generic response type for the block +export type OktaResponse = + | OktaListUsersResponse + | OktaGetUserResponse + | OktaCreateUserResponse + | OktaUpdateUserResponse + | OktaDeactivateUserResponse + | OktaSuspendUserResponse + | OktaUnsuspendUserResponse + | OktaActivateUserResponse + | OktaResetPasswordResponse + | OktaDeleteUserResponse + | OktaListGroupsResponse + | OktaGetGroupResponse + | OktaCreateGroupResponse + | OktaUpdateGroupResponse + | OktaDeleteGroupResponse + | OktaAddUserToGroupResponse + | OktaRemoveUserFromGroupResponse + | OktaListGroupMembersResponse diff --git a/apps/sim/tools/okta/unsuspend_user.ts b/apps/sim/tools/okta/unsuspend_user.ts new file mode 100644 index 00000000000..08548ac1110 --- /dev/null +++ b/apps/sim/tools/okta/unsuspend_user.ts @@ -0,0 +1,80 @@ +import { createLogger } from '@sim/logger' +import type { + OktaApiError, + OktaUnsuspendUserParams, + OktaUnsuspendUserResponse, +} from '@/tools/okta/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OktaUnsuspendUser') + +export const oktaUnsuspendUserTool: ToolConfig = + { + id: 'okta_unsuspend_user', + name: 'Unsuspend User in Okta', + description: + 'Unsuspend a previously suspended user in your Okta organization. Returns the user to ACTIVE status.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta API token for authentication', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta domain (e.g., dev-123456.okta.com)', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'User ID or login to unsuspend', + }, + }, + + request: { + url: (params) => { + const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}/lifecycle/unsuspend` + }, + method: 'POST', + headers: (params) => ({ + Authorization: `SSWS ${params.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params) => { + if (!response.ok) { + let error: OktaApiError = {} + try { + error = await response.json() + } catch { + // empty response body + } + logger.error('Okta API request failed', { data: error, status: response.status }) + throw new Error(error.errorSummary || 'Failed to unsuspend user in Okta') + } + + return { + success: true, + output: { + userId: params?.userId ?? '', + unsuspended: true, + success: true, + }, + } + }, + + outputs: { + userId: { type: 'string', description: 'Unsuspended user ID' }, + unsuspended: { type: 'boolean', description: 'Whether the user was unsuspended' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, + } diff --git a/apps/sim/tools/okta/update_group.ts b/apps/sim/tools/okta/update_group.ts new file mode 100644 index 00000000000..68aeffe8bc2 --- /dev/null +++ b/apps/sim/tools/okta/update_group.ts @@ -0,0 +1,109 @@ +import { createLogger } from '@sim/logger' +import type { + OktaApiError, + OktaGroup, + OktaUpdateGroupParams, + OktaUpdateGroupResponse, +} from '@/tools/okta/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OktaUpdateGroup') + +export const oktaUpdateGroupTool: ToolConfig = { + id: 'okta_update_group', + name: 'Update Group in Okta', + description: + 'Update a group profile in your Okta organization. Only groups of OKTA_GROUP type can be updated. All profile properties must be specified (full replacement).', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta API token for authentication', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta domain (e.g., dev-123456.okta.com)', + }, + groupId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Group ID to update', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Updated group name', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated group description', + }, + }, + + request: { + url: (params) => { + const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId)}` + }, + method: 'PUT', + headers: (params) => ({ + Authorization: `SSWS ${params.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + body: (params) => { + const profile: Record = { name: params.name } + if (params.description) profile.description = params.description + return { profile } + }, + }, + + transformResponse: async (response: Response) => { + const data: OktaGroup | OktaApiError = await response.json() + + if (!response.ok) { + const error = data as OktaApiError + logger.error('Okta API request failed', { data: error, status: response.status }) + throw new Error(error.errorSummary || 'Failed to update group in Okta') + } + + const group = data as OktaGroup + return { + success: true, + output: { + id: group.id, + name: group.profile?.name ?? '', + description: group.profile?.description ?? null, + type: group.type, + created: group.created, + lastUpdated: group.lastUpdated, + lastMembershipUpdated: group.lastMembershipUpdated ?? null, + success: true, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Group ID' }, + name: { type: 'string', description: 'Group name' }, + description: { type: 'string', description: 'Group description', optional: true }, + type: { type: 'string', description: 'Group type' }, + created: { type: 'string', description: 'Creation timestamp' }, + lastUpdated: { type: 'string', description: 'Last update timestamp' }, + lastMembershipUpdated: { + type: 'string', + description: 'Last membership change timestamp', + optional: true, + }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/okta/update_user.ts b/apps/sim/tools/okta/update_user.ts new file mode 100644 index 00000000000..627cb6490db --- /dev/null +++ b/apps/sim/tools/okta/update_user.ts @@ -0,0 +1,144 @@ +import { createLogger } from '@sim/logger' +import type { + OktaApiError, + OktaUpdateUserParams, + OktaUpdateUserResponse, + OktaUser, +} from '@/tools/okta/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OktaUpdateUser') + +export const oktaUpdateUserTool: ToolConfig = { + id: 'okta_update_user', + name: 'Update User in Okta', + description: 'Update a user profile in your Okta organization', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta API token for authentication', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Okta domain (e.g., dev-123456.okta.com)', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'User ID or login to update', + }, + firstName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated first name', + }, + lastName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated last name', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated email address', + }, + login: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated login', + }, + mobilePhone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated mobile phone number', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated job title', + }, + department: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated department', + }, + }, + + request: { + url: (params) => { + const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}` + }, + method: 'POST', + headers: (params) => ({ + Authorization: `SSWS ${params.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + body: (params) => { + const profile: Record = {} + + if (params.firstName) profile.firstName = params.firstName + if (params.lastName) profile.lastName = params.lastName + if (params.email) profile.email = params.email + if (params.login) profile.login = params.login + if (params.mobilePhone) profile.mobilePhone = params.mobilePhone + if (params.title) profile.title = params.title + if (params.department) profile.department = params.department + + return { profile } + }, + }, + + transformResponse: async (response: Response) => { + const data: OktaUser | OktaApiError = await response.json() + + if (!response.ok) { + const error = data as OktaApiError + logger.error('Okta API request failed', { data: error, status: response.status }) + throw new Error(error.errorSummary || 'Failed to update user in Okta') + } + + const user = data as OktaUser + return { + success: true, + output: { + id: user.id, + status: user.status, + firstName: user.profile?.firstName ?? null, + lastName: user.profile?.lastName ?? null, + email: user.profile?.email ?? null, + login: user.profile?.login ?? null, + created: user.created, + lastUpdated: user.lastUpdated, + success: true, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'User ID' }, + status: { type: 'string', description: 'User status' }, + firstName: { type: 'string', description: 'First name', optional: true }, + lastName: { type: 'string', description: 'Last name', optional: true }, + email: { type: 'string', description: 'Email address', optional: true }, + login: { type: 'string', description: 'Login', optional: true }, + created: { type: 'string', description: 'Creation timestamp' }, + lastUpdated: { type: 'string', description: 'Last update timestamp' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 416a22b0900..b09b422d465 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1552,6 +1552,26 @@ import { obsidianPatchNoteTool, obsidianSearchTool, } from '@/tools/obsidian' +import { + oktaActivateUserTool, + oktaAddUserToGroupTool, + oktaCreateGroupTool, + oktaCreateUserTool, + oktaDeactivateUserTool, + oktaDeleteGroupTool, + oktaDeleteUserTool, + oktaGetGroupTool, + oktaGetUserTool, + oktaListGroupMembersTool, + oktaListGroupsTool, + oktaListUsersTool, + oktaRemoveUserFromGroupTool, + oktaResetPasswordTool, + oktaSuspendUserTool, + oktaUnsuspendUserTool, + oktaUpdateGroupTool, + oktaUpdateUserTool, +} from '@/tools/okta' import { onedriveCreateFolderTool, onedriveDeleteTool, @@ -2898,6 +2918,24 @@ export const tools: Record = { obsidian_patch_active: obsidianPatchActiveTool, obsidian_patch_note: obsidianPatchNoteTool, obsidian_search: obsidianSearchTool, + okta_list_users: oktaListUsersTool, + okta_get_user: oktaGetUserTool, + okta_create_user: oktaCreateUserTool, + okta_update_user: oktaUpdateUserTool, + okta_activate_user: oktaActivateUserTool, + okta_deactivate_user: oktaDeactivateUserTool, + okta_suspend_user: oktaSuspendUserTool, + okta_unsuspend_user: oktaUnsuspendUserTool, + okta_reset_password: oktaResetPasswordTool, + okta_delete_user: oktaDeleteUserTool, + okta_list_groups: oktaListGroupsTool, + okta_get_group: oktaGetGroupTool, + okta_create_group: oktaCreateGroupTool, + okta_update_group: oktaUpdateGroupTool, + okta_delete_group: oktaDeleteGroupTool, + okta_add_user_to_group: oktaAddUserToGroupTool, + okta_remove_user_from_group: oktaRemoveUserFromGroupTool, + okta_list_group_members: oktaListGroupMembersTool, onepassword_list_vaults: onepasswordListVaultsTool, onepassword_get_vault: onepasswordGetVaultTool, onepassword_list_items: onepasswordListItemsTool, From 1a4e90b3dbb91e5ccf172c90dfefb6355ad8af11 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 19 Mar 2026 14:48:54 -0700 Subject: [PATCH 2/7] docs(okta): add manual description section to generated docs Co-Authored-By: Claude Opus 4.6 --- apps/docs/content/docs/en/tools/okta.mdx | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/apps/docs/content/docs/en/tools/okta.mdx b/apps/docs/content/docs/en/tools/okta.mdx index 62b12d8e49e..09b2f4ec0af 100644 --- a/apps/docs/content/docs/en/tools/okta.mdx +++ b/apps/docs/content/docs/en/tools/okta.mdx @@ -5,11 +5,30 @@ description: Manage users and groups in Okta import { BlockInfoCard } from "@/components/ui/block-info-card" - +{/* MANUAL-CONTENT-START:intro */} +[Okta](https://www.okta.com/) is an identity and access management platform that provides secure authentication, authorization, and user management for organizations. + +With the Okta integration in Sim, you can: + +- **List and search users**: Retrieve users from your Okta org with SCIM search expressions and filters +- **Manage user lifecycle**: Create, activate, deactivate, suspend, unsuspend, and delete users +- **Update user profiles**: Modify user attributes like name, email, phone, title, and department +- **Reset passwords**: Trigger password reset flows with optional email notification +- **Manage groups**: Create, update, delete, and list groups in your organization +- **Manage group membership**: Add or remove users from groups, and list group members + +In Sim, the Okta integration enables your agents to automate identity management tasks as part of their workflows. This allows for scenarios such as onboarding new employees, offboarding departing users, managing group-based access, auditing user status, and responding to security events by suspending or deactivating accounts. + +## Need Help? + +If you encounter issues with the Okta integration, contact us at [help@sim.ai](mailto:help@sim.ai) +{/* MANUAL-CONTENT-END */} + ## Usage Instructions Integrate Okta identity management into your workflow. List, create, update, activate, suspend, and delete users. Reset passwords. Manage groups and group membership. From 93f13f5744fb9f84427fffe9433c0904af417e2d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 19 Mar 2026 14:57:46 -0700 Subject: [PATCH 3/7] =?UTF-8?q?fix(okta):=20address=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20SSRF=20prevention,=20safe=20response=20parsing,=20c?= =?UTF-8?q?onsistent=20sendEmail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add validateOktaDomain() to prevent SSRF via user-supplied domain param - Fix 9 tools to check response.ok before calling response.json() - Make sendEmail query param explicit in deactivate_user and delete_user Co-Authored-By: Claude Opus 4.6 --- apps/sim/tools/okta/activate_user.ts | 11 ++++---- apps/sim/tools/okta/add_user_to_group.ts | 11 ++++---- apps/sim/tools/okta/create_group.ts | 24 ++++++++++------- apps/sim/tools/okta/create_user.ts | 24 ++++++++++------- apps/sim/tools/okta/deactivate_user.ts | 18 ++++++------- apps/sim/tools/okta/delete_group.ts | 11 ++++---- apps/sim/tools/okta/delete_user.ts | 16 +++++++----- apps/sim/tools/okta/get_group.ts | 24 ++++++++++------- apps/sim/tools/okta/get_user.ts | 24 ++++++++++------- apps/sim/tools/okta/list_group_members.ts | 26 ++++++++++++------- apps/sim/tools/okta/list_groups.ts | 26 ++++++++++++------- apps/sim/tools/okta/list_users.ts | 26 ++++++++++++------- apps/sim/tools/okta/remove_user_from_group.ts | 11 ++++---- apps/sim/tools/okta/reset_password.ts | 11 ++++---- apps/sim/tools/okta/suspend_user.ts | 11 ++++---- apps/sim/tools/okta/types.ts | 17 ++++++++++++ apps/sim/tools/okta/unsuspend_user.ts | 11 ++++---- apps/sim/tools/okta/update_group.ts | 24 ++++++++++------- apps/sim/tools/okta/update_user.ts | 24 ++++++++++------- 19 files changed, 208 insertions(+), 142 deletions(-) diff --git a/apps/sim/tools/okta/activate_user.ts b/apps/sim/tools/okta/activate_user.ts index 8375f6f9484..35ba195ce96 100644 --- a/apps/sim/tools/okta/activate_user.ts +++ b/apps/sim/tools/okta/activate_user.ts @@ -1,8 +1,9 @@ import { createLogger } from '@sim/logger' -import type { - OktaActivateUserParams, - OktaActivateUserResponse, - OktaApiError, +import { + type OktaActivateUserParams, + type OktaActivateUserResponse, + type OktaApiError, + validateOktaDomain, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' @@ -44,7 +45,7 @@ export const oktaActivateUserTool: ToolConfig { - const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const domain = validateOktaDomain(params.domain) const sendEmail = params.sendEmail !== false return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}/lifecycle/activate?sendEmail=${sendEmail}` }, diff --git a/apps/sim/tools/okta/add_user_to_group.ts b/apps/sim/tools/okta/add_user_to_group.ts index d4529540a4e..e64430fe782 100644 --- a/apps/sim/tools/okta/add_user_to_group.ts +++ b/apps/sim/tools/okta/add_user_to_group.ts @@ -1,8 +1,9 @@ import { createLogger } from '@sim/logger' -import type { - OktaAddUserToGroupParams, - OktaAddUserToGroupResponse, - OktaApiError, +import { + type OktaAddUserToGroupParams, + type OktaAddUserToGroupResponse, + type OktaApiError, + validateOktaDomain, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' @@ -46,7 +47,7 @@ export const oktaAddUserToGroupTool: ToolConfig< request: { url: (params) => { - const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const domain = validateOktaDomain(params.domain) return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId)}/users/${encodeURIComponent(params.userId)}` }, method: 'PUT', diff --git a/apps/sim/tools/okta/create_group.ts b/apps/sim/tools/okta/create_group.ts index 288cfea0d99..a5e3a5c67c4 100644 --- a/apps/sim/tools/okta/create_group.ts +++ b/apps/sim/tools/okta/create_group.ts @@ -1,9 +1,10 @@ import { createLogger } from '@sim/logger' -import type { - OktaApiError, - OktaCreateGroupParams, - OktaCreateGroupResponse, - OktaGroup, +import { + type OktaApiError, + type OktaCreateGroupParams, + type OktaCreateGroupResponse, + type OktaGroup, + validateOktaDomain, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' @@ -44,7 +45,7 @@ export const oktaCreateGroupTool: ToolConfig { - const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const domain = validateOktaDomain(params.domain) return `https://${domain}/api/v1/groups` }, method: 'POST', @@ -61,15 +62,18 @@ export const oktaCreateGroupTool: ToolConfig { - const data: OktaGroup | OktaApiError = await response.json() - if (!response.ok) { - const error = data as OktaApiError + let error: OktaApiError = {} + try { + error = await response.json() + } catch { + // non-JSON error body + } logger.error('Okta API request failed', { data: error, status: response.status }) throw new Error(error.errorSummary || 'Failed to create group in Okta') } - const group = data as OktaGroup + const group: OktaGroup = await response.json() return { success: true, output: { diff --git a/apps/sim/tools/okta/create_user.ts b/apps/sim/tools/okta/create_user.ts index f26e8a72df8..4f2925edb80 100644 --- a/apps/sim/tools/okta/create_user.ts +++ b/apps/sim/tools/okta/create_user.ts @@ -1,9 +1,10 @@ import { createLogger } from '@sim/logger' -import type { - OktaApiError, - OktaCreateUserParams, - OktaCreateUserResponse, - OktaUser, +import { + type OktaApiError, + type OktaCreateUserParams, + type OktaCreateUserResponse, + type OktaUser, + validateOktaDomain, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' @@ -86,7 +87,7 @@ export const oktaCreateUserTool: ToolConfig { - const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const domain = validateOktaDomain(params.domain) const activate = params.activate !== false return `https://${domain}/api/v1/users?activate=${activate}` }, @@ -121,15 +122,18 @@ export const oktaCreateUserTool: ToolConfig { - const data: OktaUser | OktaApiError = await response.json() - if (!response.ok) { - const error = data as OktaApiError + let error: OktaApiError = {} + try { + error = await response.json() + } catch { + // non-JSON error body + } logger.error('Okta API request failed', { data: error, status: response.status }) throw new Error(error.errorSummary || 'Failed to create user in Okta') } - const user = data as OktaUser + const user: OktaUser = await response.json() return { success: true, output: { diff --git a/apps/sim/tools/okta/deactivate_user.ts b/apps/sim/tools/okta/deactivate_user.ts index f2cde3d78c6..d8d65975684 100644 --- a/apps/sim/tools/okta/deactivate_user.ts +++ b/apps/sim/tools/okta/deactivate_user.ts @@ -1,8 +1,9 @@ import { createLogger } from '@sim/logger' -import type { - OktaApiError, - OktaDeactivateUserParams, - OktaDeactivateUserResponse, +import { + type OktaApiError, + type OktaDeactivateUserParams, + type OktaDeactivateUserResponse, + validateOktaDomain, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' @@ -47,12 +48,9 @@ export const oktaDeactivateUserTool: ToolConfig< request: { url: (params) => { - const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') - const queryParams = new URLSearchParams() - if (params.sendEmail) queryParams.append('sendEmail', 'true') - const queryString = queryParams.toString() - const base = `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}/lifecycle/deactivate` - return queryString ? `${base}?${queryString}` : base + const domain = validateOktaDomain(params.domain) + const sendEmail = params.sendEmail === true + return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}/lifecycle/deactivate?sendEmail=${sendEmail}` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/okta/delete_group.ts b/apps/sim/tools/okta/delete_group.ts index 997632b8f4b..b73c0ab70a4 100644 --- a/apps/sim/tools/okta/delete_group.ts +++ b/apps/sim/tools/okta/delete_group.ts @@ -1,8 +1,9 @@ import { createLogger } from '@sim/logger' -import type { - OktaApiError, - OktaDeleteGroupParams, - OktaDeleteGroupResponse, +import { + type OktaApiError, + type OktaDeleteGroupParams, + type OktaDeleteGroupResponse, + validateOktaDomain, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' @@ -38,7 +39,7 @@ export const oktaDeleteGroupTool: ToolConfig { - const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const domain = validateOktaDomain(params.domain) return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId)}` }, method: 'DELETE', diff --git a/apps/sim/tools/okta/delete_user.ts b/apps/sim/tools/okta/delete_user.ts index 9d8ebb4373b..18b6b359714 100644 --- a/apps/sim/tools/okta/delete_user.ts +++ b/apps/sim/tools/okta/delete_user.ts @@ -1,5 +1,10 @@ import { createLogger } from '@sim/logger' -import type { OktaApiError, OktaDeleteUserParams, OktaDeleteUserResponse } from '@/tools/okta/types' +import { + type OktaApiError, + type OktaDeleteUserParams, + type OktaDeleteUserResponse, + validateOktaDomain, +} from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' const logger = createLogger('OktaDeleteUser') @@ -40,12 +45,9 @@ export const oktaDeleteUserTool: ToolConfig { - const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') - const queryParams = new URLSearchParams() - if (params.sendEmail) queryParams.append('sendEmail', 'true') - const queryString = queryParams.toString() - const base = `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}` - return queryString ? `${base}?${queryString}` : base + const domain = validateOktaDomain(params.domain) + const sendEmail = params.sendEmail === true + return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}?sendEmail=${sendEmail}` }, method: 'DELETE', headers: (params) => ({ diff --git a/apps/sim/tools/okta/get_group.ts b/apps/sim/tools/okta/get_group.ts index 56d085a8ad6..483a2ba1d94 100644 --- a/apps/sim/tools/okta/get_group.ts +++ b/apps/sim/tools/okta/get_group.ts @@ -1,9 +1,10 @@ import { createLogger } from '@sim/logger' -import type { - OktaApiError, - OktaGetGroupParams, - OktaGetGroupResponse, - OktaGroup, +import { + type OktaApiError, + type OktaGetGroupParams, + type OktaGetGroupResponse, + type OktaGroup, + validateOktaDomain, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' @@ -38,7 +39,7 @@ export const oktaGetGroupTool: ToolConfig { - const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const domain = validateOktaDomain(params.domain) return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId)}` }, method: 'GET', @@ -50,15 +51,18 @@ export const oktaGetGroupTool: ToolConfig { - const data: OktaGroup | OktaApiError = await response.json() - if (!response.ok) { - const error = data as OktaApiError + let error: OktaApiError = {} + try { + error = await response.json() + } catch { + // non-JSON error body + } logger.error('Okta API request failed', { data: error, status: response.status }) throw new Error(error.errorSummary || 'Failed to get group from Okta') } - const group = data as OktaGroup + const group: OktaGroup = await response.json() return { success: true, output: { diff --git a/apps/sim/tools/okta/get_user.ts b/apps/sim/tools/okta/get_user.ts index d03cf78a697..257ede4b172 100644 --- a/apps/sim/tools/okta/get_user.ts +++ b/apps/sim/tools/okta/get_user.ts @@ -1,9 +1,10 @@ import { createLogger } from '@sim/logger' -import type { - OktaApiError, - OktaGetUserParams, - OktaGetUserResponse, - OktaUser, +import { + type OktaApiError, + type OktaGetUserParams, + type OktaGetUserResponse, + type OktaUser, + validateOktaDomain, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' @@ -38,7 +39,7 @@ export const oktaGetUserTool: ToolConfig request: { url: (params) => { - const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const domain = validateOktaDomain(params.domain) return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}` }, method: 'GET', @@ -50,15 +51,18 @@ export const oktaGetUserTool: ToolConfig }, transformResponse: async (response: Response) => { - const data: OktaUser | OktaApiError = await response.json() - if (!response.ok) { - const error = data as OktaApiError + let error: OktaApiError = {} + try { + error = await response.json() + } catch { + // non-JSON error body + } logger.error('Okta API request failed', { data: error, status: response.status }) throw new Error(error.errorSummary || 'Failed to get user from Okta') } - const user = data as OktaUser + const user: OktaUser = await response.json() return { success: true, output: { diff --git a/apps/sim/tools/okta/list_group_members.ts b/apps/sim/tools/okta/list_group_members.ts index e696bf4ce5f..b8d77013cfd 100644 --- a/apps/sim/tools/okta/list_group_members.ts +++ b/apps/sim/tools/okta/list_group_members.ts @@ -1,9 +1,10 @@ import { createLogger } from '@sim/logger' -import type { - OktaApiError, - OktaListGroupMembersParams, - OktaListGroupMembersResponse, - OktaUser, +import { + type OktaApiError, + type OktaListGroupMembersParams, + type OktaListGroupMembersResponse, + type OktaUser, + validateOktaDomain, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' @@ -47,7 +48,7 @@ export const oktaListGroupMembersTool: ToolConfig< request: { url: (params) => { - const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const domain = validateOktaDomain(params.domain) const queryParams = new URLSearchParams() if (params.limit) queryParams.append('limit', params.limit.toString()) @@ -65,15 +66,20 @@ export const oktaListGroupMembersTool: ToolConfig< }, transformResponse: async (response: Response) => { - const data: OktaUser[] | OktaApiError = await response.json() - if (!response.ok) { - const error = data as OktaApiError + let error: OktaApiError = {} + try { + error = await response.json() + } catch { + // non-JSON error body + } logger.error('Okta API request failed', { data: error, status: response.status }) throw new Error(error.errorSummary || 'Failed to list group members from Okta') } - const members = (data as OktaUser[]).map((user) => ({ + const data: OktaUser[] = await response.json() + + const members = data.map((user) => ({ id: user.id, status: user.status, firstName: user.profile?.firstName ?? null, diff --git a/apps/sim/tools/okta/list_groups.ts b/apps/sim/tools/okta/list_groups.ts index 24e7583b7c4..6b396e521c4 100644 --- a/apps/sim/tools/okta/list_groups.ts +++ b/apps/sim/tools/okta/list_groups.ts @@ -1,9 +1,10 @@ import { createLogger } from '@sim/logger' -import type { - OktaApiError, - OktaGroup, - OktaListGroupsParams, - OktaListGroupsResponse, +import { + type OktaApiError, + type OktaGroup, + type OktaListGroupsParams, + type OktaListGroupsResponse, + validateOktaDomain, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' @@ -51,7 +52,7 @@ export const oktaListGroupsTool: ToolConfig { - const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const domain = validateOktaDomain(params.domain) const queryParams = new URLSearchParams() if (params.search) queryParams.append('search', params.search) @@ -72,15 +73,20 @@ export const oktaListGroupsTool: ToolConfig { - const data: OktaGroup[] | OktaApiError = await response.json() - if (!response.ok) { - const error = data as OktaApiError + let error: OktaApiError = {} + try { + error = await response.json() + } catch { + // non-JSON error body + } logger.error('Okta API request failed', { data: error, status: response.status }) throw new Error(error.errorSummary || 'Failed to list groups from Okta') } - const groups = (data as OktaGroup[]).map((group) => ({ + const data: OktaGroup[] = await response.json() + + const groups = data.map((group) => ({ id: group.id, name: group.profile?.name ?? '', description: group.profile?.description ?? null, diff --git a/apps/sim/tools/okta/list_users.ts b/apps/sim/tools/okta/list_users.ts index 43679f4c28e..401c50a7260 100644 --- a/apps/sim/tools/okta/list_users.ts +++ b/apps/sim/tools/okta/list_users.ts @@ -1,9 +1,10 @@ import { createLogger } from '@sim/logger' -import type { - OktaApiError, - OktaListUsersParams, - OktaListUsersResponse, - OktaUser, +import { + type OktaApiError, + type OktaListUsersParams, + type OktaListUsersResponse, + type OktaUser, + validateOktaDomain, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' @@ -51,7 +52,7 @@ export const oktaListUsersTool: ToolConfig { - const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const domain = validateOktaDomain(params.domain) const queryParams = new URLSearchParams() if (params.search) queryParams.append('search', params.search) @@ -72,15 +73,20 @@ export const oktaListUsersTool: ToolConfig { - const data: OktaUser[] | OktaApiError = await response.json() - if (!response.ok) { - const error = data as OktaApiError + let error: OktaApiError = {} + try { + error = await response.json() + } catch { + // non-JSON error body + } logger.error('Okta API request failed', { data: error, status: response.status }) throw new Error(error.errorSummary || 'Failed to list users from Okta') } - const users = (data as OktaUser[]).map((user) => ({ + const data: OktaUser[] = await response.json() + + const users = data.map((user) => ({ id: user.id, status: user.status, firstName: user.profile?.firstName ?? null, diff --git a/apps/sim/tools/okta/remove_user_from_group.ts b/apps/sim/tools/okta/remove_user_from_group.ts index 0db9e2d56fc..d3a4e78f3c3 100644 --- a/apps/sim/tools/okta/remove_user_from_group.ts +++ b/apps/sim/tools/okta/remove_user_from_group.ts @@ -1,8 +1,9 @@ import { createLogger } from '@sim/logger' -import type { - OktaApiError, - OktaRemoveUserFromGroupParams, - OktaRemoveUserFromGroupResponse, +import { + type OktaApiError, + type OktaRemoveUserFromGroupParams, + type OktaRemoveUserFromGroupResponse, + validateOktaDomain, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' @@ -46,7 +47,7 @@ export const oktaRemoveUserFromGroupTool: ToolConfig< request: { url: (params) => { - const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const domain = validateOktaDomain(params.domain) return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId)}/users/${encodeURIComponent(params.userId)}` }, method: 'DELETE', diff --git a/apps/sim/tools/okta/reset_password.ts b/apps/sim/tools/okta/reset_password.ts index 5c23fcda0b9..0e00c4ec170 100644 --- a/apps/sim/tools/okta/reset_password.ts +++ b/apps/sim/tools/okta/reset_password.ts @@ -1,8 +1,9 @@ import { createLogger } from '@sim/logger' -import type { - OktaApiError, - OktaResetPasswordParams, - OktaResetPasswordResponse, +import { + type OktaApiError, + type OktaResetPasswordParams, + type OktaResetPasswordResponse, + validateOktaDomain, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' @@ -45,7 +46,7 @@ export const oktaResetPasswordTool: ToolConfig { - const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const domain = validateOktaDomain(params.domain) const sendEmail = params.sendEmail !== false return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}/lifecycle/reset_password?sendEmail=${sendEmail}` }, diff --git a/apps/sim/tools/okta/suspend_user.ts b/apps/sim/tools/okta/suspend_user.ts index a866e81599c..2945764beba 100644 --- a/apps/sim/tools/okta/suspend_user.ts +++ b/apps/sim/tools/okta/suspend_user.ts @@ -1,8 +1,9 @@ import { createLogger } from '@sim/logger' -import type { - OktaApiError, - OktaSuspendUserParams, - OktaSuspendUserResponse, +import { + type OktaApiError, + type OktaSuspendUserParams, + type OktaSuspendUserResponse, + validateOktaDomain, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' @@ -38,7 +39,7 @@ export const oktaSuspendUserTool: ToolConfig { - const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const domain = validateOktaDomain(params.domain) return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}/lifecycle/suspend` }, method: 'POST', diff --git a/apps/sim/tools/okta/types.ts b/apps/sim/tools/okta/types.ts index ac3081a92e9..db5dcfb840d 100644 --- a/apps/sim/tools/okta/types.ts +++ b/apps/sim/tools/okta/types.ts @@ -1,5 +1,22 @@ import type { ToolResponse } from '@/tools/types' +const OKTA_DOMAIN_PATTERN = + /^[a-zA-Z0-9][a-zA-Z0-9-]*\.(okta|okta-gov|okta-emea|oktapreview|trexcloud)\.com$/ + +/** + * Validates and sanitizes an Okta domain to prevent SSRF. + * Ensures the domain matches a known Okta domain suffix. + */ +export function validateOktaDomain(rawDomain: string): string { + const domain = rawDomain.replace(/^https?:\/\//, '').replace(/\/$/, '') + if (!OKTA_DOMAIN_PATTERN.test(domain)) { + throw new Error( + `Invalid Okta domain: "${domain}". Must be a valid Okta domain (e.g., dev-123456.okta.com)` + ) + } + return domain +} + /** * Okta API error response */ diff --git a/apps/sim/tools/okta/unsuspend_user.ts b/apps/sim/tools/okta/unsuspend_user.ts index 08548ac1110..7dddaf13109 100644 --- a/apps/sim/tools/okta/unsuspend_user.ts +++ b/apps/sim/tools/okta/unsuspend_user.ts @@ -1,8 +1,9 @@ import { createLogger } from '@sim/logger' -import type { - OktaApiError, - OktaUnsuspendUserParams, - OktaUnsuspendUserResponse, +import { + type OktaApiError, + type OktaUnsuspendUserParams, + type OktaUnsuspendUserResponse, + validateOktaDomain, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' @@ -39,7 +40,7 @@ export const oktaUnsuspendUserTool: ToolConfig { - const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const domain = validateOktaDomain(params.domain) return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}/lifecycle/unsuspend` }, method: 'POST', diff --git a/apps/sim/tools/okta/update_group.ts b/apps/sim/tools/okta/update_group.ts index 68aeffe8bc2..eaec49a199d 100644 --- a/apps/sim/tools/okta/update_group.ts +++ b/apps/sim/tools/okta/update_group.ts @@ -1,9 +1,10 @@ import { createLogger } from '@sim/logger' -import type { - OktaApiError, - OktaGroup, - OktaUpdateGroupParams, - OktaUpdateGroupResponse, +import { + type OktaApiError, + type OktaGroup, + type OktaUpdateGroupParams, + type OktaUpdateGroupResponse, + validateOktaDomain, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' @@ -51,7 +52,7 @@ export const oktaUpdateGroupTool: ToolConfig { - const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const domain = validateOktaDomain(params.domain) return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId)}` }, method: 'PUT', @@ -68,15 +69,18 @@ export const oktaUpdateGroupTool: ToolConfig { - const data: OktaGroup | OktaApiError = await response.json() - if (!response.ok) { - const error = data as OktaApiError + let error: OktaApiError = {} + try { + error = await response.json() + } catch { + // non-JSON error body + } logger.error('Okta API request failed', { data: error, status: response.status }) throw new Error(error.errorSummary || 'Failed to update group in Okta') } - const group = data as OktaGroup + const group: OktaGroup = await response.json() return { success: true, output: { diff --git a/apps/sim/tools/okta/update_user.ts b/apps/sim/tools/okta/update_user.ts index 627cb6490db..b9367dce9db 100644 --- a/apps/sim/tools/okta/update_user.ts +++ b/apps/sim/tools/okta/update_user.ts @@ -1,9 +1,10 @@ import { createLogger } from '@sim/logger' -import type { - OktaApiError, - OktaUpdateUserParams, - OktaUpdateUserResponse, - OktaUser, +import { + type OktaApiError, + type OktaUpdateUserParams, + type OktaUpdateUserResponse, + type OktaUser, + validateOktaDomain, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' @@ -80,7 +81,7 @@ export const oktaUpdateUserTool: ToolConfig { - const domain = params.domain.replace(/^https?:\/\//, '').replace(/\/$/, '') + const domain = validateOktaDomain(params.domain) return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}` }, method: 'POST', @@ -105,15 +106,18 @@ export const oktaUpdateUserTool: ToolConfig { - const data: OktaUser | OktaApiError = await response.json() - if (!response.ok) { - const error = data as OktaApiError + let error: OktaApiError = {} + try { + error = await response.json() + } catch { + // non-JSON error body + } logger.error('Okta API request failed', { data: error, status: response.status }) throw new Error(error.errorSummary || 'Failed to update user in Okta') } - const user = data as OktaUser + const user: OktaUser = await response.json() return { success: true, output: { From ed0ee8748e2aa932d01f468713946e24a06dd434 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 19 Mar 2026 15:09:10 -0700 Subject: [PATCH 4/7] fix(okta): only forward boolean switches when explicitly true Switch subBlocks default to OFF (false), which was being forwarded to tools and overriding their default-true behavior for sendEmail and activate params. Now only forward these when explicitly toggled ON. Co-Authored-By: Claude Opus 4.6 --- apps/sim/blocks/blocks/okta.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/sim/blocks/blocks/okta.ts b/apps/sim/blocks/blocks/okta.ts index 92f1b3047ab..bdfd0572184 100644 --- a/apps/sim/blocks/blocks/okta.ts +++ b/apps/sim/blocks/blocks/okta.ts @@ -291,6 +291,10 @@ export const OktaBlock: BlockConfig = { if (params.groupName) result.name = params.groupName if (params.groupDescription) result.description = params.groupDescription + // Boolean switches: only forward when explicitly true so tool defaults apply when OFF + if (params.sendEmail === true) result.sendEmail = true + if (params.activate === true) result.activate = true + // Pass through all other non-empty params const skipKeys = new Set([ 'operation', @@ -299,6 +303,8 @@ export const OktaBlock: BlockConfig = { 'limit', 'groupName', 'groupDescription', + 'sendEmail', + 'activate', ]) for (const [key, value] of Object.entries(params)) { if (!skipKeys.has(key) && value !== undefined && value !== null && value !== '') { From 40e650a89afdee43944c3d66ce56cc02c04cd865 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 19 Mar 2026 15:20:50 -0700 Subject: [PATCH 5/7] fix(okta): use nullish coalescing for boolean switch defaults Block now forwards sendEmail/activate values as-is (including false). Tools use ?? operator so: explicit true/false from switches are respected, undefined (programmatic calls) still defaults to true. Co-Authored-By: Claude Opus 4.6 --- apps/sim/blocks/blocks/okta.ts | 6 ------ apps/sim/tools/okta/activate_user.ts | 2 +- apps/sim/tools/okta/create_user.ts | 2 +- apps/sim/tools/okta/reset_password.ts | 2 +- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/apps/sim/blocks/blocks/okta.ts b/apps/sim/blocks/blocks/okta.ts index bdfd0572184..92f1b3047ab 100644 --- a/apps/sim/blocks/blocks/okta.ts +++ b/apps/sim/blocks/blocks/okta.ts @@ -291,10 +291,6 @@ export const OktaBlock: BlockConfig = { if (params.groupName) result.name = params.groupName if (params.groupDescription) result.description = params.groupDescription - // Boolean switches: only forward when explicitly true so tool defaults apply when OFF - if (params.sendEmail === true) result.sendEmail = true - if (params.activate === true) result.activate = true - // Pass through all other non-empty params const skipKeys = new Set([ 'operation', @@ -303,8 +299,6 @@ export const OktaBlock: BlockConfig = { 'limit', 'groupName', 'groupDescription', - 'sendEmail', - 'activate', ]) for (const [key, value] of Object.entries(params)) { if (!skipKeys.has(key) && value !== undefined && value !== null && value !== '') { diff --git a/apps/sim/tools/okta/activate_user.ts b/apps/sim/tools/okta/activate_user.ts index 35ba195ce96..34f3d82f06b 100644 --- a/apps/sim/tools/okta/activate_user.ts +++ b/apps/sim/tools/okta/activate_user.ts @@ -46,7 +46,7 @@ export const oktaActivateUserTool: ToolConfig { const domain = validateOktaDomain(params.domain) - const sendEmail = params.sendEmail !== false + const sendEmail = params.sendEmail ?? true return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}/lifecycle/activate?sendEmail=${sendEmail}` }, method: 'POST', diff --git a/apps/sim/tools/okta/create_user.ts b/apps/sim/tools/okta/create_user.ts index 4f2925edb80..9ab1ba0e00e 100644 --- a/apps/sim/tools/okta/create_user.ts +++ b/apps/sim/tools/okta/create_user.ts @@ -88,7 +88,7 @@ export const oktaCreateUserTool: ToolConfig { const domain = validateOktaDomain(params.domain) - const activate = params.activate !== false + const activate = params.activate ?? true return `https://${domain}/api/v1/users?activate=${activate}` }, method: 'POST', diff --git a/apps/sim/tools/okta/reset_password.ts b/apps/sim/tools/okta/reset_password.ts index 0e00c4ec170..bddf730890c 100644 --- a/apps/sim/tools/okta/reset_password.ts +++ b/apps/sim/tools/okta/reset_password.ts @@ -47,7 +47,7 @@ export const oktaResetPasswordTool: ToolConfig { const domain = validateOktaDomain(params.domain) - const sendEmail = params.sendEmail !== false + const sendEmail = params.sendEmail ?? true return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}/lifecycle/reset_password?sendEmail=${sendEmail}` }, method: 'POST', From c613e78bb691c073fb04d74ff7a08f5020c806cf Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 19 Mar 2026 15:33:02 -0700 Subject: [PATCH 6/7] fix(okta): prevent silent data loss in update operations - update_group: always include description in PUT body (defaults to '') since PUT replaces the full profile object - update_user: use !== undefined checks so empty strings can clear fields via Okta's POST partial update - block: allow empty strings through passthrough loop and use !== undefined for groupDescription mapping Co-Authored-By: Claude Opus 4.6 --- apps/sim/blocks/blocks/okta.ts | 5 +++-- apps/sim/tools/okta/update_group.ts | 11 ++++++----- apps/sim/tools/okta/update_user.ts | 14 +++++++------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/apps/sim/blocks/blocks/okta.ts b/apps/sim/blocks/blocks/okta.ts index 92f1b3047ab..e0b284c1bb6 100644 --- a/apps/sim/blocks/blocks/okta.ts +++ b/apps/sim/blocks/blocks/okta.ts @@ -289,9 +289,10 @@ export const OktaBlock: BlockConfig = { // Map group-specific UI fields to tool param names if (params.groupName) result.name = params.groupName - if (params.groupDescription) result.description = params.groupDescription + if (params.groupDescription !== undefined) result.description = params.groupDescription // Pass through all other non-empty params + // Allow empty strings so users can clear fields (e.g. update_user partial updates) const skipKeys = new Set([ 'operation', 'apiKey', @@ -301,7 +302,7 @@ export const OktaBlock: BlockConfig = { 'groupDescription', ]) for (const [key, value] of Object.entries(params)) { - if (!skipKeys.has(key) && value !== undefined && value !== null && value !== '') { + if (!skipKeys.has(key) && value !== undefined && value !== null) { result[key] = value } } diff --git a/apps/sim/tools/okta/update_group.ts b/apps/sim/tools/okta/update_group.ts index eaec49a199d..b318a7a845f 100644 --- a/apps/sim/tools/okta/update_group.ts +++ b/apps/sim/tools/okta/update_group.ts @@ -61,11 +61,12 @@ export const oktaUpdateGroupTool: ToolConfig { - const profile: Record = { name: params.name } - if (params.description) profile.description = params.description - return { profile } - }, + body: (params) => ({ + profile: { + name: params.name, + description: params.description ?? '', + }, + }), }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/okta/update_user.ts b/apps/sim/tools/okta/update_user.ts index b9367dce9db..747dfa0f432 100644 --- a/apps/sim/tools/okta/update_user.ts +++ b/apps/sim/tools/okta/update_user.ts @@ -93,13 +93,13 @@ export const oktaUpdateUserTool: ToolConfig { const profile: Record = {} - if (params.firstName) profile.firstName = params.firstName - if (params.lastName) profile.lastName = params.lastName - if (params.email) profile.email = params.email - if (params.login) profile.login = params.login - if (params.mobilePhone) profile.mobilePhone = params.mobilePhone - if (params.title) profile.title = params.title - if (params.department) profile.department = params.department + if (params.firstName !== undefined) profile.firstName = params.firstName + if (params.lastName !== undefined) profile.lastName = params.lastName + if (params.email !== undefined) profile.email = params.email + if (params.login !== undefined) profile.login = params.login + if (params.mobilePhone !== undefined) profile.mobilePhone = params.mobilePhone + if (params.title !== undefined) profile.title = params.title + if (params.department !== undefined) profile.department = params.department return { profile } }, From 67096560db5764e8acef435aa74a27764502206b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 19 Mar 2026 15:53:17 -0700 Subject: [PATCH 7/7] refactor(okta): move validateOktaDomain to centralized input-validation - Moved validateOktaDomain from tools/okta/types.ts to lib/core/security/input-validation.ts alongside other validation utils - Added .trim() to handle copy-paste whitespace in domain input - Updated all 18 tool files to import from the new location Co-Authored-By: Claude Opus 4.6 --- .../sim/lib/core/security/input-validation.ts | 30 +++++++++++++++++++ apps/sim/tools/okta/activate_user.ts | 10 +++---- apps/sim/tools/okta/add_user_to_group.ts | 10 +++---- apps/sim/tools/okta/create_group.ts | 12 ++++---- apps/sim/tools/okta/create_user.ts | 12 ++++---- apps/sim/tools/okta/deactivate_user.ts | 10 +++---- apps/sim/tools/okta/delete_group.ts | 10 +++---- apps/sim/tools/okta/delete_user.ts | 8 ++--- apps/sim/tools/okta/get_group.ts | 12 ++++---- apps/sim/tools/okta/get_user.ts | 12 ++++---- apps/sim/tools/okta/list_group_members.ts | 12 ++++---- apps/sim/tools/okta/list_groups.ts | 12 ++++---- apps/sim/tools/okta/list_users.ts | 12 ++++---- apps/sim/tools/okta/remove_user_from_group.ts | 10 +++---- apps/sim/tools/okta/reset_password.ts | 10 +++---- apps/sim/tools/okta/suspend_user.ts | 10 +++---- apps/sim/tools/okta/types.ts | 17 ----------- apps/sim/tools/okta/unsuspend_user.ts | 10 +++---- apps/sim/tools/okta/update_group.ts | 12 ++++---- apps/sim/tools/okta/update_user.ts | 12 ++++---- 20 files changed, 126 insertions(+), 117 deletions(-) diff --git a/apps/sim/lib/core/security/input-validation.ts b/apps/sim/lib/core/security/input-validation.ts index 21abb77c545..ce803fdef53 100644 --- a/apps/sim/lib/core/security/input-validation.ts +++ b/apps/sim/lib/core/security/input-validation.ts @@ -1192,3 +1192,33 @@ export function validateCallbackUrl(url: string): boolean { return false } } + +const OKTA_DOMAIN_PATTERN = + /^[a-zA-Z0-9][a-zA-Z0-9-]*\.(okta|okta-gov|okta-emea|oktapreview|trexcloud)\.com$/ + +/** + * Validates and sanitizes an Okta domain to prevent SSRF. + * Ensures the domain matches a known Okta domain suffix. + * + * @param rawDomain - The raw domain string (may include protocol, trailing slash, or whitespace) + * @returns The cleaned, validated domain string + * @throws Error if the domain does not match a known Okta domain suffix + * + * @example + * ```typescript + * const domain = validateOktaDomain(params.domain) + * // Returns: "dev-123456.okta.com" + * ``` + */ +export function validateOktaDomain(rawDomain: string): string { + const domain = rawDomain + .trim() + .replace(/^https?:\/\//, '') + .replace(/\/$/, '') + if (!OKTA_DOMAIN_PATTERN.test(domain)) { + throw new Error( + `Invalid Okta domain: "${domain}". Must be a valid Okta domain (e.g., dev-123456.okta.com)` + ) + } + return domain +} diff --git a/apps/sim/tools/okta/activate_user.ts b/apps/sim/tools/okta/activate_user.ts index 34f3d82f06b..9f8d40cb0d5 100644 --- a/apps/sim/tools/okta/activate_user.ts +++ b/apps/sim/tools/okta/activate_user.ts @@ -1,9 +1,9 @@ import { createLogger } from '@sim/logger' -import { - type OktaActivateUserParams, - type OktaActivateUserResponse, - type OktaApiError, - validateOktaDomain, +import { validateOktaDomain } from '@/lib/core/security/input-validation' +import type { + OktaActivateUserParams, + OktaActivateUserResponse, + OktaApiError, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' diff --git a/apps/sim/tools/okta/add_user_to_group.ts b/apps/sim/tools/okta/add_user_to_group.ts index e64430fe782..2122344b884 100644 --- a/apps/sim/tools/okta/add_user_to_group.ts +++ b/apps/sim/tools/okta/add_user_to_group.ts @@ -1,9 +1,9 @@ import { createLogger } from '@sim/logger' -import { - type OktaAddUserToGroupParams, - type OktaAddUserToGroupResponse, - type OktaApiError, - validateOktaDomain, +import { validateOktaDomain } from '@/lib/core/security/input-validation' +import type { + OktaAddUserToGroupParams, + OktaAddUserToGroupResponse, + OktaApiError, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' diff --git a/apps/sim/tools/okta/create_group.ts b/apps/sim/tools/okta/create_group.ts index a5e3a5c67c4..188daa3e0f1 100644 --- a/apps/sim/tools/okta/create_group.ts +++ b/apps/sim/tools/okta/create_group.ts @@ -1,10 +1,10 @@ import { createLogger } from '@sim/logger' -import { - type OktaApiError, - type OktaCreateGroupParams, - type OktaCreateGroupResponse, - type OktaGroup, - validateOktaDomain, +import { validateOktaDomain } from '@/lib/core/security/input-validation' +import type { + OktaApiError, + OktaCreateGroupParams, + OktaCreateGroupResponse, + OktaGroup, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' diff --git a/apps/sim/tools/okta/create_user.ts b/apps/sim/tools/okta/create_user.ts index 9ab1ba0e00e..d014d18438a 100644 --- a/apps/sim/tools/okta/create_user.ts +++ b/apps/sim/tools/okta/create_user.ts @@ -1,10 +1,10 @@ import { createLogger } from '@sim/logger' -import { - type OktaApiError, - type OktaCreateUserParams, - type OktaCreateUserResponse, - type OktaUser, - validateOktaDomain, +import { validateOktaDomain } from '@/lib/core/security/input-validation' +import type { + OktaApiError, + OktaCreateUserParams, + OktaCreateUserResponse, + OktaUser, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' diff --git a/apps/sim/tools/okta/deactivate_user.ts b/apps/sim/tools/okta/deactivate_user.ts index d8d65975684..d6aaf782cd6 100644 --- a/apps/sim/tools/okta/deactivate_user.ts +++ b/apps/sim/tools/okta/deactivate_user.ts @@ -1,9 +1,9 @@ import { createLogger } from '@sim/logger' -import { - type OktaApiError, - type OktaDeactivateUserParams, - type OktaDeactivateUserResponse, - validateOktaDomain, +import { validateOktaDomain } from '@/lib/core/security/input-validation' +import type { + OktaApiError, + OktaDeactivateUserParams, + OktaDeactivateUserResponse, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' diff --git a/apps/sim/tools/okta/delete_group.ts b/apps/sim/tools/okta/delete_group.ts index b73c0ab70a4..f1b0363fa5e 100644 --- a/apps/sim/tools/okta/delete_group.ts +++ b/apps/sim/tools/okta/delete_group.ts @@ -1,9 +1,9 @@ import { createLogger } from '@sim/logger' -import { - type OktaApiError, - type OktaDeleteGroupParams, - type OktaDeleteGroupResponse, - validateOktaDomain, +import { validateOktaDomain } from '@/lib/core/security/input-validation' +import type { + OktaApiError, + OktaDeleteGroupParams, + OktaDeleteGroupResponse, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' diff --git a/apps/sim/tools/okta/delete_user.ts b/apps/sim/tools/okta/delete_user.ts index 18b6b359714..5c58fb9c429 100644 --- a/apps/sim/tools/okta/delete_user.ts +++ b/apps/sim/tools/okta/delete_user.ts @@ -1,10 +1,6 @@ import { createLogger } from '@sim/logger' -import { - type OktaApiError, - type OktaDeleteUserParams, - type OktaDeleteUserResponse, - validateOktaDomain, -} from '@/tools/okta/types' +import { validateOktaDomain } from '@/lib/core/security/input-validation' +import type { OktaApiError, OktaDeleteUserParams, OktaDeleteUserResponse } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' const logger = createLogger('OktaDeleteUser') diff --git a/apps/sim/tools/okta/get_group.ts b/apps/sim/tools/okta/get_group.ts index 483a2ba1d94..04c3ac67838 100644 --- a/apps/sim/tools/okta/get_group.ts +++ b/apps/sim/tools/okta/get_group.ts @@ -1,10 +1,10 @@ import { createLogger } from '@sim/logger' -import { - type OktaApiError, - type OktaGetGroupParams, - type OktaGetGroupResponse, - type OktaGroup, - validateOktaDomain, +import { validateOktaDomain } from '@/lib/core/security/input-validation' +import type { + OktaApiError, + OktaGetGroupParams, + OktaGetGroupResponse, + OktaGroup, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' diff --git a/apps/sim/tools/okta/get_user.ts b/apps/sim/tools/okta/get_user.ts index 257ede4b172..516fd312e78 100644 --- a/apps/sim/tools/okta/get_user.ts +++ b/apps/sim/tools/okta/get_user.ts @@ -1,10 +1,10 @@ import { createLogger } from '@sim/logger' -import { - type OktaApiError, - type OktaGetUserParams, - type OktaGetUserResponse, - type OktaUser, - validateOktaDomain, +import { validateOktaDomain } from '@/lib/core/security/input-validation' +import type { + OktaApiError, + OktaGetUserParams, + OktaGetUserResponse, + OktaUser, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' diff --git a/apps/sim/tools/okta/list_group_members.ts b/apps/sim/tools/okta/list_group_members.ts index b8d77013cfd..e0b5a036959 100644 --- a/apps/sim/tools/okta/list_group_members.ts +++ b/apps/sim/tools/okta/list_group_members.ts @@ -1,10 +1,10 @@ import { createLogger } from '@sim/logger' -import { - type OktaApiError, - type OktaListGroupMembersParams, - type OktaListGroupMembersResponse, - type OktaUser, - validateOktaDomain, +import { validateOktaDomain } from '@/lib/core/security/input-validation' +import type { + OktaApiError, + OktaListGroupMembersParams, + OktaListGroupMembersResponse, + OktaUser, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' diff --git a/apps/sim/tools/okta/list_groups.ts b/apps/sim/tools/okta/list_groups.ts index 6b396e521c4..9136edc5472 100644 --- a/apps/sim/tools/okta/list_groups.ts +++ b/apps/sim/tools/okta/list_groups.ts @@ -1,10 +1,10 @@ import { createLogger } from '@sim/logger' -import { - type OktaApiError, - type OktaGroup, - type OktaListGroupsParams, - type OktaListGroupsResponse, - validateOktaDomain, +import { validateOktaDomain } from '@/lib/core/security/input-validation' +import type { + OktaApiError, + OktaGroup, + OktaListGroupsParams, + OktaListGroupsResponse, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' diff --git a/apps/sim/tools/okta/list_users.ts b/apps/sim/tools/okta/list_users.ts index 401c50a7260..fa0cd8ed0eb 100644 --- a/apps/sim/tools/okta/list_users.ts +++ b/apps/sim/tools/okta/list_users.ts @@ -1,10 +1,10 @@ import { createLogger } from '@sim/logger' -import { - type OktaApiError, - type OktaListUsersParams, - type OktaListUsersResponse, - type OktaUser, - validateOktaDomain, +import { validateOktaDomain } from '@/lib/core/security/input-validation' +import type { + OktaApiError, + OktaListUsersParams, + OktaListUsersResponse, + OktaUser, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' diff --git a/apps/sim/tools/okta/remove_user_from_group.ts b/apps/sim/tools/okta/remove_user_from_group.ts index d3a4e78f3c3..7a3c7c42dc3 100644 --- a/apps/sim/tools/okta/remove_user_from_group.ts +++ b/apps/sim/tools/okta/remove_user_from_group.ts @@ -1,9 +1,9 @@ import { createLogger } from '@sim/logger' -import { - type OktaApiError, - type OktaRemoveUserFromGroupParams, - type OktaRemoveUserFromGroupResponse, - validateOktaDomain, +import { validateOktaDomain } from '@/lib/core/security/input-validation' +import type { + OktaApiError, + OktaRemoveUserFromGroupParams, + OktaRemoveUserFromGroupResponse, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' diff --git a/apps/sim/tools/okta/reset_password.ts b/apps/sim/tools/okta/reset_password.ts index bddf730890c..f485ea047b2 100644 --- a/apps/sim/tools/okta/reset_password.ts +++ b/apps/sim/tools/okta/reset_password.ts @@ -1,9 +1,9 @@ import { createLogger } from '@sim/logger' -import { - type OktaApiError, - type OktaResetPasswordParams, - type OktaResetPasswordResponse, - validateOktaDomain, +import { validateOktaDomain } from '@/lib/core/security/input-validation' +import type { + OktaApiError, + OktaResetPasswordParams, + OktaResetPasswordResponse, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' diff --git a/apps/sim/tools/okta/suspend_user.ts b/apps/sim/tools/okta/suspend_user.ts index 2945764beba..1615905b7c4 100644 --- a/apps/sim/tools/okta/suspend_user.ts +++ b/apps/sim/tools/okta/suspend_user.ts @@ -1,9 +1,9 @@ import { createLogger } from '@sim/logger' -import { - type OktaApiError, - type OktaSuspendUserParams, - type OktaSuspendUserResponse, - validateOktaDomain, +import { validateOktaDomain } from '@/lib/core/security/input-validation' +import type { + OktaApiError, + OktaSuspendUserParams, + OktaSuspendUserResponse, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' diff --git a/apps/sim/tools/okta/types.ts b/apps/sim/tools/okta/types.ts index db5dcfb840d..ac3081a92e9 100644 --- a/apps/sim/tools/okta/types.ts +++ b/apps/sim/tools/okta/types.ts @@ -1,22 +1,5 @@ import type { ToolResponse } from '@/tools/types' -const OKTA_DOMAIN_PATTERN = - /^[a-zA-Z0-9][a-zA-Z0-9-]*\.(okta|okta-gov|okta-emea|oktapreview|trexcloud)\.com$/ - -/** - * Validates and sanitizes an Okta domain to prevent SSRF. - * Ensures the domain matches a known Okta domain suffix. - */ -export function validateOktaDomain(rawDomain: string): string { - const domain = rawDomain.replace(/^https?:\/\//, '').replace(/\/$/, '') - if (!OKTA_DOMAIN_PATTERN.test(domain)) { - throw new Error( - `Invalid Okta domain: "${domain}". Must be a valid Okta domain (e.g., dev-123456.okta.com)` - ) - } - return domain -} - /** * Okta API error response */ diff --git a/apps/sim/tools/okta/unsuspend_user.ts b/apps/sim/tools/okta/unsuspend_user.ts index 7dddaf13109..3b3a2f7569e 100644 --- a/apps/sim/tools/okta/unsuspend_user.ts +++ b/apps/sim/tools/okta/unsuspend_user.ts @@ -1,9 +1,9 @@ import { createLogger } from '@sim/logger' -import { - type OktaApiError, - type OktaUnsuspendUserParams, - type OktaUnsuspendUserResponse, - validateOktaDomain, +import { validateOktaDomain } from '@/lib/core/security/input-validation' +import type { + OktaApiError, + OktaUnsuspendUserParams, + OktaUnsuspendUserResponse, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' diff --git a/apps/sim/tools/okta/update_group.ts b/apps/sim/tools/okta/update_group.ts index b318a7a845f..dd8d2d6c8bc 100644 --- a/apps/sim/tools/okta/update_group.ts +++ b/apps/sim/tools/okta/update_group.ts @@ -1,10 +1,10 @@ import { createLogger } from '@sim/logger' -import { - type OktaApiError, - type OktaGroup, - type OktaUpdateGroupParams, - type OktaUpdateGroupResponse, - validateOktaDomain, +import { validateOktaDomain } from '@/lib/core/security/input-validation' +import type { + OktaApiError, + OktaGroup, + OktaUpdateGroupParams, + OktaUpdateGroupResponse, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types' diff --git a/apps/sim/tools/okta/update_user.ts b/apps/sim/tools/okta/update_user.ts index 747dfa0f432..7775872b7cc 100644 --- a/apps/sim/tools/okta/update_user.ts +++ b/apps/sim/tools/okta/update_user.ts @@ -1,10 +1,10 @@ import { createLogger } from '@sim/logger' -import { - type OktaApiError, - type OktaUpdateUserParams, - type OktaUpdateUserResponse, - type OktaUser, - validateOktaDomain, +import { validateOktaDomain } from '@/lib/core/security/input-validation' +import type { + OktaApiError, + OktaUpdateUserParams, + OktaUpdateUserResponse, + OktaUser, } from '@/tools/okta/types' import type { ToolConfig } from '@/tools/types'