Skip to content

Commit

Permalink
[nan-350] add in connection test logic (#1656)
Browse files Browse the repository at this point in the history
* [nan-350] add in connection test logic

* [nan-350] indentation

* [nan-350] don't cast connection

* [nan-350] remove head

* [nan-350] move check if no provider

* [nan-350] pr feedback
  • Loading branch information
khaliqgant authored Feb 12, 2024
1 parent 8f1d6f3 commit 10b6bb4
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 17 deletions.
77 changes: 68 additions & 9 deletions packages/server/lib/controllers/apiAuth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Request, Response, NextFunction } from 'express';
import tracer from '../tracer.js';
import type { LogLevel } from '@nangohq/shared';
import {
getAccount,
Expand All @@ -10,6 +11,10 @@ import {
AuthOperation,
connectionCreated as connectionCreatedHook,
connectionCreationFailed as connectionCreationFailedHook,
ApiKeyCredentials,
BasicApiCredentials,
connectionTest as connectionTestHook,
isErr,
createActivityLogMessage,
updateSuccess as updateSuccessActivityLog,
updateProvider as updateProviderActivityLog,
Expand Down Expand Up @@ -134,6 +139,36 @@ class ApiAuthController {

const { apiKey } = req.body;

const credentials: ApiKeyCredentials = {
type: AuthModes.ApiKey,
apiKey
};

const connectionResponse = await connectionTestHook(
config?.provider,
template,
credentials,
connectionId,
providerConfigKey,
environmentId,
connectionConfig,
tracer
);

if (isErr(connectionResponse)) {
await createActivityLogMessageAndEnd({
level: 'error',
environment_id: environmentId,
activity_log_id: activityLogId as number,
content: `The credentials provided were not valid for the ${config?.provider} provider`,
timestamp: Date.now()
});

errorManager.errResFromNangoErr(res, connectionResponse.err);

return;
}

await createActivityLogMessage({
level: 'info',
environment_id: environmentId,
Expand All @@ -148,10 +183,7 @@ class ApiAuthController {
connectionId as string,
providerConfigKey as string,
config?.provider as string,
{
type: AuthModes.ApiKey,
apiKey
},
credentials,
connectionConfig,
environmentId,
accountId
Expand Down Expand Up @@ -313,6 +345,37 @@ class ApiAuthController {
return;
}

const credentials: BasicApiCredentials = {
type: AuthModes.Basic,
username,
password
};

const connectionResponse = await connectionTestHook(
config?.provider,
template,
credentials,
connectionId,
providerConfigKey,
environmentId,
connectionConfig,
tracer
);

if (isErr(connectionResponse)) {
await createActivityLogMessageAndEnd({
level: 'error',
environment_id: environmentId,
activity_log_id: activityLogId as number,
content: `The credentials provided were not valid for the ${config?.provider} provider`,
timestamp: Date.now()
});

errorManager.errResFromNangoErr(res, connectionResponse.err);

return;
}

await updateProviderActivityLog(activityLogId as number, String(config?.provider));

await createActivityLogMessage({
Expand All @@ -329,11 +392,7 @@ class ApiAuthController {
connectionId as string,
providerConfigKey as string,
config?.provider as string,
{
type: AuthModes.Basic,
username,
password
},
credentials,
connectionConfig,
environmentId,
accountId
Expand Down
98 changes: 97 additions & 1 deletion packages/shared/lib/hooks/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import axios from 'axios';
import type { Span, Tracer } from 'dd-trace';
import SyncClient from '../clients/sync.client.js';
import type { RecentlyCreatedConnection } from '../models/Connection.js';
import type { ApiKeyCredentials, BasicApiCredentials } from '../models/Auth.js';
import type { RecentlyCreatedConnection, Connection, ConnectionConfig } from '../models/Connection.js';
import type { ApplicationConstructedProxyConfiguration, InternalProxyConfiguration } from '../models/Proxy.js';
import proxyService from '../services/proxy.service.js';
import type { HTTP_VERB } from '../models/Generic.js';
import type { Template as ProviderTemplate } from '../models/Provider.js';
import integrationPostConnectionScript from '../integrations/scripts/connection/connection.manager.js';
import webhookService from '../services/notification/webhook.service.js';
import { SpanTypes } from '../utils/metrics.manager.js';
import { isCloud, isLocal, isEnterprise } from '../utils/utils.js';
import { Result, resultOk, resultErr } from '../utils/result.js';
import { NangoError } from '../utils/error.js';

export const connectionCreated = async (
connection: RecentlyCreatedConnection,
Expand All @@ -27,3 +37,89 @@ export const connectionCreated = async (
export const connectionCreationFailed = async (connection: RecentlyCreatedConnection, provider: string, activityLogId: number | null): Promise<void> => {
await webhookService.sendAuthUpdate(connection, provider, false, activityLogId);
};

export const connectionTest = async (
provider: string,
template: ProviderTemplate,
credentials: ApiKeyCredentials | BasicApiCredentials,
connectionId: string,
providerConfigKey: string,
environment_id: number,
connection_config: ConnectionConfig,
tracer: Tracer
): Promise<Result<boolean, NangoError>> => {
const providerVerification = template?.proxy?.verification;

if (!providerVerification) {
return resultOk(true);
}
const active = tracer.scope().active();
const span = tracer.startSpan(SpanTypes.CONNECTION_TEST, {
childOf: active as Span,
tags: {
'nango.provider': provider,
'nango.providerConfigKey': providerConfigKey,
'nango.connectionId': connectionId
}
});

const { method, endpoint } = providerVerification;

const connection: Connection = {
id: -1,
provider_config_key: providerConfigKey,
connection_id: connectionId,
credentials,
connection_config,
environment_id
};

const configBody: ApplicationConstructedProxyConfiguration = {
endpoint,
method: method?.toUpperCase() as HTTP_VERB,
template,
token: credentials,
provider: provider,
providerConfigKey,
connectionId,
headers: {
'Content-Type': 'application/json'
},
connection
};

const internalConfig: InternalProxyConfiguration = {
provider,
connection
};

try {
const { response } = await proxyService.route(configBody, internalConfig);

if (axios.isAxiosError(response)) {
span.setTag('nango.error', response);
const error = new NangoError('connection_test_failed', response, response.response?.status);
return resultErr(error);
}

if (!response) {
const error = new NangoError('connection_test_failed');
span.setTag('nango.error', response);
return resultErr(error);
}

if (response.status && (response?.status < 200 || response?.status > 300)) {
const error = new NangoError('connection_test_failed');
span.setTag('nango.error', response);
return resultErr(error);
}

return resultOk(true);
} catch (e) {
const error = new NangoError('connection_test_failed');
span.setTag('nango.error', e);
return resultErr(error);
} finally {
span.finish();
}
};
6 changes: 5 additions & 1 deletion packages/shared/lib/models/Provider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { RetryHeaderConfig, CursorPagination, LinkPagination, OffsetPagination } from './Proxy.js';
import type { AuthModes } from './Auth.js';
import type { TimestampsAndDeleted } from './Generic.js';
import type { HTTP_VERB, TimestampsAndDeleted } from './Generic.js';
import type { SyncConfig, Action } from './Sync.js';

export interface Config extends TimestampsAndDeleted {
Expand Down Expand Up @@ -32,6 +32,10 @@ export interface Template {
retry?: RetryHeaderConfig;
decompress?: boolean;
paginate?: LinkPagination | CursorPagination | OffsetPagination;
verification?: {
method: HTTP_VERB;
endpoint: string;
};
};
authorization_url: string;
authorization_params?: Record<string, string>;
Expand Down
8 changes: 4 additions & 4 deletions packages/shared/lib/services/proxy.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,13 @@ class ProxyService {
}

const configBody: ApplicationConstructedProxyConfiguration = {
endpoint: endpoint,
endpoint,
method: method?.toUpperCase() as HTTP_VERB,
template,
token: token || '',
provider: provider,
providerConfigKey: providerConfigKey,
connectionId: connectionId,
providerConfigKey,
connectionId,
headers: headers as Record<string, string>,
data,
retries: retries || 0,
Expand All @@ -194,7 +194,7 @@ class ProxyService {
// Coming from a flow it is not a proxy call since the worker
// makes the request so we don't allow an override in that case
decompress: (externalConfig as UserProvidedProxyConfiguration).decompress === 'true' || externalConfig.decompress === true,
connection: connection,
connection,
params: externalConfig.params as Record<string, string>,
paramsSerializer: externalConfig.paramsSerializer as ParamsSerializerOptions,
responseType: externalConfig.responseType as ResponseType
Expand Down
5 changes: 5 additions & 0 deletions packages/shared/lib/utils/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,11 @@ export class NangoError extends Error {
this.message = 'Provider configuration cannot be edited for API key based authentication.';
break;

case 'connection_test_failed':
this.status = status || 400;
this.message = `The given credentials were found to be invalid${status ? ` and received a ${status} on a test API call` : ''}. Please check the credentials and try again.`;
break;

case 'invalid_auth_mode':
this.status = 400;
this.message = 'Invalid auth mode. The provider does not support this auth mode.';
Expand Down
5 changes: 3 additions & 2 deletions packages/shared/lib/utils/metrics.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ export enum MetricTypes {
}

export enum SpanTypes {
JOBS_IDLE_DEMO = 'nango.jobs.cron.idleDemos',
JOBS_CLEAN_ACTIVITY_LOGS = 'nango.jobs.cron.cleanActivityLogs'
CONNECTION_TEST = 'nango.server.hooks.connectionTest',
JOBS_CLEAN_ACTIVITY_LOGS = 'nango.jobs.cron.cleanActivityLogs',
JOBS_IDLE_DEMO = 'nango.jobs.cron.idleDemos'
}

class MetricsManager {
Expand Down
12 changes: 12 additions & 0 deletions packages/shared/providers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ ashby:
auth_mode: BASIC
proxy:
base_url: https://api.ashbyhq.com
verification:
method: POST
endpoint: apiKey.info
docs: https://developers.ashbyhq.com/reference/introduction
atlassian:
auth_mode: OAUTH2
authorization_url: https://auth.atlassian.com/authorize
Expand Down Expand Up @@ -152,6 +156,10 @@ bamboohr-basic:
auth_mode: BASIC
proxy:
base_url: https://api.bamboohr.com/api/gateway.php/${connectionConfig.subdomain}
verification:
method: GET
endpoint: /v1/meta/fields
docs: https://documentation.bamboohr.com/reference
battlenet:
auth_mode: OAUTH2
authorization_url: https://oauth.battle.${connectionConfig.extension}/authorize
Expand Down Expand Up @@ -659,6 +667,10 @@ hibob-service-user:
auth_mode: BASIC
proxy:
base_url: https://api.hibob.com
verification:
method: GET
endpoint: /v1/company/named-lists
docs: https://apidocs.hibob.com/
highlevel:
auth_mode: OAUTH2
authorization_url: https://marketplace.gohighlevel.com/oauth/chooselocation
Expand Down

0 comments on commit 10b6bb4

Please sign in to comment.