diff --git a/packages/nodes-base/nodes/Aws/IAM/AwsIam.node.json b/packages/nodes-base/nodes/Aws/IAM/AwsIam.node.json
new file mode 100644
index 0000000000..ead92b3ae4
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/AwsIam.node.json
@@ -0,0 +1,18 @@
+{
+ "node": "n8n-nodes-base.awsiam",
+ "nodeVersion": "1.0",
+ "codexVersion": "1.0",
+ "categories": ["Development"],
+ "resources": {
+ "credentialDocumentation": [
+ {
+ "url": "https://docs.n8n.io/integrations/builtin/credentials/aws/"
+ }
+ ],
+ "primaryDocumentation": [
+ {
+ "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.awsiam/"
+ }
+ ]
+ }
+}
diff --git a/packages/nodes-base/nodes/Aws/IAM/AwsIam.node.ts b/packages/nodes-base/nodes/Aws/IAM/AwsIam.node.ts
new file mode 100644
index 0000000000..1d7c90d1f8
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/AwsIam.node.ts
@@ -0,0 +1,69 @@
+import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
+import { NodeConnectionTypes } from 'n8n-workflow';
+
+import { user, group } from './descriptions';
+import { BASE_URL } from './helpers/constants';
+import { encodeBodyAsFormUrlEncoded } from './helpers/utils';
+import { searchGroups, searchUsers, searchGroupsForUser } from './methods/listSearch';
+
+export class AwsIam implements INodeType {
+ description: INodeTypeDescription = {
+ displayName: 'AWS IAM',
+ name: 'awsIam',
+ icon: 'file:AwsIam.svg',
+ group: ['output'],
+ version: 1,
+ subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
+ description: 'Interacts with Amazon IAM',
+ defaults: { name: 'AWS IAM' },
+ inputs: [NodeConnectionTypes.Main],
+ outputs: [NodeConnectionTypes.Main],
+ credentials: [
+ {
+ name: 'aws',
+ required: true,
+ },
+ ],
+ requestDefaults: {
+ baseURL: BASE_URL,
+ json: true,
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ },
+ properties: [
+ {
+ displayName: 'Resource',
+ name: 'resource',
+ type: 'options',
+ noDataExpression: true,
+ default: 'user',
+ options: [
+ {
+ name: 'User',
+ value: 'user',
+ },
+ {
+ name: 'Group',
+ value: 'group',
+ },
+ ],
+ routing: {
+ send: {
+ preSend: [encodeBodyAsFormUrlEncoded],
+ },
+ },
+ },
+ ...user.description,
+ ...group.description,
+ ],
+ };
+
+ methods = {
+ listSearch: {
+ searchGroups,
+ searchUsers,
+ searchGroupsForUser,
+ },
+ };
+}
diff --git a/packages/nodes-base/nodes/Aws/IAM/AwsIam.svg b/packages/nodes-base/nodes/Aws/IAM/AwsIam.svg
new file mode 100644
index 0000000000..618d0b4a39
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/AwsIam.svg
@@ -0,0 +1,18 @@
+
+
\ No newline at end of file
diff --git a/packages/nodes-base/nodes/Aws/IAM/descriptions/common.ts b/packages/nodes-base/nodes/Aws/IAM/descriptions/common.ts
new file mode 100644
index 0000000000..a501397f38
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/descriptions/common.ts
@@ -0,0 +1,158 @@
+import type { INodeProperties } from 'n8n-workflow';
+
+import { validateName } from '../helpers/utils';
+
+export const paginationParameters: INodeProperties[] = [
+ {
+ displayName: 'Return All',
+ name: 'returnAll',
+ type: 'boolean',
+ default: false,
+ description: 'Whether to return all results or only up to a given limit',
+ },
+ {
+ displayName: 'Limit',
+ name: 'limit',
+ default: 100,
+ type: 'number',
+ validateType: 'number',
+ typeOptions: {
+ minValue: 1,
+ },
+ description: 'Max number of results to return',
+ displayOptions: {
+ hide: {
+ returnAll: [true],
+ },
+ },
+ routing: {
+ send: {
+ property: 'MaxItems',
+ type: 'body',
+ value: '={{ $value }}',
+ },
+ },
+ },
+];
+
+export const userLocator: INodeProperties = {
+ displayName: 'User',
+ name: 'user',
+ required: true,
+ type: 'resourceLocator',
+ default: {
+ mode: 'list',
+ value: '',
+ },
+ modes: [
+ {
+ displayName: 'From list',
+ name: 'list',
+ type: 'list',
+ typeOptions: {
+ searchListMethod: 'searchUsers',
+ searchable: true,
+ },
+ },
+ {
+ displayName: 'By Name',
+ name: 'userName',
+ type: 'string',
+ placeholder: 'e.g. Admins',
+ hint: 'Enter the user name',
+ validation: [
+ {
+ type: 'regex',
+ properties: {
+ regex: '^[\\w+=,.@-]+$',
+ errorMessage: 'The user name must follow the allowed pattern',
+ },
+ },
+ ],
+ },
+ ],
+};
+
+export const groupLocator: INodeProperties = {
+ displayName: 'Group',
+ name: 'group',
+ required: true,
+ type: 'resourceLocator',
+ default: {
+ mode: 'list',
+ value: '',
+ },
+ modes: [
+ {
+ displayName: 'From list',
+ name: 'list',
+ type: 'list',
+ typeOptions: {
+ searchListMethod: 'searchGroups',
+ searchable: true,
+ },
+ },
+ {
+ displayName: 'By Name',
+ name: 'groupName',
+ type: 'string',
+ placeholder: 'e.g. Admins',
+ hint: 'Enter the group name',
+ validation: [
+ {
+ type: 'regex',
+ properties: {
+ regex: '^[\\w+=,.@-]+$',
+ errorMessage: 'The group name must follow the allowed pattern.',
+ },
+ },
+ ],
+ },
+ ],
+};
+
+export const pathParameter: INodeProperties = {
+ displayName: 'Path',
+ name: 'path',
+ type: 'string',
+ validateType: 'string',
+ default: '/',
+};
+
+export const groupNameParameter: INodeProperties = {
+ displayName: 'Group Name',
+ name: 'groupName',
+ required: true,
+ type: 'string',
+ validateType: 'string',
+ typeOptions: {
+ maxLength: 128,
+ regex: '^[+=,.@\\-_A-Za-z0-9]+$',
+ },
+ default: '',
+ placeholder: 'e.g. GroupName',
+ routing: {
+ send: {
+ preSend: [validateName],
+ },
+ },
+};
+
+export const userNameParameter: INodeProperties = {
+ displayName: 'User Name',
+ name: 'userName',
+ required: true,
+ type: 'string',
+ validateType: 'string',
+ default: '',
+ placeholder: 'e.g. JohnSmith',
+ typeOptions: {
+ maxLength: 64,
+ regex: '^[A-Za-z0-9+=,\\.@_-]+$',
+ },
+ routing: {
+ send: {
+ preSend: [validateName],
+ },
+ },
+};
diff --git a/packages/nodes-base/nodes/Aws/IAM/descriptions/group/Group.resource.ts b/packages/nodes-base/nodes/Aws/IAM/descriptions/group/Group.resource.ts
new file mode 100644
index 0000000000..9abdd1be8a
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/descriptions/group/Group.resource.ts
@@ -0,0 +1,145 @@
+import type { INodeProperties } from 'n8n-workflow';
+
+import * as create from './create.operation';
+import * as del from './delete.operation';
+import * as get from './get.operation';
+import * as getAll from './getAll.operation';
+import * as update from './update.operation';
+import { CURRENT_VERSION } from '../../helpers/constants';
+import { handleError } from '../../helpers/errorHandler';
+import {
+ deleteGroupMembers,
+ simplifyGetAllGroupsResponse,
+ simplifyGetGroupsResponse,
+} from '../../helpers/utils';
+
+export const description: INodeProperties[] = [
+ {
+ displayName: 'Operation',
+ name: 'operation',
+ type: 'options',
+ noDataExpression: true,
+ default: 'getAll',
+ displayOptions: {
+ show: {
+ resource: ['group'],
+ },
+ },
+ options: [
+ {
+ name: 'Create',
+ value: 'create',
+ action: 'Create group',
+ description: 'Create a new group',
+ routing: {
+ request: {
+ method: 'POST',
+ url: '',
+ body: {
+ Action: 'CreateGroup',
+ Version: CURRENT_VERSION,
+ GroupName: '={{ $parameter["groupName"] }}',
+ },
+ ignoreHttpStatusErrors: true,
+ },
+ output: {
+ postReceive: [handleError],
+ },
+ },
+ },
+ {
+ name: 'Delete',
+ value: 'delete',
+ action: 'Delete group',
+ description: 'Delete an existing group',
+ routing: {
+ send: {
+ preSend: [deleteGroupMembers],
+ },
+ request: {
+ method: 'POST',
+ url: '',
+ body: {
+ Action: 'DeleteGroup',
+ Version: CURRENT_VERSION,
+ GroupName: '={{ $parameter["group"] }}',
+ },
+ ignoreHttpStatusErrors: true,
+ },
+ output: {
+ postReceive: [handleError],
+ },
+ },
+ },
+ {
+ name: 'Get',
+ value: 'get',
+ action: 'Get group',
+ description: 'Retrieve details of an existing group',
+ routing: {
+ request: {
+ method: 'POST',
+ url: '',
+ body: {
+ Action: 'GetGroup',
+ Version: CURRENT_VERSION,
+ GroupName: '={{ $parameter["group"] }}',
+ },
+ ignoreHttpStatusErrors: true,
+ },
+ output: {
+ postReceive: [handleError, simplifyGetGroupsResponse],
+ },
+ },
+ },
+ {
+ name: 'Get Many',
+ value: 'getAll',
+ action: 'Get many groups',
+ description: 'Retrieve a list of groups',
+ routing: {
+ request: {
+ method: 'POST',
+ url: '',
+ body: {
+ Action: 'ListGroups',
+ Version: CURRENT_VERSION,
+ },
+ ignoreHttpStatusErrors: true,
+ },
+ output: {
+ postReceive: [handleError, simplifyGetAllGroupsResponse],
+ },
+ },
+ },
+ {
+ name: 'Update',
+ value: 'update',
+ action: 'Update group',
+ description: 'Update an existing group',
+ routing: {
+ request: {
+ method: 'POST',
+ url: '',
+ body: {
+ Action: 'UpdateGroup',
+ Version: CURRENT_VERSION,
+ GroupName: '={{ $parameter["group"] }}',
+ NewGroupName: '={{ $parameter["groupName"] }}',
+ },
+ ignoreHttpStatusErrors: true,
+ },
+ output: {
+ postReceive: [handleError],
+ },
+ },
+ },
+ ],
+ },
+
+ ...create.description,
+ ...del.description,
+ ...get.description,
+ ...getAll.description,
+ ...update.description,
+];
diff --git a/packages/nodes-base/nodes/Aws/IAM/descriptions/group/create.operation.ts b/packages/nodes-base/nodes/Aws/IAM/descriptions/group/create.operation.ts
new file mode 100644
index 0000000000..e12c5e9b87
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/descriptions/group/create.operation.ts
@@ -0,0 +1,43 @@
+import type { INodeProperties } from 'n8n-workflow';
+import { updateDisplayOptions } from 'n8n-workflow';
+
+import { validatePath } from '../../helpers/utils';
+import { groupNameParameter, pathParameter } from '../common';
+
+const properties: INodeProperties[] = [
+ {
+ ...groupNameParameter,
+ description: 'The name of the new group to create',
+ placeholder: 'e.g. GroupName',
+ },
+ {
+ displayName: 'Additional Fields',
+ name: 'additionalFields',
+ default: {},
+ options: [
+ {
+ ...pathParameter,
+ placeholder: 'e.g. /division_abc/engineering/',
+ description: 'The path to the group, if it is not included, it defaults to a slash (/)',
+ routing: {
+ send: {
+ preSend: [validatePath],
+ property: 'Path',
+ type: 'query',
+ },
+ },
+ },
+ ],
+ placeholder: 'Add Option',
+ type: 'collection',
+ },
+];
+
+const displayOptions = {
+ show: {
+ resource: ['group'],
+ operation: ['create'],
+ },
+};
+
+export const description = updateDisplayOptions(displayOptions, properties);
diff --git a/packages/nodes-base/nodes/Aws/IAM/descriptions/group/delete.operation.ts b/packages/nodes-base/nodes/Aws/IAM/descriptions/group/delete.operation.ts
new file mode 100644
index 0000000000..c6f865eb62
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/descriptions/group/delete.operation.ts
@@ -0,0 +1,20 @@
+import type { INodeProperties } from 'n8n-workflow';
+import { updateDisplayOptions } from 'n8n-workflow';
+
+import { groupLocator } from '../common';
+
+const properties: INodeProperties[] = [
+ {
+ ...groupLocator,
+ description: 'Select the group you want to delete',
+ },
+];
+
+const displayOptions = {
+ show: {
+ resource: ['group'],
+ operation: ['delete'],
+ },
+};
+
+export const description = updateDisplayOptions(displayOptions, properties);
diff --git a/packages/nodes-base/nodes/Aws/IAM/descriptions/group/get.operation.ts b/packages/nodes-base/nodes/Aws/IAM/descriptions/group/get.operation.ts
new file mode 100644
index 0000000000..11bde3fe93
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/descriptions/group/get.operation.ts
@@ -0,0 +1,27 @@
+import type { INodeProperties } from 'n8n-workflow';
+import { updateDisplayOptions } from 'n8n-workflow';
+
+import { groupLocator } from '../common';
+
+const properties: INodeProperties[] = [
+ {
+ ...groupLocator,
+ description: 'Select the group you want to retrieve',
+ },
+ {
+ displayName: 'Include Users',
+ name: 'includeUsers',
+ type: 'boolean',
+ default: false,
+ description: 'Whether to include a list of users in the group',
+ },
+];
+
+const displayOptions = {
+ show: {
+ resource: ['group'],
+ operation: ['get'],
+ },
+};
+
+export const description = updateDisplayOptions(displayOptions, properties);
diff --git a/packages/nodes-base/nodes/Aws/IAM/descriptions/group/getAll.operation.ts b/packages/nodes-base/nodes/Aws/IAM/descriptions/group/getAll.operation.ts
new file mode 100644
index 0000000000..ec0ae87131
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/descriptions/group/getAll.operation.ts
@@ -0,0 +1,24 @@
+import type { INodeProperties } from 'n8n-workflow';
+import { updateDisplayOptions } from 'n8n-workflow';
+
+import { paginationParameters } from '../common';
+
+const properties: INodeProperties[] = [
+ ...paginationParameters,
+ {
+ displayName: 'Include Users',
+ name: 'includeUsers',
+ type: 'boolean',
+ default: false,
+ description: 'Whether to include a list of users in the group',
+ },
+];
+
+const displayOptions = {
+ show: {
+ resource: ['group'],
+ operation: ['getAll'],
+ },
+};
+
+export const description = updateDisplayOptions(displayOptions, properties);
diff --git a/packages/nodes-base/nodes/Aws/IAM/descriptions/group/update.operation.ts b/packages/nodes-base/nodes/Aws/IAM/descriptions/group/update.operation.ts
new file mode 100644
index 0000000000..53f3acb8d5
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/descriptions/group/update.operation.ts
@@ -0,0 +1,47 @@
+import type { INodeProperties } from 'n8n-workflow';
+import { updateDisplayOptions } from 'n8n-workflow';
+
+import { validatePath } from '../../helpers/utils';
+import { groupLocator, groupNameParameter, pathParameter } from '../common';
+
+const properties: INodeProperties[] = [
+ {
+ ...groupLocator,
+ description: 'Select the group you want to update',
+ },
+ {
+ ...groupNameParameter,
+ description: 'The new name of the group',
+ placeholder: 'e.g. GroupName',
+ },
+ {
+ displayName: 'Additional Fields',
+ name: 'additionalFields',
+ type: 'collection',
+ placeholder: 'Add Option',
+ default: {},
+ options: [
+ {
+ ...pathParameter,
+ placeholder: 'e.g. /division_abc/engineering/',
+ description: 'The new path to the group, if it is not included, it defaults to a slash (/)',
+ routing: {
+ send: {
+ preSend: [validatePath],
+ property: 'NewPath',
+ type: 'query',
+ },
+ },
+ },
+ ],
+ },
+];
+
+const displayOptions = {
+ show: {
+ resource: ['group'],
+ operation: ['update'],
+ },
+};
+
+export const description = updateDisplayOptions(displayOptions, properties);
diff --git a/packages/nodes-base/nodes/Aws/IAM/descriptions/index.ts b/packages/nodes-base/nodes/Aws/IAM/descriptions/index.ts
new file mode 100644
index 0000000000..ad27c9ef85
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/descriptions/index.ts
@@ -0,0 +1,2 @@
+export * as group from './group/Group.resource';
+export * as user from './user/User.resource';
diff --git a/packages/nodes-base/nodes/Aws/IAM/descriptions/user/User.resource.ts b/packages/nodes-base/nodes/Aws/IAM/descriptions/user/User.resource.ts
new file mode 100644
index 0000000000..a7905c4f7f
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/descriptions/user/User.resource.ts
@@ -0,0 +1,197 @@
+import type { INodeProperties } from 'n8n-workflow';
+
+import * as addToGroup from './addToGroup.operation';
+import * as create from './create.operation';
+import * as del from './delete.operation';
+import * as get from './get.operation';
+import * as getAll from './getAll.operation';
+import * as removeFromGroup from './removeFromGroup.operation';
+import * as update from './update.operation';
+import { CURRENT_VERSION } from '../../helpers/constants';
+import { handleError } from '../../helpers/errorHandler';
+import { removeUserFromGroups, simplifyGetAllUsersResponse } from '../../helpers/utils';
+
+export const description: INodeProperties[] = [
+ {
+ displayName: 'Operation',
+ name: 'operation',
+ type: 'options',
+ noDataExpression: true,
+ default: 'getAll',
+ displayOptions: {
+ show: {
+ resource: ['user'],
+ },
+ },
+ options: [
+ {
+ name: 'Add to Group',
+ value: 'addToGroup',
+ description: 'Add an existing user to a group',
+ action: 'Add user to group',
+ routing: {
+ request: {
+ method: 'POST',
+ url: '',
+ body: {
+ Action: 'AddUserToGroup',
+ Version: CURRENT_VERSION,
+ UserName: '={{ $parameter["user"] }}',
+ GroupName: '={{ $parameter["group"] }}',
+ },
+ ignoreHttpStatusErrors: true,
+ },
+ output: {
+ postReceive: [handleError],
+ },
+ },
+ },
+ {
+ name: 'Create',
+ value: 'create',
+ description: 'Create a new user',
+ action: 'Create user',
+ routing: {
+ request: {
+ method: 'POST',
+ url: '',
+ body: {
+ Action: 'CreateUser',
+ Version: CURRENT_VERSION,
+ UserName: '={{ $parameter["userName"] }}',
+ },
+ ignoreHttpStatusErrors: true,
+ },
+ output: {
+ postReceive: [handleError],
+ },
+ },
+ },
+ {
+ name: 'Delete',
+ value: 'delete',
+ description: 'Delete a user',
+ action: 'Delete user',
+ routing: {
+ send: {
+ preSend: [removeUserFromGroups],
+ },
+ request: {
+ method: 'POST',
+ url: '',
+ body: {
+ Action: 'DeleteUser',
+ Version: CURRENT_VERSION,
+ UserName: '={{ $parameter["user"] }}',
+ },
+ ignoreHttpStatusErrors: true,
+ },
+ output: {
+ postReceive: [handleError],
+ },
+ },
+ },
+ {
+ name: 'Get',
+ value: 'get',
+ description: 'Retrieve a user',
+ action: 'Get user',
+ routing: {
+ request: {
+ method: 'POST',
+ url: '',
+ body: {
+ Action: 'GetUser',
+ Version: CURRENT_VERSION,
+ UserName: '={{ $parameter["user"] }}',
+ },
+ ignoreHttpStatusErrors: true,
+ },
+ output: {
+ postReceive: [
+ {
+ type: 'rootProperty',
+ properties: {
+ property: 'GetUserResponse.GetUserResult.User',
+ },
+ },
+ handleError,
+ ],
+ },
+ },
+ },
+ {
+ name: 'Get Many',
+ value: 'getAll',
+ description: 'Retrieve a list of users',
+ routing: {
+ request: {
+ method: 'POST',
+ url: '',
+ body: {
+ Action: 'ListUsers',
+ Version: CURRENT_VERSION,
+ },
+ ignoreHttpStatusErrors: true,
+ },
+ output: {
+ postReceive: [handleError, simplifyGetAllUsersResponse],
+ },
+ },
+ action: 'Get many users',
+ },
+ {
+ name: 'Remove From Group',
+ value: 'removeFromGroup',
+ description: 'Remove a user from a group',
+ action: 'Remove user from group',
+ routing: {
+ request: {
+ method: 'POST',
+ url: '',
+ body: {
+ Action: 'RemoveUserFromGroup',
+ Version: CURRENT_VERSION,
+ UserName: '={{ $parameter["user"] }}',
+ GroupName: '={{ $parameter["group"] }}',
+ },
+ ignoreHttpStatusErrors: true,
+ },
+ output: {
+ postReceive: [handleError],
+ },
+ },
+ },
+ {
+ name: 'Update',
+ value: 'update',
+ description: 'Update a user',
+ action: 'Update user',
+ routing: {
+ request: {
+ method: 'POST',
+ url: '',
+ body: {
+ Action: 'UpdateUser',
+ Version: CURRENT_VERSION,
+ NewUserName: '={{ $parameter["userName"] }}',
+ UserName: '={{ $parameter["user"] }}',
+ },
+ ignoreHttpStatusErrors: true,
+ },
+ output: {
+ postReceive: [handleError],
+ },
+ },
+ },
+ ],
+ },
+
+ ...addToGroup.description,
+ ...create.description,
+ ...del.description,
+ ...get.description,
+ ...getAll.description,
+ ...update.description,
+ ...removeFromGroup.description,
+];
diff --git a/packages/nodes-base/nodes/Aws/IAM/descriptions/user/addToGroup.operation.ts b/packages/nodes-base/nodes/Aws/IAM/descriptions/user/addToGroup.operation.ts
new file mode 100644
index 0000000000..b1e93edbf9
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/descriptions/user/addToGroup.operation.ts
@@ -0,0 +1,24 @@
+import type { INodeProperties } from 'n8n-workflow';
+import { updateDisplayOptions } from 'n8n-workflow';
+
+import { groupLocator, userLocator } from '../common';
+
+const properties: INodeProperties[] = [
+ {
+ ...userLocator,
+ description: 'Select the user you want to add to the group',
+ },
+ {
+ ...groupLocator,
+ description: 'Select the group you want to add the user to',
+ },
+];
+
+const displayOptions = {
+ show: {
+ resource: ['user'],
+ operation: ['addToGroup'],
+ },
+};
+
+export const description = updateDisplayOptions(displayOptions, properties);
diff --git a/packages/nodes-base/nodes/Aws/IAM/descriptions/user/create.operation.ts b/packages/nodes-base/nodes/Aws/IAM/descriptions/user/create.operation.ts
new file mode 100644
index 0000000000..1620c1c472
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/descriptions/user/create.operation.ts
@@ -0,0 +1,96 @@
+import type { INodeProperties } from 'n8n-workflow';
+import { updateDisplayOptions } from 'n8n-workflow';
+
+import { preprocessTags, validatePath, validatePermissionsBoundary } from '../../helpers/utils';
+import { pathParameter, userNameParameter } from '../common';
+
+const properties: INodeProperties[] = [
+ {
+ ...userNameParameter,
+ description: 'The username of the new user to create',
+ placeholder: 'e.g. UserName',
+ },
+ {
+ displayName: 'Additional Fields',
+ name: 'additionalFields',
+ type: 'collection',
+ placeholder: 'Add Option',
+ default: {},
+ options: [
+ {
+ ...pathParameter,
+ description: 'The path for the user name',
+ placeholder: 'e.g. /division_abc/subdivision_xyz/',
+ routing: {
+ send: {
+ preSend: [validatePath],
+ property: 'Path',
+ type: 'query',
+ },
+ },
+ },
+ {
+ displayName: 'Permissions Boundary',
+ name: 'permissionsBoundary',
+ default: '',
+ description:
+ 'The ARN of the managed policy that is used to set the permissions boundary for the user',
+ placeholder: 'e.g. arn:aws:iam::123456789012:policy/ExampleBoundaryPolicy',
+ type: 'string',
+ validateType: 'string',
+ routing: {
+ send: {
+ preSend: [validatePermissionsBoundary],
+ },
+ },
+ },
+ {
+ displayName: 'Tags',
+ name: 'tags',
+ type: 'fixedCollection',
+ description: 'A list of tags that you want to attach to the new user',
+ default: [],
+ placeholder: 'Add Tag',
+ typeOptions: {
+ multipleValues: true,
+ },
+ options: [
+ {
+ name: 'tags',
+ displayName: 'Tag',
+ values: [
+ {
+ displayName: 'Key',
+ name: 'key',
+ type: 'string',
+ default: '',
+ placeholder: 'e.g., Department',
+ },
+ {
+ displayName: 'Value',
+ name: 'value',
+ type: 'string',
+ default: '',
+ placeholder: 'e.g., Engineering',
+ },
+ ],
+ },
+ ],
+ routing: {
+ send: {
+ preSend: [preprocessTags],
+ },
+ },
+ },
+ ],
+ },
+];
+
+const displayOptions = {
+ show: {
+ resource: ['user'],
+ operation: ['create'],
+ },
+};
+
+export const description = updateDisplayOptions(displayOptions, properties);
diff --git a/packages/nodes-base/nodes/Aws/IAM/descriptions/user/delete.operation.ts b/packages/nodes-base/nodes/Aws/IAM/descriptions/user/delete.operation.ts
new file mode 100644
index 0000000000..b6bbe568f2
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/descriptions/user/delete.operation.ts
@@ -0,0 +1,20 @@
+import type { INodeProperties } from 'n8n-workflow';
+import { updateDisplayOptions } from 'n8n-workflow';
+
+import { userLocator } from '../common';
+
+const properties: INodeProperties[] = [
+ {
+ ...userLocator,
+ description: 'Select the user you want to delete',
+ },
+];
+
+const displayOptions = {
+ show: {
+ resource: ['user'],
+ operation: ['delete'],
+ },
+};
+
+export const description = updateDisplayOptions(displayOptions, properties);
diff --git a/packages/nodes-base/nodes/Aws/IAM/descriptions/user/get.operation.ts b/packages/nodes-base/nodes/Aws/IAM/descriptions/user/get.operation.ts
new file mode 100644
index 0000000000..8bd5644518
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/descriptions/user/get.operation.ts
@@ -0,0 +1,20 @@
+import type { INodeProperties } from 'n8n-workflow';
+import { updateDisplayOptions } from 'n8n-workflow';
+
+import { userLocator } from '../common';
+
+const properties: INodeProperties[] = [
+ {
+ ...userLocator,
+ description: 'Select the user you want to retrieve',
+ },
+];
+
+const displayOptions = {
+ show: {
+ resource: ['user'],
+ operation: ['get'],
+ },
+};
+
+export const description = updateDisplayOptions(displayOptions, properties);
diff --git a/packages/nodes-base/nodes/Aws/IAM/descriptions/user/getAll.operation.ts b/packages/nodes-base/nodes/Aws/IAM/descriptions/user/getAll.operation.ts
new file mode 100644
index 0000000000..4e8dd67375
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/descriptions/user/getAll.operation.ts
@@ -0,0 +1,43 @@
+import type { INodeProperties } from 'n8n-workflow';
+import { updateDisplayOptions } from 'n8n-workflow';
+
+import { validateUserPath } from '../../helpers/utils';
+import { paginationParameters } from '../common';
+
+const properties: INodeProperties[] = [
+ ...paginationParameters,
+ {
+ displayName: 'Additional Fields',
+ name: 'additionalFields',
+ type: 'collection',
+ placeholder: 'Add Field',
+ default: {},
+ options: [
+ {
+ displayName: 'Path Prefix',
+ name: 'pathPrefix',
+ type: 'string',
+ validateType: 'string',
+ default: '/',
+ description: 'The path prefix for filtering the results',
+ placeholder: 'e.g. /division_abc/subdivision_xyz/',
+ routing: {
+ send: {
+ preSend: [validateUserPath],
+ property: 'PathPrefix',
+ value: '={{ $value }}',
+ },
+ },
+ },
+ ],
+ },
+];
+
+const displayOptions = {
+ show: {
+ resource: ['user'],
+ operation: ['getAll'],
+ },
+};
+
+export const description = updateDisplayOptions(displayOptions, properties);
diff --git a/packages/nodes-base/nodes/Aws/IAM/descriptions/user/removeFromGroup.operation.ts b/packages/nodes-base/nodes/Aws/IAM/descriptions/user/removeFromGroup.operation.ts
new file mode 100644
index 0000000000..101c0e6dfa
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/descriptions/user/removeFromGroup.operation.ts
@@ -0,0 +1,51 @@
+import type { INodeProperties } from 'n8n-workflow';
+import { updateDisplayOptions } from 'n8n-workflow';
+
+import { groupLocator, userLocator } from '../common';
+
+const properties: INodeProperties[] = [
+ {
+ ...userLocator,
+ description: 'Select the user you want to remove from the group',
+ },
+ {
+ ...groupLocator,
+ description: 'Select the group you want to remove the user from',
+ modes: [
+ {
+ displayName: 'From list',
+ name: 'list',
+ type: 'list',
+ typeOptions: {
+ searchListMethod: 'searchGroupsForUser',
+ searchable: true,
+ },
+ },
+ {
+ displayName: 'By Name',
+ name: 'groupName',
+ type: 'string',
+ hint: 'Enter the group name',
+ validation: [
+ {
+ type: 'regex',
+ properties: {
+ regex: '^[\\w+=,.@-]+$',
+ errorMessage: 'The group name must follow the allowed pattern',
+ },
+ },
+ ],
+ placeholder: 'e.g. Admins',
+ },
+ ],
+ },
+];
+
+const displayOptions = {
+ show: {
+ resource: ['user'],
+ operation: ['removeFromGroup'],
+ },
+};
+
+export const description = updateDisplayOptions(displayOptions, properties);
diff --git a/packages/nodes-base/nodes/Aws/IAM/descriptions/user/update.operation.ts b/packages/nodes-base/nodes/Aws/IAM/descriptions/user/update.operation.ts
new file mode 100644
index 0000000000..606e4f0747
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/descriptions/user/update.operation.ts
@@ -0,0 +1,45 @@
+import type { INodeProperties } from 'n8n-workflow';
+import { updateDisplayOptions } from 'n8n-workflow';
+
+import { validatePath } from '../../helpers/utils';
+import { pathParameter, userLocator, userNameParameter } from '../common';
+
+const properties: INodeProperties[] = [
+ {
+ ...userLocator,
+ description: 'Select the user you want to update',
+ },
+ {
+ ...userNameParameter,
+ description: 'The new name of the user',
+ },
+ {
+ displayName: 'Additional Fields',
+ name: 'additionalFields',
+ type: 'collection',
+ placeholder: 'Add Option',
+ default: {},
+ options: [
+ {
+ ...pathParameter,
+ placeholder: 'e.g. /division_abc/subdivision_xyz/',
+ routing: {
+ send: {
+ preSend: [validatePath],
+ property: 'NewPath',
+ type: 'query',
+ },
+ },
+ },
+ ],
+ },
+];
+
+const displayOptions = {
+ show: {
+ resource: ['user'],
+ operation: ['update'],
+ },
+};
+
+export const description = updateDisplayOptions(displayOptions, properties);
diff --git a/packages/nodes-base/nodes/Aws/IAM/helpers/constants.ts b/packages/nodes-base/nodes/Aws/IAM/helpers/constants.ts
new file mode 100644
index 0000000000..66ca22edee
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/helpers/constants.ts
@@ -0,0 +1,15 @@
+export const CURRENT_VERSION = '2010-05-08';
+export const BASE_URL = 'https://iam.amazonaws.com';
+export const ERROR_DESCRIPTIONS = {
+ EntityAlreadyExists: {
+ User: 'The given user name already exists - try entering a unique name for the user.',
+ Group: 'The given group name already exists - try entering a unique name for the group.',
+ },
+ NoSuchEntity: {
+ User: 'The given user was not found - try entering a different user.',
+ Group: 'The given group was not found - try entering a different group.',
+ },
+ DeleteConflict: {
+ Default: 'Cannot delete entity, please remove users from group first.',
+ },
+};
diff --git a/packages/nodes-base/nodes/Aws/IAM/helpers/errorHandler.ts b/packages/nodes-base/nodes/Aws/IAM/helpers/errorHandler.ts
new file mode 100644
index 0000000000..3993074b3e
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/helpers/errorHandler.ts
@@ -0,0 +1,86 @@
+import type {
+ JsonObject,
+ IDataObject,
+ IExecuteSingleFunctions,
+ IN8nHttpFullResponse,
+ INodeExecutionData,
+} from 'n8n-workflow';
+import { NodeApiError } from 'n8n-workflow';
+
+import { ERROR_DESCRIPTIONS } from './constants';
+import type { AwsError, ErrorMessage } from './types';
+
+function mapErrorToResponse(errorCode: string, errorMessage: string): ErrorMessage | undefined {
+ const isUser = /user/i.test(errorMessage);
+ const isGroup = /group/i.test(errorMessage);
+
+ switch (errorCode) {
+ case 'EntityAlreadyExists':
+ if (isUser) {
+ return {
+ message: errorMessage,
+ description: ERROR_DESCRIPTIONS.EntityAlreadyExists.User,
+ };
+ }
+ if (isGroup) {
+ return {
+ message: errorMessage,
+ description: ERROR_DESCRIPTIONS.EntityAlreadyExists.Group,
+ };
+ }
+ break;
+
+ case 'NoSuchEntity':
+ if (isUser) {
+ return {
+ message: errorMessage,
+ description: ERROR_DESCRIPTIONS.NoSuchEntity.User,
+ };
+ }
+ if (isGroup) {
+ return {
+ message: errorMessage,
+ description: ERROR_DESCRIPTIONS.NoSuchEntity.Group,
+ };
+ }
+ break;
+
+ case 'DeleteConflict':
+ return {
+ message: errorMessage,
+ description: ERROR_DESCRIPTIONS.DeleteConflict.Default,
+ };
+ }
+
+ return undefined;
+}
+
+export async function handleError(
+ this: IExecuteSingleFunctions,
+ data: INodeExecutionData[],
+ response: IN8nHttpFullResponse,
+): Promise {
+ const statusCode = String(response.statusCode);
+
+ if (!statusCode.startsWith('4') && !statusCode.startsWith('5')) {
+ return data;
+ }
+
+ const responseBody = response.body as IDataObject;
+ const error = responseBody.Error as AwsError;
+
+ if (!error) {
+ throw new NodeApiError(this.getNode(), response as unknown as JsonObject);
+ }
+
+ const specificError = mapErrorToResponse(error.Code, error.Message);
+
+ if (specificError) {
+ throw new NodeApiError(this.getNode(), response as unknown as JsonObject, specificError);
+ } else {
+ throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
+ message: error.Code,
+ description: error.Message,
+ });
+ }
+}
diff --git a/packages/nodes-base/nodes/Aws/IAM/helpers/types.ts b/packages/nodes-base/nodes/Aws/IAM/helpers/types.ts
new file mode 100644
index 0000000000..ee9dff6100
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/helpers/types.ts
@@ -0,0 +1,76 @@
+export type Group = {
+ Arn: string;
+ CreateDate: number;
+ GroupId: string;
+ GroupName: string;
+ Path?: string;
+};
+
+export type User = {
+ Arn: string;
+ CreateDate: number;
+ PasswordLastUsed?: number;
+ Path?: string;
+ PermissionsBoundary?: string;
+ Tags: Array<{ Key: string; Value: string }>;
+ UserId: string;
+ UserName: string;
+};
+
+export type Tags = {
+ tags: Array<{ key: string; value: string }>;
+};
+
+export type GetUserResponseBody = {
+ GetUserResponse: {
+ GetUserResult: {
+ User: User;
+ };
+ };
+};
+
+export type GetGroupResponseBody = {
+ GetGroupResponse: {
+ GetGroupResult: {
+ Group: Group;
+ Users?: User[];
+ };
+ };
+};
+
+export type GetAllUsersResponseBody = {
+ ListUsersResponse: {
+ ListUsersResult: {
+ Users: User[];
+ IsTruncated: boolean;
+ Marker: string;
+ };
+ };
+};
+
+export type GetAllGroupsResponseBody = {
+ ListGroupsResponse: {
+ ListGroupsResult: {
+ Groups: Group[];
+ IsTruncated: boolean;
+ Marker: string;
+ };
+ };
+};
+
+export type AwsError = {
+ Code: string;
+ Message: string;
+};
+
+export type ErrorResponse = {
+ Error: {
+ Code: string;
+ Message: string;
+ };
+};
+
+export type ErrorMessage = {
+ message: string;
+ description: string;
+};
diff --git a/packages/nodes-base/nodes/Aws/IAM/helpers/utils.ts b/packages/nodes-base/nodes/Aws/IAM/helpers/utils.ts
new file mode 100644
index 0000000000..01a66b515a
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/helpers/utils.ts
@@ -0,0 +1,323 @@
+import type {
+ IHttpRequestOptions,
+ IDataObject,
+ IExecuteSingleFunctions,
+ IN8nHttpFullResponse,
+ INodeExecutionData,
+ JsonObject,
+} from 'n8n-workflow';
+import { NodeApiError, NodeOperationError } from 'n8n-workflow';
+
+import { CURRENT_VERSION } from './constants';
+import type {
+ GetAllGroupsResponseBody,
+ GetAllUsersResponseBody,
+ GetGroupResponseBody,
+ Tags,
+} from './types';
+import { searchGroupsForUser } from '../methods/listSearch';
+import { awsApiRequest } from '../transport';
+
+export async function encodeBodyAsFormUrlEncoded(
+ this: IExecuteSingleFunctions,
+ requestOptions: IHttpRequestOptions,
+): Promise {
+ if (requestOptions.body) {
+ requestOptions.body = new URLSearchParams(
+ requestOptions.body as Record,
+ ).toString();
+ }
+ return requestOptions;
+}
+
+export async function findUsersForGroup(
+ this: IExecuteSingleFunctions,
+ groupName: string,
+): Promise {
+ const options: IHttpRequestOptions = {
+ method: 'POST',
+ url: '',
+ body: new URLSearchParams({
+ Action: 'GetGroup',
+ Version: CURRENT_VERSION,
+ GroupName: groupName,
+ }).toString(),
+ };
+ const responseData = (await awsApiRequest.call(this, options)) as GetGroupResponseBody;
+ return responseData?.GetGroupResponse?.GetGroupResult?.Users ?? [];
+}
+
+export async function simplifyGetGroupsResponse(
+ this: IExecuteSingleFunctions,
+ _: INodeExecutionData[],
+ response: IN8nHttpFullResponse,
+): Promise {
+ const includeUsers = this.getNodeParameter('includeUsers', false);
+ const responseBody = response.body as GetGroupResponseBody;
+ const groupData = responseBody.GetGroupResponse.GetGroupResult;
+ const group = groupData.Group;
+ return [
+ { json: includeUsers ? { ...group, Users: groupData.Users ?? [] } : group },
+ ] as INodeExecutionData[];
+}
+
+export async function simplifyGetAllGroupsResponse(
+ this: IExecuteSingleFunctions,
+ items: INodeExecutionData[],
+ response: IN8nHttpFullResponse,
+): Promise {
+ const includeUsers = this.getNodeParameter('includeUsers', false);
+ const responseBody = response.body as GetAllGroupsResponseBody;
+ const groups = responseBody.ListGroupsResponse.ListGroupsResult.Groups ?? [];
+
+ if (groups.length === 0) {
+ return items;
+ }
+
+ if (!includeUsers) {
+ return this.helpers.returnJsonArray(groups);
+ }
+
+ const processedItems: IDataObject[] = [];
+ for (const group of groups) {
+ const users = await findUsersForGroup.call(this, group.GroupName);
+ processedItems.push({ ...group, Users: users });
+ }
+ return this.helpers.returnJsonArray(processedItems);
+}
+
+export async function simplifyGetAllUsersResponse(
+ this: IExecuteSingleFunctions,
+ _items: INodeExecutionData[],
+ response: IN8nHttpFullResponse,
+): Promise {
+ if (!response.body) {
+ return [];
+ }
+ const responseBody = response.body as GetAllUsersResponseBody;
+ const users = responseBody?.ListUsersResponse?.ListUsersResult?.Users ?? [];
+ return this.helpers.returnJsonArray(users);
+}
+
+export async function deleteGroupMembers(
+ this: IExecuteSingleFunctions,
+ requestOptions: IHttpRequestOptions,
+): Promise {
+ const groupName = this.getNodeParameter('group', undefined, { extractValue: true }) as string;
+
+ const users = await findUsersForGroup.call(this, groupName);
+ if (!users.length) {
+ return requestOptions;
+ }
+
+ await Promise.all(
+ users.map(async (user) => {
+ const userName = user.UserName as string;
+ if (!user.UserName) {
+ return;
+ }
+
+ try {
+ await awsApiRequest.call(this, {
+ method: 'POST',
+ url: '',
+ body: {
+ Action: 'RemoveUserFromGroup',
+ GroupName: groupName,
+ UserName: userName,
+ Version: CURRENT_VERSION,
+ },
+ ignoreHttpStatusErrors: true,
+ });
+ } catch (error) {
+ throw new NodeApiError(this.getNode(), error as JsonObject, {
+ message: `Failed to remove user "${userName}" from "${groupName}"!`,
+ });
+ }
+ }),
+ );
+
+ return requestOptions;
+}
+
+export async function validatePath(
+ this: IExecuteSingleFunctions,
+ requestOptions: IHttpRequestOptions,
+): Promise {
+ const path = this.getNodeParameter('additionalFields.path') as string;
+ if (path.length < 1 || path.length > 512) {
+ throw new NodeOperationError(
+ this.getNode(),
+ 'The "Path" parameter must be between 1 and 512 characters long.',
+ );
+ }
+
+ const validPathRegex = /^\/[\u0021-\u007E]*\/$/;
+ if (!validPathRegex.test(path) && path !== '/') {
+ throw new NodeOperationError(
+ this.getNode(),
+ 'Ensure the path is structured correctly, e.g. /division_abc/subdivision_xyz/',
+ );
+ }
+
+ return requestOptions;
+}
+
+export async function validateUserPath(
+ this: IExecuteSingleFunctions,
+ requestOptions: IHttpRequestOptions,
+): Promise {
+ const prefix = this.getNodeParameter('additionalFields.pathPrefix') as string;
+
+ let formattedPrefix = prefix;
+ if (!formattedPrefix.startsWith('/')) {
+ formattedPrefix = '/' + formattedPrefix;
+ }
+ if (!formattedPrefix.endsWith('/') && formattedPrefix !== '/') {
+ formattedPrefix = formattedPrefix + '/';
+ }
+
+ if (requestOptions.body && typeof requestOptions.body === 'object') {
+ Object.assign(requestOptions.body, { PathPrefix: formattedPrefix });
+ }
+
+ const options: IHttpRequestOptions = {
+ method: 'POST',
+ url: '',
+ body: {
+ Action: 'ListUsers',
+ Version: CURRENT_VERSION,
+ },
+ };
+ const responseData = (await awsApiRequest.call(this, options)) as GetAllUsersResponseBody;
+
+ const users = responseData.ListUsersResponse.ListUsersResult.Users;
+ if (!users || users.length === 0) {
+ throw new NodeOperationError(
+ this.getNode(),
+ 'No users found. Please adjust the "Path" parameter and try again.',
+ );
+ }
+
+ const userPaths = users.map((user) => user.Path).filter(Boolean);
+ const isPathValid = userPaths.some((path) => path?.startsWith(formattedPrefix));
+ if (!isPathValid) {
+ throw new NodeOperationError(
+ this.getNode(),
+ `The "${formattedPrefix}" path was not found in your users. Try entering a different path.`,
+ );
+ }
+ return requestOptions;
+}
+
+export async function validateName(
+ this: IExecuteSingleFunctions,
+ requestOptions: IHttpRequestOptions,
+): Promise {
+ const resource = this.getNodeParameter('resource') as string;
+ const nameParam = resource === 'user' ? 'userName' : 'groupName';
+ const name = this.getNodeParameter(nameParam) as string;
+
+ const maxLength = resource === 'user' ? 64 : 128;
+ const capitalizedResource = resource.replace(/^./, (c) => c.toUpperCase());
+ const validNamePattern = /^[a-zA-Z0-9-_]+$/;
+
+ const isInvalid = !validNamePattern.test(name) || name.length > maxLength;
+
+ if (/\s/.test(name)) {
+ throw new NodeOperationError(
+ this.getNode(),
+ `${capitalizedResource} name should not contain spaces.`,
+ );
+ }
+
+ if (isInvalid) {
+ throw new NodeOperationError(
+ this.getNode(),
+ `${capitalizedResource} name can have up to ${maxLength} characters. Valid characters: letters, numbers, hyphens (-), and underscores (_).`,
+ );
+ }
+
+ return requestOptions;
+}
+
+export async function validatePermissionsBoundary(
+ this: IExecuteSingleFunctions,
+ requestOptions: IHttpRequestOptions,
+): Promise {
+ const permissionsBoundary = this.getNodeParameter(
+ 'additionalFields.permissionsBoundary',
+ ) as string;
+
+ if (permissionsBoundary) {
+ const arnPattern = /^arn:aws:iam::\d{12}:policy\/[\w\-+\/=._]+$/;
+
+ if (!arnPattern.test(permissionsBoundary)) {
+ throw new NodeOperationError(
+ this.getNode(),
+ 'Permissions boundaries must be provided in ARN format (e.g. arn:aws:iam::123456789012:policy/ExampleBoundaryPolicy). These can be found at the top of the permissions boundary detail page in the IAM dashboard.',
+ );
+ }
+
+ if (requestOptions.body) {
+ Object.assign(requestOptions.body, { PermissionsBoundary: permissionsBoundary });
+ } else {
+ requestOptions.body = {
+ PermissionsBoundary: permissionsBoundary,
+ };
+ }
+ }
+ return requestOptions;
+}
+
+export async function preprocessTags(
+ this: IExecuteSingleFunctions,
+ requestOptions: IHttpRequestOptions,
+): Promise {
+ const tagsData = this.getNodeParameter('additionalFields.tags') as Tags;
+ const tags = tagsData?.tags || [];
+
+ let bodyObj: Record = {};
+ if (typeof requestOptions.body === 'string') {
+ const params = new URLSearchParams(requestOptions.body);
+ bodyObj = Object.fromEntries(params.entries());
+ }
+
+ tags.forEach((tag, index) => {
+ if (!tag.key || !tag.value) {
+ throw new NodeOperationError(
+ this.getNode(),
+ `Tag at position ${index + 1} is missing '${!tag.key ? 'Key' : 'Value'}'. Both 'Key' and 'Value' are required.`,
+ );
+ }
+ bodyObj[`Tags.member.${index + 1}.Key`] = tag.key;
+ bodyObj[`Tags.member.${index + 1}.Value`] = tag.value;
+ });
+
+ requestOptions.body = new URLSearchParams(bodyObj).toString();
+
+ return requestOptions;
+}
+
+export async function removeUserFromGroups(
+ this: IExecuteSingleFunctions,
+ requestOptions: IHttpRequestOptions,
+): Promise {
+ const userName = this.getNodeParameter('user', undefined, { extractValue: true });
+ const userGroups = await searchGroupsForUser.call(this);
+
+ for (const group of userGroups.results) {
+ await awsApiRequest.call(this, {
+ method: 'POST',
+ url: '',
+ body: {
+ Action: 'RemoveUserFromGroup',
+ Version: CURRENT_VERSION,
+ GroupName: group.value,
+ UserName: userName,
+ },
+ });
+ }
+
+ return requestOptions;
+}
diff --git a/packages/nodes-base/nodes/Aws/IAM/methods/index.ts b/packages/nodes-base/nodes/Aws/IAM/methods/index.ts
new file mode 100644
index 0000000000..c7fb720e47
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/methods/index.ts
@@ -0,0 +1 @@
+export * as listSearch from './listSearch';
diff --git a/packages/nodes-base/nodes/Aws/IAM/methods/listSearch.ts b/packages/nodes-base/nodes/Aws/IAM/methods/listSearch.ts
new file mode 100644
index 0000000000..e4bb83f5ff
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/methods/listSearch.ts
@@ -0,0 +1,159 @@
+import type {
+ IDataObject,
+ IExecuteSingleFunctions,
+ IHttpRequestOptions,
+ ILoadOptionsFunctions,
+ INodeListSearchItems,
+ INodeListSearchResult,
+ JsonObject,
+} from 'n8n-workflow';
+import { NodeApiError } from 'n8n-workflow';
+
+import { CURRENT_VERSION } from '../helpers/constants';
+import type {
+ GetAllGroupsResponseBody,
+ GetAllUsersResponseBody,
+ GetGroupResponseBody,
+} from '../helpers/types';
+import { awsApiRequest } from '../transport';
+
+function formatSearchResults(
+ items: IDataObject[],
+ propertyName: string,
+ filter?: string,
+): INodeListSearchItems[] {
+ return items
+ .map((item) => ({
+ name: String(item[propertyName] ?? ''),
+ value: String(item[propertyName] ?? ''),
+ }))
+ .filter(({ name }) => !filter || name.includes(filter))
+ .sort((a, b) => a.name.localeCompare(b.name));
+}
+
+export async function searchUsers(
+ this: ILoadOptionsFunctions,
+ filter?: string,
+ paginationToken?: string,
+): Promise {
+ const options: IHttpRequestOptions = {
+ method: 'POST',
+ url: '',
+ body: {
+ Action: 'ListUsers',
+ Version: CURRENT_VERSION,
+ ...(paginationToken ? { Marker: paginationToken } : {}),
+ },
+ };
+ const responseData = (await awsApiRequest.call(this, options)) as GetAllUsersResponseBody;
+
+ const users = responseData.ListUsersResponse.ListUsersResult.Users || [];
+ const nextMarker = responseData.ListUsersResponse.ListUsersResult.IsTruncated
+ ? responseData.ListUsersResponse.ListUsersResult.Marker
+ : undefined;
+
+ return {
+ results: formatSearchResults(users, 'UserName', filter),
+ paginationToken: nextMarker,
+ };
+}
+
+export async function searchGroups(
+ this: ILoadOptionsFunctions,
+ filter?: string,
+ paginationToken?: string,
+): Promise {
+ const options: IHttpRequestOptions = {
+ method: 'POST',
+ url: '',
+ body: {
+ Action: 'ListGroups',
+ Version: CURRENT_VERSION,
+ ...(paginationToken ? { Marker: paginationToken } : {}),
+ },
+ };
+
+ const responseData = (await awsApiRequest.call(this, options)) as GetAllGroupsResponseBody;
+
+ const groups = responseData.ListGroupsResponse.ListGroupsResult.Groups || [];
+ const nextMarker = responseData.ListGroupsResponse.ListGroupsResult.IsTruncated
+ ? responseData.ListGroupsResponse.ListGroupsResult.Marker
+ : undefined;
+
+ return {
+ results: formatSearchResults(groups, 'GroupName', filter),
+ paginationToken: nextMarker,
+ };
+}
+
+export async function searchGroupsForUser(
+ this: ILoadOptionsFunctions | IExecuteSingleFunctions,
+ filter?: string,
+): Promise {
+ const userName = this.getNodeParameter('user', undefined, { extractValue: true });
+ let allGroups: IDataObject[] = [];
+ let nextMarkerGroups: string | undefined;
+ do {
+ const options: IHttpRequestOptions = {
+ method: 'POST',
+ url: '',
+ body: {
+ Action: 'ListGroups',
+ Version: CURRENT_VERSION,
+ ...(nextMarkerGroups ? { Marker: nextMarkerGroups } : {}),
+ },
+ };
+
+ const groupsData = (await awsApiRequest.call(this, options)) as GetAllGroupsResponseBody;
+
+ const groups = groupsData.ListGroupsResponse?.ListGroupsResult?.Groups || [];
+ nextMarkerGroups = groupsData.ListGroupsResponse?.ListGroupsResult?.IsTruncated
+ ? groupsData.ListGroupsResponse?.ListGroupsResult?.Marker
+ : undefined;
+
+ allGroups = [...allGroups, ...groups];
+ } while (nextMarkerGroups);
+
+ if (allGroups.length === 0) {
+ return { results: [] };
+ }
+
+ const groupCheckPromises = allGroups.map(async (group) => {
+ const groupName = group.GroupName as string;
+ if (!groupName) {
+ return null;
+ }
+
+ try {
+ const options: IHttpRequestOptions = {
+ method: 'POST',
+ url: '',
+ body: {
+ Action: 'GetGroup',
+ Version: CURRENT_VERSION,
+ GroupName: groupName,
+ },
+ };
+
+ const getGroupResponse = (await awsApiRequest.call(this, options)) as GetGroupResponseBody;
+ const groupResult = getGroupResponse?.GetGroupResponse?.GetGroupResult;
+ const userExists = groupResult?.Users?.some((user) => user.UserName === userName);
+
+ if (userExists) {
+ return { UserName: userName, GroupName: groupName };
+ }
+ } catch (error) {
+ throw new NodeApiError(this.getNode(), error as JsonObject, {
+ message: `Failed to get group ${groupName}: ${error?.message ?? 'Unknown error'}`,
+ });
+ }
+
+ return null;
+ });
+
+ const validUserGroups = (await Promise.all(groupCheckPromises)).filter(Boolean) as IDataObject[];
+
+ return {
+ results: formatSearchResults(validUserGroups, 'GroupName', filter),
+ };
+}
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/group/create.test.ts b/packages/nodes-base/nodes/Aws/IAM/test/group/create.test.ts
new file mode 100644
index 0000000000..5137aef4fe
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/group/create.test.ts
@@ -0,0 +1,45 @@
+import { NodeTestHarness } from '@nodes-testing/node-test-harness';
+import nock from 'nock';
+
+import { BASE_URL, CURRENT_VERSION } from '../../helpers/constants';
+
+describe('AWS IAM - Create Group', () => {
+ beforeEach(() => {
+ nock.cleanAll();
+ nock(BASE_URL)
+ .persist()
+ .defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
+ .post('/', {
+ Action: 'CreateGroup',
+ Version: CURRENT_VERSION,
+ GroupName: 'NewGroupTest',
+ })
+ .reply(200, {
+ CreateGroupResponse: {
+ CreateGroupResult: {
+ Group: {
+ GroupName: 'NewGroupTest',
+ Arn: 'arn:aws:iam::130450532146:group/NewGroupTest',
+ GroupId: 'AGPAR4X3VE4ZI7H42C2XW',
+ Path: '/',
+ CreateDate: 1743409792,
+ },
+ },
+ ResponseMetadata: {
+ RequestId: '50bf4fdb-34b9-4c99-8da8-358d50783c8d',
+ },
+ },
+ });
+ });
+
+ new NodeTestHarness().setupTests({
+ workflowFiles: ['create.workflow.json'],
+ credentials: {
+ aws: {
+ region: 'eu-central-1',
+ accessKeyId: 'test',
+ secretAccessKey: 'test',
+ },
+ },
+ });
+});
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/group/create.workflow.json b/packages/nodes-base/nodes/Aws/IAM/test/group/create.workflow.json
new file mode 100644
index 0000000000..7f15f0887e
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/group/create.workflow.json
@@ -0,0 +1,78 @@
+{
+ "nodes": [
+ {
+ "parameters": {},
+ "type": "n8n-nodes-base.manualTrigger",
+ "typeVersion": 1,
+ "position": [-40, -120],
+ "id": "7da2ce49-9a9d-4240-b082-ff1b12d101b1",
+ "name": "When clicking ‘Test workflow’"
+ },
+ {
+ "parameters": {
+ "resource": "group",
+ "operation": "create",
+ "groupName": "NewGroupTest",
+ "additionalFields": {},
+ "requestOptions": {}
+ },
+ "type": "n8n-nodes-base.awsIam",
+ "typeVersion": 1,
+ "position": [160, -120],
+ "id": "23fd4c79-516d-49dc-81c8-e68052a294d1",
+ "name": "createGroup",
+ "credentials": {
+ "aws": {
+ "id": "exampleId",
+ "name": "AWS US EAST"
+ }
+ }
+ }
+ ],
+ "pinData": {
+ "createGroup": [
+ {
+ "json": {
+ "CreateGroupResponse": {
+ "CreateGroupResult": {
+ "Group": {
+ "Arn": "arn:aws:iam::130450532146:group/NewGroupTest",
+ "CreateDate": 1743409792,
+ "GroupId": "AGPAR4X3VE4ZI7H42C2XW",
+ "GroupName": "NewGroupTest",
+ "Path": "/"
+ }
+ },
+ "ResponseMetadata": {
+ "RequestId": "50bf4fdb-34b9-4c99-8da8-358d50783c8d"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "connections": {
+ "When clicking ‘Test workflow’": {
+ "main": [
+ [
+ {
+ "node": "createGroup",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ },
+ "active": false,
+ "settings": {
+ "executionOrder": "v1"
+ },
+ "versionId": "da88fb89-6234-4eb3-b6fb-31a7a299c7ec",
+ "meta": {
+ "templateCredsSetupCompleted": true,
+ "instanceId": "26f38eb23ad84214537831cfef4032299ffac994ff18d5cb72e82d31ac4ceac4"
+ },
+ "id": "0qqycx08fTBpfbC9",
+ "tags": []
+}
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/group/delete.test.ts b/packages/nodes-base/nodes/Aws/IAM/test/group/delete.test.ts
new file mode 100644
index 0000000000..cd2a097ab8
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/group/delete.test.ts
@@ -0,0 +1,61 @@
+import { NodeTestHarness } from '@nodes-testing/node-test-harness';
+import nock from 'nock';
+
+import { BASE_URL, CURRENT_VERSION } from '../../helpers/constants';
+
+describe('AWS IAM - Delete Group', () => {
+ beforeEach(() => {
+ nock.cleanAll();
+ nock(BASE_URL)
+ .persist()
+ .defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
+ .post('/', {
+ Action: 'GetGroup',
+ Version: CURRENT_VERSION,
+ GroupName: 'GroupForTest1',
+ })
+ .reply(200, {
+ GetGroupResponse: {
+ GetGroupResult: {
+ Users: [{ UserName: 'User1' }],
+ },
+ },
+ });
+ nock(BASE_URL)
+ .persist()
+ .defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
+ .post('/', {
+ Action: 'RemoveUserFromGroup',
+ Version: CURRENT_VERSION,
+ GroupName: 'GroupForTest1',
+ UserName: 'User1',
+ })
+ .reply(200, {});
+ nock(BASE_URL)
+ .persist()
+ .defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
+ .post('/', {
+ Action: 'DeleteGroup',
+ Version: CURRENT_VERSION,
+ GroupName: 'GroupForTest1',
+ })
+ .reply(200, {
+ DeleteGroupResponse: {
+ ResponseMetadata: {
+ RequestId: 'b9cc2642-db2c-4935-aaaf-eacf10e4f00a',
+ },
+ },
+ });
+ });
+
+ new NodeTestHarness().setupTests({
+ workflowFiles: ['delete.workflow.json'],
+ credentials: {
+ aws: {
+ region: 'eu-central-1',
+ accessKeyId: 'test',
+ secretAccessKey: 'test',
+ },
+ },
+ });
+});
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/group/delete.workflow.json b/packages/nodes-base/nodes/Aws/IAM/test/group/delete.workflow.json
new file mode 100644
index 0000000000..e828136fee
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/group/delete.workflow.json
@@ -0,0 +1,62 @@
+{
+ "nodes": [
+ {
+ "parameters": {},
+ "type": "n8n-nodes-base.manualTrigger",
+ "typeVersion": 1,
+ "position": [200, 120],
+ "id": "b4205abf-7102-4e53-8aed-7bd047acfaf4",
+ "name": "When clicking ‘Test workflow’"
+ },
+ {
+ "parameters": {
+ "resource": "group",
+ "operation": "delete",
+ "group": {
+ "__rl": true,
+ "value": "GroupForTest1",
+ "mode": "list",
+ "cachedResultName": "GroupForTest1"
+ },
+ "requestOptions": {}
+ },
+ "type": "n8n-nodes-base.awsIam",
+ "typeVersion": 1,
+ "position": [380, 120],
+ "id": "bba99f6d-ed9c-4603-95f0-4ecce6e6f976",
+ "name": "AWS IAM13",
+ "credentials": {
+ "aws": {
+ "id": "exampleId",
+ "name": "AWS US EAST"
+ }
+ }
+ }
+ ],
+ "connections": {
+ "When clicking ‘Test workflow’": {
+ "main": [
+ [
+ {
+ "node": "AWS IAM13",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ },
+ "pinData": {
+ "AWS IAM13": [
+ {
+ "json": {
+ "DeleteGroupResponse": {
+ "ResponseMetadata": {
+ "RequestId": "b9cc2642-db2c-4935-aaaf-eacf10e4f00a"
+ }
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/group/get.test.ts b/packages/nodes-base/nodes/Aws/IAM/test/group/get.test.ts
new file mode 100644
index 0000000000..bc63cc8324
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/group/get.test.ts
@@ -0,0 +1,54 @@
+import { NodeTestHarness } from '@nodes-testing/node-test-harness';
+import nock from 'nock';
+
+import { BASE_URL, CURRENT_VERSION } from '../../helpers/constants';
+
+describe('AWS IAM - Get Group', () => {
+ beforeEach(() => {
+ nock.cleanAll();
+ nock(BASE_URL)
+ .persist()
+ .defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
+ .post('/', {
+ Action: 'GetGroup',
+ Version: CURRENT_VERSION,
+ GroupName: 'GroupNameUpdated2',
+ })
+ .reply(200, {
+ GetGroupResponse: {
+ GetGroupResult: {
+ Group: {
+ Arn: 'arn:aws:iam::130450532146:group/New/Path/GroupNameUpdated2',
+ CreateDate: 1739193696,
+ GroupId: 'AGPAR4X3VE4ZKHNKBQHBZ',
+ GroupName: 'GroupNameUpdated2',
+ Path: '/New/Path/',
+ },
+ Users: [
+ {
+ Arn: 'arn:aws:iam::130450532146:user/rhis/path/Jonas',
+ CreateDate: 1739198295,
+ PasswordLastUsed: null,
+ PermissionsBoundary: null,
+ Tags: null,
+ Path: '/rhis/path/',
+ UserId: 'AIDAR4X3VE4ZDJJFKI6OU',
+ UserName: 'Jonas',
+ },
+ ],
+ },
+ },
+ });
+ });
+
+ new NodeTestHarness().setupTests({
+ workflowFiles: ['get.workflow.json'],
+ credentials: {
+ aws: {
+ region: 'eu-central-1',
+ accessKeyId: 'test',
+ secretAccessKey: 'test',
+ },
+ },
+ });
+});
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/group/get.workflow.json b/packages/nodes-base/nodes/Aws/IAM/test/group/get.workflow.json
new file mode 100644
index 0000000000..a6d9020ef3
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/group/get.workflow.json
@@ -0,0 +1,86 @@
+{
+ "nodes": [
+ {
+ "parameters": {},
+ "type": "n8n-nodes-base.manualTrigger",
+ "typeVersion": 1,
+ "position": [-80, -100],
+ "id": "7da2ce49-9a9d-4240-b082-ff1b12d101b1",
+ "name": "When clicking ‘Test workflow’"
+ },
+ {
+ "parameters": {
+ "resource": "group",
+ "operation": "get",
+ "group": {
+ "__rl": true,
+ "value": "GroupNameUpdated2",
+ "mode": "list",
+ "cachedResultName": "GroupNameUpdated2"
+ },
+ "includeUsers": true,
+ "requestOptions": {}
+ },
+ "type": "n8n-nodes-base.awsIam",
+ "typeVersion": 1,
+ "position": [160, -100],
+ "id": "7990c8f3-738e-4150-855e-a9c0a965b75d",
+ "name": "getGroup",
+ "credentials": {
+ "aws": {
+ "id": "exampleId",
+ "name": "AWS US EAST"
+ }
+ }
+ }
+ ],
+ "pinData": {
+ "getGroup": [
+ {
+ "json": {
+ "Arn": "arn:aws:iam::130450532146:group/New/Path/GroupNameUpdated2",
+ "CreateDate": 1739193696,
+ "GroupId": "AGPAR4X3VE4ZKHNKBQHBZ",
+ "GroupName": "GroupNameUpdated2",
+ "Path": "/New/Path/",
+ "Users": [
+ {
+ "Arn": "arn:aws:iam::130450532146:user/rhis/path/Jonas",
+ "CreateDate": 1739198295,
+ "PasswordLastUsed": null,
+ "Path": "/rhis/path/",
+ "PermissionsBoundary": null,
+ "Tags": null,
+ "UserId": "AIDAR4X3VE4ZDJJFKI6OU",
+ "UserName": "Jonas"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "connections": {
+ "When clicking ‘Test workflow’": {
+ "main": [
+ [
+ {
+ "node": "getGroup",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ },
+ "active": false,
+ "settings": {
+ "executionOrder": "v1"
+ },
+ "versionId": "47e9f15a-50b7-4f12-ac5b-c7b24bb9a400",
+ "meta": {
+ "templateCredsSetupCompleted": true,
+ "instanceId": "26f38eb23ad84214537831cfef4032299ffac994ff18d5cb72e82d31ac4ceac4"
+ },
+ "id": "0qqycx08fTBpfbC9",
+ "tags": []
+}
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/group/getAll.test.ts b/packages/nodes-base/nodes/Aws/IAM/test/group/getAll.test.ts
new file mode 100644
index 0000000000..45728e2cd1
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/group/getAll.test.ts
@@ -0,0 +1,58 @@
+import { NodeTestHarness } from '@nodes-testing/node-test-harness';
+import nock from 'nock';
+
+import { BASE_URL, CURRENT_VERSION } from '../../helpers/constants';
+
+describe('AWS IAM - Get All Groups', () => {
+ beforeEach(() => {
+ nock.cleanAll();
+ nock(BASE_URL)
+ .persist()
+ .defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
+ .post('/', {
+ Action: 'ListGroups',
+ Version: CURRENT_VERSION,
+ MaxItems: 100,
+ })
+ .reply(200, {
+ ListGroupsResponse: {
+ ListGroupsResult: {
+ Groups: [
+ {
+ Arn: 'arn:aws:iam::130450532146:group/test/Admin7',
+ CreateDate: 1733436631,
+ GroupId: 'AGPAR4X3VE4ZAFFY5EDUJ',
+ GroupName: 'Admin7',
+ Path: '/test/',
+ },
+ {
+ Arn: 'arn:aws:iam::130450532146:group/cognito',
+ CreateDate: 1730804196,
+ GroupId: 'AGPAR4X3VE4ZMVEFLBSRB',
+ GroupName: 'cognito',
+ Path: '/',
+ },
+ {
+ Arn: 'arn:aws:iam::130450532146:group/GroupCreatedAfter',
+ CreateDate: 1741589366,
+ GroupId: 'AGPAR4X3VE4ZF5VE6UF2U',
+ GroupName: 'GroupCreatedAfter',
+ Path: '/',
+ },
+ ],
+ },
+ },
+ });
+ });
+
+ new NodeTestHarness().setupTests({
+ workflowFiles: ['getAll.workflow.json'],
+ credentials: {
+ aws: {
+ region: 'eu-central-1',
+ accessKeyId: 'test',
+ secretAccessKey: 'test',
+ },
+ },
+ });
+});
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/group/getAll.workflow.json b/packages/nodes-base/nodes/Aws/IAM/test/group/getAll.workflow.json
new file mode 100644
index 0000000000..2dd72f5be8
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/group/getAll.workflow.json
@@ -0,0 +1,85 @@
+{
+ "nodes": [
+ {
+ "parameters": {},
+ "type": "n8n-nodes-base.manualTrigger",
+ "typeVersion": 1,
+ "position": [-40, -220],
+ "id": "7da2ce49-9a9d-4240-b082-ff1b12d101b1",
+ "name": "When clicking ‘Test workflow’"
+ },
+ {
+ "parameters": {
+ "resource": "group",
+ "includeUsers": false,
+ "requestOptions": {}
+ },
+ "type": "n8n-nodes-base.awsIam",
+ "typeVersion": 1,
+ "position": [160, -220],
+ "id": "23fd4c79-516d-49dc-81c8-e68052a294d1",
+ "name": "getAllGroups",
+ "credentials": {
+ "aws": {
+ "id": "exampleId",
+ "name": "AWS US EAST"
+ }
+ }
+ }
+ ],
+ "pinData": {
+ "getAllGroups": [
+ {
+ "json": {
+ "Arn": "arn:aws:iam::130450532146:group/test/Admin7",
+ "CreateDate": 1733436631,
+ "GroupId": "AGPAR4X3VE4ZAFFY5EDUJ",
+ "GroupName": "Admin7",
+ "Path": "/test/"
+ }
+ },
+ {
+ "json": {
+ "Arn": "arn:aws:iam::130450532146:group/cognito",
+ "CreateDate": 1730804196,
+ "GroupId": "AGPAR4X3VE4ZMVEFLBSRB",
+ "GroupName": "cognito",
+ "Path": "/"
+ }
+ },
+ {
+ "json": {
+ "Arn": "arn:aws:iam::130450532146:group/GroupCreatedAfter",
+ "CreateDate": 1741589366,
+ "GroupId": "AGPAR4X3VE4ZF5VE6UF2U",
+ "GroupName": "GroupCreatedAfter",
+ "Path": "/"
+ }
+ }
+ ]
+ },
+ "connections": {
+ "When clicking ‘Test workflow’": {
+ "main": [
+ [
+ {
+ "node": "getAllGroups",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ },
+ "active": false,
+ "settings": {
+ "executionOrder": "v1"
+ },
+ "versionId": "51beb4c0-4163-4c57-a715-6eb45f1ffb9b",
+ "meta": {
+ "templateCredsSetupCompleted": true,
+ "instanceId": "26f38eb23ad84214537831cfef4032299ffac994ff18d5cb72e82d31ac4ceac4"
+ },
+ "id": "0qqycx08fTBpfbC9",
+ "tags": []
+}
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/group/update.test.ts b/packages/nodes-base/nodes/Aws/IAM/test/group/update.test.ts
new file mode 100644
index 0000000000..274d2f88e2
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/group/update.test.ts
@@ -0,0 +1,36 @@
+import { NodeTestHarness } from '@nodes-testing/node-test-harness';
+import nock from 'nock';
+
+import { BASE_URL, CURRENT_VERSION } from '../../helpers/constants';
+
+describe('AWS IAM - Update Group', () => {
+ beforeEach(() => {
+ nock(BASE_URL)
+ .persist()
+ .defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
+ .post('/', {
+ Action: 'UpdateGroup',
+ Version: CURRENT_VERSION,
+ GroupName: 'GroupNameUpdated',
+ NewGroupName: 'GroupNameUpdated2',
+ })
+ .reply(200, {
+ UpdateGroupResponse: {
+ ResponseMetadata: {
+ RequestId: '16ada465-a981-44ab-841f-3ca3247f7405',
+ },
+ },
+ });
+ });
+
+ new NodeTestHarness().setupTests({
+ workflowFiles: ['update.workflow.json'],
+ credentials: {
+ aws: {
+ region: 'eu-central-1',
+ accessKeyId: 'test',
+ secretAccessKey: 'test',
+ },
+ },
+ });
+});
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/group/update.workflow.json b/packages/nodes-base/nodes/Aws/IAM/test/group/update.workflow.json
new file mode 100644
index 0000000000..2e3d5deea0
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/group/update.workflow.json
@@ -0,0 +1,74 @@
+{
+ "nodes": [
+ {
+ "parameters": {},
+ "type": "n8n-nodes-base.manualTrigger",
+ "typeVersion": 1,
+ "position": [-40, -120],
+ "id": "7da2ce49-9a9d-4240-b082-ff1b12d101b1",
+ "name": "When clicking ‘Test workflow’"
+ },
+ {
+ "parameters": {
+ "resource": "group",
+ "operation": "update",
+ "group": {
+ "__rl": true,
+ "value": "GroupNameUpdated",
+ "mode": "list",
+ "cachedResultName": "GroupNameUpdated"
+ },
+ "groupName": "GroupNameUpdated2",
+ "requestOptions": {}
+ },
+ "type": "n8n-nodes-base.awsIam",
+ "typeVersion": 1,
+ "position": [140, -120],
+ "id": "6299cf80-7aea-4cf6-8604-368a42290da7",
+ "name": "updateGroup",
+ "credentials": {
+ "aws": {
+ "id": "exampleId",
+ "name": "AWS US EAST"
+ }
+ }
+ }
+ ],
+ "pinData": {
+ "updateGroup": [
+ {
+ "json": {
+ "UpdateGroupResponse": {
+ "ResponseMetadata": {
+ "RequestId": "16ada465-a981-44ab-841f-3ca3247f7405"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "connections": {
+ "When clicking ‘Test workflow’": {
+ "main": [
+ [
+ {
+ "node": "updateGroup",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ },
+ "active": false,
+ "settings": {
+ "executionOrder": "v1"
+ },
+ "versionId": "bce6ece0-c45a-4389-9e39-6edea3d2e0da",
+ "meta": {
+ "templateCredsSetupCompleted": true,
+ "instanceId": "26f38eb23ad84214537831cfef4032299ffac994ff18d5cb72e82d31ac4ceac4"
+ },
+ "id": "0qqycx08fTBpfbC9",
+ "tags": []
+}
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/helpers/errorHandler.test.ts b/packages/nodes-base/nodes/Aws/IAM/test/helpers/errorHandler.test.ts
new file mode 100644
index 0000000000..624a95607d
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/helpers/errorHandler.test.ts
@@ -0,0 +1,130 @@
+import { NodeApiError } from 'n8n-workflow';
+import type { INodeExecutionData, IN8nHttpFullResponse, JsonObject } from 'n8n-workflow';
+
+import { ERROR_DESCRIPTIONS } from '../../helpers/constants';
+import { handleError } from '../../helpers/errorHandler';
+
+const mockExecuteSingleFunctions = {
+ getNode: jest.fn(() => ({ name: 'MockNode' })),
+ getNodeParameter: jest.fn(),
+} as any;
+
+describe('handleError', () => {
+ let response: IN8nHttpFullResponse;
+ let data: INodeExecutionData[];
+
+ beforeEach(() => {
+ data = [{}] as INodeExecutionData[];
+ response = { statusCode: 200, body: {} } as IN8nHttpFullResponse;
+ });
+
+ test('should return data when no error occurs', async () => {
+ const result = await handleError.call(mockExecuteSingleFunctions, data, response);
+ expect(result).toBe(data);
+ });
+
+ test('should throw NodeApiError for EntityAlreadyExists with user conflict', async () => {
+ mockExecuteSingleFunctions.getNodeParameter = jest
+ .fn()
+ .mockReturnValueOnce('user')
+ .mockReturnValueOnce('existingUserName');
+
+ response.statusCode = 400;
+ response.body = {
+ Error: { Code: 'EntityAlreadyExists', Message: 'User "existingUserName" already exists' },
+ } as JsonObject;
+
+ await expect(handleError.call(mockExecuteSingleFunctions, data, response)).rejects.toThrow(
+ new NodeApiError(mockExecuteSingleFunctions.getNode(), response.body as JsonObject, {
+ message: 'User "existingUserName" already exists',
+ description: ERROR_DESCRIPTIONS.EntityAlreadyExists.User,
+ }),
+ );
+ });
+
+ test('should throw NodeApiError for NoSuchEntity with user not found', async () => {
+ mockExecuteSingleFunctions.getNodeParameter
+ .mockReturnValueOnce('user')
+ .mockReturnValueOnce('nonExistentUser');
+
+ response.statusCode = 404;
+ response.body = {
+ Error: { Code: 'NoSuchEntity', Message: 'User "nonExistentUser" does not exist' },
+ } as JsonObject;
+
+ await expect(handleError.call(mockExecuteSingleFunctions, data, response)).rejects.toThrowError(
+ new NodeApiError(mockExecuteSingleFunctions.getNode(), response.body as JsonObject, {
+ message: 'User "nonExistentUser" does not exist',
+ description: ERROR_DESCRIPTIONS.NoSuchEntity.User,
+ }),
+ );
+ });
+
+ test('should throw generic error if no specific mapping exists', async () => {
+ mockExecuteSingleFunctions.getNodeParameter.mockReturnValue('container');
+
+ response.statusCode = 400;
+ response.body = { Error: { Code: 'BadRequest', Message: 'Invalid request' } } as JsonObject;
+
+ await expect(handleError.call(mockExecuteSingleFunctions, data, response)).rejects.toThrow(
+ new NodeApiError(mockExecuteSingleFunctions.getNode(), response.body as JsonObject, {
+ message: 'BadRequest',
+ description: 'Invalid request',
+ }),
+ );
+ });
+
+ test('should throw NodeApiError for EntityAlreadyExists with group conflict', async () => {
+ mockExecuteSingleFunctions.getNodeParameter
+ .mockReturnValueOnce('group')
+ .mockReturnValue('existingGroupName');
+
+ response.statusCode = 400;
+ response.body = {
+ Error: { Code: 'EntityAlreadyExists', Message: 'Group "existingGroupName" already exists' },
+ } as JsonObject;
+
+ await expect(handleError.call(mockExecuteSingleFunctions, data, response)).rejects.toThrow(
+ new NodeApiError(mockExecuteSingleFunctions.getNode(), response.body as JsonObject, {
+ message: 'Group "existingGroupName" already exists',
+ description: ERROR_DESCRIPTIONS.EntityAlreadyExists.Group,
+ }),
+ );
+ });
+
+ test('should throw NodeApiError for NoSuchEntity with group not found', async () => {
+ mockExecuteSingleFunctions.getNodeParameter
+ .mockReturnValueOnce('group')
+ .mockReturnValue('nonExistentGroup');
+
+ response.statusCode = 404;
+ response.body = {
+ Error: { Code: 'NoSuchEntity', Message: 'Group "nonExistentGroup" does not exist' },
+ } as JsonObject;
+
+ await expect(handleError.call(mockExecuteSingleFunctions, data, response)).rejects.toThrow(
+ new NodeApiError(mockExecuteSingleFunctions.getNode(), response.body as JsonObject, {
+ message: 'Group "nonExistentGroup" does not exist',
+ description: ERROR_DESCRIPTIONS.NoSuchEntity.Group,
+ }),
+ );
+ });
+
+ test('should throw NodeApiError for DeleteConflict', async () => {
+ mockExecuteSingleFunctions.getNodeParameter
+ .mockReturnValueOnce('user')
+ .mockReturnValue('userInGroup');
+
+ response.statusCode = 400;
+ response.body = {
+ Error: { Code: 'DeleteConflict', Message: 'User "userIngroup" is in a group' },
+ } as JsonObject;
+
+ await expect(handleError.call(mockExecuteSingleFunctions, data, response)).rejects.toThrow(
+ new NodeApiError(mockExecuteSingleFunctions.getNode(), response.body as JsonObject, {
+ message: 'User "userIngroup" is in a group',
+ description: 'This entity is still in use. Remove users from the group before deleting.',
+ }),
+ );
+ });
+});
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/helpers/utils.test.ts b/packages/nodes-base/nodes/Aws/IAM/test/helpers/utils.test.ts
new file mode 100644
index 0000000000..a413a034e5
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/helpers/utils.test.ts
@@ -0,0 +1,421 @@
+import type { IHttpRequestOptions } from 'n8n-workflow';
+import { NodeOperationError } from 'n8n-workflow';
+
+import {
+ preprocessTags,
+ deleteGroupMembers,
+ validatePath,
+ validateUserPath,
+ removeUserFromGroups,
+ findUsersForGroup,
+ simplifyGetGroupsResponse,
+ simplifyGetAllGroupsResponse,
+ simplifyGetAllUsersResponse,
+ validateName,
+ validatePermissionsBoundary,
+ encodeBodyAsFormUrlEncoded,
+} from '../../helpers/utils';
+import { awsApiRequest } from '../../transport';
+
+jest.mock('../../transport', () => ({
+ awsApiRequest: jest.fn(),
+}));
+
+describe('AWS IAM - Helper Functions', () => {
+ let mockNode: any;
+
+ beforeEach(() => {
+ mockNode = {
+ getNodeParameter: jest.fn(),
+ getNode: jest.fn(),
+ helpers: {
+ returnJsonArray: jest.fn((input: unknown[]) => input.map((i) => ({ json: i }))),
+ },
+ };
+ });
+
+ describe('encodeBodyAsFormUrlEncoded', () => {
+ it('should encode the body as application/x-www-form-urlencoded', async () => {
+ const requestOptions: IHttpRequestOptions = {
+ body: {
+ client_id: 'myClient',
+ client_secret: 'mySecret',
+ grant_type: 'client_credentials',
+ },
+ url: '',
+ headers: {},
+ };
+
+ const result = await encodeBodyAsFormUrlEncoded.call(mockNode, requestOptions);
+
+ expect(result.body).toBe(
+ 'client_id=myClient&client_secret=mySecret&grant_type=client_credentials',
+ );
+ });
+
+ it('should return unchanged options if no body is present', async () => {
+ const requestOptions: IHttpRequestOptions = { url: '', headers: {} };
+ const result = await encodeBodyAsFormUrlEncoded.call(mockNode, requestOptions);
+
+ expect(result).toEqual({ url: '', headers: {} });
+ });
+ });
+
+ describe('findUsersForGroup', () => {
+ it('should return users for a valid groupName', async () => {
+ mockNode.getNodeParameter.mockReturnValue('groupName');
+ const mockResponse = {
+ GetGroupResponse: { GetGroupResult: { Users: [{ UserName: 'user1' }] } },
+ };
+ (awsApiRequest as jest.Mock).mockResolvedValue(mockResponse);
+
+ const result = await findUsersForGroup.call(mockNode, 'groupName');
+ expect(result).toEqual([{ UserName: 'user1' }]);
+ });
+ });
+
+ describe('preprocessTags', () => {
+ it('should preprocess tags correctly into request body', async () => {
+ mockNode.getNodeParameter.mockReturnValue({
+ tags: [
+ { key: 'Department', value: 'Engineering' },
+ { key: 'Role', value: 'Developer' },
+ ],
+ });
+
+ const requestOptions = { body: '', headers: {}, url: '' };
+ const result = await preprocessTags.call(mockNode, requestOptions);
+
+ expect(result.body).toBe(
+ 'Tags.member.1.Key=Department&Tags.member.1.Value=Engineering&Tags.member.2.Key=Role&Tags.member.2.Value=Developer',
+ );
+ });
+
+ it('should throw error if a tag is missing a key', async () => {
+ mockNode.getNodeParameter.mockReturnValue({
+ tags: [{ key: '', value: 'Engineering' }],
+ });
+
+ const requestOptions = { body: '', headers: {}, url: '' };
+
+ await expect(preprocessTags.call(mockNode, requestOptions)).rejects.toThrow(
+ NodeOperationError,
+ );
+
+ await expect(preprocessTags.call(mockNode, requestOptions)).rejects.toThrow(
+ "Tag at position 1 is missing 'Key'. Both 'Key' and 'Value' are required.",
+ );
+ });
+
+ it('should throw error if a tag is missing a value', async () => {
+ mockNode.getNodeParameter.mockReturnValue({
+ tags: [{ key: 'Department', value: '' }],
+ });
+
+ const requestOptions = { body: '', headers: {}, url: '' };
+
+ await expect(preprocessTags.call(mockNode, requestOptions)).rejects.toThrow(
+ NodeOperationError,
+ );
+
+ await expect(preprocessTags.call(mockNode, requestOptions)).rejects.toThrow(
+ "Tag at position 1 is missing 'Value'. Both 'Key' and 'Value' are required.",
+ );
+ });
+ });
+
+ describe('deleteGroupMembers', () => {
+ it('should attempt to remove users from a group', async () => {
+ mockNode.getNodeParameter.mockImplementation((param: string) => {
+ if (param === 'group') {
+ return 'groupName';
+ }
+ return null;
+ });
+
+ const mockUsers = [{ UserName: 'user1' }];
+ (awsApiRequest as jest.Mock).mockResolvedValue(mockUsers);
+
+ const requestOptions = { headers: {}, url: '' };
+ const result = await deleteGroupMembers.call(mockNode, requestOptions);
+
+ expect(result).toEqual(requestOptions);
+
+ expect(awsApiRequest).toHaveBeenCalledWith(
+ expect.objectContaining({
+ method: 'POST',
+ body: expect.stringContaining('GroupName=groupName'),
+ }),
+ );
+ });
+ });
+
+ describe('validatePath', () => {
+ it('should throw an error for invalid path length', async () => {
+ mockNode.getNodeParameter.mockReturnValue('');
+ await expect(validatePath.call(mockNode, { headers: {}, url: '' })).rejects.toThrowError(
+ NodeOperationError,
+ );
+ });
+
+ it('should throw an error for invalid path format', async () => {
+ mockNode.getNodeParameter.mockReturnValue('/invalidPath');
+ await expect(validatePath.call(mockNode, { url: '' })).rejects.toThrowError(
+ NodeOperationError,
+ );
+ });
+
+ it('should pass for a valid path', async () => {
+ mockNode.getNodeParameter.mockReturnValue('/valid/path/');
+ const result = await validatePath.call(mockNode, { headers: {}, url: '' });
+ expect(result).toEqual({ headers: {}, url: '' });
+ });
+ });
+
+ describe('validateUserPath', () => {
+ it('should throw an error for invalid path prefix', async () => {
+ mockNode.getNodeParameter.mockReturnValue('/invalidPrefix');
+
+ const mockResponse = {
+ ListUsersResponse: {
+ ListUsersResult: {
+ Users: [{ UserName: 'user1', Path: '/validPrefix/user1' }],
+ },
+ },
+ };
+
+ (awsApiRequest as jest.Mock).mockResolvedValue(mockResponse);
+
+ await expect(validateUserPath.call(mockNode, { headers: {}, url: '' })).rejects.toThrowError(
+ NodeOperationError,
+ );
+ });
+
+ it('should modify the request body with a valid path', async () => {
+ mockNode.getNodeParameter.mockReturnValue('/validPrefix/');
+ const requestOptions = { body: {}, headers: {}, url: '' };
+
+ const mockResponse = {
+ ListUsersResponse: {
+ ListUsersResult: {
+ Users: [{ UserName: 'user1', Path: '/validPrefix/user1' }],
+ },
+ },
+ };
+
+ (awsApiRequest as jest.Mock).mockResolvedValue(mockResponse);
+
+ const result = await validateUserPath.call(mockNode, requestOptions);
+ expect(result.body).toHaveProperty('PathPrefix', '/validPrefix/');
+ });
+ });
+
+ describe('validateName', () => {
+ const requestOptions: IHttpRequestOptions = { body: {}, url: '' };
+
+ it('should throw an error if userName contains spaces', async () => {
+ mockNode.getNodeParameter.mockImplementation((param: string) => {
+ if (param === 'resource') return 'user';
+ if (param === 'userName') return 'John Doe';
+ return '';
+ });
+
+ await expect(validateName.call(mockNode, requestOptions)).rejects.toThrowError(
+ new NodeOperationError(mockNode.getNode(), 'User name should not contain spaces.'),
+ );
+ });
+
+ it('should throw an error if userName contains invalid characters', async () => {
+ mockNode.getNodeParameter.mockImplementation((param: string) => {
+ if (param === 'resource') return 'user';
+ if (param === 'userName') return 'John@Doe';
+ return '';
+ });
+
+ await expect(validateName.call(mockNode, requestOptions)).rejects.toThrowError(
+ new NodeOperationError(
+ mockNode.getNode(),
+ 'User name can have up to 64 characters. Valid characters: letters, numbers, hyphens (-), and underscores (_).',
+ ),
+ );
+ });
+
+ it('should pass validation for valid userName', async () => {
+ mockNode.getNodeParameter.mockImplementation((param: string) => {
+ if (param === 'resource') return 'user';
+ if (param === 'userName') return 'John_Doe123';
+ return '';
+ });
+
+ await expect(validateName.call(mockNode, requestOptions)).resolves.toEqual(requestOptions);
+ });
+
+ it('should throw an error if groupName contains spaces', async () => {
+ mockNode.getNodeParameter.mockImplementation((param: string) => {
+ if (param === 'resource') return 'group';
+ if (param === 'groupName') return 'Group Name';
+ return '';
+ });
+
+ await expect(validateName.call(mockNode, requestOptions)).rejects.toThrowError(
+ new NodeOperationError(mockNode.getNode(), 'Group name should not contain spaces.'),
+ );
+ });
+
+ it('should throw an error if groupName contains invalid characters', async () => {
+ mockNode.getNodeParameter.mockImplementation((param: string) => {
+ if (param === 'resource') return 'group';
+ if (param === 'groupName') return 'Group@Name';
+ return '';
+ });
+
+ await expect(validateName.call(mockNode, requestOptions)).rejects.toThrowError(
+ new NodeOperationError(
+ mockNode.getNode(),
+ 'Group name can have up to 128 characters. Valid characters: letters, numbers, hyphens (-), and underscores (_).',
+ ),
+ );
+ });
+
+ it('should pass validation for valid groupName', async () => {
+ mockNode.getNodeParameter.mockImplementation((param: string) => {
+ if (param === 'resource') return 'group';
+ if (param === 'groupName') return 'Group_Name-123';
+ return '';
+ });
+
+ await expect(validateName.call(mockNode, requestOptions)).resolves.toEqual(requestOptions);
+ });
+ });
+
+ describe('validatePermissionsBoundary', () => {
+ const requestOptions: IHttpRequestOptions = { body: {}, url: '' };
+
+ it('should return the request options unchanged if no permissions boundary is set', async () => {
+ mockNode.getNodeParameter.mockReturnValue(undefined);
+
+ const result = await validatePermissionsBoundary.call(mockNode, requestOptions);
+
+ expect(result).toEqual(requestOptions);
+ });
+
+ it('should add a valid permissions boundary to the request body', async () => {
+ const validArn = 'arn:aws:iam::123456789012:policy/ExamplePolicy';
+ mockNode.getNodeParameter.mockReturnValue(validArn);
+
+ const result = await validatePermissionsBoundary.call(mockNode, requestOptions);
+
+ expect(result.body).toEqual({ PermissionsBoundary: validArn });
+ });
+
+ it('should throw an error for invalid permissions boundary format', async () => {
+ const invalidArn = 'invalid:arn:format';
+ mockNode.getNodeParameter.mockReturnValue(invalidArn);
+
+ await expect(
+ validatePermissionsBoundary.call(mockNode, { body: {}, url: '', headers: {} }),
+ ).rejects.toThrow(NodeOperationError);
+ });
+ });
+
+ describe('simplifyGetGroupsResponse', () => {
+ it('should return group data', async () => {
+ mockNode.getNodeParameter.mockReturnValue(false);
+ const mockResponse = {
+ body: { GetGroupResponse: { GetGroupResult: { Group: { GroupName: 'TestGroup' } } } },
+ headers: {},
+ url: '',
+ statusCode: 200,
+ };
+
+ const result = await simplifyGetGroupsResponse.call(mockNode, [], mockResponse);
+
+ expect(result).toEqual([{ json: { GroupName: 'TestGroup' } }]);
+ });
+
+ it('should include users if "includeUsers" is true', async () => {
+ mockNode.getNodeParameter.mockReturnValue(true);
+ const mockResponse = {
+ body: {
+ GetGroupResponse: {
+ GetGroupResult: { Group: { GroupName: 'TestGroup' }, Users: [{ UserName: 'user1' }] },
+ },
+ },
+ headers: {},
+ url: '',
+ statusCode: 200,
+ };
+
+ const result = await simplifyGetGroupsResponse.call(mockNode, [], mockResponse);
+
+ expect(result).toEqual([
+ { json: { GroupName: 'TestGroup', Users: [{ UserName: 'user1' }] } },
+ ]);
+ });
+ });
+
+ describe('simplifyGetAllGroupsResponse', () => {
+ it('should return groups without users if "includeUsers" is false', async () => {
+ mockNode.getNodeParameter.mockReturnValue(false);
+ const mockResponse = {
+ body: {
+ ListGroupsResponse: { ListGroupsResult: { Groups: [{ GroupName: 'TestGroup' }] } },
+ },
+ headers: {},
+ url: '',
+ statusCode: 200,
+ };
+
+ const result = await simplifyGetAllGroupsResponse.call(mockNode, [], mockResponse);
+
+ expect(result).toEqual([{ json: { GroupName: 'TestGroup' } }]);
+ });
+
+ it('should return groups with users if "includeUsers" is true', async () => {
+ mockNode.getNodeParameter.mockReturnValue(true);
+ const mockResponse = {
+ body: {
+ ListGroupsResponse: { ListGroupsResult: { Groups: [{ GroupName: 'TestGroup' }] } },
+ },
+ headers: {},
+ url: '',
+ statusCode: 200,
+ };
+
+ const mockUsers = [{ UserName: 'user1' }];
+ (awsApiRequest as jest.Mock).mockResolvedValueOnce({
+ GetGroupResponse: { GetGroupResult: { Users: mockUsers } },
+ });
+
+ const result = await simplifyGetAllGroupsResponse.call(mockNode, [], mockResponse);
+
+ expect(result).toEqual([{ json: { GroupName: 'TestGroup', Users: mockUsers } }]);
+ });
+ });
+
+ describe('simplifyGetAllUsersResponse', () => {
+ it('should return all users', async () => {
+ const mockResponse = {
+ body: { ListUsersResponse: { ListUsersResult: { Users: [{ UserName: 'user1' }] } } },
+ headers: {},
+ url: '',
+ statusCode: 200,
+ };
+ const result = await simplifyGetAllUsersResponse.call(mockNode, [], mockResponse);
+ expect(result).toEqual([{ json: { UserName: 'user1' } }]);
+ });
+ });
+
+ describe('removeUserFromGroups', () => {
+ it('should remove a user from all groups', async () => {
+ mockNode.getNodeParameter.mockReturnValue('user1');
+ const mockUserGroups = { results: [{ value: 'group1' }, { value: 'group2' }] };
+ (awsApiRequest as jest.Mock).mockResolvedValue(mockUserGroups);
+
+ const requestOptions = { headers: {}, url: '' };
+ const result = await removeUserFromGroups.call(mockNode, requestOptions);
+
+ expect(result).toEqual(requestOptions);
+ });
+ });
+});
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/listSearch/listSearch.test.ts b/packages/nodes-base/nodes/Aws/IAM/test/listSearch/listSearch.test.ts
new file mode 100644
index 0000000000..03190a5fd2
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/listSearch/listSearch.test.ts
@@ -0,0 +1,119 @@
+import type { ILoadOptionsFunctions } from 'n8n-workflow';
+
+import { searchUsers, searchGroups, searchGroupsForUser } from '../../methods/listSearch';
+import { awsApiRequest } from '../../transport';
+
+jest.mock('../../transport', () => ({
+ awsApiRequest: jest.fn(),
+}));
+
+describe('AWS IAM - List search', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const mockContext = {
+ helpers: {
+ requestWithAuthentication: jest.fn(),
+ },
+ getNodeParameter: jest.fn(),
+ getCredentials: jest.fn(),
+ } as unknown as ILoadOptionsFunctions;
+
+ describe('searchUsers', () => {
+ it('should return an empty result if no users are found', async () => {
+ const responseData = { ListUsersResponse: { ListUsersResult: { Users: [] } } };
+ (awsApiRequest as jest.Mock).mockResolvedValue(responseData);
+
+ const result = await searchUsers.call(mockContext);
+ expect(result.results).toEqual([]);
+ });
+
+ it('should return formatted user results when users are found', async () => {
+ const responseData = {
+ ListUsersResponse: {
+ ListUsersResult: {
+ Users: [{ UserName: 'User1' }, { UserName: 'User2' }],
+ },
+ },
+ };
+ (awsApiRequest as jest.Mock).mockResolvedValue(responseData);
+
+ const result = await searchUsers.call(mockContext);
+ expect(result.results).toEqual([
+ { name: 'User1', value: 'User1' },
+ { name: 'User2', value: 'User2' },
+ ]);
+ });
+
+ it('should apply filter to the user results', async () => {
+ const responseData = {
+ ListUsersResponse: {
+ ListUsersResult: {
+ Users: [{ UserName: 'User1' }, { UserName: 'User2' }],
+ },
+ },
+ };
+ (awsApiRequest as jest.Mock).mockResolvedValue(responseData);
+
+ const result = await searchUsers.call(mockContext, 'User1');
+ expect(result.results).toEqual([{ name: 'User1', value: 'User1' }]);
+ });
+ });
+
+ describe('searchGroups', () => {
+ it('should return an empty result if no groups are found', async () => {
+ const responseData = { ListGroupsResponse: { ListGroupsResult: { Groups: [] } } };
+ (awsApiRequest as jest.Mock).mockResolvedValue(responseData);
+
+ const result = await searchGroups.call(mockContext);
+ expect(result.results).toEqual([]);
+ });
+
+ it('should return formatted group results when groups are found', async () => {
+ const responseData = {
+ ListGroupsResponse: {
+ ListGroupsResult: {
+ Groups: [{ GroupName: 'Group1' }, { GroupName: 'Group2' }],
+ },
+ },
+ };
+ (awsApiRequest as jest.Mock).mockResolvedValue(responseData);
+
+ const result = await searchGroups.call(mockContext);
+ expect(result.results).toEqual([
+ { name: 'Group1', value: 'Group1' },
+ { name: 'Group2', value: 'Group2' },
+ ]);
+ });
+
+ it('should apply filter to the group results', async () => {
+ const responseData = {
+ ListGroupsResponse: {
+ ListGroupsResult: {
+ Groups: [{ GroupName: 'Group1' }, { GroupName: 'Group2' }],
+ },
+ },
+ };
+ (awsApiRequest as jest.Mock).mockResolvedValue(responseData);
+
+ const result = await searchGroups.call(mockContext, 'Group1');
+ expect(result.results).toEqual([{ name: 'Group1', value: 'Group1' }]);
+ });
+ });
+
+ describe('searchGroupsForUser', () => {
+ it('should return empty if no user groups are found', async () => {
+ mockContext.getNodeParameter = jest.fn().mockReturnValue('user1');
+
+ const responseData = {
+ ListGroupsResponse: { ListGroupsResult: { Groups: [] } },
+ };
+ (awsApiRequest as jest.Mock).mockResolvedValue(responseData);
+
+ const result = await searchGroupsForUser.call(mockContext);
+
+ expect(result.results).toEqual([]);
+ });
+ });
+});
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/user/addToGroup.test.ts b/packages/nodes-base/nodes/Aws/IAM/test/user/addToGroup.test.ts
new file mode 100644
index 0000000000..18af309772
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/user/addToGroup.test.ts
@@ -0,0 +1,38 @@
+import { NodeTestHarness } from '@nodes-testing/node-test-harness';
+import nock from 'nock';
+
+import { CURRENT_VERSION } from '../../helpers/constants';
+
+describe('AWS IAM - Add User to Group', () => {
+ beforeEach(() => {
+ const baseUrl = 'https://iam.amazonaws.com/';
+ nock.cleanAll();
+ nock(baseUrl)
+ .persist()
+ .defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
+ .post('/', {
+ Action: 'AddUserToGroup',
+ Version: CURRENT_VERSION,
+ UserName: 'Jonas',
+ GroupName: 'GroupNameUpdated2',
+ })
+ .reply(200, {
+ AddUserToGroupResponse: {
+ ResponseMetadata: {
+ RequestId: '8192250c-9225-4903-af62-a521ce939968',
+ },
+ },
+ });
+ });
+
+ new NodeTestHarness().setupTests({
+ workflowFiles: ['addToGroup.workflow.json'],
+ credentials: {
+ aws: {
+ region: 'eu-central-1',
+ accessKeyId: 'test',
+ secretAccessKey: 'test',
+ },
+ },
+ });
+});
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/user/addToGroup.workflow.json b/packages/nodes-base/nodes/Aws/IAM/test/user/addToGroup.workflow.json
new file mode 100644
index 0000000000..a0955f4e3f
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/user/addToGroup.workflow.json
@@ -0,0 +1,78 @@
+{
+ "nodes": [
+ {
+ "parameters": {},
+ "type": "n8n-nodes-base.manualTrigger",
+ "typeVersion": 1,
+ "position": [-40, -120],
+ "id": "7da2ce49-9a9d-4240-b082-ff1b12d101b1",
+ "name": "When clicking ‘Test workflow’"
+ },
+ {
+ "parameters": {
+ "operation": "addToGroup",
+ "user": {
+ "__rl": true,
+ "value": "Jonas",
+ "mode": "list",
+ "cachedResultName": "Jonas"
+ },
+ "group": {
+ "__rl": true,
+ "value": "GroupNameUpdated2",
+ "mode": "list",
+ "cachedResultName": "GroupNameUpdated2"
+ },
+ "requestOptions": {}
+ },
+ "type": "n8n-nodes-base.awsIam",
+ "typeVersion": 1,
+ "position": [160, -120],
+ "id": "982d8c6e-e94a-414c-90ba-35743676eef8",
+ "name": "addToGroup",
+ "credentials": {
+ "aws": {
+ "id": "exampleId",
+ "name": "AWS US EAST"
+ }
+ }
+ }
+ ],
+ "pinData": {
+ "addToGroup": [
+ {
+ "json": {
+ "AddUserToGroupResponse": {
+ "ResponseMetadata": {
+ "RequestId": "8192250c-9225-4903-af62-a521ce939968"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "connections": {
+ "When clicking ‘Test workflow’": {
+ "main": [
+ [
+ {
+ "node": "addToGroup",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ },
+ "active": false,
+ "settings": {
+ "executionOrder": "v1"
+ },
+ "versionId": "2e9e5c7a-705f-49a7-83db-397f1bb48798",
+ "meta": {
+ "templateCredsSetupCompleted": true,
+ "instanceId": "26f38eb23ad84214537831cfef4032299ffac994ff18d5cb72e82d31ac4ceac4"
+ },
+ "id": "0qqycx08fTBpfbC9",
+ "tags": []
+}
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/user/create.test.ts b/packages/nodes-base/nodes/Aws/IAM/test/user/create.test.ts
new file mode 100644
index 0000000000..333d176002
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/user/create.test.ts
@@ -0,0 +1,47 @@
+import { NodeTestHarness } from '@nodes-testing/node-test-harness';
+import nock from 'nock';
+
+import { BASE_URL, CURRENT_VERSION } from '../../helpers/constants';
+
+describe('AWS IAM - Create user', () => {
+ beforeEach(() => {
+ nock.cleanAll();
+ nock(BASE_URL)
+ .persist()
+ .defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
+ .post('/', {
+ Action: 'CreateUser',
+ Version: CURRENT_VERSION,
+ UserName: 'UserTest1',
+ })
+ .reply(200, {
+ CreateUserResponse: {
+ CreateUserResult: {
+ User: {
+ Arn: 'arn:aws:iam::130450532146:user/UserTest1',
+ CreateDate: 1744115235,
+ PasswordLastUsed: null,
+ Path: '/',
+ PermissionsBoundary: null,
+ UserId: 'AIDAR4X3VE4ZHHMNF7NBB',
+ UserName: 'UserTest1',
+ },
+ },
+ ResponseMetadata: {
+ RequestId: 'ce14481c-5629-4ae4-9eae-3722f48bb3e0',
+ },
+ },
+ });
+ });
+
+ new NodeTestHarness().setupTests({
+ workflowFiles: ['create.workflow.json'],
+ credentials: {
+ aws: {
+ region: 'eu-central-1',
+ accessKeyId: 'test',
+ secretAccessKey: 'test',
+ },
+ },
+ });
+});
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/user/create.workflow.json b/packages/nodes-base/nodes/Aws/IAM/test/user/create.workflow.json
new file mode 100644
index 0000000000..fbb7f5ece0
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/user/create.workflow.json
@@ -0,0 +1,70 @@
+{
+ "nodes": [
+ {
+ "parameters": {},
+ "type": "n8n-nodes-base.manualTrigger",
+ "typeVersion": 1,
+ "position": [200, 120],
+ "id": "b4205abf-7102-4e53-8aed-7bd047acfaf4",
+ "name": "When clicking ‘Test workflow’"
+ },
+ {
+ "parameters": {
+ "operation": "create",
+ "userName": "UserTest1",
+ "additionalFields": {
+ "path": "/new/path/"
+ },
+ "requestOptions": {}
+ },
+ "type": "n8n-nodes-base.awsIam",
+ "typeVersion": 1,
+ "position": [400, 120],
+ "id": "64bbfce7-cc7d-4e69-82d3-d4ecac5ad389",
+ "name": "AWS IAM12",
+ "credentials": {
+ "aws": {
+ "id": "exampleId",
+ "name": "AWS US EAST"
+ }
+ }
+ }
+ ],
+ "connections": {
+ "When clicking ‘Test workflow’": {
+ "main": [
+ [
+ {
+ "node": "AWS IAM12",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ },
+ "pinData": {
+ "AWS IAM12": [
+ {
+ "json": {
+ "CreateUserResponse": {
+ "CreateUserResult": {
+ "User": {
+ "Arn": "arn:aws:iam::130450532146:user/UserTest1",
+ "CreateDate": 1744115235,
+ "PasswordLastUsed": null,
+ "Path": "/",
+ "PermissionsBoundary": null,
+ "UserId": "AIDAR4X3VE4ZHHMNF7NBB",
+ "UserName": "UserTest1"
+ }
+ },
+ "ResponseMetadata": {
+ "RequestId": "ce14481c-5629-4ae4-9eae-3722f48bb3e0"
+ }
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/user/delete.test.ts b/packages/nodes-base/nodes/Aws/IAM/test/user/delete.test.ts
new file mode 100644
index 0000000000..963344150e
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/user/delete.test.ts
@@ -0,0 +1,96 @@
+import { NodeTestHarness } from '@nodes-testing/node-test-harness';
+import nock from 'nock';
+
+import { BASE_URL, CURRENT_VERSION } from '../../helpers/constants';
+
+describe('AWS IAM - Delete user', () => {
+ beforeEach(() => {
+ nock.cleanAll();
+ nock(BASE_URL)
+ .persist()
+ .defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
+ .post('/', {
+ Action: 'GetUser',
+ Version: CURRENT_VERSION,
+ UserName: 'JohnThis10',
+ })
+ .reply(200, {
+ GetUserResponse: {
+ GetUserResult: {
+ User: {
+ UserName: 'JohnThis10',
+ },
+ },
+ },
+ });
+ nock(BASE_URL)
+ .persist()
+ .defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
+ .post('/', {
+ Action: 'ListGroups',
+ Version: CURRENT_VERSION,
+ })
+ .reply(200, {
+ ListGroupsResponse: {
+ ListGroupsResult: {
+ Groups: [{ GroupName: 'GroupA' }],
+ },
+ },
+ });
+ nock(BASE_URL)
+ .persist()
+ .defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
+ .post('/', {
+ Action: 'GetGroup',
+ Version: CURRENT_VERSION,
+ GroupName: 'GroupA',
+ })
+ .reply(200, {
+ GetGroupResponse: {
+ GetGroupResult: {
+ Users: [{ UserName: 'JohnThis10' }],
+ },
+ },
+ });
+ nock(BASE_URL)
+ .persist()
+ .defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
+ .post('/', {
+ Action: 'RemoveUserFromGroup',
+ Version: CURRENT_VERSION,
+ GroupName: 'GroupA',
+ UserName: 'JohnThis10',
+ })
+ .reply(200, {
+ ResponseMetadata: {
+ RequestId: 'remove-groupA-id',
+ },
+ });
+ nock(BASE_URL)
+ .persist()
+ .defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
+ .post('/', {
+ Action: 'DeleteUser',
+ Version: CURRENT_VERSION,
+ UserName: 'JohnThis10',
+ })
+ .reply(200, {
+ DeleteUserResponse: {
+ ResponseMetadata: {
+ RequestId: '44c7c6c0-260b-4dfd-beee-2cce8f05bed3',
+ },
+ },
+ });
+ });
+
+ new NodeTestHarness().setupTests({
+ workflowFiles: ['delete.workflow.json'],
+ credentials: {
+ aws: {
+ region: 'eu-central-1',
+ accessKeyId: 'test',
+ secretAccessKey: 'test',
+ },
+ },
+ });
+});
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/user/delete.workflow.json b/packages/nodes-base/nodes/Aws/IAM/test/user/delete.workflow.json
new file mode 100644
index 0000000000..a1a66d9d84
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/user/delete.workflow.json
@@ -0,0 +1,72 @@
+{
+ "nodes": [
+ {
+ "parameters": {},
+ "type": "n8n-nodes-base.manualTrigger",
+ "typeVersion": 1,
+ "position": [-80, -100],
+ "id": "7da2ce49-9a9d-4240-b082-ff1b12d101b1",
+ "name": "When clicking ‘Test workflow’"
+ },
+ {
+ "parameters": {
+ "operation": "delete",
+ "user": {
+ "__rl": true,
+ "value": "JohnThis10",
+ "mode": "list",
+ "cachedResultName": "JohnThis10"
+ },
+ "requestOptions": {}
+ },
+ "type": "n8n-nodes-base.awsIam",
+ "typeVersion": 1,
+ "position": [180, -100],
+ "id": "f8bf0d9d-78aa-48e6-80bd-3018c0bac0ea",
+ "name": "deleteUser",
+ "credentials": {
+ "aws": {
+ "id": "exampleId",
+ "name": "AWS US EAST"
+ }
+ }
+ }
+ ],
+ "pinData": {
+ "deleteUser": [
+ {
+ "json": {
+ "DeleteUserResponse": {
+ "ResponseMetadata": {
+ "RequestId": "44c7c6c0-260b-4dfd-beee-2cce8f05bed3"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "connections": {
+ "When clicking ‘Test workflow’": {
+ "main": [
+ [
+ {
+ "node": "deleteUser",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ },
+ "active": false,
+ "settings": {
+ "executionOrder": "v1"
+ },
+ "versionId": "731b81df-c6a0-4074-a3f8-e0ad0e9c884a",
+ "meta": {
+ "templateCredsSetupCompleted": true,
+ "instanceId": "26f38eb23ad84214537831cfef4032299ffac994ff18d5cb72e82d31ac4ceac4"
+ },
+ "id": "0qqycx08fTBpfbC9",
+ "tags": []
+}
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/user/get.test.ts b/packages/nodes-base/nodes/Aws/IAM/test/user/get.test.ts
new file mode 100644
index 0000000000..338e0327f1
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/user/get.test.ts
@@ -0,0 +1,49 @@
+import { NodeTestHarness } from '@nodes-testing/node-test-harness';
+import nock from 'nock';
+
+import { BASE_URL, CURRENT_VERSION } from '../../helpers/constants';
+
+describe('AWS IAM - Get User', () => {
+ beforeEach(() => {
+ nock(BASE_URL)
+ .persist()
+ .defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
+ .post('/', {
+ Action: 'GetUser',
+ Version: CURRENT_VERSION,
+ UserName: 'accounts@this.de',
+ })
+ .reply(200, {
+ GetUserResponse: {
+ GetUserResult: {
+ User: {
+ UserName: 'accounts@this.de',
+ UserId: 'AIDAR4X3VE4ZANWXRN2L2',
+ Arn: 'arn:aws:iam::130450532146:user/accounts@this.de',
+ CreateDate: 1733911052,
+ Path: '/',
+ Tags: [
+ {
+ Key: 'AKIAR4X3VE4ZALQYFEMT',
+ Value: 'API dev',
+ },
+ ],
+ PasswordLastUsed: null,
+ PermissionsBoundary: null,
+ },
+ },
+ },
+ });
+ });
+
+ new NodeTestHarness().setupTests({
+ workflowFiles: ['get.workflow.json'],
+ credentials: {
+ aws: {
+ region: 'eu-central-1',
+ accessKeyId: 'test',
+ secretAccessKey: 'test',
+ },
+ },
+ });
+});
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/user/get.workflow.json b/packages/nodes-base/nodes/Aws/IAM/test/user/get.workflow.json
new file mode 100644
index 0000000000..774885c2c1
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/user/get.workflow.json
@@ -0,0 +1,79 @@
+{
+ "nodes": [
+ {
+ "parameters": {},
+ "type": "n8n-nodes-base.manualTrigger",
+ "typeVersion": 1,
+ "position": [-40, -120],
+ "id": "7da2ce49-9a9d-4240-b082-ff1b12d101b1",
+ "name": "When clicking ‘Test workflow’"
+ },
+ {
+ "parameters": {
+ "operation": "get",
+ "user": {
+ "__rl": true,
+ "mode": "list",
+ "value": "accounts@this.de"
+ },
+ "requestOptions": {}
+ },
+ "type": "n8n-nodes-base.awsIam",
+ "typeVersion": 1,
+ "position": [140, -100],
+ "id": "982d8c6e-e94a-414c-90ba-35743676eef8",
+ "name": "getUser",
+ "credentials": {
+ "aws": {
+ "id": "exampleId",
+ "name": "AWS US EAST"
+ }
+ }
+ }
+ ],
+ "pinData": {
+ "getUser": [
+ {
+ "json": {
+ "Arn": "arn:aws:iam::130450532146:user/accounts@this.de",
+ "CreateDate": 1733911052,
+ "PasswordLastUsed": null,
+ "Path": "/",
+ "PermissionsBoundary": null,
+ "Tags": [
+ {
+ "Key": "AKIAR4X3VE4ZALQYFEMT",
+ "Value": "API dev"
+ }
+ ],
+ "UserId": "AIDAR4X3VE4ZANWXRN2L2",
+ "UserName": "accounts@this.de"
+ }
+ }
+ ]
+ },
+ "connections": {
+ "When clicking ‘Test workflow’": {
+ "main": [
+ [
+ {
+ "node": "getUser",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ },
+ "active": false,
+ "settings": {
+ "executionOrder": "v1"
+ },
+ "versionId": "5ce54c9d-de6e-499a-b042-0bb89c41208c",
+ "meta": {
+ "templateCredsSetupCompleted": true,
+ "instanceId": "26f38eb23ad84214537831cfef4032299ffac994ff18d5cb72e82d31ac4ceac4"
+ },
+ "id": "0qqycx08fTBpfbC9",
+ "tags": []
+}
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/user/getAll.test.ts b/packages/nodes-base/nodes/Aws/IAM/test/user/getAll.test.ts
new file mode 100644
index 0000000000..95dcd7937c
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/user/getAll.test.ts
@@ -0,0 +1,57 @@
+import { NodeTestHarness } from '@nodes-testing/node-test-harness';
+import nock from 'nock';
+
+import { BASE_URL, CURRENT_VERSION } from '../../helpers/constants';
+
+describe('AWS IAM - Get All Users', () => {
+ beforeEach(() => {
+ nock.cleanAll();
+ nock(BASE_URL)
+ .persist()
+ .defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
+ .post('/', {
+ Action: 'ListUsers',
+ Version: CURRENT_VERSION,
+ MaxItems: 100,
+ })
+ .reply(200, {
+ ListUsersResponse: {
+ ListUsersResult: {
+ Users: [
+ {
+ Arn: 'arn:aws:iam::130450532146:user/Johnnn',
+ UserName: 'Johnnn',
+ UserId: 'AIDAR4X3VE4ZAJGXLCVOP',
+ Path: '/',
+ CreateDate: 1739198010,
+ PasswordLastUsed: null,
+ PermissionsBoundary: null,
+ Tags: null,
+ },
+ {
+ Arn: 'arn:aws:iam::130450532146:user/rhis/path/Jonas',
+ UserName: 'Jonas',
+ UserId: 'AIDAR4X3VE4ZDJJFKI6OU',
+ Path: '/rhis/path/',
+ CreateDate: 1739198295,
+ PasswordLastUsed: null,
+ PermissionsBoundary: null,
+ Tags: null,
+ },
+ ],
+ },
+ },
+ });
+ });
+
+ new NodeTestHarness().setupTests({
+ workflowFiles: ['getAll.workflow.json'],
+ credentials: {
+ aws: {
+ region: 'eu-central-1',
+ accessKeyId: 'test',
+ secretAccessKey: 'test',
+ },
+ },
+ });
+});
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/user/getAll.workflow.json b/packages/nodes-base/nodes/Aws/IAM/test/user/getAll.workflow.json
new file mode 100644
index 0000000000..a5474e4a6c
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/user/getAll.workflow.json
@@ -0,0 +1,81 @@
+{
+ "nodes": [
+ {
+ "parameters": {},
+ "type": "n8n-nodes-base.manualTrigger",
+ "typeVersion": 1,
+ "position": [-40, -220],
+ "id": "7da2ce49-9a9d-4240-b082-ff1b12d101b1",
+ "name": "When clicking ‘Test workflow’"
+ },
+ {
+ "parameters": {
+ "additionalFields": {},
+ "requestOptions": {}
+ },
+ "type": "n8n-nodes-base.awsIam",
+ "typeVersion": 1,
+ "position": [160, -220],
+ "id": "23fd4c79-516d-49dc-81c8-e68052a294d1",
+ "name": "getAllUsers",
+ "credentials": {
+ "aws": {
+ "id": "exampleId",
+ "name": "AWS US EAST"
+ }
+ }
+ }
+ ],
+ "pinData": {
+ "getAllUsers": [
+ {
+ "json": {
+ "Arn": "arn:aws:iam::130450532146:user/Johnnn",
+ "CreateDate": 1739198010,
+ "PasswordLastUsed": null,
+ "Path": "/",
+ "PermissionsBoundary": null,
+ "Tags": null,
+ "UserId": "AIDAR4X3VE4ZAJGXLCVOP",
+ "UserName": "Johnnn"
+ }
+ },
+ {
+ "json": {
+ "Arn": "arn:aws:iam::130450532146:user/rhis/path/Jonas",
+ "CreateDate": 1739198295,
+ "PasswordLastUsed": null,
+ "Path": "/rhis/path/",
+ "PermissionsBoundary": null,
+ "Tags": null,
+ "UserId": "AIDAR4X3VE4ZDJJFKI6OU",
+ "UserName": "Jonas"
+ }
+ }
+ ]
+ },
+ "connections": {
+ "When clicking ‘Test workflow’": {
+ "main": [
+ [
+ {
+ "node": "getAllUsers",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ },
+ "active": false,
+ "settings": {
+ "executionOrder": "v1"
+ },
+ "versionId": "c23e7348-6add-4711-b567-53922f951cbe",
+ "meta": {
+ "templateCredsSetupCompleted": true,
+ "instanceId": "26f38eb23ad84214537831cfef4032299ffac994ff18d5cb72e82d31ac4ceac4"
+ },
+ "id": "0qqycx08fTBpfbC9",
+ "tags": []
+}
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/user/removeFromGroup.test.ts b/packages/nodes-base/nodes/Aws/IAM/test/user/removeFromGroup.test.ts
new file mode 100644
index 0000000000..8c9ec02e17
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/user/removeFromGroup.test.ts
@@ -0,0 +1,37 @@
+import { NodeTestHarness } from '@nodes-testing/node-test-harness';
+import nock from 'nock';
+
+import { BASE_URL, CURRENT_VERSION } from '../../helpers/constants';
+
+describe('AWS IAM - Remove User From Group', () => {
+ beforeEach(() => {
+ nock.cleanAll();
+ nock(BASE_URL)
+ .persist()
+ .defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
+ .post('/', {
+ Action: 'RemoveUserFromGroup',
+ Version: CURRENT_VERSION,
+ UserName: 'UserTest1',
+ GroupName: 'GroupCreatedAfter',
+ })
+ .reply(200, {
+ RemoveUserFromGroupResponse: {
+ ResponseMetadata: {
+ RequestId: '48508b51-1506-496c-8455-7135269209f0',
+ },
+ },
+ });
+ });
+
+ new NodeTestHarness().setupTests({
+ workflowFiles: ['removeFromGroup.workflow.json'],
+ credentials: {
+ aws: {
+ region: 'eu-central-1',
+ accessKeyId: 'test',
+ secretAccessKey: 'test',
+ },
+ },
+ });
+});
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/user/removeFromGroup.workflow.json b/packages/nodes-base/nodes/Aws/IAM/test/user/removeFromGroup.workflow.json
new file mode 100644
index 0000000000..4ee3d12299
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/user/removeFromGroup.workflow.json
@@ -0,0 +1,78 @@
+{
+ "nodes": [
+ {
+ "parameters": {},
+ "type": "n8n-nodes-base.manualTrigger",
+ "typeVersion": 1,
+ "position": [-40, -120],
+ "id": "7da2ce49-9a9d-4240-b082-ff1b12d101b1",
+ "name": "When clicking ‘Test workflow’"
+ },
+ {
+ "parameters": {
+ "operation": "removeFromGroup",
+ "user": {
+ "__rl": true,
+ "value": "UserTest1",
+ "mode": "list",
+ "cachedResultName": "UserTest1"
+ },
+ "group": {
+ "__rl": true,
+ "value": "GroupCreatedAfter",
+ "mode": "list",
+ "cachedResultName": "GroupCreatedAfter"
+ },
+ "requestOptions": {}
+ },
+ "type": "n8n-nodes-base.awsIam",
+ "typeVersion": 1,
+ "position": [140, -120],
+ "id": "5f05f1ad-aaae-49c1-b03d-c77c1442a0c9",
+ "name": "removeFromGroup",
+ "credentials": {
+ "aws": {
+ "id": "exampleId",
+ "name": "AWS US EAST"
+ }
+ }
+ }
+ ],
+ "pinData": {
+ "removeFromGroup": [
+ {
+ "json": {
+ "RemoveUserFromGroupResponse": {
+ "ResponseMetadata": {
+ "RequestId": "48508b51-1506-496c-8455-7135269209f0"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "connections": {
+ "When clicking ‘Test workflow’": {
+ "main": [
+ [
+ {
+ "node": "removeFromGroup",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ },
+ "active": false,
+ "settings": {
+ "executionOrder": "v1"
+ },
+ "versionId": "9cb635bc-9e0f-4903-8705-c0ec4495c39f",
+ "meta": {
+ "templateCredsSetupCompleted": true,
+ "instanceId": "26f38eb23ad84214537831cfef4032299ffac994ff18d5cb72e82d31ac4ceac4"
+ },
+ "id": "0qqycx08fTBpfbC9",
+ "tags": []
+}
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/user/update.test.ts b/packages/nodes-base/nodes/Aws/IAM/test/user/update.test.ts
new file mode 100644
index 0000000000..d35ab7ba12
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/user/update.test.ts
@@ -0,0 +1,37 @@
+import { NodeTestHarness } from '@nodes-testing/node-test-harness';
+import nock from 'nock';
+
+import { BASE_URL, CURRENT_VERSION } from '../../helpers/constants';
+
+describe('AWS IAM - Update User', () => {
+ beforeEach(() => {
+ nock.cleanAll();
+ nock(BASE_URL)
+ .persist()
+ .defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
+ .post('/', {
+ Action: 'UpdateUser',
+ Version: CURRENT_VERSION,
+ UserName: 'NewUser',
+ NewUserName: 'UserTest',
+ })
+ .reply(200, {
+ UpdateUserResponse: {
+ ResponseMetadata: {
+ RequestId: 'bdb4a8b5-627a-41a7-aba9-5733b7869c16',
+ },
+ },
+ });
+ });
+
+ new NodeTestHarness().setupTests({
+ workflowFiles: ['update.workflow.json'],
+ credentials: {
+ aws: {
+ region: 'eu-central-1',
+ accessKeyId: 'test',
+ secretAccessKey: 'test',
+ },
+ },
+ });
+});
diff --git a/packages/nodes-base/nodes/Aws/IAM/test/user/update.workflow.json b/packages/nodes-base/nodes/Aws/IAM/test/user/update.workflow.json
new file mode 100644
index 0000000000..9cfe70391f
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/test/user/update.workflow.json
@@ -0,0 +1,62 @@
+{
+ "nodes": [
+ {
+ "parameters": {},
+ "type": "n8n-nodes-base.manualTrigger",
+ "typeVersion": 1,
+ "position": [200, 120],
+ "id": "b4205abf-7102-4e53-8aed-7bd047acfaf4",
+ "name": "When clicking ‘Test workflow’"
+ },
+ {
+ "parameters": {
+ "operation": "update",
+ "user": {
+ "__rl": true,
+ "value": "NewUser",
+ "mode": "list",
+ "cachedResultName": "NewUser"
+ },
+ "userName": "UserTest",
+ "requestOptions": {}
+ },
+ "type": "n8n-nodes-base.awsIam",
+ "typeVersion": 1,
+ "position": [400, 120],
+ "id": "64bbfce7-cc7d-4e69-82d3-d4ecac5ad389",
+ "name": "AWS IAM12",
+ "credentials": {
+ "aws": {
+ "id": "exampleId",
+ "name": "AWS US EAST"
+ }
+ }
+ }
+ ],
+ "connections": {
+ "When clicking ‘Test workflow’": {
+ "main": [
+ [
+ {
+ "node": "AWS IAM12",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ },
+ "pinData": {
+ "AWS IAM12": [
+ {
+ "json": {
+ "UpdateUserResponse": {
+ "ResponseMetadata": {
+ "RequestId": "bdb4a8b5-627a-41a7-aba9-5733b7869c16"
+ }
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/packages/nodes-base/nodes/Aws/IAM/transport/index.ts b/packages/nodes-base/nodes/Aws/IAM/transport/index.ts
new file mode 100644
index 0000000000..1bb898487b
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/IAM/transport/index.ts
@@ -0,0 +1,54 @@
+import type {
+ IExecuteSingleFunctions,
+ IDataObject,
+ IHttpRequestOptions,
+ ILoadOptionsFunctions,
+ IPollFunctions,
+ JsonObject,
+} from 'n8n-workflow';
+import { NodeApiError } from 'n8n-workflow';
+
+import { BASE_URL } from '../helpers/constants';
+
+const errorMapping: IDataObject = {
+ 403: 'The AWS credentials are not valid!',
+};
+
+export async function awsApiRequest(
+ this: ILoadOptionsFunctions | IPollFunctions | IExecuteSingleFunctions,
+ opts: IHttpRequestOptions,
+): Promise {
+ const requestOptions: IHttpRequestOptions = {
+ baseURL: BASE_URL,
+ json: true,
+ ...opts,
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ ...(opts.headers ?? {}),
+ },
+ };
+
+ if (opts.body) {
+ requestOptions.body = new URLSearchParams(opts.body as Record).toString();
+ }
+
+ try {
+ const response = (await this.helpers.requestWithAuthentication.call(
+ this,
+ 'aws',
+ requestOptions,
+ )) as IDataObject;
+
+ return response;
+ } catch (error) {
+ const statusCode = (error?.statusCode || error?.cause?.statusCode) as string;
+
+ if (statusCode && errorMapping[statusCode]) {
+ throw new NodeApiError(this.getNode(), {
+ message: `AWS error response [${statusCode}]: ${errorMapping[statusCode] as string}`,
+ });
+ } else {
+ throw new NodeApiError(this.getNode(), error as JsonObject);
+ }
+ }
+}
diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json
index e7c85fbe87..c6a3c6d795 100644
--- a/packages/nodes-base/package.json
+++ b/packages/nodes-base/package.json
@@ -435,6 +435,7 @@
"dist/nodes/Aws/Comprehend/AwsComprehend.node.js",
"dist/nodes/Aws/DynamoDB/AwsDynamoDB.node.js",
"dist/nodes/Aws/ELB/AwsElb.node.js",
+ "dist/nodes/Aws/IAM/AwsIam.node.js",
"dist/nodes/Aws/Rekognition/AwsRekognition.node.js",
"dist/nodes/Aws/S3/AwsS3.node.js",
"dist/nodes/Aws/SES/AwsSes.node.js",