Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ Comprehensive user management operations:
- **Get Many** - Enumerate users for a partner
- **Update** - Update user information

### Installation

- **Generate Token** - Generate an agent installation token for a partner, returning `installationToken`

## API Authentication

Cove Data Protection uses a visa-based authentication system:
Expand All @@ -89,6 +93,11 @@ Cove Data Protection uses a visa-based authentication system:
- Each API response includes a new visa to maintain the session
- The node automatically manages visa renewal and caching

Most operations use the JSON-RPC endpoint at `https://api.backup.management/jsonapi`. The
installation token operation uses the REST endpoint at
`https://api.backup.management/agent/installation`, which takes the same visa as an
`Authorization: Bearer` header.

## Resources

- [Cove Data Protection API Documentation](https://documentation.n-able.com/covedataprotection/USERGUIDE/documentation/Content/service-management/json-api/home.htm)
Expand Down
120 changes: 101 additions & 19 deletions nodes/CoveDataProtection/CoveDataProtection.node.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as partners from './actions/partners';
import * as accounts from './actions/accounts';
import * as users from './actions/users';
import * as installation from './actions/installation';

import {
IExecuteFunctions,
Expand All @@ -14,6 +15,77 @@ import {

import { jsonRpcRequest, getPartnerId } from './transport';

// Profile and product lists come back as { Id, Name } records, but the enumerate
// methods are inconsistent about prefixing those fields, so accept either form.
async function enumerateNamedOptions(
this: ILoadOptionsFunctions,
method: string,
): Promise<INodePropertyOptions[]> {
const partnerId =
(this.getCurrentNodeParameter('partnerId') as number) || (await getPartnerId.call(this));

const result = await jsonRpcRequest.call(this, method, { partnerId });
const records = Array.isArray(result?.result) ? result.result : result;

if (!Array.isArray(records)) {
return [];
}

const options: INodePropertyOptions[] = records.map((record: any) => {
const id = record.Id ?? record.ProfileId ?? record.PolicyId ?? record.RetentionPolicyId;
return {
name: record.Name ?? record.ProfileName ?? record.PolicyName ?? `ID ${id}`,
value: id,
};
});

options.sort((a, b) => a.name.localeCompare(b.name));

return options;
}

// EnumeratePartners returns descendants only, so the root partner itself is added by
// each caller under whatever label suits its field.
async function getDescendantPartnerOptions(
this: ILoadOptionsFunctions,
parentPartnerId: number,
): Promise<INodePropertyOptions[]> {
const params = {
parentPartnerId,
fetchRecursively: true,
fields: [0, 1],
};

const result = await jsonRpcRequest.call(this, 'EnumeratePartners', params);
const partners: INodePropertyOptions[] = [];

if (result && result.result && Array.isArray(result.result)) {
for (const partner of result.result) {
partners.push({
name: partner.Name || `Partner ${partner.Id}`,
value: partner.Id,
});
}
}

partners.sort((a, b) => a.name.localeCompare(b.name));

return partners;
}

async function getPartnerName(
this: ILoadOptionsFunctions,
partnerId: number,
): Promise<string> {
try {
const result = await jsonRpcRequest.call(this, 'GetPartnerInfoById', { partnerId });
return result?.result?.Name ?? result?.Name ?? '';
} catch {
// A missing name only costs us a nicer label, so fall back to the plain one.
return '';
}
}

export class CoveDataProtection implements INodeType {
description: INodeTypeDescription = {
displayName: 'Cove Data Protection',
Expand Down Expand Up @@ -47,6 +119,10 @@ export class CoveDataProtection implements INodeType {
name: 'Account',
value: 'accounts',
},
{
name: 'Installation',
value: 'installation',
},
{
name: 'Partner',
value: 'partners',
Expand All @@ -61,39 +137,45 @@ export class CoveDataProtection implements INodeType {
...accounts.description,
...partners.description,
...users.description,
...installation.description,
],
};

methods = {
loadOptions: {
async getPartners(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const parentPartnerId = await getPartnerId.call(this);
const params = {
parentPartnerId,
fetchRecursively: true,
fields: [0, 1],
};

const result = await jsonRpcRequest.call(this, 'EnumeratePartners', params);
const partners: INodePropertyOptions[] = [];

if (result && result.result && Array.isArray(result.result)) {
for (const partner of result.result) {
partners.push({
name: partner.Name || `Partner ${partner.Id}`,
value: partner.Id,
});
}
}

partners.sort((a, b) => a.name.localeCompare(b.name));
const partners = await getDescendantPartnerOptions.call(this, parentPartnerId);

partners.unshift({
name: 'All Partners (Top Level)',
value: parentPartnerId,
});

return partners;
},

async getCustomers(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const rootPartnerId = await getPartnerId.call(this);
const partners = await getDescendantPartnerOptions.call(this, rootPartnerId);
const rootName = await getPartnerName.call(this, rootPartnerId);

partners.unshift({
name: rootName ? `Root Partner (${rootName})` : 'Root Partner',
value: rootPartnerId,
});

return partners;
},

async getProfiles(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
return await enumerateNamedOptions.call(this, 'EnumerateAccountProfiles');
},

// Retention policies are surfaced by the API as products.
async getRetentionPolicies(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
return await enumerateNamedOptions.call(this, 'EnumerateProducts');
},
},
};

Expand Down
2 changes: 1 addition & 1 deletion nodes/CoveDataProtection/actions/Interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type CoveDataProtection = {
resource: 'partners' | 'accounts' | 'users';
resource: 'partners' | 'accounts' | 'users' | 'installation';
operation: string;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { INodeProperties } from 'n8n-workflow';

export const generateTokenDescription: INodeProperties[] = [
{
displayName: 'Customer Name or ID',
name: 'partnerId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getCustomers',
},
displayOptions: {
show: {
resource: ['installation'],
operation: ['generateToken'],
},
},
default: '',
description: 'The customer the installation token is issued for. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
required: true,
},
{
displayName: 'Retention Policy Name or ID',
name: 'retentionPolicyId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getRetentionPolicies',
loadOptionsDependsOn: ['partnerId'],
},
displayOptions: {
show: {
resource: ['installation'],
operation: ['generateToken'],
},
},
default: '',
description: 'The retention policy assigned to devices installed with this token. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
required: true,
},
{
displayName: 'Profile Name or ID',
name: 'profileId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getProfiles',
loadOptionsDependsOn: ['partnerId'],
},
displayOptions: {
show: {
resource: ['installation'],
operation: ['generateToken'],
},
},
default: '',
description: 'The backup profile assigned to devices installed with this token. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
required: true,
},
{
displayName: 'Encryption',
name: 'encryption',
type: 'options',
displayOptions: {
show: {
resource: ['installation'],
operation: ['generateToken'],
},
},
options: [
{
name: 'Managed',
value: 'managed',
description: 'Cove generates and stores the encryption key',
},
{
name: 'Self-Managed',
value: 'selfManaged',
description: 'The encryption key is supplied at install time and not stored by Cove',
},
],
default: 'managed',
description: 'Who holds the encryption key for devices installed with this token',
},
{
displayName: 'Device Name',
name: 'accountName',
type: 'string',
displayOptions: {
show: {
resource: ['installation'],
operation: ['generateToken'],
},
},
default: '',
description: 'Name given to the device created by this token. Leave empty to let Cove assign one.',
},
{
displayName: 'Never Expires',
name: 'unlimitedExpiration',
type: 'boolean',
displayOptions: {
show: {
resource: ['installation'],
operation: ['generateToken'],
},
},
default: true,
description: 'Whether the installer stays valid indefinitely',
},
{
displayName: 'Installer Expiry Date',
name: 'expirationTimestamp',
type: 'dateTime',
displayOptions: {
show: {
resource: ['installation'],
operation: ['generateToken'],
unlimitedExpiration: [false],
},
},
default: '',
description: 'When the installer stops working. Sent to the API as a Unix timestamp in seconds; an expression returning one directly is also accepted.',
required: true,
},
{
displayName: 'Unlimited Devices',
name: 'unlimitedCount',
type: 'boolean',
displayOptions: {
show: {
resource: ['installation'],
operation: ['generateToken'],
},
},
default: true,
description: 'Whether the installer can be used on an unlimited number of devices',
},
{
displayName: 'Number of Devices',
name: 'installationCount',
type: 'number',
displayOptions: {
show: {
resource: ['installation'],
operation: ['generateToken'],
unlimitedCount: [false],
},
},
typeOptions: {
minValue: 1,
},
default: 1,
description: 'How many devices the installer may be used on',
},
{
displayName: 'Extra Properties',
name: 'extraProperties',
type: 'json',
displayOptions: {
show: {
resource: ['installation'],
operation: ['generateToken'],
},
},
default: '{}',
description: 'Additional JSON properties merged into the request body, for fields this node does not expose yet',
},
];
Loading
Loading