feat(Citrix ADC): add Citrix ADC node (#4274)

*  Citrix ADC node

* 🐛 Fix typo in codex file

*  Remove trailing slash if there is one

*  Add certificate resource

* 🐛 Fix merge conflict issue
This commit is contained in:
Ricardo Espinoza 2022-10-07 09:10:02 -04:00 committed by GitHub
parent 9b3f30d584
commit 7abc7e6408
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 823 additions and 0 deletions

View file

@ -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',
},
};
}

View file

@ -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',
},
],
},
];

View file

@ -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/"
}
]
}
}

View file

@ -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<INodeExecutionData[][]> {
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[]];
}
}

View file

@ -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'],
},
},
},
];

View file

@ -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<any> {
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);
}
}

View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg2"
xml:space="preserve"
width="147"
height="147"
viewBox="0 0 147 147"
sodipodi:docname="citrix.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs6" /><sodipodi:namedview
id="namedview4"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="2.7846068"
inkscape:cx="0"
inkscape:cy="68.052696"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="0"
inkscape:window-y="40"
inkscape:window-maximized="1"
inkscape:current-layer="g8" /><g
id="g8"
inkscape:groupmode="layer"
inkscape:label="ink_ext_XXXXXX"
transform="matrix(1.3333333,0,0,-1.3333333,-337.63332,148.36666)"><g
id="g10"
transform="matrix(0.1,0,0,0.1,18.085912,0)"><path
d="M 2904.09,362.301 2622.82,33.3789 c -11.12,-14.3594 -25.42,-20.6992 -41.33,-20.6992 -28.6,0 -49.24,22.2617 -49.24,47.6797 0,11.1289 3.17,23.8711 14.3,36.5508 l 293.98,332.1248 -282.85,324.16 c -11.13,12.743 -17.47,23.871 -17.47,38.168 0,25.426 20.64,47.68 50.85,47.68 15.85,0 26.98,-6.398 39.72,-20.703 L 2904.09,497.383 3177.4,818.34 c 12.75,14.305 23.88,20.703 39.73,20.703 30.21,0 50.85,-22.254 50.85,-47.68 0,-14.297 -6.34,-25.425 -17.47,-38.168 L 2967.66,429.035 3261.64,96.9102 c 11.13,-12.6797 14.3,-25.4219 14.3,-36.5508 0,-25.418 -20.64,-47.6797 -49.24,-47.6797 -15.92,0 -30.21,6.3398 -41.34,20.6992 L 2904.09,362.301"
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path24" /><path
d="m 2985.26,1031.59 c 0,-44.824 -36.34,-81.164 -81.17,-81.164 -44.82,0 -81.16,36.34 -81.16,81.164 0,44.82 36.34,81.16 81.16,81.16 44.83,0 81.17,-36.34 81.17,-81.16"
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path28" /></g></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -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",