diff --git a/packages/nodes-base/credentials/Ldap.credentials.ts b/packages/nodes-base/credentials/Ldap.credentials.ts new file mode 100644 index 000000000..b3c2550ee --- /dev/null +++ b/packages/nodes-base/credentials/Ldap.credentials.ts @@ -0,0 +1,91 @@ +/* eslint-disable n8n-nodes-base/cred-class-name-unsuffixed,n8n-nodes-base/cred-class-field-name-unsuffixed,n8n-nodes-base/cred-class-field-display-name-missing-api */ +import { ICredentialType, INodeProperties } from 'n8n-workflow'; + +export class Ldap implements ICredentialType { + name = 'ldap'; + + displayName = 'LDAP'; + + properties: INodeProperties[] = [ + { + displayName: 'LDAP Server Address', + name: 'hostname', + type: 'string', + default: '', + required: true, + description: 'IP or domain of the LDAP server', + }, + { + displayName: 'LDAP Server Port', + name: 'port', + type: 'string', + default: '389', + description: 'Port used to connect to the LDAP server', + }, + { + displayName: 'Binding DN', + name: 'bindDN', + type: 'string', + default: '', + description: 'Distinguished Name of the user to connect as', + required: true, + }, + { + displayName: 'Binding Password', + name: 'bindPassword', + type: 'string', + typeOptions: { + password: true, + }, + default: '', + description: 'Password of the user provided in the Binding DN field above', + required: true, + }, + { + displayName: 'Connection Security', + name: 'connectionSecurity', + type: 'options', + default: 'none', + options: [ + { + name: 'None', + value: 'none', + }, + { + name: 'TLS', + value: 'tls', + }, + { + name: 'STARTTLS', + value: 'startTls', + }, + ], + }, + { + displayName: 'Ignore SSL/TLS Issues', + name: 'allowUnauthorizedCerts', + type: 'boolean', + description: 'Whether to connect even if SSL/TLS certificate validation is not possible', + default: false, + displayOptions: { + hide: { + connectionSecurity: ['none'], + }, + }, + }, + { + displayName: 'CA Certificate', + name: 'caCertificate', + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + hide: { + connectionSecurity: ['none'], + }, + }, + type: 'string', + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Ldap/Helpers.ts b/packages/nodes-base/nodes/Ldap/Helpers.ts new file mode 100644 index 000000000..b0bf69895 --- /dev/null +++ b/packages/nodes-base/nodes/Ldap/Helpers.ts @@ -0,0 +1,53 @@ +import { Client } from 'ldapts'; +import type { ClientOptions, Entry } from 'ldapts'; +import type { ICredentialDataDecryptedObject, IDataObject } from 'n8n-workflow'; +import { LoggerProxy as Logger } from 'n8n-workflow'; +export const BINARY_AD_ATTRIBUTES = ['objectGUID', 'objectSid']; + +const resolveEntryBinaryAttributes = (entry: Entry): Entry => { + Object.entries(entry) + .filter(([k]) => BINARY_AD_ATTRIBUTES.includes(k)) + .forEach(([k]) => { + entry[k] = (entry[k] as Buffer).toString('hex'); + }); + return entry; +}; + +export const resolveBinaryAttributes = (entries: Entry[]): void => { + entries.forEach((entry) => resolveEntryBinaryAttributes(entry)); +}; + +export async function createLdapClient( + credentials: ICredentialDataDecryptedObject, + nodeDebug?: boolean, + nodeType?: string, + nodeName?: string, +): Promise { + const protocol = credentials.connectionSecurity === 'tls' ? 'ldaps' : 'ldap'; + const url = `${protocol}://${credentials.hostname}:${credentials.port}`; + + const ldapOptions: ClientOptions = { url }; + const tlsOptions: IDataObject = {}; + + if (credentials.connectionSecurity !== 'none') { + tlsOptions.rejectUnauthorized = credentials.allowUnauthorizedCerts === false; + if (credentials.caCertificate) { + tlsOptions.ca = [credentials.caCertificate as string]; + } + if (credentials.connectionSecurity !== 'startTls') { + ldapOptions.tlsOptions = tlsOptions; + } + } + + if (nodeDebug) { + Logger.info( + `[${nodeType} | ${nodeName}] - LDAP Options: ${JSON.stringify(ldapOptions, null, 2)}`, + ); + } + + const client = new Client(ldapOptions); + if (credentials.connectionSecurity === 'startTls') { + await client.startTLS(tlsOptions); + } + return client; +} diff --git a/packages/nodes-base/nodes/Ldap/Ldap.node.json b/packages/nodes-base/nodes/Ldap/Ldap.node.json new file mode 100644 index 000000000..7975f2493 --- /dev/null +++ b/packages/nodes-base/nodes/Ldap/Ldap.node.json @@ -0,0 +1,19 @@ +{ + "node": "n8n-nodes-base.ldap", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Development", "Developer Tools"], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/ldap" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.ldap/" + } + ] + }, + "alias": ["ad", "active directory"] +} diff --git a/packages/nodes-base/nodes/Ldap/Ldap.node.ts b/packages/nodes-base/nodes/Ldap/Ldap.node.ts new file mode 100644 index 000000000..e31c526b3 --- /dev/null +++ b/packages/nodes-base/nodes/Ldap/Ldap.node.ts @@ -0,0 +1,405 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { IExecuteFunctions } from 'n8n-core'; +import type { + ICredentialDataDecryptedObject, + ICredentialsDecrypted, + ICredentialTestFunctions, + IDataObject, + ILoadOptionsFunctions, + INodeCredentialTestResult, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; +import { LoggerProxy as Logger, NodeOperationError } from 'n8n-workflow'; + +import { Attribute, Change } from 'ldapts'; +import { ldapFields } from './LdapDescription'; +import { BINARY_AD_ATTRIBUTES, createLdapClient, resolveBinaryAttributes } from './Helpers'; + +export class Ldap implements INodeType { + description: INodeTypeDescription = { + displayName: 'Ldap', + name: 'ldap', + icon: 'file:ldap.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Interact with LDAP servers', + defaults: { + name: 'LDAP', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + // eslint-disable-next-line n8n-nodes-base/node-class-description-credentials-name-unsuffixed + name: 'ldap', + required: true, + testedBy: 'ldapConnectionTest', + }, + ], + properties: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Compare', + value: 'compare', + description: 'Compare an attribute', + action: 'Compare an attribute', + }, + { + name: 'Create', + value: 'create', + description: 'Create a new entry', + action: 'Create a new entry', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an entry', + action: 'Delete an entry', + }, + { + name: 'Rename', + value: 'rename', + description: 'Rename the DN of an existing entry', + action: 'Rename the DN of an existing entry', + }, + { + name: 'Search', + value: 'search', + description: 'Search LDAP', + action: 'Search LDAP', + }, + { + name: 'Update', + value: 'update', + description: 'Update attributes', + action: 'Update attributes', + }, + ], + default: 'search', + }, + { + displayName: 'Debug', + name: 'nodeDebug', + type: 'boolean', + isNodeSetting: true, + default: false, + noDataExpression: true, + }, + ...ldapFields, + ], + }; + + methods = { + credentialTest: { + async ldapConnectionTest( + this: ICredentialTestFunctions, + credential: ICredentialsDecrypted, + ): Promise { + const credentials = credential.data as ICredentialDataDecryptedObject; + try { + const client = await createLdapClient(credentials); + await client.bind(credentials.bindDN as string, credentials.bindPassword as string); + } catch (error) { + return { + status: 'Error', + message: error.message, + }; + } + return { + status: 'OK', + message: 'Connection successful!', + }; + }, + }, + loadOptions: { + async getAttributes(this: ILoadOptionsFunctions) { + const credentials = await this.getCredentials('ldap'); + const client = await createLdapClient(credentials); + + try { + await client.bind(credentials.bindDN as string, credentials.bindPassword as string); + } catch (error) { + console.log(error); + } + + const baseDN = this.getNodeParameter('baseDN', 0) as string; + const results = await client.search(baseDN, { sizeLimit: 200, paged: false }); // should this size limit be set in credentials? + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const unique = Object.keys(Object.assign({}, ...results.searchEntries)); + return unique.map((x) => ({ + name: x, + value: x, + })); + }, + + async getObjectClasses(this: ILoadOptionsFunctions) { + const credentials = await this.getCredentials('ldap'); + const client = await createLdapClient(credentials); + try { + await client.bind(credentials.bindDN as string, credentials.bindPassword as string); + } catch (error) { + console.log(error); + } + + const baseDN = this.getNodeParameter('baseDN', 0) as string; + const results = await client.search(baseDN, { sizeLimit: 10, paged: false }); // should this size limit be set in credentials? + const objects = []; + for (const entry of results.searchEntries) { + if (typeof entry.objectClass === 'string') { + objects.push(entry.objectClass); + } else { + objects.push(...entry.objectClass); + } + } + + const unique = [...new Set(objects)]; + unique.push('custom'); + const result = []; + for (const value of unique) { + if (value === 'custom') { + result.push({ name: 'custom', value: 'custom' }); + } else result.push({ name: value as string, value: `(objectclass=${value})` }); + } + return result; + }, + + async getAttributesForDn(this: ILoadOptionsFunctions) { + const credentials = await this.getCredentials('ldap'); + const client = await createLdapClient(credentials); + + try { + await client.bind(credentials.bindDN as string, credentials.bindPassword as string); + } catch (error) { + console.log(error); + } + + const baseDN = this.getNodeParameter('dn', 0) as string; + const results = await client.search(baseDN, { sizeLimit: 1, paged: false }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const unique = Object.keys(Object.assign({}, ...results.searchEntries)); + return unique.map((x) => ({ + name: x, + value: x, + })); + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const nodeDebug = this.getNodeParameter('nodeDebug', 0) as boolean; + + const items = this.getInputData(); + const returnItems: INodeExecutionData[] = []; + + if (nodeDebug) { + Logger.info( + `[${this.getNode().type} | ${this.getNode().name}] - Starting with ${ + items.length + } input items`, + ); + } + + const credentials = await this.getCredentials('ldap'); + const client = await createLdapClient( + credentials, + nodeDebug, + this.getNode().type, + this.getNode().name, + ); + + try { + await client.bind(credentials.bindDN as string, credentials.bindPassword as string); + } catch (error) { + delete error.cert; + if (this.continueOnFail()) { + return [ + items.map((x) => { + x.json.error = error.reason || 'LDAP connection error occurred'; + return x; + }), + ]; + } else { + throw new NodeOperationError(this.getNode(), error as Error, {}); + } + } + + const operation = this.getNodeParameter('operation', 0); + + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + try { + if (operation === 'compare') { + const dn = this.getNodeParameter('dn', itemIndex) as string; + const attributeId = this.getNodeParameter('id', itemIndex) as string; + const value = this.getNodeParameter('value', itemIndex, '') as string; + + const res = await client.compare(dn, attributeId, value); + + returnItems.push({ + json: { dn, attribute: attributeId, result: res }, + pairedItem: { item: itemIndex }, + }); + } else if (operation === 'create') { + const dn = this.getNodeParameter('dn', itemIndex) as string; + const attributeFields = this.getNodeParameter('attributes', itemIndex) as IDataObject; + + const attributes: IDataObject = {}; + + if (Object.keys(attributeFields).length) { + //@ts-ignore + attributeFields.attribute.map((attr) => { + attributes[attr.id as string] = attr.value; + }); + } + + await client.add(dn, attributes as unknown as Attribute[]); + + returnItems.push({ + json: { dn, result: 'success' }, + pairedItem: { item: itemIndex }, + }); + } else if (operation === 'delete') { + const dn = this.getNodeParameter('dn', itemIndex) as string; + + await client.del(dn); + + returnItems.push({ + json: { dn, result: 'success' }, + pairedItem: { item: itemIndex }, + }); + } else if (operation === 'rename') { + const dn = this.getNodeParameter('dn', itemIndex) as string; + const targetDn = this.getNodeParameter('targetDn', itemIndex) as string; + + await client.modifyDN(dn, targetDn); + + returnItems.push({ + json: { dn: targetDn, result: 'success' }, + pairedItem: { item: itemIndex }, + }); + } else if (operation === 'update') { + const dn = this.getNodeParameter('dn', itemIndex) as string; + const attributes = this.getNodeParameter('attributes', itemIndex, {}) as IDataObject; + const changes: Change[] = []; + + for (const [action, attrs] of Object.entries(attributes)) { + //@ts-ignore + attrs.map((attr) => + changes.push( + new Change({ + // @ts-ignore + operation: action, + modification: new Attribute({ + type: attr.id as string, + values: [attr.value], + }), + }), + ), + ); + } + + await client.modify(dn, changes); + + returnItems.push({ + json: { dn, result: 'success', changes }, + pairedItem: { item: itemIndex }, + }); + } else if (operation === 'search') { + const baseDN = this.getNodeParameter('baseDN', itemIndex) as string; + let searchFor = this.getNodeParameter('searchFor', itemIndex) as string; + const returnAll = this.getNodeParameter('returnAll', itemIndex); + const limit = this.getNodeParameter('limit', itemIndex, 0); + const options = this.getNodeParameter('options', itemIndex); + const pageSize = this.getNodeParameter( + 'options.pageSize', + itemIndex, + 1000, + ) as IDataObject; + + // Set paging settings + delete options.pageSize; + options.sizeLimit = returnAll ? 0 : limit; + if (pageSize) { + options.paged = { pageSize }; + } + + // Set attributes to retrieve + if (typeof options.attributes === 'string') { + options.attributes = options.attributes.split(',').map((attribute) => attribute.trim()); + } + options.explicitBufferAttributes = BINARY_AD_ATTRIBUTES; + + if (searchFor === 'custom') { + searchFor = this.getNodeParameter('customFilter', itemIndex) as string; + } else { + const searchText = this.getNodeParameter('searchText', itemIndex) as string; + const attribute = this.getNodeParameter('attribute', itemIndex) as string; + searchFor = `(&${searchFor}(${attribute}=${searchText}))`; + } + + // Replace escaped filter special chars for ease of use + // Character ASCII value + // --------------------------- + // * 0x2a + // ( 0x28 + // ) 0x29 + // \ 0x5c + searchFor = searchFor.replace(/\\\\/g, '\\5c'); + searchFor = searchFor.replace(/\\\*/g, '\\2a'); + searchFor = searchFor.replace(/\\\(/g, '\\28'); + searchFor = searchFor.replace(/\\\)/g, '\\29'); + options.filter = searchFor; + + if (nodeDebug) { + Logger.info( + `[${this.getNode().type} | ${this.getNode().name}] - Search Options ${JSON.stringify( + options, + null, + 2, + )}`, + ); + } + + const results = await client.search(baseDN, options); + + // Not all LDAP servers respect the sizeLimit + if (!returnAll) { + results.searchEntries = results.searchEntries.slice(0, limit); + } + resolveBinaryAttributes(results.searchEntries); + + returnItems.push.apply( + returnItems, + results.searchEntries.map((result) => ({ + json: result, + pairedItem: { item: itemIndex }, + })), + ); + } + } catch (error) { + if (this.continueOnFail()) { + returnItems.push({ json: items[itemIndex].json, error, pairedItem: itemIndex }); + } else { + if (error.context) { + error.context.itemIndex = itemIndex; + throw error; + } + throw new NodeOperationError(this.getNode(), error as Error, { + itemIndex, + }); + } + } + } + if (nodeDebug) { + Logger.info(`[${this.getNode().type} | ${this.getNode().name}] - Finished`); + } + return this.prepareOutputData(returnItems); + } +} diff --git a/packages/nodes-base/nodes/Ldap/LdapDescription.ts b/packages/nodes-base/nodes/Ldap/LdapDescription.ts new file mode 100644 index 000000000..bae1aa6d6 --- /dev/null +++ b/packages/nodes-base/nodes/Ldap/LdapDescription.ts @@ -0,0 +1,439 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const ldapFields: INodeProperties[] = [ + // ---------------------------------- + // Common + // ---------------------------------- + { + displayName: 'DN', + name: 'dn', + type: 'string', + default: '', + placeholder: 'e.g. ou=users,dc=n8n,dc=io', + required: true, + typeOptions: { + alwaysOpenEditWindow: false, + }, + displayOptions: { + show: { + operation: ['compare'], + }, + }, + description: 'The distinguished name of the entry to compare', + }, + { + displayName: 'DN', + name: 'dn', + type: 'string', + default: '', + placeholder: 'e.g. ou=users,dc=n8n,dc=io', + required: true, + typeOptions: { + alwaysOpenEditWindow: false, + }, + displayOptions: { + show: { + operation: ['create'], + }, + }, + description: 'The distinguished name of the entry to create', + }, + { + displayName: 'DN', + name: 'dn', + type: 'string', + default: '', + placeholder: 'e.g. ou=users,dc=n8n,dc=io', + required: true, + typeOptions: { + alwaysOpenEditWindow: false, + }, + displayOptions: { + show: { + operation: ['delete'], + }, + }, + description: 'The distinguished name of the entry to delete', + }, + { + displayName: 'DN', + name: 'dn', + type: 'string', + default: '', + placeholder: 'e.g. cn=john,ou=users,dc=n8n,dc=io', + required: true, + typeOptions: { + alwaysOpenEditWindow: false, + }, + displayOptions: { + show: { + operation: ['rename'], + }, + }, + description: 'The distinguished name of the entry to rename', + }, + { + displayName: 'DN', + name: 'dn', + type: 'string', + default: '', + placeholder: 'e.g. ou=users,dc=n8n,dc=io', + required: true, + typeOptions: { + alwaysOpenEditWindow: false, + }, + displayOptions: { + show: { + operation: ['modify'], + }, + }, + description: 'The distinguished name of the entry to modify', + }, + // ---------------------------------- + // Compare + // ---------------------------------- + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Attribute ID', + name: 'id', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getAttributesForDn', + }, + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options + description: 'The ID of the attribute to compare', + displayOptions: { + show: { + operation: ['compare'], + }, + }, + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'The value to compare', + displayOptions: { + show: { + operation: ['compare'], + }, + }, + }, + // ---------------------------------- + // Rename + // ---------------------------------- + { + displayName: 'New DN', + name: 'targetDn', + type: 'string', + default: '', + placeholder: 'e.g. cn=nathan,ou=users,dc=n8n,dc=io', + required: true, + displayOptions: { + show: { + operation: ['rename'], + }, + }, + description: 'The new distinguished name for the entry', + }, + // ---------------------------------- + // Create + // ---------------------------------- + { + displayName: 'Attributes', + name: 'attributes', + placeholder: 'Add Attributes', + description: 'Attributes to add to the entry', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + operation: ['create'], + }, + }, + default: {}, + options: [ + { + name: 'attribute', + displayName: 'Attribute', + values: [ + { + displayName: 'Attribute ID', + name: 'id', + type: 'string', + default: '', + description: 'The ID of the attribute to add', + required: true, + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the attribute to set', + }, + ], + }, + ], + }, + // ---------------------------------- + // Update + // ---------------------------------- + { + displayName: 'Update Attributes', + name: 'attributes', + placeholder: 'Update Attributes', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + sortable: true, + }, + displayOptions: { + show: { + operation: ['update'], + }, + }, + description: 'Update entry attributes', + default: {}, + options: [ + { + name: 'add', + displayName: 'Add', + values: [ + { + displayName: 'Attribute ID', + name: 'id', + type: 'string', + default: '', + description: 'The ID of the attribute to add', + required: true, + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the attribute to set', + }, + ], + }, + { + name: 'replace', + displayName: 'Replace', + values: [ + { + displayName: 'Attribute ID', + name: 'id', + type: 'string', + default: '', + description: 'The ID of the attribute to replace', + required: true, + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the attribute to replace', + }, + ], + }, + { + name: 'delete', + displayName: 'Remove', + values: [ + { + displayName: 'Attribute ID', + name: 'id', + type: 'string', + default: '', + description: 'The ID of the attribute to remove', + required: true, + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the attribute to remove', + }, + ], + }, + ], + }, + // ---------------------------------- + // Search + // ---------------------------------- + { + displayName: 'Base DN', + name: 'baseDN', + type: 'string', + default: '', + placeholder: 'e.g. ou=users, dc=n8n, dc=io', + required: true, + displayOptions: { + show: { + operation: ['search'], + }, + }, + description: 'The distinguished name of the subtree to search in', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Search For', + name: 'searchFor', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getObjectClasses', + }, + displayOptions: { + show: { + operation: ['search'], + }, + }, + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options + description: 'Directory object class to search for', + }, + { + displayName: 'Custom Filter', + name: 'customFilter', + type: 'string', + default: '(objectclass=*)', + displayOptions: { + show: { + operation: ['search'], + searchFor: ['custom'], + }, + }, + description: 'Custom LDAP filter. Escape these chars * ( ) \\ with a backslash "\\".', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Attribute', + name: 'attribute', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getAttributes', + }, + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options + description: 'Attribute to search for', + displayOptions: { + show: { + operation: ['search'], + }, + hide: { + searchFor: ['custom'], + }, + }, + }, + { + displayName: 'Search Text', + name: 'searchText', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: ['search'], + }, + hide: { + searchFor: ['custom'], + }, + }, + description: 'Text to search for, Use * for a wildcard', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + operation: ['search'], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'Max number of results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + operation: ['search'], + returnAll: [false], + }, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: ['search'], + }, + }, + options: [ + { + displayName: 'Attribute Names or IDs', + name: 'attributes', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getAttributes', + }, + default: [], + description: + 'Comma-separated list of attributes to return. Choose from the list, or specify IDs using an expression.', + }, + { + displayName: 'Page Size', + name: 'pageSize', + type: 'number', + default: 1000, + typeOptions: { + minValue: 0, + }, + description: + 'Maximum number of results to request at one time. Set to 0 to disable paging.', + }, + { + displayName: 'Scope', + name: 'scope', + default: 'sub', + description: + 'The set of entries at or below the BaseDN that may be considered potential matches', + type: 'options', + options: [ + { + name: 'Base Object', + value: 'base', + }, + { + name: 'Single Level', + value: 'one', + }, + { + name: 'Whole Subtree', + value: 'sub', + }, + ], + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Ldap/ldap.svg b/packages/nodes-base/nodes/Ldap/ldap.svg new file mode 100644 index 000000000..d26ad23af --- /dev/null +++ b/packages/nodes-base/nodes/Ldap/ldap.svg @@ -0,0 +1 @@ + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 358c05fb5..f2cf9b422 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -170,6 +170,7 @@ "dist/credentials/KeapOAuth2Api.credentials.js", "dist/credentials/KitemakerApi.credentials.js", "dist/credentials/KoBoToolboxApi.credentials.js", + "dist/credentials/Ldap.credentials.js", "dist/credentials/LemlistApi.credentials.js", "dist/credentials/LinearApi.credentials.js", "dist/credentials/LineNotifyOAuth2Api.credentials.js", @@ -532,6 +533,7 @@ "dist/nodes/Kitemaker/Kitemaker.node.js", "dist/nodes/KoBoToolbox/KoBoToolbox.node.js", "dist/nodes/KoBoToolbox/KoBoToolboxTrigger.node.js", + "dist/nodes/Ldap/Ldap.node.js", "dist/nodes/Lemlist/Lemlist.node.js", "dist/nodes/Lemlist/LemlistTrigger.node.js", "dist/nodes/Line/Line.node.js",