From 7abc7e64082b60fa48f99f4b1f41d176fbb6d6ad Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 7 Oct 2022 09:10:02 -0400 Subject: [PATCH] feat(Citrix ADC): add Citrix ADC node (#4274) * :sparkles: Citrix ADC node * :bug: Fix typo in codex file * :zap: Remove trailing slash if there is one * :zap: Add certificate resource * :bug: Fix merge conflict issue --- .../credentials/CitrixAdcApi.credentials.ts | 55 ++++ .../Citrix/ADC/CertificateDescription.ts | 308 ++++++++++++++++++ .../nodes/Citrix/ADC/CitrixAdc.node.json | 18 + .../nodes/Citrix/ADC/CitrixAdc.node.ts | 218 +++++++++++++ .../nodes/Citrix/ADC/FileDescription.ts | 128 ++++++++ .../nodes/Citrix/ADC/GenericFunctions.ts | 44 +++ .../nodes-base/nodes/Citrix/ADC/citrix.svg | 50 +++ packages/nodes-base/package.json | 2 + 8 files changed, 823 insertions(+) create mode 100644 packages/nodes-base/credentials/CitrixAdcApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Citrix/ADC/CertificateDescription.ts create mode 100644 packages/nodes-base/nodes/Citrix/ADC/CitrixAdc.node.json create mode 100644 packages/nodes-base/nodes/Citrix/ADC/CitrixAdc.node.ts create mode 100644 packages/nodes-base/nodes/Citrix/ADC/FileDescription.ts create mode 100644 packages/nodes-base/nodes/Citrix/ADC/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Citrix/ADC/citrix.svg diff --git a/packages/nodes-base/credentials/CitrixAdcApi.credentials.ts b/packages/nodes-base/credentials/CitrixAdcApi.credentials.ts new file mode 100644 index 0000000000..0bd0496b05 --- /dev/null +++ b/packages/nodes-base/credentials/CitrixAdcApi.credentials.ts @@ -0,0 +1,55 @@ +import { + IAuthenticateGeneric, + ICredentialTestRequest, + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class CitrixAdcApi implements ICredentialType { + name = 'citrixAdcApi'; + displayName = 'Citrix ADC API'; + documentationUrl = 'citrix'; + properties: INodeProperties[] = [ + { + displayName: 'URL', + name: 'url', + type: 'string', + default: '', + required: true, + }, + { + displayName: 'Username', + name: 'username', + type: 'string', + default: '', + required: true, + }, + { + displayName: 'Password', + name: 'password', + type: 'string', + default: '', + required: true, + typeOptions: { + password: true, + }, + }, + ]; + + authenticate: IAuthenticateGeneric = { + type: 'generic', + properties: { + headers: { + 'X-NITRO-USER': '={{$credentials.username}}', + 'X-NITRO-PASS': '={{$credentials.password}}', + }, + }, + }; + + test: ICredentialTestRequest = { + request: { + baseURL: '={{$credentials.url}}', + url: '/nitro/v1/config/nspartition?view=summary', + }, + }; +} diff --git a/packages/nodes-base/nodes/Citrix/ADC/CertificateDescription.ts b/packages/nodes-base/nodes/Citrix/ADC/CertificateDescription.ts new file mode 100644 index 0000000000..fe17bfa733 --- /dev/null +++ b/packages/nodes-base/nodes/Citrix/ADC/CertificateDescription.ts @@ -0,0 +1,308 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const certificateDescription: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Create', + value: 'create', + action: 'Create a certificate', + }, + ], + default: 'create', + displayOptions: { + show: { + resource: ['certificate'], + }, + }, + }, + { + displayName: 'Certificate File Name', + name: 'certificateFileName', + type: 'string', + required: true, + displayOptions: { + show: { + resource: ['certificate'], + operation: ['create'], + }, + }, + default: '', + description: + 'Name for and, optionally, path to the generated certificate file. /nsconfig/ssl/ is the default path.', + }, + { + displayName: 'Certificate Format', + name: 'certificateFormat', + type: 'options', + options: [ + { + name: 'PEM', + value: 'PEM', + }, + { + name: 'DER', + value: 'DER', + }, + ], + required: true, + displayOptions: { + show: { + resource: ['certificate'], + operation: ['create'], + }, + }, + default: 'PEM', + description: 'Format in which the certificate is stored on the appliance', + }, + { + displayName: 'Certificate Type', + name: 'certificateType', + type: 'options', + options: [ + { + name: 'Root-CA', + value: 'ROOT_CERT', + description: + 'You must specify the key file name. The generated Root-CA certificate can be used for signing end-user client or server certificates or to create Intermediate-CA certificates.', + }, + { + name: 'Intermediate-CA', + value: 'INTM_CERT', + description: 'Intermediate-CA certificate', + }, + { + name: 'Server', + value: 'SRVR_CERT', + description: 'SSL server certificate used on SSL servers for end-to-end encryption', + }, + { + name: 'Client', + value: 'CLNT_CERT', + description: 'End-user client certificate used for client authentication', + }, + ], + required: true, + displayOptions: { + show: { + resource: ['certificate'], + operation: ['create'], + }, + }, + default: 'ROOT_CERT', + }, + { + displayName: 'Certificate Request File Name', + name: 'certificateRequestFileName', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: ['create'], + resource: ['certificate'], + }, + }, + description: + 'Name for and, optionally, path to the certificate-signing request (CSR). /nsconfig/ssl/ is the default path.', + }, + { + displayName: 'CA Certificate File Name', + name: 'caCertificateFileName', + type: 'string', + required: true, + displayOptions: { + show: { + resource: ['certificate'], + operation: ['create'], + certificateType: ['INTM_CERT', 'SRVR_CERT', 'CLNT_CERT'], + }, + }, + default: '', + description: + 'Name of the CA certificate file that issues and signs the Intermediate-CA certificate or the end-user client and server certificates', + }, + { + displayName: 'CA Certificate File Format', + name: 'caCertificateFileFormat', + type: 'options', + options: [ + { + name: 'PEM', + value: 'PEM', + }, + { + name: 'DER', + value: 'DER', + }, + ], + required: true, + displayOptions: { + show: { + resource: ['certificate'], + operation: ['create'], + certificateType: ['INTM_CERT', 'SRVR_CERT', 'CLNT_CERT'], + }, + }, + default: 'PEM', + description: 'Format of the CA certificate', + }, + { + displayName: 'CA Private Key File Name', + name: 'caPrivateKeyFileName', + type: 'string', + required: true, + displayOptions: { + show: { + resource: ['certificate'], + operation: ['create'], + certificateType: ['INTM_CERT', 'SRVR_CERT', 'CLNT_CERT'], + }, + }, + default: '', + description: + 'Private key, associated with the CA certificate that is used to sign the Intermediate-CA certificate or the end-user client and server certificate. If the CA key file is password protected, the user is prompted to enter the pass phrase that was used to encrypt the key.', + }, + { + displayName: 'CA Private Key File Format', + name: 'caPrivateKeyFileFormat', + type: 'options', + options: [ + { + name: 'PEM', + value: 'PEM', + }, + { + name: 'DER', + value: 'DER', + }, + ], + required: true, + displayOptions: { + show: { + resource: ['certificate'], + operation: ['create'], + certificateType: ['INTM_CERT', 'SRVR_CERT', 'CLNT_CERT'], + }, + }, + default: 'PEM', + description: 'Format of the CA certificate', + }, + { + displayName: 'Private Key File Name', + name: 'privateKeyFileName', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: ['create'], + resource: ['certificate'], + certificateType: ['ROOT_CERT'], + }, + }, + description: + 'Name for and, optionally, path to the private key. You can either use an existing RSA or DSA key that you own or create a new private key on the Citrix ADC. This file is required only when creating a self-signed Root-CA certificate. The key file is stored in the /nsconfig/ssl directory by default.', + }, + { + displayName: 'CA Serial File Number', + name: 'caSerialFileNumber', + type: 'string', + required: true, + displayOptions: { + show: { + resource: ['certificate'], + operation: ['create'], + certificateType: ['INTM_CERT', 'SRVR_CERT', 'CLNT_CERT'], + }, + }, + default: '', + description: 'Serial number file maintained for the CA certificate. This file contains the serial number of the next certificate to be issued or signed by the CA.', + }, + { + displayName: 'Private Key Format', + name: 'privateKeyFormat', + type: 'options', + options: [ + { + name: 'PEM', + value: 'PEM', + }, + { + name: 'DER', + value: 'DER', + }, + ], + required: true, + displayOptions: { + show: { + resource: ['certificate'], + operation: ['create'], + certificateType: ['ROOT_CERT'], + }, + }, + default: 'PEM', + description: 'Format in which the key is stored on the appliance', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['certificate'], + operation: ['create'], + }, + }, + options: [ + { + displayName: 'PEM Passphrase (For Encrypted Key)', + name: 'pempassphrase', + type: 'string', + displayOptions: { + show: { + '/certificateType': ['ROOT_CERT'], + }, + }, + default: '', + description: + 'Name for and, optionally, path to the private key. You can either use an existing RSA or DSA key that you own or create a new private key on the Citrix ADC. This file is required only when creating a self-signed Root-CA certificate. The key file is stored in the /nsconfig/ssl directory by default.', + }, + { + displayName: 'PEM Passphrase (For Encrypted CA Key)', + name: 'pempassphrase', + type: 'string', + displayOptions: { + hide: { + '/certificateType': ['ROOT_CERT'], + }, + }, + default: '', + description: + 'Name for and, optionally, path to the private key. You can either use an existing RSA or DSA key that you own or create a new private key on the Citrix ADC. This file is required only when creating a self-signed Root-CA certificate. The key file is stored in the /nsconfig/ssl directory by default.', + }, + { + displayName: 'Subject Alternative Name', + name: 'subjectaltname', + type: 'string', + default: '', + description: + 'Subject Alternative Name (SAN) is an extension to X.509 that allows various values to be associated with a security certificate using a subjectAltName field', + }, + { + displayName: 'Validity Period (Number of Days)', + name: 'days', + type: 'string', + default: '', + description: + 'Number of days for which the certificate will be valid, beginning with the time and day (system time) of creation', + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Citrix/ADC/CitrixAdc.node.json b/packages/nodes-base/nodes/Citrix/ADC/CitrixAdc.node.json new file mode 100644 index 0000000000..482c858b1d --- /dev/null +++ b/packages/nodes-base/nodes/Citrix/ADC/CitrixAdc.node.json @@ -0,0 +1,18 @@ +{ + "node": "n8n-nodes-base.citrixAdc", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Development"], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/citrixAdc" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.citrixAdc/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/Citrix/ADC/CitrixAdc.node.ts b/packages/nodes-base/nodes/Citrix/ADC/CitrixAdc.node.ts new file mode 100644 index 0000000000..6ae9bac31a --- /dev/null +++ b/packages/nodes-base/nodes/Citrix/ADC/CitrixAdc.node.ts @@ -0,0 +1,218 @@ +import { IExecuteFunctions } from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, + JsonObject, + NodeOperationError, +} from 'n8n-workflow'; + +import { citrixADCApiRequest } from './GenericFunctions'; + +import { fileDescription } from './FileDescription'; + +import { certificateDescription } from './CertificateDescription'; + +export class CitrixAdc implements INodeType { + description: INodeTypeDescription = { + displayName: 'Citrix ADC', + name: 'citrixAdc', + icon: 'file:citrix.svg', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Citrix ADC API', + defaults: { + name: 'Citrix ADC', + }, + credentials: [ + { + name: 'citrixAdcApi', + required: true, + }, + ], + inputs: ['main'], + outputs: ['main'], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Certificate', + value: 'certificate', + }, + { + name: 'File', + value: 'file', + }, + ], + default: 'file', + }, + ...certificateDescription, + ...fileDescription, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + let responseData: IDataObject | IDataObject[] = {}; + + for (let i = 0; i < items.length; i++) { + try { + if (resource === 'file') { + if (operation === 'upload') { + const fileLocation = this.getNodeParameter('fileLocation', i) as string; + const binaryProperty = this.getNodeParameter('binaryProperty', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + const endpoint = `/config/systemfile`; + + const item = items[i]; + + if (item.binary === undefined) { + throw new NodeOperationError(this.getNode(), 'No binary data exists on item!'); + } + + if (item.binary[binaryProperty] === undefined) { + throw new NodeOperationError( + this.getNode(), + `No binary data property "${binaryProperty}" does not exists on item!`, + ); + } + + const buffer = await this.helpers.getBinaryDataBuffer(i, binaryProperty); + + const body = { + systemfile: { + filename: item.binary[binaryProperty].fileName, + filecontent: Buffer.from(buffer).toString('base64'), + filelocation: fileLocation, + fileencoding: 'BASE64', + }, + }; + + if (options.fileName) { + body.systemfile.filename = options.fileName as string; + } + + await citrixADCApiRequest.call(this, 'POST', endpoint, body); + responseData = { success: true }; + } + if (operation === 'delete') { + const fileName = this.getNodeParameter('fileName', i) as string; + const fileLocation = this.getNodeParameter('fileLocation', i) as string; + + const endpoint = `/config/systemfile?args=filename:${fileName},filelocation:${encodeURIComponent( + fileLocation, + )}`; + + await citrixADCApiRequest.call(this, 'DELETE', endpoint); + responseData = { success: true }; + } + if (operation === 'download') { + const fileName = this.getNodeParameter('fileName', i) as string; + const fileLocation = this.getNodeParameter('fileLocation', i) as string; + const binaryProperty = this.getNodeParameter('binaryProperty', i) as string; + + const endpoint = `/config/systemfile?args=filename:${fileName},filelocation:${encodeURIComponent( + fileLocation, + )}`; + + const { systemfile } = await citrixADCApiRequest.call(this, 'GET', endpoint); + + const file = systemfile[0]; + + const binaryData = await this.helpers.prepareBinaryData( + Buffer.from(file.filecontent, 'base64'), + file.filename, + ); + + responseData = { + json: file, + binary: { + [binaryProperty]: binaryData, + }, + }; + } + } + + if (resource === 'certificate') { + if (operation === 'create') { + const certificateFileName = this.getNodeParameter('certificateFileName', i) as string; + const certificateFormat = this.getNodeParameter('certificateFormat', i) as string; + const certificateType = this.getNodeParameter('certificateType', i) as string; + const certificateRequestFileName = this.getNodeParameter( + 'certificateRequestFileName', + i, + ) as string; + const additionalFields = this.getNodeParameter( + 'additionalFields', + i, + {}, + ) as IDataObject; + + let body: IDataObject = { + reqfile: certificateRequestFileName, + certfile: certificateFileName, + certform: certificateFormat, + certType: certificateType, + ...additionalFields, + }; + + if (certificateType === 'ROOT_CERT') { + const privateKeyFileName = this.getNodeParameter('privateKeyFileName', i) as string; + body = { + ...body, + keyfile: privateKeyFileName, + }; + + } else { + const caCertificateFileName = this.getNodeParameter('caCertificateFileName', i) as string; + const caCertificateFileFormat = this.getNodeParameter('caCertificateFileFormat', i) as string; + const caPrivateKeyFileFormat = this.getNodeParameter('caPrivateKeyFileFormat', i) as string; + const caPrivateKeyFileName = this.getNodeParameter('caPrivateKeyFileName', i) as string; + const caSerialFileNumber = this.getNodeParameter('caSerialFileNumber', i) as string; + + body = { + ...body, + cacert: caCertificateFileName, + cacertform: caCertificateFileFormat, + cakey: caPrivateKeyFileName, + cakeyform: caPrivateKeyFileFormat, + caserial: caSerialFileNumber, + }; + } + + const endpoint = `/config/sslcert?action=create`; + + await citrixADCApiRequest.call(this, 'POST', endpoint, { sslcert: body }); + + responseData = { success: true }; + } + } + + returnData.push( + ...this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray(responseData), { + itemData: { item: i }, + }), + ); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: (error as JsonObject).toString() }); + continue; + } + + throw error; + } + } + + return [returnData as INodeExecutionData[]]; + } +} diff --git a/packages/nodes-base/nodes/Citrix/ADC/FileDescription.ts b/packages/nodes-base/nodes/Citrix/ADC/FileDescription.ts new file mode 100644 index 0000000000..865e9492bc --- /dev/null +++ b/packages/nodes-base/nodes/Citrix/ADC/FileDescription.ts @@ -0,0 +1,128 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const fileDescription: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Delete', + value: 'delete', + action: 'Delete a file', + }, + { + name: 'Download', + value: 'download', + action: 'Download a file', + }, + { + name: 'Upload', + value: 'upload', + action: 'Upload a file', + }, + ], + default: 'upload', + displayOptions: { + show: { + resource: [ + 'file', + ], + }, + }, + }, + // Upload -------------------------------------------------------------------------- + { + displayName: 'File Location', + name: 'fileLocation', + type: 'string', + required: true, + displayOptions: { + show: { + operation: ['upload'], + resource: ['file'], + }, + }, + default: '/nsconfig/ssl/', + }, + { + displayName: 'Input Data Field Name', + name: 'binaryProperty', + type: 'string', + required: true, + displayOptions: { + show: { + operation: ['upload'], + resource: ['file'], + }, + }, + default: 'data', + description: 'The name of the incoming field containing the binary file data to be processed', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: ['upload'], + resource: ['file'], + }, + }, + options: [ + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + default: '', + description: 'Name of the file. It should not include filepath.', + }, + ], + }, + // Delete, Download --------------------------------------------------------------- + { + displayName: 'File Location', + name: 'fileLocation', + type: 'string', + required: true, + displayOptions: { + show: { + operation: ['delete', 'download' ], + resource: ['file'], + }, + }, + default: '/nsconfig/ssl/', + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + default: '', + required: true, + description: 'Name of the file. It should not include filepath.', + displayOptions: { + show: { + operation: ['delete', 'download' ], + resource: ['file'], + }, + }, + }, + { + displayName: 'Put Output in Field', + name: 'binaryProperty', + type: 'string', + required: true, + default: 'data', + description: + 'The name of the output field to put the binary file data in', + displayOptions: { + show: { + operation: ['download' ], + resource: ['file'], + }, + }, + }, +]; diff --git a/packages/nodes-base/nodes/Citrix/ADC/GenericFunctions.ts b/packages/nodes-base/nodes/Citrix/ADC/GenericFunctions.ts new file mode 100644 index 0000000000..b80c81b473 --- /dev/null +++ b/packages/nodes-base/nodes/Citrix/ADC/GenericFunctions.ts @@ -0,0 +1,44 @@ +import { OptionsWithUri } from 'request'; + +import { IExecuteFunctions, ILoadOptionsFunctions } from 'n8n-core'; + +import { + IDataObject, + IHookFunctions, + IWebhookFunctions, + JsonObject, + NodeApiError, +} from 'n8n-workflow'; + +export async function citrixADCApiRequest( + this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, + method: string, + resource: string, + body: IDataObject = {}, + qs: IDataObject = {}, + uri?: string, + option: IDataObject = {}, + // tslint:disable-next-line:no-any + ): Promise { + + const { url } = (await this.getCredentials('citrixAdcApi')) as { url: string }; + + let options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `${url.replace(new RegExp('/$'), '')}/nitro/v1${resource}`, + json: true, + }; + + options = Object.assign({}, options, option); + + try { + return await this.helpers.requestWithAuthentication.call(this, 'citrixAdcApi', options); + } catch (error) { + throw new NodeApiError(this.getNode(), error as JsonObject); + } +} diff --git a/packages/nodes-base/nodes/Citrix/ADC/citrix.svg b/packages/nodes-base/nodes/Citrix/ADC/citrix.svg new file mode 100644 index 0000000000..f91f84b508 --- /dev/null +++ b/packages/nodes-base/nodes/Citrix/ADC/citrix.svg @@ -0,0 +1,50 @@ + + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 1f643f929e..232658f50c 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -60,6 +60,7 @@ "dist/credentials/ChargebeeApi.credentials.js", "dist/credentials/CircleCiApi.credentials.js", "dist/credentials/CiscoWebexOAuth2Api.credentials.js", + "dist/credentials/CitrixAdcApi.credentials.js", "dist/credentials/CloudflareApi.credentials.js", "dist/credentials/ClearbitApi.credentials.js", "dist/credentials/ClickUpApi.credentials.js", @@ -387,6 +388,7 @@ "dist/nodes/Chargebee/ChargebeeTrigger.node.js", "dist/nodes/CircleCi/CircleCi.node.js", "dist/nodes/Cisco/Webex/CiscoWebex.node.js", + "dist/nodes/Citrix/ADC/CitrixAdc.node.js", "dist/nodes/Cisco/Webex/CiscoWebexTrigger.node.js", "dist/nodes/Cloudflare/Cloudflare.node.js", "dist/nodes/Clearbit/Clearbit.node.js",