Skip to content

Commit cbbcca9

Browse files
authored
fix(okta): stop partial updates erasing stored profile data (#6751)
* fix(okta): stop partial updates erasing stored profile data Post-merge audit of the Okta integration (follows #6741), verified against the OpenAPI spec bundled in okta-sdk-golang/.generator. Two updates could silently destroy data: - `update_group` targets `PUT /api/v1/groups/{groupId}`, which Okta documents as `replaceGroup` — it swaps the profile wholesale. Sending only the two fields the tool exposes erased the stored description on every rename, and dropped every org-defined custom attribute along with it. The tool now reads the group and overlays the supplied fields before replacing, matching the read-modify- write `salesforce_update_custom_field` already uses for the same hazard. - `update_user` gated its profile fields on `!== undefined`, so an empty string reached Okta and blanked the stored value. The block strips blanks before they get there, but the tool is `user-or-llm` and a model routinely emits `""` for a field it has nothing to say about, so the guard belongs on the tool. Also corrected: - `forgetDevices` defaults to true at Okta, so the unseeded switch rendered off while remembered factors were in fact being cleared. - Group rules take a plain keyword on `search`, not the SCIM-style expression the shared Search field's wand generates, so they get their own field. - `get_logs` dropped `limit=0`, which the spec documents as valid. - `get_user` emitted an activation timestamp under `activated`, which the block declares as the lifecycle boolean; the timestamp is now `activatedAt`. - Descriptions that overstated what an endpoint does: `list_users` omits DEPROVISIONED users, `delete_user` deactivates before it deletes, `delete_group_rule` answers 202, and `excludedGroupIds` is always empty because Okta does not support group exclusions. * fix(okta): forward the abort signal through the group read-modify-write * test(okta): rename the shared body-builder helper * fix(okta): key the send-email and search mappings off the operation * docs(okta): use TSDoc for the new block annotations
1 parent 852906e commit cbbcca9

17 files changed

Lines changed: 449 additions & 70 deletions

File tree

apps/docs/content/docs/en/integrations/okta.mdx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Integrate Okta identity management into your workflow. Manage users, groups, and
4040

4141
### List Users from Okta
4242

43-
List all users in your Okta organization with optional search and filtering
43+
List users in your Okta organization with optional search and filtering. Users with a DEPROVISIONED status are omitted unless a search or filter expression selects them.
4444

4545
#### Input
4646

@@ -308,7 +308,7 @@ Permanently delete a user from your Okta organization. Can only be performed on
308308
| Parameter | Type | Description |
309309
| --------- | ---- | ----------- |
310310
| `userId` | string | Deleted user ID |
311-
| `deleted` | boolean | Whether the user was deleted |
311+
| `deleted` | boolean | Whether the delete request was accepted. An ACTIVE user is deactivated by the first call and needs a second call to actually be deleted. |
312312
| `success` | boolean | Operation success status |
313313

314314
### List Groups from Okta
@@ -396,7 +396,7 @@ Create a new group in your Okta organization
396396

397397
### Update Group in Okta
398398

399-
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).
399+
Update a group profile in your Okta organization. Only groups of OKTA_GROUP type can be updated. Fields left blank keep their stored value.
400400

401401
#### Input
402402

@@ -552,7 +552,7 @@ List the group rules in your Okta organization. Each rule assigns users to group
552552
|`expressionType` | string | Expression language, typically urn:okta:expression:1.0 |
553553
|`assignUserToGroupIds` | array | Groups that matching users are assigned to |
554554
|`excludedUserIds` | array | Users excluded from the rule |
555-
|`excludedGroupIds` | array | Groups excluded from the rule |
555+
|`excludedGroupIds` | array | Groups excluded from the rule. Always empty — Okta does not currently support group exclusions. |
556556
| `count` | number | Number of rules returned |
557557
| `nextCursor` | string | Cursor for the next page, or null on the last page |
558558
| `hasMore` | boolean | Whether more rules are available |
@@ -584,7 +584,7 @@ Retrieve a single Okta group rule by ID, including the expression that decides w
584584
| `expressionType` | string | Expression language, typically urn:okta:expression:1.0 |
585585
| `assignUserToGroupIds` | array | Groups that matching users are assigned to |
586586
| `excludedUserIds` | array | Users excluded from the rule |
587-
| `excludedGroupIds` | array | Groups excluded from the rule |
587+
| `excludedGroupIds` | array | Groups excluded from the rule. Always empty — Okta does not currently support group exclusions. |
588588
| `success` | boolean | Operation success status |
589589

590590
### Create Group Rule in Okta
@@ -616,7 +616,7 @@ Create a group rule that automatically assigns users matching an Okta expression
616616
| `expressionType` | string | Expression language, typically urn:okta:expression:1.0 |
617617
| `assignUserToGroupIds` | array | Groups that matching users are assigned to |
618618
| `excludedUserIds` | array | Users excluded from the rule |
619-
| `excludedGroupIds` | array | Groups excluded from the rule |
619+
| `excludedGroupIds` | array | Groups excluded from the rule. Always empty — Okta does not currently support group exclusions. |
620620
| `success` | boolean | Operation success status |
621621

622622
### Activate Group Rule in Okta
@@ -677,7 +677,7 @@ Permanently delete a group rule. Destructive and irreversible. Optionally also r
677677
| Parameter | Type | Description |
678678
| --------- | ---- | ----------- |
679679
| `groupRuleId` | string | Deleted group rule ID |
680-
| `deleted` | boolean | Whether the rule was deleted |
680+
| `deleted` | boolean | Whether the deletion was accepted. Okta answers 202 and removes the rule asynchronously. |
681681
| `success` | boolean | Operation success status |
682682

683683
### List Factors from Okta

apps/sim/blocks/blocks/okta.ts

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ function toFiniteNumber(value: unknown): number | undefined {
1717
return Number.isFinite(parsed) ? parsed : undefined
1818
}
1919

20+
/** Operations where Okta sends the notification email unless told otherwise. */
21+
const SEND_EMAIL_DEFAULT_ON_OPERATIONS = ['okta_activate_user', 'okta_reset_password']
22+
23+
const SEND_EMAIL_DEFAULT_ON = new Set(SEND_EMAIL_DEFAULT_ON_OPERATIONS)
24+
2025
/** Treats a blank subBlock value as absent. */
2126
function blankToUndefined(value: unknown): unknown {
2227
return value === null || value === '' ? undefined : value
@@ -157,7 +162,7 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
157162
],
158163
okta_list_group_rules: [
159164
'List group rules',
160-
{ text: ', matching', field: 'search' },
165+
{ text: ', matching', field: 'ruleSearch' },
161166
{ text: ', up to', field: 'limit' },
162167
],
163168
okta_get_group_rule: [{ text: 'Read group rule', field: 'groupRuleId', core: true }],
@@ -252,7 +257,7 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
252257
placeholder: 'profile.firstName eq "John"',
253258
condition: {
254259
field: 'operation',
255-
value: ['okta_list_users', 'okta_list_groups', 'okta_list_group_rules'],
260+
value: ['okta_list_users', 'okta_list_groups'],
256261
},
257262
wandConfig: {
258263
enabled: true,
@@ -288,6 +293,19 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
288293
value: ['okta_get_logs', 'okta_list_apps', 'okta_list_app_users', 'okta_list_app_groups'],
289294
},
290295
},
296+
{
297+
/**
298+
* Group rules take a plain keyword on `search`, not the SCIM-style
299+
* expression the Search field's wand generates, so they get their own
300+
* field rather than sharing one that would produce a silently
301+
* non-matching query.
302+
*/
303+
id: 'ruleSearch',
304+
title: 'Search',
305+
type: 'short-input',
306+
placeholder: 'Keyword to search rules for',
307+
condition: { field: 'operation', value: 'okta_list_group_rules' },
308+
},
291309
// User ID (shared across user operations that need it)
292310
{
293311
id: 'userId',
@@ -469,20 +487,30 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
469487
placeholder: 'Description for the group',
470488
condition: { field: 'operation', value: ['okta_create_group', 'okta_update_group'] },
471489
},
472-
// Send email option (activate, reset password, delete)
490+
/**
491+
* Okta's `sendEmail` default is not uniform: activation and password reset
492+
* default to sending, deactivation and removal default to not sending. One
493+
* shared switch could only be seeded for one of those, so the two groups get
494+
* their own field and the params mapper picks by operation.
495+
*/
473496
{
474497
id: 'sendEmail',
475498
title: 'Send Email',
476499
type: 'switch',
500+
value: () => 'true',
477501
condition: {
478502
field: 'operation',
479-
value: [
480-
'okta_activate_user',
481-
'okta_deactivate_user',
482-
'okta_reset_password',
483-
'okta_delete_user',
484-
'okta_remove_user_from_app',
485-
],
503+
value: SEND_EMAIL_DEFAULT_ON_OPERATIONS,
504+
},
505+
mode: 'advanced',
506+
},
507+
{
508+
id: 'sendDeactivationEmail',
509+
title: 'Send Email',
510+
type: 'switch',
511+
condition: {
512+
field: 'operation',
513+
value: ['okta_deactivate_user', 'okta_delete_user', 'okta_remove_user_from_app'],
486514
},
487515
mode: 'advanced',
488516
},
@@ -658,6 +686,11 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
658686
id: 'forgetDevices',
659687
title: 'Forget Devices',
660688
type: 'switch',
689+
/**
690+
* Okta defaults this to true, so an unseeded switch would render off while
691+
* remembered factors were in fact being cleared.
692+
*/
693+
value: () => 'true',
661694
condition: { field: 'operation', value: 'okta_clear_user_sessions' },
662695
mode: 'advanced',
663696
},
@@ -945,9 +978,23 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
945978
domain: params.domain,
946979
limit: toFiniteNumber(params.limit),
947980
priority: toFiniteNumber(params.priority),
948-
// Group-specific UI fields carry the tool's generic param names.
981+
/** Group-specific UI fields carry the tool's generic param names. */
949982
name: blankToUndefined(params.groupName),
950983
description: blankToUndefined(params.groupDescription),
984+
/** Group rules get their own keyword field but the same wire param. */
985+
search:
986+
params.operation === 'okta_list_group_rules'
987+
? blankToUndefined(params.ruleSearch)
988+
: blankToUndefined(params.search),
989+
/**
990+
* Keyed off the operation rather than `??`: both switches are advanced,
991+
* and `shouldSerializeSubBlock` skips `condition` for advanced fields,
992+
* so a stale value from a previously selected operation can still be
993+
* present here.
994+
*/
995+
sendEmail: SEND_EMAIL_DEFAULT_ON.has(String(params.operation))
996+
? blankToUndefined(params.sendEmail)
997+
: blankToUndefined(params.sendDeactivationEmail),
951998
}
952999

9531000
const mappedKeys = new Set([
@@ -958,6 +1005,10 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
9581005
'priority',
9591006
'groupName',
9601007
'groupDescription',
1008+
'search',
1009+
'ruleSearch',
1010+
'sendEmail',
1011+
'sendDeactivationEmail',
9611012
])
9621013
for (const [key, value] of Object.entries(params)) {
9631014
if (!mappedKeys.has(key)) result[key] = blankToUndefined(value)
@@ -975,6 +1026,7 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
9751026
userId: { type: 'string', description: 'User ID or login' },
9761027
groupId: { type: 'string', description: 'Group ID' },
9771028
search: { type: 'string', description: 'Search expression' },
1029+
ruleSearch: { type: 'string', description: 'Keyword to search group rules for' },
9781030
filter: { type: 'string', description: 'Filter expression' },
9791031
limit: { type: 'number', description: 'Max results to return' },
9801032
firstName: { type: 'string', description: 'First name' },
@@ -989,6 +1041,10 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
9891041
groupName: { type: 'string', description: 'Group name' },
9901042
groupDescription: { type: 'string', description: 'Group description' },
9911043
sendEmail: { type: 'boolean', description: 'Whether to send email notification' },
1044+
sendDeactivationEmail: {
1045+
type: 'boolean',
1046+
description: 'Whether to send the deactivation or removal email notification',
1047+
},
9921048
q: { type: 'string', description: 'Keyword search query' },
9931049
after: { type: 'string', description: 'Cursor for the next page of results' },
9941050
since: { type: 'string', description: 'Start of the System Log time window' },
@@ -1137,7 +1193,11 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
11371193
accessibility: { type: 'json', description: 'Application accessibility settings' },
11381194
assignUserToGroupIds: { type: 'json', description: 'Groups a rule assigns matching users to' },
11391195
excludedUserIds: { type: 'json', description: 'Users excluded from a group rule' },
1140-
excludedGroupIds: { type: 'json', description: 'Groups excluded from a group rule' },
1196+
excludedGroupIds: {
1197+
type: 'json',
1198+
description:
1199+
'Groups excluded from a group rule. Always empty — Okta does not currently support group exclusions.',
1200+
},
11411201
amr: { type: 'json', description: 'Authentication methods used to establish a session' },
11421202
features: { type: 'json', description: 'Provisioning features enabled on an application' },
11431203
label: { type: 'string', description: 'Application or role label' },

apps/sim/lib/integrations/integrations.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"updatedAt": "2026-08-15",
2+
"updatedAt": "2026-08-16",
33
"integrations": [
44
{
55
"type": "onepassword",
@@ -13731,7 +13731,7 @@
1373113731
"operations": [
1373213732
{
1373313733
"name": "List Users",
13734-
"description": "List all users in your Okta organization with optional search and filtering"
13734+
"description": "List users in your Okta organization with optional search and filtering. Users with a DEPROVISIONED status are omitted unless a search or filter expression selects them."
1373513735
},
1373613736
{
1373713737
"name": "Get User",
@@ -13783,7 +13783,7 @@
1378313783
},
1378413784
{
1378513785
"name": "Update Group",
13786-
"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)."
13786+
"description": "Update a group profile in your Okta organization. Only groups of OKTA_GROUP type can be updated. Fields left blank keep their stored value."
1378713787
},
1378813788
{
1378913789
"name": "Delete Group",

apps/sim/tools/generated/tool-metadata.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apps/sim/tools/generated/tool-outputs.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apps/sim/tools/okta/create_group_rule.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ export const oktaCreateGroupRuleTool: ToolConfig<
150150
},
151151
excludedGroupIds: {
152152
type: 'array',
153-
description: 'Groups excluded from the rule',
153+
description:
154+
'Groups excluded from the rule. Always empty — Okta does not currently support group exclusions.',
154155
items: { type: 'string', description: 'Group ID' },
155156
},
156157
success: { type: 'boolean', description: 'Operation success status' },

apps/sim/tools/okta/delete_group_rule.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@ export const oktaDeleteGroupRuleTool: ToolConfig<
7171

7272
outputs: {
7373
groupRuleId: { type: 'string', description: 'Deleted group rule ID' },
74-
deleted: { type: 'boolean', description: 'Whether the rule was deleted' },
74+
deleted: {
75+
type: 'boolean',
76+
description:
77+
'Whether the deletion was accepted. Okta answers 202 and removes the rule asynchronously.',
78+
},
7579
success: { type: 'boolean', description: 'Operation success status' },
7680
},
7781
}

apps/sim/tools/okta/delete_user.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,11 @@ export const oktaDeleteUserTool: ToolConfig<OktaDeleteUserParams, OktaDeleteUser
6767

6868
outputs: {
6969
userId: { type: 'string', description: 'Deleted user ID' },
70-
deleted: { type: 'boolean', description: 'Whether the user was deleted' },
70+
deleted: {
71+
type: 'boolean',
72+
description:
73+
'Whether the delete request was accepted. An ACTIVE user is deactivated by the first call and needs a second call to actually be deleted.',
74+
},
7175
success: { type: 'boolean', description: 'Operation success status' },
7276
},
7377
}

apps/sim/tools/okta/get_group_rule.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ export const oktaGetGroupRuleTool: ToolConfig<OktaGetGroupRuleParams, OktaGetGro
9292
},
9393
excludedGroupIds: {
9494
type: 'array',
95-
description: 'Groups excluded from the rule',
95+
description:
96+
'Groups excluded from the rule. Always empty — Okta does not currently support group exclusions.',
9697
items: { type: 'string', description: 'Group ID' },
9798
},
9899
success: { type: 'boolean', description: 'Operation success status' },

apps/sim/tools/okta/get_logs.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,10 @@ export const oktaGetLogsTool: ToolConfig<OktaGetLogsParams, OktaGetLogsResponse>
8484
if (params.q) queryParams.append('q', params.q)
8585
if (params.sortOrder) queryParams.append('sortOrder', params.sortOrder)
8686
if (params.after) queryParams.append('after', params.after)
87-
if (params.limit) queryParams.append('limit', params.limit.toString())
87+
/** `0` is a documented limit on this endpoint, so it must not read as absent. */
88+
if (params.limit !== undefined && params.limit !== null) {
89+
queryParams.append('limit', params.limit.toString())
90+
}
8891

8992
const queryString = queryParams.toString()
9093
return queryString

0 commit comments

Comments
 (0)