diff --git a/apps/sim/blocks/blocks/microsoft_ad.ts b/apps/sim/blocks/blocks/microsoft_ad.ts index f5b662dacac..443a21d19fb 100644 --- a/apps/sim/blocks/blocks/microsoft_ad.ts +++ b/apps/sim/blocks/blocks/microsoft_ad.ts @@ -189,6 +189,17 @@ export const MicrosoftAdBlock: BlockConfig = { condition: { field: 'operation', value: ['list_users', 'list_groups'] }, mode: 'advanced', }, + { + id: 'nextLink', + title: 'Next Page', + type: 'short-input', + placeholder: "Paste the previous response's nextLink to fetch the next page", + condition: { + field: 'operation', + value: ['list_users', 'list_groups', 'list_group_members'], + }, + mode: 'advanced', + }, // Group ID field { id: 'groupId', @@ -212,7 +223,6 @@ export const MicrosoftAdBlock: BlockConfig = { 'get_group', 'update_group', 'delete_group', - 'list_group_members', 'add_group_member', 'remove_group_member', ], @@ -296,6 +306,7 @@ export const MicrosoftAdBlock: BlockConfig = { options: [ { label: 'Private', id: 'Private' }, { label: 'Public', id: 'Public' }, + { label: 'Hidden Membership (Microsoft 365 groups only)', id: 'HiddenMembership' }, ], value: () => 'Private', condition: { field: 'operation', value: 'create_group' }, @@ -334,6 +345,7 @@ export const MicrosoftAdBlock: BlockConfig = { if (params.top) result.top = Number(params.top) if (params.filter) result.filter = params.filter if (params.search) result.search = params.search + if (params.nextLink) result.nextLink = params.nextLink if (params.operation === 'update_user') { if (params.accountEnabled) result.accountEnabled = params.accountEnabled === 'true' } else if (params.operation === 'create_user') { @@ -375,6 +387,7 @@ export const MicrosoftAdBlock: BlockConfig = { top: { type: 'string' }, filter: { type: 'string' }, search: { type: 'string' }, + nextLink: { type: 'string' }, groupId: { type: 'string' }, groupDisplayName: { type: 'string' }, groupMailNickname: { type: 'string' }, @@ -390,7 +403,7 @@ export const MicrosoftAdBlock: BlockConfig = { response: { type: 'json', description: - 'Azure AD operation response. User operations return id, displayName, userPrincipalName, mail, jobTitle, department. Group operations return id, displayName, description, mailEnabled, securityEnabled, groupTypes. Member operations return id, displayName, mail, odataType.', + 'Azure AD operation response. User operations return id, displayName, userPrincipalName, mail, jobTitle, department. Group operations return id, displayName, description, mailEnabled, securityEnabled, groupTypes. Member operations return id, displayName, mail, odataType. List operations also return nextLink for fetching additional pages.', }, }, } @@ -487,7 +500,21 @@ export const MicrosoftAdBlockMeta = { description: 'List the members of an Azure AD group for an access review. Use for periodic attestation of privileged or sensitive groups.', content: - '# Audit Group Membership\n\nProduce a current membership snapshot for a group.\n\n## Steps\n1. Resolve the target group with Get Group or List Groups (filter or search by name).\n2. Call List Group Members for the group id, raising Max Results if the group is large.\n3. For each member, optionally call Get User to enrich with job title, department, and account-enabled status.\n\n## Output\nReturn a table of members with id, display name, email, department, and whether the account is enabled. Highlight disabled or stale accounts that still hold membership and should be reviewed for removal.', + '# Audit Group Membership\n\nProduce a current membership snapshot for a group.\n\n## Steps\n1. Resolve the target group with Get Group or List Groups (filter or search by name).\n2. Call List Group Members for the group id, raising Max Results if the group is large. If the response includes a Next Page link, keep calling List Group Members with that link until it comes back empty to capture every member.\n3. For each member, optionally call Get User to enrich with job title, department, and account-enabled status.\n\n## Output\nReturn a table of members with id, display name, email, department, and whether the account is enabled. Highlight disabled or stale accounts that still hold membership and should be reviewed for removal.', + }, + { + name: 'search-directory-users', + description: + 'Search Azure AD (Entra ID) for users matching a name, department, or other attribute. Use for directory lookups and reporting.', + content: + "# Search Directory Users\n\nFind users in the directory by attribute instead of enumerating everyone.\n\n## Steps\n1. Use List Users with Search set to the name or email fragment, or Filter set to an OData expression (e.g. `department eq 'Sales'`) for attribute-based lookups. Search and Filter cannot be combined in one call.\n2. If the result set is large, follow the Next Page link returned in the response to page through additional results.\n3. Optionally call Get User for a specific match to retrieve full profile detail.\n\n## Output\nReturn the matching users with id, display name, user principal name, department, and account-enabled status. State clearly when Max Results or pagination limits mean the list may be incomplete.", + }, + { + name: 'manage-group-membership', + description: + 'Add or remove specific users from an Azure AD (Entra ID) group on demand, outside of onboarding/offboarding flows. Use for ad hoc access changes and team restructuring.', + content: + "# Manage Group Membership\n\nApply a one-off membership change to a group.\n\n## Steps\n1. Resolve the group with Get Group or List Groups, and resolve each affected user with Get User or List Users.\n2. Call Add Group Member or Remove Group Member with the group id and each user id.\n3. Confirm the change with List Group Members.\n\n## Output\nReturn which users were added or removed and the group's current member count. Report any member that failed to add or remove (e.g. already a member, or not found) instead of silently skipping it.", }, ], } as const satisfies BlockMeta diff --git a/apps/sim/tools/microsoft_ad/create_group.ts b/apps/sim/tools/microsoft_ad/create_group.ts index 28ffe642594..62fcc3f06e9 100644 --- a/apps/sim/tools/microsoft_ad/create_group.ts +++ b/apps/sim/tools/microsoft_ad/create_group.ts @@ -65,7 +65,8 @@ export const createGroupTool: ToolConfig< type: 'string', required: false, visibility: 'user-or-llm', - description: 'Group visibility: "Private" or "Public"', + description: + 'Group visibility: "Private" or "Public" (can be changed later), or "HiddenMembership" (Microsoft 365 groups only; can only be set at creation and never changed afterward)', }, }, request: { diff --git a/apps/sim/tools/microsoft_ad/create_user.ts b/apps/sim/tools/microsoft_ad/create_user.ts index d414583824f..794bedd7941 100644 --- a/apps/sim/tools/microsoft_ad/create_user.ts +++ b/apps/sim/tools/microsoft_ad/create_user.ts @@ -93,7 +93,7 @@ export const createUserTool: ToolConfig< }, }, request: { - url: 'https://graph.microsoft.com/v1.0/users', + url: 'https://graph.microsoft.com/v1.0/users?$select=id,displayName,givenName,surname,userPrincipalName,mail,jobTitle,department,officeLocation,mobilePhone,accountEnabled', method: 'POST', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}`, diff --git a/apps/sim/tools/microsoft_ad/list_group_members.ts b/apps/sim/tools/microsoft_ad/list_group_members.ts index 89d21d98581..701fae23a23 100644 --- a/apps/sim/tools/microsoft_ad/list_group_members.ts +++ b/apps/sim/tools/microsoft_ad/list_group_members.ts @@ -3,6 +3,7 @@ import type { MicrosoftAdListGroupMembersResponse, } from '@/tools/microsoft_ad/types' import { MEMBER_OUTPUT_PROPERTIES } from '@/tools/microsoft_ad/types' +import { assertGraphNextPageUrl, getGraphNextPageUrl } from '@/tools/sharepoint/utils' import type { ToolConfig } from '@/tools/types' export const listGroupMembersTool: ToolConfig< @@ -27,9 +28,9 @@ export const listGroupMembersTool: ToolConfig< }, groupId: { type: 'string', - required: true, + required: false, visibility: 'user-or-llm', - description: 'Group ID', + description: 'Group ID. Not needed when Next Page is provided to fetch a later page.', }, top: { type: 'number', @@ -37,9 +38,17 @@ export const listGroupMembersTool: ToolConfig< visibility: 'user-or-llm', description: 'Maximum number of members to return (default 100, max 999)', }, + nextLink: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Continuation URL from a previous response\'s "nextLink" output, used to fetch the next page of results', + }, }, request: { url: (params) => { + if (params.nextLink) return assertGraphNextPageUrl(params.nextLink) const groupId = params.groupId?.trim() if (!groupId) throw new Error('Group ID is required') const queryParts = ['$select=id,displayName,mail'] @@ -64,6 +73,7 @@ export const listGroupMembersTool: ToolConfig< output: { members, memberCount: members.length, + nextLink: getGraphNextPageUrl(data) ?? null, }, } }, @@ -74,5 +84,10 @@ export const listGroupMembersTool: ToolConfig< properties: MEMBER_OUTPUT_PROPERTIES, }, memberCount: { type: 'number', description: 'Number of members returned' }, + nextLink: { + type: 'string', + description: 'Continuation URL for the next page of results, or null if there are no more', + optional: true, + }, }, } diff --git a/apps/sim/tools/microsoft_ad/list_groups.ts b/apps/sim/tools/microsoft_ad/list_groups.ts index 13ceb572514..57af49ae26b 100644 --- a/apps/sim/tools/microsoft_ad/list_groups.ts +++ b/apps/sim/tools/microsoft_ad/list_groups.ts @@ -3,6 +3,7 @@ import type { MicrosoftAdListGroupsResponse, } from '@/tools/microsoft_ad/types' import { GROUP_OUTPUT_PROPERTIES } from '@/tools/microsoft_ad/types' +import { assertGraphNextPageUrl, getGraphNextPageUrl } from '@/tools/sharepoint/utils' import type { ToolConfig } from '@/tools/types' export const listGroupsTool: ToolConfig< @@ -43,20 +44,28 @@ export const listGroupsTool: ToolConfig< visibility: 'user-or-llm', description: 'Search string to filter groups by displayName or description', }, + nextLink: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Continuation URL from a previous response\'s "nextLink" output, used to fetch the next page of results', + }, }, request: { url: (params) => { + if (params.nextLink) return assertGraphNextPageUrl(params.nextLink) const queryParts: string[] = [] queryParts.push( '$select=id,displayName,description,mail,mailEnabled,mailNickname,securityEnabled,groupTypes,visibility,createdDateTime' ) if (params.top) queryParts.push(`$top=${params.top}`) - if (params.search && params.filter) { - throw new Error('$search and $filter cannot be used together in Microsoft Graph API') - } if (params.filter) queryParts.push(`$filter=${encodeURIComponent(params.filter)}`) if (params.search) { - queryParts.push(`$search="${encodeURIComponent(params.search)}"`) + const term = params.search.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + queryParts.push( + `$search=${encodeURIComponent(`"displayName:${term}" OR "description:${term}"`)}` + ) queryParts.push('$count=true') } return `https://graph.microsoft.com/v1.0/groups?${queryParts.join('&')}` @@ -86,6 +95,7 @@ export const listGroupsTool: ToolConfig< output: { groups, groupCount: groups.length, + nextLink: getGraphNextPageUrl(data) ?? null, }, } }, @@ -96,5 +106,10 @@ export const listGroupsTool: ToolConfig< properties: GROUP_OUTPUT_PROPERTIES, }, groupCount: { type: 'number', description: 'Number of groups returned' }, + nextLink: { + type: 'string', + description: 'Continuation URL for the next page of results, or null if there are no more', + optional: true, + }, }, } diff --git a/apps/sim/tools/microsoft_ad/list_users.ts b/apps/sim/tools/microsoft_ad/list_users.ts index d3a77beea05..ed9901ebc22 100644 --- a/apps/sim/tools/microsoft_ad/list_users.ts +++ b/apps/sim/tools/microsoft_ad/list_users.ts @@ -3,6 +3,7 @@ import type { MicrosoftAdListUsersResponse, } from '@/tools/microsoft_ad/types' import { USER_OUTPUT_PROPERTIES } from '@/tools/microsoft_ad/types' +import { assertGraphNextPageUrl, getGraphNextPageUrl } from '@/tools/sharepoint/utils' import type { ToolConfig } from '@/tools/types' export const listUsersTool: ToolConfig = { @@ -40,20 +41,26 @@ export const listUsersTool: ToolConfig { + if (params.nextLink) return assertGraphNextPageUrl(params.nextLink) const queryParts: string[] = [] queryParts.push( '$select=id,displayName,givenName,surname,userPrincipalName,mail,jobTitle,department,officeLocation,mobilePhone,accountEnabled' ) if (params.top) queryParts.push(`$top=${params.top}`) - if (params.search && params.filter) { - throw new Error('$search and $filter cannot be used together in Microsoft Graph API') - } if (params.filter) queryParts.push(`$filter=${encodeURIComponent(params.filter)}`) if (params.search) { - queryParts.push(`$search="${encodeURIComponent(params.search)}"`) + const term = params.search.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + queryParts.push(`$search=${encodeURIComponent(`"displayName:${term}" OR "mail:${term}"`)}`) queryParts.push('$count=true') } return `https://graph.microsoft.com/v1.0/users?${queryParts.join('&')}` @@ -84,6 +91,7 @@ export const listUsersTool: ToolConfig> userCount: number + nextLink: string | null } } @@ -162,6 +166,7 @@ export interface MicrosoftAdListGroupsResponse extends ToolResponse { output: { groups: Array> groupCount: number + nextLink: string | null } } @@ -195,6 +200,7 @@ export interface MicrosoftAdListGroupMembersResponse extends ToolResponse { output: { members: Array> memberCount: number + nextLink: string | null } }