mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Connector] API for adding OAuth support to ServiceNow connectors (#131084)
* Adding new OAuth fields to ServiceNow ExternalIncidentServiceConfigurationBase and ExternalIncidentServiceSecretConfiguration * Creating new function in ConnectorTokenClient for updating or replacing token * Update servicenow executors to get Oauth access tokens if configured. Still need to update unit tests for services * Creating wrapper function for createService to only create one axios instance * Fixing translation check error * Adding migration for adding isOAuth to service now connectors * Fixing unit tests * Fixing functional test * Not requiring privateKeyPassword * Fixing tests * Adding functional tests for connector creation * Adding functional tests * Fixing functional test * PR feedback * Fixing test * PR feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
cf46ec9950
commit
9d15ab1ec2
33 changed files with 2694 additions and 298 deletions
|
@ -14,6 +14,7 @@ const createConnectorTokenClientMock = () => {
|
|||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
deleteConnectorTokens: jest.fn(),
|
||||
updateOrReplace: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
|
|
@ -357,3 +357,144 @@ describe('delete()', () => {
|
|||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateOrReplace()', () => {
|
||||
test('creates new SO if current token is null', async () => {
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'connector_token',
|
||||
attributes: {
|
||||
connectorId: '123',
|
||||
tokenType: 'access_token',
|
||||
token: 'testtokenvalue',
|
||||
expiresAt: new Date().toISOString(),
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
await connectorTokenClient.updateOrReplace({
|
||||
connectorId: '1',
|
||||
token: null,
|
||||
newToken: 'newToken',
|
||||
expiresInSec: 1000,
|
||||
deleteExisting: false,
|
||||
});
|
||||
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1);
|
||||
expect((unsecuredSavedObjectsClient.create.mock.calls[0][1] as ConnectorToken).token).toBe(
|
||||
'newToken'
|
||||
);
|
||||
|
||||
expect(unsecuredSavedObjectsClient.find).not.toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('creates new SO and deletes all existing tokens for connector if current token is null and deleteExisting is true', async () => {
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'connector_token',
|
||||
attributes: {
|
||||
connectorId: '123',
|
||||
tokenType: 'access_token',
|
||||
token: 'testtokenvalue',
|
||||
expiresAt: new Date().toISOString(),
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
unsecuredSavedObjectsClient.find.mockResolvedValueOnce({
|
||||
total: 1,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'connector_token',
|
||||
attributes: {
|
||||
connectorId: '123',
|
||||
tokenType: 'access_token',
|
||||
createdAt: new Date().toISOString(),
|
||||
expiresAt: new Date().toISOString(),
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'connector_token',
|
||||
attributes: {
|
||||
connectorId: '123',
|
||||
tokenType: 'access_token',
|
||||
createdAt: new Date().toISOString(),
|
||||
expiresAt: new Date().toISOString(),
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
await connectorTokenClient.updateOrReplace({
|
||||
connectorId: '1',
|
||||
token: null,
|
||||
newToken: 'newToken',
|
||||
expiresInSec: 1000,
|
||||
deleteExisting: true,
|
||||
});
|
||||
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1);
|
||||
expect((unsecuredSavedObjectsClient.create.mock.calls[0][1] as ConnectorToken).token).toBe(
|
||||
'newToken'
|
||||
);
|
||||
|
||||
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1);
|
||||
expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('updates existing SO if current token exists', async () => {
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'connector_token',
|
||||
attributes: {
|
||||
connectorId: '123',
|
||||
tokenType: 'access_token',
|
||||
token: 'testtokenvalue',
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
unsecuredSavedObjectsClient.checkConflicts.mockResolvedValueOnce({
|
||||
errors: [],
|
||||
});
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'connector_token',
|
||||
attributes: {
|
||||
connectorId: '123',
|
||||
tokenType: 'access_token',
|
||||
token: 'testtokenvalue',
|
||||
expiresAt: new Date().toISOString(),
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
await connectorTokenClient.updateOrReplace({
|
||||
connectorId: '1',
|
||||
token: {
|
||||
id: '3',
|
||||
connectorId: '123',
|
||||
tokenType: 'access_token',
|
||||
token: 'testtokenvalue',
|
||||
createdAt: new Date().toISOString(),
|
||||
expiresAt: new Date().toISOString(),
|
||||
},
|
||||
newToken: 'newToken',
|
||||
expiresInSec: 1000,
|
||||
deleteExisting: true,
|
||||
});
|
||||
|
||||
expect(unsecuredSavedObjectsClient.find).not.toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.delete).not.toHaveBeenCalled();
|
||||
|
||||
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
|
||||
expect(unsecuredSavedObjectsClient.checkConflicts).toHaveBeenCalledTimes(1);
|
||||
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1);
|
||||
expect((unsecuredSavedObjectsClient.create.mock.calls[0][1] as ConnectorToken).token).toBe(
|
||||
'newToken'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -33,6 +33,13 @@ export interface UpdateOptions {
|
|||
tokenType?: string;
|
||||
}
|
||||
|
||||
interface UpdateOrReplaceOptions {
|
||||
connectorId: string;
|
||||
token: ConnectorToken | null;
|
||||
newToken: string;
|
||||
expiresInSec: number;
|
||||
deleteExisting: boolean;
|
||||
}
|
||||
export class ConnectorTokenClient {
|
||||
private readonly logger: Logger;
|
||||
private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
|
@ -245,4 +252,36 @@ export class ConnectorTokenClient {
|
|||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
public async updateOrReplace({
|
||||
connectorId,
|
||||
token,
|
||||
newToken,
|
||||
expiresInSec,
|
||||
deleteExisting,
|
||||
}: UpdateOrReplaceOptions) {
|
||||
expiresInSec = expiresInSec ?? 3600;
|
||||
if (token === null) {
|
||||
if (deleteExisting) {
|
||||
await this.deleteConnectorTokens({
|
||||
connectorId,
|
||||
tokenType: 'access_token',
|
||||
});
|
||||
}
|
||||
|
||||
await this.create({
|
||||
connectorId,
|
||||
token: newToken,
|
||||
expiresAtMillis: new Date(Date.now() + expiresInSec * 1000).toISOString(),
|
||||
tokenType: 'access_token',
|
||||
});
|
||||
} else {
|
||||
await this.update({
|
||||
id: token.id!.toString(),
|
||||
token: newToken,
|
||||
expiresAtMillis: new Date(Date.now() + expiresInSec * 1000).toISOString(),
|
||||
tokenType: 'access_token',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ import { createJWTAssertion } from './create_jwt_assertion';
|
|||
const jwtSign = jwt.sign as jest.Mock;
|
||||
const mockLogger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
Date.now = jest.fn(() => 0);
|
||||
|
||||
describe('createJWTAssertion', () => {
|
||||
test('creating a JWT token from provided claims with default values', () => {
|
||||
jwtSign.mockReturnValueOnce('123456qwertyjwttoken');
|
||||
|
@ -27,6 +29,28 @@ describe('createJWTAssertion', () => {
|
|||
subject: 'test@gmail.com',
|
||||
});
|
||||
|
||||
expect(jwtSign).toHaveBeenCalledWith(
|
||||
{ aud: '1', exp: 3600, iat: 0, iss: 'someappid', sub: 'test@gmail.com' },
|
||||
{ key: 'test', passphrase: '123456' },
|
||||
{ algorithm: 'RS256' }
|
||||
);
|
||||
expect(assertion).toMatchInlineSnapshot('"123456qwertyjwttoken"');
|
||||
});
|
||||
|
||||
test('creating a JWT token when private key password is null', () => {
|
||||
jwtSign.mockReturnValueOnce('123456qwertyjwttoken');
|
||||
|
||||
const assertion = createJWTAssertion(mockLogger, 'test', null, {
|
||||
audience: '1',
|
||||
issuer: 'someappid',
|
||||
subject: 'test@gmail.com',
|
||||
});
|
||||
|
||||
expect(jwtSign).toHaveBeenCalledWith(
|
||||
{ aud: '1', exp: 3600, iat: 0, iss: 'someappid', sub: 'test@gmail.com' },
|
||||
'test',
|
||||
{ algorithm: 'RS256' }
|
||||
);
|
||||
expect(assertion).toMatchInlineSnapshot('"123456qwertyjwttoken"');
|
||||
});
|
||||
|
||||
|
|
|
@ -12,18 +12,18 @@ export interface JWTClaims {
|
|||
audience: string;
|
||||
subject: string;
|
||||
issuer: string;
|
||||
expireInMilisecons?: number;
|
||||
expireInMilliseconds?: number;
|
||||
keyId?: string;
|
||||
}
|
||||
|
||||
export function createJWTAssertion(
|
||||
logger: Logger,
|
||||
privateKey: string,
|
||||
privateKeyPassword: string,
|
||||
privateKeyPassword: string | null,
|
||||
reservedClaims: JWTClaims,
|
||||
customClaims?: Record<string, string>
|
||||
): string {
|
||||
const { subject, audience, issuer, expireInMilisecons, keyId } = reservedClaims;
|
||||
const { subject, audience, issuer, expireInMilliseconds, keyId } = reservedClaims;
|
||||
const iat = Math.floor(Date.now() / 1000);
|
||||
|
||||
const headerObj = { algorithm: 'RS256' as Algorithm, ...(keyId ? { keyid: keyId } : {}) };
|
||||
|
@ -33,17 +33,19 @@ export function createJWTAssertion(
|
|||
aud: audience, // audience claim identifies the recipients that the JWT is intended for
|
||||
iss: issuer, // issuer claim identifies the principal that issued the JWT
|
||||
iat, // issued at claim identifies the time at which the JWT was issued
|
||||
exp: iat + (expireInMilisecons ?? 3600), // expiration time claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing
|
||||
exp: iat + (expireInMilliseconds ?? 3600), // expiration time claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing
|
||||
...(customClaims ?? {}),
|
||||
};
|
||||
|
||||
try {
|
||||
const jwtToken = jwt.sign(
|
||||
JSON.stringify(payloadObj),
|
||||
{
|
||||
key: privateKey,
|
||||
passphrase: privateKeyPassword,
|
||||
},
|
||||
payloadObj,
|
||||
privateKeyPassword
|
||||
? {
|
||||
key: privateKey,
|
||||
passphrase: privateKeyPassword,
|
||||
}
|
||||
: privateKey,
|
||||
headerObj
|
||||
);
|
||||
return jwtToken;
|
||||
|
|
|
@ -601,7 +601,7 @@ describe('send_email module', () => {
|
|||
|
||||
await sendEmail(mockLogger, sendEmailOptions, connectorTokenClientM);
|
||||
expect(requestOAuthClientCredentialsTokenMock.mock.calls.length).toBe(1);
|
||||
expect(connectorTokenClientM.deleteConnectorTokens.mock.calls.length).toBe(1);
|
||||
expect(connectorTokenClientM.updateOrReplace.mock.calls.length).toBe(1);
|
||||
|
||||
delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities;
|
||||
sendEmailGraphApiMock.mock.calls[0].pop();
|
||||
|
|
|
@ -105,30 +105,13 @@ async function sendEmailWithExchange(
|
|||
|
||||
// try to update connector_token SO
|
||||
try {
|
||||
if (connectorToken === null) {
|
||||
if (hasErrors) {
|
||||
// delete existing access tokens
|
||||
await connectorTokenClient.deleteConnectorTokens({
|
||||
connectorId,
|
||||
tokenType: 'access_token',
|
||||
});
|
||||
}
|
||||
await connectorTokenClient.create({
|
||||
connectorId,
|
||||
token: accessToken,
|
||||
// convert MS Exchange expiresIn from seconds to milliseconds
|
||||
expiresAtMillis: new Date(Date.now() + tokenResult.expiresIn * 1000).toISOString(),
|
||||
tokenType: 'access_token',
|
||||
});
|
||||
} else {
|
||||
await connectorTokenClient.update({
|
||||
id: connectorToken.id!.toString(),
|
||||
token: accessToken,
|
||||
// convert MS Exchange expiresIn from seconds to milliseconds
|
||||
expiresAtMillis: new Date(Date.now() + tokenResult.expiresIn * 1000).toISOString(),
|
||||
tokenType: 'access_token',
|
||||
});
|
||||
}
|
||||
await connectorTokenClient.updateOrReplace({
|
||||
connectorId,
|
||||
token: connectorToken,
|
||||
newToken: accessToken,
|
||||
expiresInSec: tokenResult.expiresIn,
|
||||
deleteExisting: hasErrors,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`Not able to update connector token for connectorId: ${connectorId} due to error: ${err.message}`
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { createServiceWrapper } from './create_service_wrapper';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { actionsConfigMock } from '../../actions_config.mock';
|
||||
import { connectorTokenClientMock } from '../lib/connector_token_client.mock';
|
||||
import { snExternalServiceConfig } from './config';
|
||||
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
const connectorTokenClient = connectorTokenClientMock.create();
|
||||
const configurationUtilities = actionsConfigMock.create();
|
||||
|
||||
jest.mock('axios');
|
||||
axios.create = jest.fn(() => axios);
|
||||
|
||||
describe('createServiceWrapper', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('creates axios instance with apiUrl', () => {
|
||||
const createServiceFn = jest.fn();
|
||||
const credentials = {
|
||||
config: {
|
||||
apiUrl: 'https://test-sn.service-now.com',
|
||||
},
|
||||
secrets: {
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
},
|
||||
};
|
||||
const serviceConfig = snExternalServiceConfig['.servicenow'];
|
||||
createServiceWrapper({
|
||||
connectorId: '123',
|
||||
credentials,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
serviceConfig,
|
||||
connectorTokenClient,
|
||||
createServiceFn,
|
||||
});
|
||||
|
||||
expect(createServiceFn).toHaveBeenCalledWith({
|
||||
credentials,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
serviceConfig,
|
||||
axiosInstance: axios,
|
||||
});
|
||||
});
|
||||
|
||||
test('handles apiUrl with trailing slash', () => {
|
||||
const createServiceFn = jest.fn();
|
||||
const credentials = {
|
||||
config: {
|
||||
apiUrl: 'https://test-sn.service-now.com/',
|
||||
},
|
||||
secrets: {
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
},
|
||||
};
|
||||
const serviceConfig = snExternalServiceConfig['.servicenow'];
|
||||
createServiceWrapper({
|
||||
connectorId: '123',
|
||||
credentials,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
serviceConfig,
|
||||
connectorTokenClient,
|
||||
createServiceFn,
|
||||
});
|
||||
|
||||
expect(createServiceFn).toHaveBeenCalledWith({
|
||||
credentials,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
serviceConfig,
|
||||
axiosInstance: axios,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { ExternalService, ExternalServiceCredentials, SNProductsConfigValue } from './types';
|
||||
|
||||
import { ServiceNowPublicConfigurationType, ServiceFactory } from './types';
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import { getAxiosInstance } from './utils';
|
||||
import { ConnectorTokenClientContract } from '../../types';
|
||||
|
||||
interface CreateServiceWrapperOpts<T = ExternalService> {
|
||||
connectorId: string;
|
||||
credentials: ExternalServiceCredentials;
|
||||
logger: Logger;
|
||||
configurationUtilities: ActionsConfigurationUtilities;
|
||||
serviceConfig: SNProductsConfigValue;
|
||||
connectorTokenClient: ConnectorTokenClientContract;
|
||||
createServiceFn: ServiceFactory<T>;
|
||||
}
|
||||
|
||||
export function createServiceWrapper<T = ExternalService>({
|
||||
connectorId,
|
||||
credentials,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
serviceConfig,
|
||||
connectorTokenClient,
|
||||
createServiceFn,
|
||||
}: CreateServiceWrapperOpts<T>): T {
|
||||
const { config } = credentials;
|
||||
const { apiUrl: url } = config as ServiceNowPublicConfigurationType;
|
||||
const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url;
|
||||
const axiosInstance = getAxiosInstance({
|
||||
connectorId,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
credentials,
|
||||
snServiceUrl: urlWithoutTrailingSlash,
|
||||
connectorTokenClient,
|
||||
});
|
||||
|
||||
return createServiceFn({
|
||||
credentials,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
serviceConfig,
|
||||
axiosInstance,
|
||||
});
|
||||
}
|
|
@ -39,6 +39,7 @@ import {
|
|||
ExternalServiceApiITOM,
|
||||
ExternalServiceITOM,
|
||||
ServiceNowPublicConfigurationBaseType,
|
||||
ExternalService,
|
||||
} from './types';
|
||||
import {
|
||||
ServiceNowITOMActionTypeId,
|
||||
|
@ -53,6 +54,7 @@ import { apiSIR } from './api_sir';
|
|||
import { throwIfSubActionIsNotSupported } from './utils';
|
||||
import { createExternalServiceITOM } from './service_itom';
|
||||
import { apiITOM } from './api_itom';
|
||||
import { createServiceWrapper } from './create_service_wrapper';
|
||||
|
||||
export {
|
||||
ServiceNowITSMActionTypeId,
|
||||
|
@ -97,6 +99,7 @@ export function getServiceNowITSMActionType(
|
|||
secrets: schema.object(ExternalIncidentServiceSecretConfiguration, {
|
||||
validate: curry(validate.secrets)(configurationUtilities),
|
||||
}),
|
||||
connector: validate.connector,
|
||||
params: ExecutorParamsSchemaITSM,
|
||||
},
|
||||
executor: curry(executor)({
|
||||
|
@ -124,6 +127,7 @@ export function getServiceNowSIRActionType(
|
|||
secrets: schema.object(ExternalIncidentServiceSecretConfiguration, {
|
||||
validate: curry(validate.secrets)(configurationUtilities),
|
||||
}),
|
||||
connector: validate.connector,
|
||||
params: ExecutorParamsSchemaSIR,
|
||||
},
|
||||
executor: curry(executor)({
|
||||
|
@ -151,6 +155,7 @@ export function getServiceNowITOMActionType(
|
|||
secrets: schema.object(ExternalIncidentServiceSecretConfiguration, {
|
||||
validate: curry(validate.secrets)(configurationUtilities),
|
||||
}),
|
||||
connector: validate.connector,
|
||||
params: ExecutorParamsSchemaITOM,
|
||||
},
|
||||
executor: curry(executorITOM)({
|
||||
|
@ -184,20 +189,24 @@ async function executor(
|
|||
ExecutorParams
|
||||
>
|
||||
): Promise<ActionTypeExecutorResult<ServiceNowExecutorResultData | {}>> {
|
||||
const { actionId, config, params, secrets } = execOptions;
|
||||
const { actionId, config, params, secrets, services } = execOptions;
|
||||
const { subAction, subActionParams } = params;
|
||||
const connectorTokenClient = services.connectorTokenClient;
|
||||
const externalServiceConfig = snExternalServiceConfig[actionTypeId];
|
||||
let data: ServiceNowExecutorResultData | null = null;
|
||||
|
||||
const externalService = createService(
|
||||
{
|
||||
const externalService = createServiceWrapper<ExternalService>({
|
||||
connectorId: actionId,
|
||||
credentials: {
|
||||
config,
|
||||
secrets,
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
externalServiceConfig
|
||||
);
|
||||
serviceConfig: externalServiceConfig,
|
||||
connectorTokenClient,
|
||||
createServiceFn: createService,
|
||||
});
|
||||
|
||||
const apiAsRecord = api as unknown as Record<string, unknown>;
|
||||
throwIfSubActionIsNotSupported({ api: apiAsRecord, subAction, supportedSubActions, logger });
|
||||
|
@ -260,18 +269,22 @@ async function executorITOM(
|
|||
): Promise<ActionTypeExecutorResult<ServiceNowExecutorResultData | {}>> {
|
||||
const { actionId, config, params, secrets } = execOptions;
|
||||
const { subAction, subActionParams } = params;
|
||||
const connectorTokenClient = execOptions.services.connectorTokenClient;
|
||||
const externalServiceConfig = snExternalServiceConfig[actionTypeId];
|
||||
let data: ServiceNowExecutorResultData | null = null;
|
||||
|
||||
const externalService = createService(
|
||||
{
|
||||
const externalService = createServiceWrapper<ExternalServiceITOM>({
|
||||
connectorId: actionId,
|
||||
credentials: {
|
||||
config,
|
||||
secrets,
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
externalServiceConfig
|
||||
) as ExternalServiceITOM;
|
||||
serviceConfig: externalServiceConfig,
|
||||
connectorTokenClient,
|
||||
createServiceFn: createService,
|
||||
});
|
||||
|
||||
const apiAsRecord = api as unknown as Record<string, unknown>;
|
||||
|
||||
|
|
|
@ -10,6 +10,10 @@ import { DEFAULT_ALERTS_GROUPING_KEY } from './config';
|
|||
|
||||
export const ExternalIncidentServiceConfigurationBase = {
|
||||
apiUrl: schema.string(),
|
||||
isOAuth: schema.boolean({ defaultValue: false }),
|
||||
userIdentifierValue: schema.nullable(schema.string()), // required if isOAuth = true
|
||||
clientId: schema.nullable(schema.string()), // required if isOAuth = true
|
||||
jwtKeyId: schema.nullable(schema.string()), // required if isOAuth = true
|
||||
};
|
||||
|
||||
export const ExternalIncidentServiceConfiguration = {
|
||||
|
@ -26,8 +30,11 @@ export const ExternalIncidentServiceConfigurationSchema = schema.object(
|
|||
);
|
||||
|
||||
export const ExternalIncidentServiceSecretConfiguration = {
|
||||
password: schema.string(),
|
||||
username: schema.string(),
|
||||
password: schema.nullable(schema.string()), // required if isOAuth = false
|
||||
username: schema.nullable(schema.string()), // required if isOAuth = false
|
||||
clientSecret: schema.nullable(schema.string()), // required if isOAuth = true
|
||||
privateKey: schema.nullable(schema.string()), // required if isOAuth = true
|
||||
privateKeyPassword: schema.nullable(schema.string()),
|
||||
};
|
||||
|
||||
export const ExternalIncidentServiceSecretConfigurationSchema = schema.object(
|
||||
|
|
|
@ -147,64 +147,240 @@ describe('ServiceNow service', () => {
|
|||
let service: ExternalService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = createExternalService(
|
||||
{
|
||||
jest.clearAllMocks();
|
||||
service = createExternalService({
|
||||
credentials: {
|
||||
// The trailing slash at the end of the url is intended.
|
||||
// All API calls need to have the trailing slash removed.
|
||||
config: { apiUrl: 'https://example.com/' },
|
||||
config: { apiUrl: 'https://example.com/', isOAuth: false },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
snExternalServiceConfig['.servicenow']
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
serviceConfig: snExternalServiceConfig['.servicenow'],
|
||||
axiosInstance: axios,
|
||||
});
|
||||
});
|
||||
|
||||
describe('createExternalService', () => {
|
||||
test('throws without url', () => {
|
||||
expect(() =>
|
||||
createExternalService(
|
||||
{
|
||||
config: { apiUrl: null },
|
||||
createExternalService({
|
||||
credentials: {
|
||||
config: { apiUrl: null, isOAuth: false },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
snExternalServiceConfig['.servicenow']
|
||||
)
|
||||
serviceConfig: snExternalServiceConfig['.servicenow'],
|
||||
axiosInstance: axios,
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test('throws without username', () => {
|
||||
expect(() =>
|
||||
createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'test.com' },
|
||||
secrets: { username: '', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
snExternalServiceConfig['.servicenow']
|
||||
)
|
||||
).toThrow();
|
||||
test('throws when isOAuth is false and basic auth required values are falsy', () => {
|
||||
const badBasicCredentials = [
|
||||
{
|
||||
config: { apiUrl: 'test.com', isOAuth: false },
|
||||
secrets: { username: '', password: 'admin' },
|
||||
},
|
||||
{
|
||||
config: { apiUrl: 'test.com', isOAuth: false },
|
||||
secrets: { username: null, password: 'admin' },
|
||||
},
|
||||
{
|
||||
config: { apiUrl: 'test.com', isOAuth: false },
|
||||
secrets: { password: 'admin' },
|
||||
},
|
||||
{
|
||||
config: { apiUrl: 'test.com', isOAuth: false },
|
||||
secrets: { username: 'admin', password: '' },
|
||||
},
|
||||
{
|
||||
config: { apiUrl: 'test.com', isOAuth: false },
|
||||
secrets: { username: 'admin', password: null },
|
||||
},
|
||||
{
|
||||
config: { apiUrl: 'test.com', isOAuth: false },
|
||||
secrets: { username: 'admin' },
|
||||
},
|
||||
];
|
||||
|
||||
badBasicCredentials.forEach((badCredentials) => {
|
||||
expect(() =>
|
||||
createExternalService({
|
||||
credentials: badCredentials,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
serviceConfig: snExternalServiceConfig['.servicenow'],
|
||||
axiosInstance: axios,
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
test('throws without password', () => {
|
||||
expect(() =>
|
||||
createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'test.com' },
|
||||
secrets: { username: '', password: undefined },
|
||||
test('throws when isOAuth is true and OAuth required values are falsy', () => {
|
||||
const badOAuthCredentials = [
|
||||
{
|
||||
config: {
|
||||
apiUrl: 'test.com',
|
||||
isOAuth: true,
|
||||
clientId: '',
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
userIdentifierValue: 'user@email.com',
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
snExternalServiceConfig['.servicenow']
|
||||
)
|
||||
).toThrow();
|
||||
secrets: { clientSecret: 'clientSecret', privateKey: 'privateKey' },
|
||||
},
|
||||
{
|
||||
config: {
|
||||
apiUrl: 'test.com',
|
||||
isOAuth: true,
|
||||
clientId: null,
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
userIdentifierValue: 'user@email.com',
|
||||
},
|
||||
secrets: { clientSecret: 'clientSecret', privateKey: 'privateKey' },
|
||||
},
|
||||
{
|
||||
config: {
|
||||
apiUrl: 'test.com',
|
||||
isOAuth: true,
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
userIdentifierValue: 'user@email.com',
|
||||
},
|
||||
secrets: { clientSecret: 'clientSecret', privateKey: 'privateKey' },
|
||||
},
|
||||
{
|
||||
config: {
|
||||
apiUrl: 'test.com',
|
||||
isOAuth: true,
|
||||
clientId: 'clientId',
|
||||
jwtKeyId: '',
|
||||
userIdentifierValue: 'user@email.com',
|
||||
},
|
||||
secrets: { clientSecret: 'clientSecret', privateKey: 'privateKey' },
|
||||
},
|
||||
{
|
||||
config: {
|
||||
apiUrl: 'test.com',
|
||||
isOAuth: true,
|
||||
clientId: 'clientId',
|
||||
jwtKeyId: null,
|
||||
userIdentifierValue: 'user@email.com',
|
||||
},
|
||||
secrets: { clientSecret: 'clientSecret', privateKey: 'privateKey' },
|
||||
},
|
||||
{
|
||||
config: {
|
||||
apiUrl: 'test.com',
|
||||
isOAuth: true,
|
||||
clientId: 'clientId',
|
||||
userIdentifierValue: 'user@email.com',
|
||||
},
|
||||
secrets: { clientSecret: 'clientSecret', privateKey: 'privateKey' },
|
||||
},
|
||||
{
|
||||
config: {
|
||||
apiUrl: 'test.com',
|
||||
isOAuth: true,
|
||||
clientId: 'clientId',
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
userIdentifierValue: '',
|
||||
},
|
||||
secrets: { clientSecret: 'clientSecret', privateKey: 'privateKey' },
|
||||
},
|
||||
{
|
||||
config: {
|
||||
apiUrl: 'test.com',
|
||||
isOAuth: true,
|
||||
clientId: 'clientId',
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
userIdentifierValue: null,
|
||||
},
|
||||
secrets: { clientSecret: 'clientSecret', privateKey: 'privateKey' },
|
||||
},
|
||||
{
|
||||
config: {
|
||||
apiUrl: 'test.com',
|
||||
isOAuth: true,
|
||||
clientId: 'clientId',
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
},
|
||||
secrets: { clientSecret: 'clientSecret', privateKey: 'privateKey' },
|
||||
},
|
||||
{
|
||||
config: {
|
||||
apiUrl: 'test.com',
|
||||
isOAuth: true,
|
||||
clientId: 'clientId',
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
userIdentifierValue: 'user@email.com',
|
||||
},
|
||||
secrets: { clientSecret: '', privateKey: 'privateKey' },
|
||||
},
|
||||
{
|
||||
config: {
|
||||
apiUrl: 'test.com',
|
||||
isOAuth: true,
|
||||
clientId: 'clientId',
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
userIdentifierValue: 'user@email.com',
|
||||
},
|
||||
secrets: { clientSecret: null, privateKey: 'privateKey' },
|
||||
},
|
||||
{
|
||||
config: {
|
||||
apiUrl: 'test.com',
|
||||
isOAuth: true,
|
||||
clientId: 'clientId',
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
userIdentifierValue: 'user@email.com',
|
||||
},
|
||||
secrets: { privateKey: 'privateKey' },
|
||||
},
|
||||
{
|
||||
config: {
|
||||
apiUrl: 'test.com',
|
||||
isOAuth: true,
|
||||
clientId: 'clientId',
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
userIdentifierValue: 'user@email.com',
|
||||
},
|
||||
secrets: { clientSecret: 'clientSecret', privateKey: '' },
|
||||
},
|
||||
{
|
||||
config: {
|
||||
apiUrl: 'test.com',
|
||||
isOAuth: true,
|
||||
clientId: 'clientId',
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
userIdentifierValue: 'user@email.com',
|
||||
},
|
||||
secrets: { clientSecret: 'clientSecret', privateKey: null },
|
||||
},
|
||||
{
|
||||
config: {
|
||||
apiUrl: 'test.com',
|
||||
isOAuth: true,
|
||||
clientId: 'clientId',
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
userIdentifierValue: 'user@email.com',
|
||||
},
|
||||
secrets: { clientSecret: 'clientSecret' },
|
||||
},
|
||||
];
|
||||
|
||||
badOAuthCredentials.forEach((badCredentials) => {
|
||||
expect(() =>
|
||||
createExternalService({
|
||||
credentials: badCredentials,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
serviceConfig: snExternalServiceConfig['.servicenow'],
|
||||
axiosInstance: axios,
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -233,15 +409,16 @@ describe('ServiceNow service', () => {
|
|||
});
|
||||
|
||||
test('it should call request with correct arguments when table changes', async () => {
|
||||
service = createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'https://example.com/' },
|
||||
service = createExternalService({
|
||||
credentials: {
|
||||
config: { apiUrl: 'https://example.com/', isOAuth: false },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
{ ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' }
|
||||
);
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' },
|
||||
axiosInstance: axios,
|
||||
});
|
||||
|
||||
requestMock.mockImplementation(() => ({
|
||||
data: { result: { sys_id: '1', number: 'INC01' } },
|
||||
|
@ -298,15 +475,16 @@ describe('ServiceNow service', () => {
|
|||
});
|
||||
|
||||
test('it should call request with correct arguments when table changes', async () => {
|
||||
service = createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'https://example.com/' },
|
||||
service = createExternalService({
|
||||
credentials: {
|
||||
config: { apiUrl: 'https://example.com/', isOAuth: false },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
snExternalServiceConfig['.servicenow-sir']
|
||||
);
|
||||
serviceConfig: snExternalServiceConfig['.servicenow-sir'],
|
||||
axiosInstance: axios,
|
||||
});
|
||||
|
||||
const res = await createIncident(service);
|
||||
|
||||
|
@ -382,15 +560,16 @@ describe('ServiceNow service', () => {
|
|||
// old connectors
|
||||
describe('table API', () => {
|
||||
beforeEach(() => {
|
||||
service = createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'https://example.com/' },
|
||||
service = createExternalService({
|
||||
credentials: {
|
||||
config: { apiUrl: 'https://example.com/', isOAuth: false },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
{ ...snExternalServiceConfig['.servicenow'], useImportAPI: false }
|
||||
);
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], useImportAPI: false },
|
||||
axiosInstance: axios,
|
||||
});
|
||||
});
|
||||
|
||||
test('it creates the incident correctly', async () => {
|
||||
|
@ -418,15 +597,16 @@ describe('ServiceNow service', () => {
|
|||
});
|
||||
|
||||
test('it should call request with correct arguments when table changes', async () => {
|
||||
service = createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'https://example.com/' },
|
||||
service = createExternalService({
|
||||
credentials: {
|
||||
config: { apiUrl: 'https://example.com/', isOAuth: false },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
{ ...snExternalServiceConfig['.servicenow-sir'], useImportAPI: false }
|
||||
);
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow-sir'], useImportAPI: false },
|
||||
axiosInstance: axios,
|
||||
});
|
||||
|
||||
mockIncidentResponse(false);
|
||||
|
||||
|
@ -468,15 +648,16 @@ describe('ServiceNow service', () => {
|
|||
});
|
||||
|
||||
test('it should call request with correct arguments when table changes', async () => {
|
||||
service = createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'https://example.com/' },
|
||||
service = createExternalService({
|
||||
credentials: {
|
||||
config: { apiUrl: 'https://example.com/', isOAuth: false },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
snExternalServiceConfig['.servicenow-sir']
|
||||
);
|
||||
serviceConfig: snExternalServiceConfig['.servicenow-sir'],
|
||||
axiosInstance: axios,
|
||||
});
|
||||
|
||||
const res = await updateIncident(service);
|
||||
expect(requestMock).toHaveBeenNthCalledWith(1, {
|
||||
|
@ -554,15 +735,16 @@ describe('ServiceNow service', () => {
|
|||
// old connectors
|
||||
describe('table API', () => {
|
||||
beforeEach(() => {
|
||||
service = createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'https://example.com/' },
|
||||
service = createExternalService({
|
||||
credentials: {
|
||||
config: { apiUrl: 'https://example.com/', isOAuth: false },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
{ ...snExternalServiceConfig['.servicenow'], useImportAPI: false }
|
||||
);
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], useImportAPI: false },
|
||||
axiosInstance: axios,
|
||||
});
|
||||
});
|
||||
|
||||
test('it updates the incident correctly', async () => {
|
||||
|
@ -591,15 +773,16 @@ describe('ServiceNow service', () => {
|
|||
});
|
||||
|
||||
test('it should call request with correct arguments when table changes', async () => {
|
||||
service = createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'https://example.com/' },
|
||||
service = createExternalService({
|
||||
credentials: {
|
||||
config: { apiUrl: 'https://example.com/', isOAuth: false },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
{ ...snExternalServiceConfig['.servicenow-sir'], useImportAPI: false }
|
||||
);
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow-sir'], useImportAPI: false },
|
||||
axiosInstance: axios,
|
||||
});
|
||||
|
||||
mockIncidentResponse(false);
|
||||
|
||||
|
@ -646,15 +829,16 @@ describe('ServiceNow service', () => {
|
|||
});
|
||||
|
||||
test('it should call request with correct arguments when table changes', async () => {
|
||||
service = createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'https://example.com/' },
|
||||
service = createExternalService({
|
||||
credentials: {
|
||||
config: { apiUrl: 'https://example.com/', isOAuth: false },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
{ ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' }
|
||||
);
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' },
|
||||
axiosInstance: axios,
|
||||
});
|
||||
|
||||
requestMock.mockImplementation(() => ({
|
||||
data: { result: serviceNowCommonFields },
|
||||
|
@ -714,15 +898,16 @@ describe('ServiceNow service', () => {
|
|||
});
|
||||
|
||||
test('it should call request with correct arguments when table changes', async () => {
|
||||
service = createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'https://example.com/' },
|
||||
service = createExternalService({
|
||||
credentials: {
|
||||
config: { apiUrl: 'https://example.com/', isOAuth: false },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
{ ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' }
|
||||
);
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' },
|
||||
axiosInstance: axios,
|
||||
});
|
||||
|
||||
requestMock.mockImplementation(() => ({
|
||||
data: { result: serviceNowChoices },
|
||||
|
@ -818,15 +1003,16 @@ describe('ServiceNow service', () => {
|
|||
});
|
||||
|
||||
test('it does not log if useOldApi = true', async () => {
|
||||
service = createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'https://example.com/' },
|
||||
service = createExternalService({
|
||||
credentials: {
|
||||
config: { apiUrl: 'https://example.com/', isOAuth: false },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
{ ...snExternalServiceConfig['.servicenow'], useImportAPI: false }
|
||||
);
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], useImportAPI: false },
|
||||
axiosInstance: axios,
|
||||
});
|
||||
await service.checkIfApplicationIsInstalled();
|
||||
expect(requestMock).not.toHaveBeenCalled();
|
||||
expect(logger.debug).not.toHaveBeenCalled();
|
||||
|
|
|
@ -5,11 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import {
|
||||
ExternalServiceCredentials,
|
||||
ExternalService,
|
||||
ExternalServiceParamsCreate,
|
||||
ExternalServiceParamsUpdate,
|
||||
|
@ -17,29 +15,41 @@ import {
|
|||
ImportSetApiResponseError,
|
||||
ServiceNowIncident,
|
||||
GetApplicationInfoResponse,
|
||||
SNProductsConfigValue,
|
||||
ServiceFactory,
|
||||
} from './types';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType } from './types';
|
||||
import { request } from '../lib/axios_utils';
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import { createServiceError, getPushedDate, prepareIncident } from './utils';
|
||||
|
||||
export const SYS_DICTIONARY_ENDPOINT = `api/now/table/sys_dictionary`;
|
||||
|
||||
export const createExternalService: ServiceFactory = (
|
||||
{ config, secrets }: ExternalServiceCredentials,
|
||||
logger: Logger,
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
{ table, importSetTable, useImportAPI, appScope }: SNProductsConfigValue
|
||||
): ExternalService => {
|
||||
const { apiUrl: url, usesTableApi: usesTableApiConfigValue } =
|
||||
config as ServiceNowPublicConfigurationType;
|
||||
const { username, password } = secrets as ServiceNowSecretConfigurationType;
|
||||
export const createExternalService: ServiceFactory = ({
|
||||
credentials,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
serviceConfig,
|
||||
axiosInstance,
|
||||
}): ExternalService => {
|
||||
const { config, secrets } = credentials;
|
||||
const { table, importSetTable, useImportAPI, appScope } = serviceConfig;
|
||||
const {
|
||||
apiUrl: url,
|
||||
usesTableApi: usesTableApiConfigValue,
|
||||
isOAuth,
|
||||
clientId,
|
||||
jwtKeyId,
|
||||
userIdentifierValue,
|
||||
} = config as ServiceNowPublicConfigurationType;
|
||||
const { username, password, clientSecret, privateKey } =
|
||||
secrets as ServiceNowSecretConfigurationType;
|
||||
|
||||
if (!url || !username || !password) {
|
||||
if (
|
||||
!url ||
|
||||
(!isOAuth && (!username || !password)) ||
|
||||
(isOAuth && (!clientSecret || !privateKey || !clientId || !jwtKeyId || !userIdentifierValue))
|
||||
) {
|
||||
throw Error(`[Action]${i18n.SERVICENOW}: Wrong configuration.`);
|
||||
}
|
||||
|
||||
|
@ -54,10 +64,6 @@ export const createExternalService: ServiceFactory = (
|
|||
*/
|
||||
const getVersionUrl = () => `${urlWithoutTrailingSlash}/api/${appScope}/elastic_api/health`;
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
auth: { username, password },
|
||||
});
|
||||
|
||||
const useTableApi = !useImportAPI || usesTableApiConfigValue;
|
||||
|
||||
const getCreateIncidentUrl = () => (useTableApi ? tableApiIncidentUrl : importSetTableUrl);
|
||||
|
|
|
@ -35,15 +35,16 @@ describe('ServiceNow SIR service', () => {
|
|||
let service: ExternalServiceITOM;
|
||||
|
||||
beforeEach(() => {
|
||||
service = createExternalServiceITOM(
|
||||
{
|
||||
config: { apiUrl: 'https://example.com/' },
|
||||
service = createExternalServiceITOM({
|
||||
credentials: {
|
||||
config: { apiUrl: 'https://example.com/', isOAuth: false },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
snExternalServiceConfig['.servicenow-itom']
|
||||
) as ExternalServiceITOM;
|
||||
serviceConfig: snExternalServiceConfig['.servicenow-itom'],
|
||||
axiosInstance: axios,
|
||||
}) as ExternalServiceITOM;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -4,41 +4,28 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import axios from 'axios';
|
||||
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import {
|
||||
ExternalServiceCredentials,
|
||||
SNProductsConfigValue,
|
||||
ServiceFactory,
|
||||
ExternalServiceITOM,
|
||||
ExecutorSubActionAddEventParams,
|
||||
} from './types';
|
||||
import { ServiceFactory, ExternalServiceITOM, ExecutorSubActionAddEventParams } from './types';
|
||||
|
||||
import { ServiceNowSecretConfigurationType } from './types';
|
||||
import { request } from '../lib/axios_utils';
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import { createExternalService } from './service';
|
||||
import { createServiceError } from './utils';
|
||||
|
||||
const getAddEventURL = (url: string) => `${url}/api/global/em/jsonv2`;
|
||||
|
||||
export const createExternalServiceITOM: ServiceFactory<ExternalServiceITOM> = (
|
||||
credentials: ExternalServiceCredentials,
|
||||
logger: Logger,
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
serviceConfig: SNProductsConfigValue
|
||||
): ExternalServiceITOM => {
|
||||
const snService = createExternalService(
|
||||
export const createExternalServiceITOM: ServiceFactory<ExternalServiceITOM> = ({
|
||||
credentials,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
serviceConfig,
|
||||
axiosInstance,
|
||||
}): ExternalServiceITOM => {
|
||||
const snService = createExternalService({
|
||||
credentials,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
serviceConfig
|
||||
);
|
||||
|
||||
const { username, password } = credentials.secrets as ServiceNowSecretConfigurationType;
|
||||
const axiosInstance = axios.create({
|
||||
auth: { username, password },
|
||||
serviceConfig,
|
||||
axiosInstance,
|
||||
});
|
||||
|
||||
const addEvent = async (params: ExecutorSubActionAddEventParams) => {
|
||||
|
|
|
@ -92,15 +92,16 @@ describe('ServiceNow SIR service', () => {
|
|||
let service: ExternalServiceSIR;
|
||||
|
||||
beforeEach(() => {
|
||||
service = createExternalServiceSIR(
|
||||
{
|
||||
config: { apiUrl: 'https://example.com/' },
|
||||
service = createExternalServiceSIR({
|
||||
credentials: {
|
||||
config: { apiUrl: 'https://example.com/', isOAuth: false },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
snExternalServiceConfig['.servicenow-sir']
|
||||
) as ExternalServiceSIR;
|
||||
serviceConfig: snExternalServiceConfig['.servicenow-sir'],
|
||||
axiosInstance: axios,
|
||||
}) as ExternalServiceSIR;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -5,21 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { Observable, ExternalServiceSIR, ObservableResponse, ServiceFactory } from './types';
|
||||
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import {
|
||||
ExternalServiceCredentials,
|
||||
SNProductsConfigValue,
|
||||
Observable,
|
||||
ExternalServiceSIR,
|
||||
ObservableResponse,
|
||||
ServiceFactory,
|
||||
} from './types';
|
||||
|
||||
import { ServiceNowSecretConfigurationType } from './types';
|
||||
import { request } from '../lib/axios_utils';
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import { createExternalService } from './service';
|
||||
import { createServiceError } from './utils';
|
||||
|
||||
|
@ -29,22 +17,19 @@ const getAddObservableToIncidentURL = (url: string, incidentID: string) =>
|
|||
const getBulkAddObservableToIncidentURL = (url: string, incidentID: string) =>
|
||||
`${url}/api/x_elas2_sir_int/elastic_api/incident/${incidentID}/observables/bulk`;
|
||||
|
||||
export const createExternalServiceSIR: ServiceFactory<ExternalServiceSIR> = (
|
||||
credentials: ExternalServiceCredentials,
|
||||
logger: Logger,
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
serviceConfig: SNProductsConfigValue
|
||||
): ExternalServiceSIR => {
|
||||
const snService = createExternalService(
|
||||
export const createExternalServiceSIR: ServiceFactory<ExternalServiceSIR> = ({
|
||||
credentials,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
serviceConfig,
|
||||
axiosInstance,
|
||||
}): ExternalServiceSIR => {
|
||||
const snService = createExternalService({
|
||||
credentials,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
serviceConfig
|
||||
);
|
||||
|
||||
const { username, password } = credentials.secrets as ServiceNowSecretConfigurationType;
|
||||
const axiosInstance = axios.create({
|
||||
auth: { username, password },
|
||||
serviceConfig,
|
||||
axiosInstance,
|
||||
});
|
||||
|
||||
const _addObservable = async (data: Observable | Observable[], url: string) => {
|
||||
|
|
|
@ -30,3 +30,42 @@ export const ALLOWED_HOSTS_ERROR = (message: string) =>
|
|||
message,
|
||||
},
|
||||
});
|
||||
|
||||
export const CREDENTIALS_ERROR = i18n.translate(
|
||||
'xpack.actions.builtin.configuration.apiCredentialsError',
|
||||
{
|
||||
defaultMessage: 'Either basic auth or OAuth credentials must be specified',
|
||||
}
|
||||
);
|
||||
|
||||
export const BASIC_AUTH_CREDENTIALS_ERROR = i18n.translate(
|
||||
'xpack.actions.builtin.configuration.apiBasicAuthCredentialsError',
|
||||
{
|
||||
defaultMessage: 'username and password must both be specified',
|
||||
}
|
||||
);
|
||||
|
||||
export const OAUTH_CREDENTIALS_ERROR = i18n.translate(
|
||||
'xpack.actions.builtin.configuration.apiOAuthCredentialsError',
|
||||
{
|
||||
defaultMessage: 'clientSecret and privateKey must both be specified',
|
||||
}
|
||||
);
|
||||
|
||||
export const VALIDATE_OAUTH_MISSING_FIELD_ERROR = (field: string, isOAuth: boolean) =>
|
||||
i18n.translate('xpack.actions.builtin.configuration.apiValidateMissingOAuthFieldError', {
|
||||
defaultMessage: '{field} must be provided when isOAuth = {isOAuth}',
|
||||
values: {
|
||||
field,
|
||||
isOAuth: isOAuth ? 'true' : 'false',
|
||||
},
|
||||
});
|
||||
|
||||
export const VALIDATE_OAUTH_POPULATED_FIELD_ERROR = (field: string, isOAuth: boolean) =>
|
||||
i18n.translate('xpack.actions.builtin.configuration.apiValidateOAuthFieldError', {
|
||||
defaultMessage: '{field} should not be provided with isOAuth = {isOAuth}',
|
||||
values: {
|
||||
field,
|
||||
isOAuth: isOAuth ? 'true' : 'false',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import {
|
||||
|
@ -78,6 +78,7 @@ export interface ExternalServiceCredentials {
|
|||
export interface ExternalServiceValidation {
|
||||
config: (configurationUtilities: ActionsConfigurationUtilities, configObject: any) => void;
|
||||
secrets: (configurationUtilities: ActionsConfigurationUtilities, secrets: any) => void;
|
||||
connector: (config: any, secrets: any) => string | null;
|
||||
}
|
||||
|
||||
export interface ExternalServiceIncidentResponse {
|
||||
|
@ -277,12 +278,21 @@ export interface ExternalServiceSIR extends ExternalService {
|
|||
) => Promise<ObservableResponse[]>;
|
||||
}
|
||||
|
||||
export type ServiceFactory<T = ExternalService> = (
|
||||
credentials: ExternalServiceCredentials,
|
||||
logger: Logger,
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
serviceConfig: SNProductsConfigValue
|
||||
) => T;
|
||||
interface ServiceFactoryOpts {
|
||||
credentials: ExternalServiceCredentials;
|
||||
logger: Logger;
|
||||
configurationUtilities: ActionsConfigurationUtilities;
|
||||
serviceConfig: SNProductsConfigValue;
|
||||
axiosInstance: AxiosInstance;
|
||||
}
|
||||
|
||||
export type ServiceFactory<T = ExternalService> = ({
|
||||
credentials,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
serviceConfig,
|
||||
axiosInstance,
|
||||
}: ServiceFactoryOpts) => T;
|
||||
|
||||
/**
|
||||
* ITOM
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AxiosError } from 'axios';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
|
@ -14,10 +14,35 @@ import {
|
|||
createServiceError,
|
||||
getPushedDate,
|
||||
throwIfSubActionIsNotSupported,
|
||||
getAccessToken,
|
||||
getAxiosInstance,
|
||||
} from './utils';
|
||||
import { connectorTokenClientMock } from '../lib/connector_token_client.mock';
|
||||
import { actionsConfigMock } from '../../actions_config.mock';
|
||||
import { createJWTAssertion } from '../lib/create_jwt_assertion';
|
||||
import { requestOAuthJWTToken } from '../lib/request_oauth_jwt_token';
|
||||
|
||||
jest.mock('../lib/create_jwt_assertion', () => ({
|
||||
createJWTAssertion: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/request_oauth_jwt_token', () => ({
|
||||
requestOAuthJWTToken: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('axios', () => ({
|
||||
create: jest.fn(),
|
||||
}));
|
||||
const createAxiosInstanceMock = axios.create as jest.Mock;
|
||||
const axiosInstanceMock = {
|
||||
interceptors: {
|
||||
request: { eject: jest.fn(), use: jest.fn() },
|
||||
response: { eject: jest.fn(), use: jest.fn() },
|
||||
},
|
||||
};
|
||||
|
||||
const connectorTokenClient = connectorTokenClientMock.create();
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
const configurationUtilities = actionsConfigMock.create();
|
||||
/**
|
||||
* The purpose of this test is to
|
||||
* prevent developers from accidentally
|
||||
|
@ -131,4 +156,285 @@ describe('utils', () => {
|
|||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAxiosInstance', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
createAxiosInstanceMock.mockReturnValue(axiosInstanceMock);
|
||||
});
|
||||
|
||||
test('creates axios instance with basic auth when isOAuth is false and username and password are defined', () => {
|
||||
getAxiosInstance({
|
||||
connectorId: '123',
|
||||
logger,
|
||||
configurationUtilities,
|
||||
credentials: {
|
||||
config: {
|
||||
apiUrl: 'https://servicenow',
|
||||
usesTableApi: true,
|
||||
isOAuth: false,
|
||||
clientId: null,
|
||||
jwtKeyId: null,
|
||||
userIdentifierValue: null,
|
||||
},
|
||||
secrets: {
|
||||
clientSecret: null,
|
||||
privateKey: null,
|
||||
privateKeyPassword: null,
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
},
|
||||
},
|
||||
snServiceUrl: 'https://dev23432523.service-now.com',
|
||||
connectorTokenClient,
|
||||
});
|
||||
|
||||
expect(createAxiosInstanceMock).toHaveBeenCalledTimes(1);
|
||||
expect(createAxiosInstanceMock).toHaveBeenCalledWith({
|
||||
auth: { password: 'password', username: 'username' },
|
||||
});
|
||||
});
|
||||
|
||||
test('creates axios instance with interceptor when isOAuth is true and OAuth fields are defined', () => {
|
||||
connectorTokenClient.get.mockResolvedValueOnce({
|
||||
hasErrors: false,
|
||||
connectorToken: {
|
||||
id: '1',
|
||||
connectorId: '123',
|
||||
tokenType: 'access_token',
|
||||
token: 'testtokenvalue',
|
||||
createdAt: new Date().toISOString(),
|
||||
expiresAt: new Date(Date.now() + 10000000000).toISOString(),
|
||||
},
|
||||
});
|
||||
getAxiosInstance({
|
||||
connectorId: '123',
|
||||
logger,
|
||||
configurationUtilities,
|
||||
credentials: {
|
||||
config: {
|
||||
apiUrl: 'https://servicenow',
|
||||
usesTableApi: true,
|
||||
isOAuth: true,
|
||||
clientId: 'clientId',
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
userIdentifierValue: 'userIdentifierValue',
|
||||
},
|
||||
secrets: {
|
||||
clientSecret: 'clientSecret',
|
||||
privateKey: 'privateKey',
|
||||
privateKeyPassword: null,
|
||||
username: null,
|
||||
password: null,
|
||||
},
|
||||
},
|
||||
snServiceUrl: 'https://dev23432523.service-now.com',
|
||||
connectorTokenClient,
|
||||
});
|
||||
|
||||
expect(createAxiosInstanceMock).toHaveBeenCalledTimes(1);
|
||||
expect(createAxiosInstanceMock).toHaveBeenCalledWith();
|
||||
expect(axiosInstanceMock.interceptors.request.use).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessToken', () => {
|
||||
const getAccessTokenOpts = {
|
||||
connectorId: '123',
|
||||
logger,
|
||||
configurationUtilities,
|
||||
credentials: {
|
||||
config: {
|
||||
apiUrl: 'https://servicenow',
|
||||
usesTableApi: true,
|
||||
isOAuth: true,
|
||||
clientId: 'clientId',
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
userIdentifierValue: 'userIdentifierValue',
|
||||
},
|
||||
secrets: {
|
||||
clientSecret: 'clientSecret',
|
||||
privateKey: 'privateKey',
|
||||
privateKeyPassword: 'privateKeyPassword',
|
||||
username: null,
|
||||
password: null,
|
||||
},
|
||||
},
|
||||
snServiceUrl: 'https://dev23432523.service-now.com',
|
||||
connectorTokenClient,
|
||||
};
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('uses stored access token if it exists', async () => {
|
||||
connectorTokenClient.get.mockResolvedValueOnce({
|
||||
hasErrors: false,
|
||||
connectorToken: {
|
||||
id: '1',
|
||||
connectorId: '123',
|
||||
tokenType: 'access_token',
|
||||
token: 'testtokenvalue',
|
||||
createdAt: new Date().toISOString(),
|
||||
expiresAt: new Date(Date.now() + 10000000000).toISOString(),
|
||||
},
|
||||
});
|
||||
const accessToken = await getAccessToken(getAccessTokenOpts);
|
||||
|
||||
expect(accessToken).toEqual('testtokenvalue');
|
||||
expect(createJWTAssertion as jest.Mock).not.toHaveBeenCalled();
|
||||
expect(requestOAuthJWTToken as jest.Mock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('creates new assertion if stored access token does not exist', async () => {
|
||||
connectorTokenClient.get.mockResolvedValueOnce({
|
||||
hasErrors: false,
|
||||
connectorToken: null,
|
||||
});
|
||||
(createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion');
|
||||
(requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({
|
||||
tokenType: 'access_token',
|
||||
accessToken: 'brandnewaccesstoken',
|
||||
expiresIn: 1000,
|
||||
});
|
||||
|
||||
const accessToken = await getAccessToken(getAccessTokenOpts);
|
||||
|
||||
expect(accessToken).toEqual('access_token brandnewaccesstoken');
|
||||
expect(createJWTAssertion as jest.Mock).toHaveBeenCalledWith(
|
||||
logger,
|
||||
'privateKey',
|
||||
'privateKeyPassword',
|
||||
{
|
||||
audience: 'clientId',
|
||||
issuer: 'clientId',
|
||||
subject: 'userIdentifierValue',
|
||||
keyId: 'jwtKeyId',
|
||||
}
|
||||
);
|
||||
expect(requestOAuthJWTToken as jest.Mock).toHaveBeenCalledWith(
|
||||
'https://dev23432523.service-now.com/oauth_token.do',
|
||||
{ clientId: 'clientId', clientSecret: 'clientSecret', assertion: 'newassertion' },
|
||||
logger,
|
||||
configurationUtilities
|
||||
);
|
||||
expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith({
|
||||
connectorId: '123',
|
||||
token: null,
|
||||
newToken: 'access_token brandnewaccesstoken',
|
||||
expiresInSec: 1000,
|
||||
deleteExisting: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('creates new assertion if stored access token exists but is expired', async () => {
|
||||
const createdAt = new Date().toISOString();
|
||||
const expiresAt = new Date(Date.now() - 100).toISOString();
|
||||
connectorTokenClient.get.mockResolvedValueOnce({
|
||||
hasErrors: false,
|
||||
connectorToken: {
|
||||
id: '1',
|
||||
connectorId: '123',
|
||||
tokenType: 'access_token',
|
||||
token: 'testtokenvalue',
|
||||
createdAt,
|
||||
expiresAt,
|
||||
},
|
||||
});
|
||||
(createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion');
|
||||
(requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({
|
||||
tokenType: 'access_token',
|
||||
accessToken: 'brandnewaccesstoken',
|
||||
expiresIn: 1000,
|
||||
});
|
||||
|
||||
const accessToken = await getAccessToken(getAccessTokenOpts);
|
||||
|
||||
expect(accessToken).toEqual('access_token brandnewaccesstoken');
|
||||
expect(createJWTAssertion as jest.Mock).toHaveBeenCalledWith(
|
||||
logger,
|
||||
'privateKey',
|
||||
'privateKeyPassword',
|
||||
{
|
||||
audience: 'clientId',
|
||||
issuer: 'clientId',
|
||||
subject: 'userIdentifierValue',
|
||||
keyId: 'jwtKeyId',
|
||||
}
|
||||
);
|
||||
expect(requestOAuthJWTToken as jest.Mock).toHaveBeenCalledWith(
|
||||
'https://dev23432523.service-now.com/oauth_token.do',
|
||||
{ clientId: 'clientId', clientSecret: 'clientSecret', assertion: 'newassertion' },
|
||||
logger,
|
||||
configurationUtilities
|
||||
);
|
||||
expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith({
|
||||
connectorId: '123',
|
||||
token: {
|
||||
id: '1',
|
||||
connectorId: '123',
|
||||
tokenType: 'access_token',
|
||||
token: 'testtokenvalue',
|
||||
createdAt,
|
||||
expiresAt,
|
||||
},
|
||||
newToken: 'access_token brandnewaccesstoken',
|
||||
expiresInSec: 1000,
|
||||
deleteExisting: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('throws error if createJWTAssertion throws error', async () => {
|
||||
connectorTokenClient.get.mockResolvedValueOnce({
|
||||
hasErrors: false,
|
||||
connectorToken: null,
|
||||
});
|
||||
(createJWTAssertion as jest.Mock).mockImplementationOnce(() => {
|
||||
throw new Error('createJWTAssertion error!!');
|
||||
});
|
||||
|
||||
await expect(getAccessToken(getAccessTokenOpts)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"createJWTAssertion error!!"`
|
||||
);
|
||||
});
|
||||
|
||||
test('throws error if requestOAuthJWTToken throws error', async () => {
|
||||
connectorTokenClient.get.mockResolvedValueOnce({
|
||||
hasErrors: false,
|
||||
connectorToken: null,
|
||||
});
|
||||
(createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion');
|
||||
(requestOAuthJWTToken as jest.Mock).mockRejectedValueOnce(
|
||||
new Error('requestOAuthJWTToken error!!')
|
||||
);
|
||||
|
||||
await expect(getAccessToken(getAccessTokenOpts)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"requestOAuthJWTToken error!!"`
|
||||
);
|
||||
});
|
||||
|
||||
test('logs warning if connectorTokenClient.updateOrReplace throws error', async () => {
|
||||
connectorTokenClient.get.mockResolvedValueOnce({
|
||||
hasErrors: false,
|
||||
connectorToken: null,
|
||||
});
|
||||
(createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion');
|
||||
(requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({
|
||||
tokenType: 'access_token',
|
||||
accessToken: 'brandnewaccesstoken',
|
||||
expiresIn: 1000,
|
||||
});
|
||||
connectorTokenClient.updateOrReplace.mockRejectedValueOnce(
|
||||
new Error('updateOrReplace error')
|
||||
);
|
||||
|
||||
const accessToken = await getAccessToken(getAccessTokenOpts);
|
||||
|
||||
expect(accessToken).toEqual('access_token brandnewaccesstoken');
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
`Not able to update ServiceNow connector token for connectorId: 123 due to error: updateOrReplace error`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,11 +5,24 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { Incident, PartialIncident, ResponseError, ServiceNowError } from './types';
|
||||
import {
|
||||
ExternalServiceCredentials,
|
||||
Incident,
|
||||
PartialIncident,
|
||||
ResponseError,
|
||||
ServiceNowError,
|
||||
ServiceNowPublicConfigurationType,
|
||||
ServiceNowSecretConfigurationType,
|
||||
} from './types';
|
||||
import { FIELD_PREFIX } from './config';
|
||||
import { addTimeZoneToDate, getErrorMessage } from '../lib/axios_utils';
|
||||
import * as i18n from './translations';
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import { ConnectorTokenClientContract } from '../../types';
|
||||
import { createJWTAssertion } from '../lib/create_jwt_assertion';
|
||||
import { requestOAuthJWTToken } from '../lib/request_oauth_jwt_token';
|
||||
|
||||
export const prepareIncident = (useOldApi: boolean, incident: PartialIncident): PartialIncident =>
|
||||
useOldApi
|
||||
|
@ -69,3 +82,129 @@ export const throwIfSubActionIsNotSupported = ({
|
|||
throw new Error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
export interface GetAccessTokenAndAxiosInstanceOpts {
|
||||
connectorId: string;
|
||||
logger: Logger;
|
||||
configurationUtilities: ActionsConfigurationUtilities;
|
||||
credentials: ExternalServiceCredentials;
|
||||
snServiceUrl: string;
|
||||
connectorTokenClient: ConnectorTokenClientContract;
|
||||
}
|
||||
|
||||
export const getAxiosInstance = ({
|
||||
connectorId,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
credentials,
|
||||
snServiceUrl,
|
||||
connectorTokenClient,
|
||||
}: GetAccessTokenAndAxiosInstanceOpts): AxiosInstance => {
|
||||
const { config, secrets } = credentials;
|
||||
const { isOAuth } = config as ServiceNowPublicConfigurationType;
|
||||
const { username, password } = secrets as ServiceNowSecretConfigurationType;
|
||||
|
||||
let axiosInstance;
|
||||
|
||||
if (!isOAuth && username && password) {
|
||||
axiosInstance = axios.create({
|
||||
auth: { username, password },
|
||||
});
|
||||
} else {
|
||||
axiosInstance = axios.create();
|
||||
axiosInstance.interceptors.request.use(
|
||||
async (axiosConfig: AxiosRequestConfig) => {
|
||||
const accessToken = await getAccessToken({
|
||||
connectorId,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
credentials: {
|
||||
config: config as ServiceNowPublicConfigurationType,
|
||||
secrets,
|
||||
},
|
||||
snServiceUrl,
|
||||
connectorTokenClient,
|
||||
});
|
||||
axiosConfig.headers.Authorization = accessToken;
|
||||
return axiosConfig;
|
||||
},
|
||||
(error) => {
|
||||
Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return axiosInstance;
|
||||
};
|
||||
|
||||
export const getAccessToken = async ({
|
||||
connectorId,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
credentials,
|
||||
snServiceUrl,
|
||||
connectorTokenClient,
|
||||
}: GetAccessTokenAndAxiosInstanceOpts) => {
|
||||
const { isOAuth, clientId, jwtKeyId, userIdentifierValue } =
|
||||
credentials.config as ServiceNowPublicConfigurationType;
|
||||
const { clientSecret, privateKey, privateKeyPassword } =
|
||||
credentials.secrets as ServiceNowSecretConfigurationType;
|
||||
|
||||
let accessToken: string;
|
||||
|
||||
// Check if there is a token stored for this connector
|
||||
const { connectorToken, hasErrors } = await connectorTokenClient.get({ connectorId });
|
||||
|
||||
if (connectorToken === null || Date.parse(connectorToken.expiresAt) <= Date.now()) {
|
||||
// generate a new assertion
|
||||
if (
|
||||
!isOAuth ||
|
||||
!clientId ||
|
||||
!clientSecret ||
|
||||
!jwtKeyId ||
|
||||
!privateKey ||
|
||||
!userIdentifierValue
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const assertion = createJWTAssertion(logger, privateKey, privateKeyPassword, {
|
||||
audience: clientId,
|
||||
issuer: clientId,
|
||||
subject: userIdentifierValue,
|
||||
keyId: jwtKeyId,
|
||||
});
|
||||
|
||||
// request access token with jwt assertion
|
||||
const tokenResult = await requestOAuthJWTToken(
|
||||
`${snServiceUrl}/oauth_token.do`,
|
||||
{
|
||||
clientId,
|
||||
clientSecret,
|
||||
assertion,
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
);
|
||||
accessToken = `${tokenResult.tokenType} ${tokenResult.accessToken}`;
|
||||
|
||||
// try to update connector_token SO
|
||||
try {
|
||||
await connectorTokenClient.updateOrReplace({
|
||||
connectorId,
|
||||
token: connectorToken,
|
||||
newToken: accessToken,
|
||||
expiresInSec: tokenResult.expiresIn,
|
||||
deleteExisting: hasErrors,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`Not able to update ServiceNow connector token for connectorId: ${connectorId} due to error: ${err.message}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// use existing valid token
|
||||
accessToken = connectorToken.token;
|
||||
}
|
||||
return accessToken;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,401 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { validateCommonConfig, validateCommonSecrets, validateCommonConnector } from './validators';
|
||||
import { actionsConfigMock } from '../../actions_config.mock';
|
||||
|
||||
const configurationUtilities = actionsConfigMock.create();
|
||||
|
||||
describe('validateCommonConfig', () => {
|
||||
test('config validation fails when apiUrl is not allowed', () => {
|
||||
expect(
|
||||
validateCommonConfig(
|
||||
{
|
||||
...configurationUtilities,
|
||||
ensureUriAllowed: (_) => {
|
||||
throw new Error(`target url is not present in allowedHosts`);
|
||||
},
|
||||
},
|
||||
{
|
||||
apiUrl: 'example.com/do-something',
|
||||
usesTableApi: true,
|
||||
isOAuth: false,
|
||||
userIdentifierValue: null,
|
||||
clientId: null,
|
||||
jwtKeyId: null,
|
||||
}
|
||||
)
|
||||
).toEqual(`error configuring connector action: target url is not present in allowedHosts`);
|
||||
});
|
||||
describe('when isOAuth = true', () => {
|
||||
test('config validation fails when userIdentifierValue is null', () => {
|
||||
expect(
|
||||
validateCommonConfig(configurationUtilities, {
|
||||
apiUrl: 'https://url',
|
||||
usesTableApi: true,
|
||||
isOAuth: true,
|
||||
userIdentifierValue: null,
|
||||
clientId: 'clientId',
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
})
|
||||
).toEqual(`userIdentifierValue must be provided when isOAuth = true`);
|
||||
});
|
||||
test('config validation fails when clientId is null', () => {
|
||||
expect(
|
||||
validateCommonConfig(configurationUtilities, {
|
||||
apiUrl: 'https://url',
|
||||
usesTableApi: true,
|
||||
isOAuth: true,
|
||||
userIdentifierValue: 'userIdentifierValue',
|
||||
clientId: null,
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
})
|
||||
).toEqual(`clientId must be provided when isOAuth = true`);
|
||||
});
|
||||
test('config validation fails when jwtKeyId is null', () => {
|
||||
expect(
|
||||
validateCommonConfig(configurationUtilities, {
|
||||
apiUrl: 'https://url',
|
||||
usesTableApi: true,
|
||||
isOAuth: true,
|
||||
userIdentifierValue: 'userIdentifierValue',
|
||||
clientId: 'clientId',
|
||||
jwtKeyId: null,
|
||||
})
|
||||
).toEqual(`jwtKeyId must be provided when isOAuth = true`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when isOAuth = false', () => {
|
||||
test('connector validation fails when username is null', () => {
|
||||
expect(
|
||||
validateCommonConnector(
|
||||
{
|
||||
apiUrl: 'https://url',
|
||||
usesTableApi: true,
|
||||
isOAuth: false,
|
||||
userIdentifierValue: null,
|
||||
clientId: null,
|
||||
jwtKeyId: null,
|
||||
},
|
||||
{
|
||||
password: 'password',
|
||||
username: null,
|
||||
clientSecret: null,
|
||||
privateKey: null,
|
||||
privateKeyPassword: null,
|
||||
}
|
||||
)
|
||||
).toEqual(`username must be provided when isOAuth = false`);
|
||||
});
|
||||
test('connector validation fails when password is null', () => {
|
||||
expect(
|
||||
validateCommonConnector(
|
||||
{
|
||||
apiUrl: 'https://url',
|
||||
usesTableApi: true,
|
||||
isOAuth: false,
|
||||
userIdentifierValue: null,
|
||||
clientId: null,
|
||||
jwtKeyId: null,
|
||||
},
|
||||
{
|
||||
password: null,
|
||||
username: 'username',
|
||||
clientSecret: null,
|
||||
privateKey: null,
|
||||
privateKeyPassword: null,
|
||||
}
|
||||
)
|
||||
).toEqual(`password must be provided when isOAuth = false`);
|
||||
});
|
||||
test('connector validation fails when any oauth related field is defined', () => {
|
||||
expect(
|
||||
validateCommonConnector(
|
||||
{
|
||||
apiUrl: 'https://url',
|
||||
usesTableApi: true,
|
||||
isOAuth: false,
|
||||
userIdentifierValue: null,
|
||||
clientId: null,
|
||||
jwtKeyId: null,
|
||||
},
|
||||
{
|
||||
password: 'password',
|
||||
username: 'username',
|
||||
clientSecret: 'clientSecret',
|
||||
privateKey: null,
|
||||
privateKeyPassword: null,
|
||||
}
|
||||
)
|
||||
).toEqual(
|
||||
`clientId, clientSecret, userIdentifierValue, jwtKeyId and privateKey should not be provided with isOAuth = false`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateCommonSecrets', () => {
|
||||
test('secrets validation fails when no credentials are defined', () => {
|
||||
expect(
|
||||
validateCommonSecrets(configurationUtilities, {
|
||||
password: null,
|
||||
username: null,
|
||||
clientSecret: null,
|
||||
privateKey: null,
|
||||
privateKeyPassword: null,
|
||||
})
|
||||
).toEqual(`Either basic auth or OAuth credentials must be specified`);
|
||||
});
|
||||
|
||||
test('secrets validation fails when username is defined and password is not', () => {
|
||||
expect(
|
||||
validateCommonSecrets(configurationUtilities, {
|
||||
password: null,
|
||||
username: 'admin',
|
||||
clientSecret: null,
|
||||
privateKey: null,
|
||||
privateKeyPassword: null,
|
||||
})
|
||||
).toEqual(`username and password must both be specified`);
|
||||
});
|
||||
|
||||
test('secrets validation fails when password is defined and username is not', () => {
|
||||
expect(
|
||||
validateCommonSecrets(configurationUtilities, {
|
||||
password: 'password',
|
||||
username: null,
|
||||
clientSecret: null,
|
||||
privateKey: null,
|
||||
privateKeyPassword: null,
|
||||
})
|
||||
).toEqual(`username and password must both be specified`);
|
||||
});
|
||||
|
||||
test('secrets validation fails when clientSecret is defined and privateKey is not', () => {
|
||||
expect(
|
||||
validateCommonSecrets(configurationUtilities, {
|
||||
password: null,
|
||||
username: null,
|
||||
clientSecret: 'secret',
|
||||
privateKey: null,
|
||||
privateKeyPassword: null,
|
||||
})
|
||||
).toEqual(`clientSecret and privateKey must both be specified`);
|
||||
});
|
||||
|
||||
test('secrets validation fails when privateKey is defined and clientSecret is not', () => {
|
||||
expect(
|
||||
validateCommonSecrets(configurationUtilities, {
|
||||
password: null,
|
||||
username: null,
|
||||
clientSecret: null,
|
||||
privateKey: 'private',
|
||||
privateKeyPassword: null,
|
||||
})
|
||||
).toEqual(`clientSecret and privateKey must both be specified`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateCommonConnector', () => {
|
||||
describe('when isOAuth = true', () => {
|
||||
test('connector validation fails when userIdentifierValue is null', () => {
|
||||
expect(
|
||||
validateCommonConnector(
|
||||
{
|
||||
apiUrl: 'https://url',
|
||||
usesTableApi: true,
|
||||
isOAuth: true,
|
||||
userIdentifierValue: null,
|
||||
clientId: 'clientId',
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
},
|
||||
{
|
||||
password: null,
|
||||
username: null,
|
||||
clientSecret: 'clientSecret',
|
||||
privateKey: 'privateKey',
|
||||
privateKeyPassword: null,
|
||||
}
|
||||
)
|
||||
).toEqual(`userIdentifierValue must be provided when isOAuth = true`);
|
||||
});
|
||||
test('connector validation fails when clientId is null', () => {
|
||||
expect(
|
||||
validateCommonConnector(
|
||||
{
|
||||
apiUrl: 'https://url',
|
||||
usesTableApi: true,
|
||||
isOAuth: true,
|
||||
userIdentifierValue: 'userIdentifierValue',
|
||||
clientId: null,
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
},
|
||||
{
|
||||
password: null,
|
||||
username: null,
|
||||
clientSecret: 'clientSecret',
|
||||
privateKey: 'privateKey',
|
||||
privateKeyPassword: null,
|
||||
}
|
||||
)
|
||||
).toEqual(`clientId must be provided when isOAuth = true`);
|
||||
});
|
||||
test('connector validation fails when jwtKeyId is null', () => {
|
||||
expect(
|
||||
validateCommonConnector(
|
||||
{
|
||||
apiUrl: 'https://url',
|
||||
usesTableApi: true,
|
||||
isOAuth: true,
|
||||
userIdentifierValue: 'userIdentifierValue',
|
||||
clientId: 'clientId',
|
||||
jwtKeyId: null,
|
||||
},
|
||||
{
|
||||
password: null,
|
||||
username: null,
|
||||
clientSecret: 'clientSecret',
|
||||
privateKey: 'privateKey',
|
||||
privateKeyPassword: null,
|
||||
}
|
||||
)
|
||||
).toEqual(`jwtKeyId must be provided when isOAuth = true`);
|
||||
});
|
||||
test('connector validation fails when clientSecret is null', () => {
|
||||
expect(
|
||||
validateCommonConnector(
|
||||
{
|
||||
apiUrl: 'https://url',
|
||||
usesTableApi: true,
|
||||
isOAuth: true,
|
||||
userIdentifierValue: 'userIdentifierValue',
|
||||
clientId: 'clientId',
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
},
|
||||
{
|
||||
password: null,
|
||||
username: null,
|
||||
clientSecret: null,
|
||||
privateKey: 'privateKey',
|
||||
privateKeyPassword: null,
|
||||
}
|
||||
)
|
||||
).toEqual(`clientSecret must be provided when isOAuth = true`);
|
||||
});
|
||||
test('connector validation fails when privateKey is null', () => {
|
||||
expect(
|
||||
validateCommonConnector(
|
||||
{
|
||||
apiUrl: 'https://url',
|
||||
usesTableApi: true,
|
||||
isOAuth: true,
|
||||
userIdentifierValue: 'userIdentifierValue',
|
||||
clientId: 'clientId',
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
},
|
||||
{
|
||||
password: null,
|
||||
username: null,
|
||||
clientSecret: 'clientSecret',
|
||||
privateKey: null,
|
||||
privateKeyPassword: null,
|
||||
}
|
||||
)
|
||||
).toEqual(`privateKey must be provided when isOAuth = true`);
|
||||
});
|
||||
test('connector validation fails when username and password are not null', () => {
|
||||
expect(
|
||||
validateCommonConnector(
|
||||
{
|
||||
apiUrl: 'https://url',
|
||||
usesTableApi: true,
|
||||
isOAuth: true,
|
||||
userIdentifierValue: 'userIdentifierValue',
|
||||
clientId: 'clientId',
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
},
|
||||
{
|
||||
password: 'password',
|
||||
username: 'username',
|
||||
clientSecret: 'clientSecret',
|
||||
privateKey: 'privateKey',
|
||||
privateKeyPassword: null,
|
||||
}
|
||||
)
|
||||
).toEqual(`Username and password should not be provided with isOAuth = true`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when isOAuth = false', () => {
|
||||
test('connector validation fails when username is null', () => {
|
||||
expect(
|
||||
validateCommonConnector(
|
||||
{
|
||||
apiUrl: 'https://url',
|
||||
usesTableApi: true,
|
||||
isOAuth: false,
|
||||
userIdentifierValue: null,
|
||||
clientId: null,
|
||||
jwtKeyId: null,
|
||||
},
|
||||
{
|
||||
password: 'password',
|
||||
username: null,
|
||||
clientSecret: null,
|
||||
privateKey: null,
|
||||
privateKeyPassword: null,
|
||||
}
|
||||
)
|
||||
).toEqual(`username must be provided when isOAuth = false`);
|
||||
});
|
||||
test('connector validation fails when password is null', () => {
|
||||
expect(
|
||||
validateCommonConnector(
|
||||
{
|
||||
apiUrl: 'https://url',
|
||||
usesTableApi: true,
|
||||
isOAuth: false,
|
||||
userIdentifierValue: null,
|
||||
clientId: null,
|
||||
jwtKeyId: null,
|
||||
},
|
||||
{
|
||||
password: null,
|
||||
username: 'username',
|
||||
clientSecret: null,
|
||||
privateKey: null,
|
||||
privateKeyPassword: null,
|
||||
}
|
||||
)
|
||||
).toEqual(`password must be provided when isOAuth = false`);
|
||||
});
|
||||
test('connector validation fails when any oauth related field is defined', () => {
|
||||
expect(
|
||||
validateCommonConnector(
|
||||
{
|
||||
apiUrl: 'https://url',
|
||||
usesTableApi: true,
|
||||
isOAuth: false,
|
||||
userIdentifierValue: null,
|
||||
clientId: null,
|
||||
jwtKeyId: null,
|
||||
},
|
||||
{
|
||||
password: 'password',
|
||||
username: 'username',
|
||||
clientSecret: 'clientSecret',
|
||||
privateKey: null,
|
||||
privateKeyPassword: null,
|
||||
}
|
||||
)
|
||||
).toEqual(
|
||||
`clientId, clientSecret, userIdentifierValue, jwtKeyId and privateKey should not be provided with isOAuth = false`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -16,21 +16,107 @@ import * as i18n from './translations';
|
|||
|
||||
export const validateCommonConfig = (
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
configObject: ServiceNowPublicConfigurationType
|
||||
config: ServiceNowPublicConfigurationType
|
||||
) => {
|
||||
const { isOAuth, apiUrl, userIdentifierValue, clientId, jwtKeyId } = config;
|
||||
|
||||
try {
|
||||
configurationUtilities.ensureUriAllowed(configObject.apiUrl);
|
||||
configurationUtilities.ensureUriAllowed(apiUrl);
|
||||
} catch (allowedListError) {
|
||||
return i18n.ALLOWED_HOSTS_ERROR(allowedListError.message);
|
||||
}
|
||||
|
||||
if (isOAuth) {
|
||||
if (userIdentifierValue == null) {
|
||||
return i18n.VALIDATE_OAUTH_MISSING_FIELD_ERROR('userIdentifierValue', true);
|
||||
}
|
||||
|
||||
if (clientId == null) {
|
||||
return i18n.VALIDATE_OAUTH_MISSING_FIELD_ERROR('clientId', true);
|
||||
}
|
||||
|
||||
if (jwtKeyId == null) {
|
||||
return i18n.VALIDATE_OAUTH_MISSING_FIELD_ERROR('jwtKeyId', true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const validateCommonSecrets = (
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
secrets: ServiceNowSecretConfigurationType
|
||||
) => {};
|
||||
) => {
|
||||
const { username, password, clientSecret, privateKey } = secrets;
|
||||
|
||||
if (!username && !password && !clientSecret && !privateKey) {
|
||||
return i18n.CREDENTIALS_ERROR;
|
||||
}
|
||||
|
||||
if (username || password) {
|
||||
// Username and password must be set and set together
|
||||
if (!username || !password) {
|
||||
return i18n.BASIC_AUTH_CREDENTIALS_ERROR;
|
||||
}
|
||||
} else if (clientSecret || privateKey) {
|
||||
// Client secret and private key must be set and set together
|
||||
if (!clientSecret || !privateKey) {
|
||||
return i18n.OAUTH_CREDENTIALS_ERROR;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const validateCommonConnector = (
|
||||
config: ServiceNowPublicConfigurationType,
|
||||
secrets: ServiceNowSecretConfigurationType
|
||||
): string | null => {
|
||||
const { isOAuth, userIdentifierValue, clientId, jwtKeyId } = config;
|
||||
const { username, password, clientSecret, privateKey } = secrets;
|
||||
|
||||
if (isOAuth) {
|
||||
if (userIdentifierValue == null) {
|
||||
return i18n.VALIDATE_OAUTH_MISSING_FIELD_ERROR('userIdentifierValue', true);
|
||||
}
|
||||
|
||||
if (clientId == null) {
|
||||
return i18n.VALIDATE_OAUTH_MISSING_FIELD_ERROR('clientId', true);
|
||||
}
|
||||
|
||||
if (jwtKeyId == null) {
|
||||
return i18n.VALIDATE_OAUTH_MISSING_FIELD_ERROR('jwtKeyId', true);
|
||||
}
|
||||
|
||||
if (clientSecret == null) {
|
||||
return i18n.VALIDATE_OAUTH_MISSING_FIELD_ERROR('clientSecret', true);
|
||||
}
|
||||
|
||||
if (privateKey == null) {
|
||||
return i18n.VALIDATE_OAUTH_MISSING_FIELD_ERROR('privateKey', true);
|
||||
}
|
||||
|
||||
if (username || password) {
|
||||
return i18n.VALIDATE_OAUTH_POPULATED_FIELD_ERROR('Username and password', true);
|
||||
}
|
||||
} else {
|
||||
if (username == null) {
|
||||
return i18n.VALIDATE_OAUTH_MISSING_FIELD_ERROR('username', false);
|
||||
}
|
||||
|
||||
if (password == null) {
|
||||
return i18n.VALIDATE_OAUTH_MISSING_FIELD_ERROR('password', false);
|
||||
}
|
||||
|
||||
if (clientSecret || clientId || userIdentifierValue || jwtKeyId || privateKey) {
|
||||
return i18n.VALIDATE_OAUTH_POPULATED_FIELD_ERROR(
|
||||
'clientId, clientSecret, userIdentifierValue, jwtKeyId and privateKey',
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const validate: ExternalServiceValidation = {
|
||||
config: validateCommonConfig,
|
||||
secrets: validateCommonSecrets,
|
||||
connector: validateCommonConnector,
|
||||
};
|
||||
|
|
|
@ -168,7 +168,7 @@ describe('successful migrations', () => {
|
|||
|
||||
test('set usesTableApi config property for .servicenow', () => {
|
||||
const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0'];
|
||||
const action = getMockDataForServiceNow();
|
||||
const action = getMockDataForServiceNow716({ usesTableApi: true });
|
||||
const migratedAction = migration716(action, context);
|
||||
|
||||
expect(migratedAction).toEqual({
|
||||
|
@ -185,7 +185,7 @@ describe('successful migrations', () => {
|
|||
|
||||
test('set usesTableApi config property for .servicenow-sir', () => {
|
||||
const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0'];
|
||||
const action = getMockDataForServiceNow({ actionTypeId: '.servicenow-sir' });
|
||||
const action = getMockDataForServiceNow716({ actionTypeId: '.servicenow-sir' });
|
||||
const migratedAction = migration716(action, context);
|
||||
|
||||
expect(migratedAction).toEqual({
|
||||
|
@ -215,6 +215,52 @@ describe('successful migrations', () => {
|
|||
expect(migration800(action, context)).toEqual(action);
|
||||
});
|
||||
});
|
||||
|
||||
describe('8.3.0', () => {
|
||||
test('set isOAuth config property for .servicenow', () => {
|
||||
const migration830 = getActionsMigrations(encryptedSavedObjectsSetup)['8.3.0'];
|
||||
const action = getMockDataForServiceNow83();
|
||||
const migratedAction = migration830(action, context);
|
||||
|
||||
expect(migratedAction.attributes.config).toEqual({
|
||||
apiUrl: 'https://example.com',
|
||||
usesTableApi: true,
|
||||
isOAuth: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('set isOAuth config property for .servicenow-sir', () => {
|
||||
const migration830 = getActionsMigrations(encryptedSavedObjectsSetup)['8.3.0'];
|
||||
const action = getMockDataForServiceNow83({ actionTypeId: '.servicenow-sir' });
|
||||
const migratedAction = migration830(action, context);
|
||||
|
||||
expect(migratedAction.attributes.config).toEqual({
|
||||
apiUrl: 'https://example.com',
|
||||
usesTableApi: true,
|
||||
isOAuth: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('set isOAuth config property for .servicenow-itom', () => {
|
||||
const migration830 = getActionsMigrations(encryptedSavedObjectsSetup)['8.3.0'];
|
||||
const action = getMockDataForServiceNow83({ actionTypeId: '.servicenow-itom' });
|
||||
const migratedAction = migration830(action, context);
|
||||
|
||||
expect(migratedAction.attributes.config).toEqual({
|
||||
apiUrl: 'https://example.com',
|
||||
usesTableApi: true,
|
||||
isOAuth: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('it does not set isOAuth config for other connectors', () => {
|
||||
const migration830 = getActionsMigrations(encryptedSavedObjectsSetup)['8.3.0'];
|
||||
const action = getMockData();
|
||||
const migratedAction = migration830(action, context);
|
||||
|
||||
expect(migratedAction).toEqual(action);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handles errors during migrations', () => {
|
||||
|
@ -348,7 +394,7 @@ function getMockData(
|
|||
};
|
||||
}
|
||||
|
||||
function getMockDataForServiceNow(
|
||||
function getMockDataForServiceNow716(
|
||||
overwrites: Record<string, unknown> = {}
|
||||
): SavedObjectUnsanitizedDoc<Omit<RawAction, 'isMissingSecrets'>> {
|
||||
return {
|
||||
|
@ -363,3 +409,11 @@ function getMockDataForServiceNow(
|
|||
type: 'action',
|
||||
};
|
||||
}
|
||||
|
||||
function getMockDataForServiceNow83(
|
||||
overwrites: Record<string, unknown> = {}
|
||||
): SavedObjectUnsanitizedDoc<Omit<RawAction, 'isMissingSecrets'>> {
|
||||
return getMockDataForServiceNow716({
|
||||
config: { apiUrl: 'https://example.com', usesTableApi: true },
|
||||
});
|
||||
}
|
||||
|
|
|
@ -78,12 +78,22 @@ export function getActionsMigrations(
|
|||
(doc) => doc // no-op
|
||||
);
|
||||
|
||||
const migrationActions830 = createEsoMigration(
|
||||
encryptedSavedObjects,
|
||||
(doc): doc is SavedObjectUnsanitizedDoc<RawAction> =>
|
||||
doc.attributes.actionTypeId === '.servicenow' ||
|
||||
doc.attributes.actionTypeId === '.servicenow-sir' ||
|
||||
doc.attributes.actionTypeId === '.servicenow-itom',
|
||||
pipeMigrations(addIsOAuthToServiceNowConnectors)
|
||||
);
|
||||
|
||||
return {
|
||||
'7.10.0': executeMigrationWithErrorHandling(migrationActionsTen, '7.10.0'),
|
||||
'7.11.0': executeMigrationWithErrorHandling(migrationActionsEleven, '7.11.0'),
|
||||
'7.14.0': executeMigrationWithErrorHandling(migrationActionsFourteen, '7.14.0'),
|
||||
'7.16.0': executeMigrationWithErrorHandling(migrationActionsSixteen, '7.16.0'),
|
||||
'8.0.0': executeMigrationWithErrorHandling(migrationActions800, '8.0.0'),
|
||||
'8.3.0': executeMigrationWithErrorHandling(migrationActions830, '8.3.0'),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -219,6 +229,29 @@ const addUsesTableApiToServiceNowConnectors = (
|
|||
};
|
||||
};
|
||||
|
||||
const addIsOAuthToServiceNowConnectors = (
|
||||
doc: SavedObjectUnsanitizedDoc<RawAction>
|
||||
): SavedObjectUnsanitizedDoc<RawAction> => {
|
||||
if (
|
||||
doc.attributes.actionTypeId !== '.servicenow' &&
|
||||
doc.attributes.actionTypeId !== '.servicenow-sir' &&
|
||||
doc.attributes.actionTypeId !== '.servicenow-itom'
|
||||
) {
|
||||
return doc;
|
||||
}
|
||||
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...doc.attributes,
|
||||
config: {
|
||||
...doc.attributes.config,
|
||||
isOAuth: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
function pipeMigrations(...migrations: ActionMigration[]): ActionMigration {
|
||||
return (doc: SavedObjectUnsanitizedDoc<RawAction>) =>
|
||||
migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc);
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import httpProxy from 'http-proxy';
|
||||
import expect from '@kbn/expect';
|
||||
import { asyncForEach } from '@kbn/std';
|
||||
import getPort from 'get-port';
|
||||
import http from 'http';
|
||||
|
||||
|
@ -19,14 +20,7 @@ export default function serviceNowITOMTest({ getService }: FtrProviderContext) {
|
|||
const supertest = getService('supertest');
|
||||
const configService = getService('config');
|
||||
|
||||
const mockServiceNow = {
|
||||
config: {
|
||||
apiUrl: 'www.servicenowisinkibanaactions.com',
|
||||
},
|
||||
secrets: {
|
||||
password: 'elastic',
|
||||
username: 'changeme',
|
||||
},
|
||||
const mockServiceNowCommon = {
|
||||
params: {
|
||||
subAction: 'addEvent',
|
||||
subActionParams: {
|
||||
|
@ -44,6 +38,30 @@ export default function serviceNowITOMTest({ getService }: FtrProviderContext) {
|
|||
},
|
||||
},
|
||||
};
|
||||
const mockServiceNowBasic = {
|
||||
...mockServiceNowCommon,
|
||||
config: {
|
||||
apiUrl: 'www.servicenowisinkibanaactions.com',
|
||||
},
|
||||
secrets: {
|
||||
password: 'elastic',
|
||||
username: 'changeme',
|
||||
},
|
||||
};
|
||||
const mockServiceNowOAuth = {
|
||||
...mockServiceNowCommon,
|
||||
config: {
|
||||
apiUrl: 'www.servicenowisinkibanaactions.com',
|
||||
isOAuth: true,
|
||||
clientId: 'abc',
|
||||
userIdentifierValue: 'elastic',
|
||||
jwtKeyId: 'def',
|
||||
},
|
||||
secrets: {
|
||||
clientSecret: 'xyz',
|
||||
privateKey: '-----BEGIN RSA PRIVATE KEY-----\nddddddd\n-----END RSA PRIVATE KEY-----',
|
||||
},
|
||||
};
|
||||
|
||||
describe('ServiceNow ITOM', () => {
|
||||
let simulatedActionId = '';
|
||||
|
@ -76,7 +94,7 @@ export default function serviceNowITOMTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
describe('ServiceNow ITOM - Action Creation', () => {
|
||||
it('should return 200 when creating a servicenow action successfully', async () => {
|
||||
it('should return 200 when creating a servicenow Basic Auth connector successfully', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
|
@ -86,7 +104,7 @@ export default function serviceNowITOMTest({ getService }: FtrProviderContext) {
|
|||
config: {
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
},
|
||||
secrets: mockServiceNow.secrets,
|
||||
secrets: mockServiceNowBasic.secrets,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
|
@ -99,6 +117,10 @@ export default function serviceNowITOMTest({ getService }: FtrProviderContext) {
|
|||
is_missing_secrets: false,
|
||||
config: {
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
isOAuth: false,
|
||||
clientId: null,
|
||||
jwtKeyId: null,
|
||||
userIdentifierValue: null,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -115,11 +137,67 @@ export default function serviceNowITOMTest({ getService }: FtrProviderContext) {
|
|||
is_missing_secrets: false,
|
||||
config: {
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
isOAuth: false,
|
||||
clientId: null,
|
||||
jwtKeyId: null,
|
||||
userIdentifierValue: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow action with no apiUrl', async () => {
|
||||
it('should return 200 when creating a servicenow OAuth connector successfully', async () => {
|
||||
const { body: createdConnector } = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A servicenow action',
|
||||
connector_type_id: '.servicenow-itom',
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
},
|
||||
secrets: mockServiceNowOAuth.secrets,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(createdConnector).to.eql({
|
||||
id: createdConnector.id,
|
||||
is_preconfigured: false,
|
||||
is_deprecated: false,
|
||||
name: 'A servicenow action',
|
||||
connector_type_id: '.servicenow-itom',
|
||||
is_missing_secrets: false,
|
||||
config: {
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
isOAuth: true,
|
||||
clientId: mockServiceNowOAuth.config.clientId,
|
||||
jwtKeyId: mockServiceNowOAuth.config.jwtKeyId,
|
||||
userIdentifierValue: mockServiceNowOAuth.config.userIdentifierValue,
|
||||
},
|
||||
});
|
||||
|
||||
const { body: fetchedConnector } = await supertest
|
||||
.get(`/api/actions/connector/${createdConnector.id}`)
|
||||
.expect(200);
|
||||
|
||||
expect(fetchedConnector).to.eql({
|
||||
id: fetchedConnector.id,
|
||||
is_preconfigured: false,
|
||||
is_deprecated: false,
|
||||
name: 'A servicenow action',
|
||||
connector_type_id: '.servicenow-itom',
|
||||
is_missing_secrets: false,
|
||||
config: {
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
isOAuth: true,
|
||||
clientId: mockServiceNowOAuth.config.clientId,
|
||||
jwtKeyId: mockServiceNowOAuth.config.jwtKeyId,
|
||||
userIdentifierValue: mockServiceNowOAuth.config.userIdentifierValue,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow Basic Auth connector with no apiUrl', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
|
@ -139,7 +217,30 @@ export default function serviceNowITOMTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow action with a not present in allowedHosts apiUrl', async () => {
|
||||
it('should respond with a 400 Bad Request when creating a servicenow OAuth connector with no apiUrl', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A servicenow action',
|
||||
connector_type_id: '.servicenow-itom',
|
||||
config: {
|
||||
isOAuth: true,
|
||||
},
|
||||
secrets: mockServiceNowOAuth.secrets,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow connector with a not present in allowedHosts apiUrl', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
|
@ -149,7 +250,7 @@ export default function serviceNowITOMTest({ getService }: FtrProviderContext) {
|
|||
config: {
|
||||
apiUrl: 'http://servicenow.mynonexistent.com',
|
||||
},
|
||||
secrets: mockServiceNow.secrets,
|
||||
secrets: mockServiceNowBasic.secrets,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
|
@ -162,7 +263,7 @@ export default function serviceNowITOMTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow action without secrets', async () => {
|
||||
it('should respond with a 400 Bad Request when creating a servicenow Basic Auth connector without secrets', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
|
@ -179,10 +280,107 @@ export default function serviceNowITOMTest({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type secrets: [password]: expected value of type [string] but got [undefined]',
|
||||
'error validating action type secrets: Either basic auth or OAuth credentials must be specified',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow OAuth connector without secrets', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A servicenow action',
|
||||
connector_type_id: '.servicenow-itom',
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
},
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type secrets: Either basic auth or OAuth credentials must be specified',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow OAuth connector with missing fields', async () => {
|
||||
const badConfigs = [
|
||||
{
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
clientId: null,
|
||||
},
|
||||
secrets: mockServiceNowOAuth.secrets,
|
||||
errorMessage: `error validating action type config: clientId must be provided when isOAuth = true`,
|
||||
},
|
||||
{
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
userIdentifierValue: null,
|
||||
},
|
||||
secrets: mockServiceNowOAuth.secrets,
|
||||
errorMessage: `error validating action type config: userIdentifierValue must be provided when isOAuth = true`,
|
||||
},
|
||||
{
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
jwtKeyId: null,
|
||||
},
|
||||
secrets: mockServiceNowOAuth.secrets,
|
||||
errorMessage: `error validating action type config: jwtKeyId must be provided when isOAuth = true`,
|
||||
},
|
||||
{
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
},
|
||||
secrets: {
|
||||
...mockServiceNowOAuth.secrets,
|
||||
clientSecret: null,
|
||||
},
|
||||
errorMessage: `error validating action type secrets: clientSecret and privateKey must both be specified`,
|
||||
},
|
||||
{
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
},
|
||||
secrets: {
|
||||
...mockServiceNowOAuth.secrets,
|
||||
privateKey: null,
|
||||
},
|
||||
errorMessage: `error validating action type secrets: clientSecret and privateKey must both be specified`,
|
||||
},
|
||||
];
|
||||
|
||||
await asyncForEach(badConfigs, async (badConfig) => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A servicenow action',
|
||||
connector_type_id: '.servicenow-itom',
|
||||
config: badConfig.config,
|
||||
secrets: badConfig.secrets,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: badConfig.errorMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ServiceNow ITOM - Executor', () => {
|
||||
|
@ -196,7 +394,7 @@ export default function serviceNowITOMTest({ getService }: FtrProviderContext) {
|
|||
config: {
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
},
|
||||
secrets: mockServiceNow.secrets,
|
||||
secrets: mockServiceNowBasic.secrets,
|
||||
});
|
||||
simulatedActionId = body.id;
|
||||
});
|
||||
|
@ -284,7 +482,7 @@ export default function serviceNowITOMTest({ getService }: FtrProviderContext) {
|
|||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: mockServiceNow.params,
|
||||
params: mockServiceNowBasic.params,
|
||||
})
|
||||
.expect(200);
|
||||
expect(result.status).to.eql('ok');
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import httpProxy from 'http-proxy';
|
||||
import expect from '@kbn/expect';
|
||||
import { asyncForEach } from '@kbn/std';
|
||||
import getPort from 'get-port';
|
||||
import http from 'http';
|
||||
|
||||
|
@ -19,15 +20,7 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
const supertest = getService('supertest');
|
||||
const configService = getService('config');
|
||||
|
||||
const mockServiceNow = {
|
||||
config: {
|
||||
apiUrl: 'www.servicenowisinkibanaactions.com',
|
||||
usesTableApi: false,
|
||||
},
|
||||
secrets: {
|
||||
password: 'elastic',
|
||||
username: 'changeme',
|
||||
},
|
||||
const mockServiceNowCommon = {
|
||||
params: {
|
||||
subAction: 'pushToService',
|
||||
subActionParams: {
|
||||
|
@ -51,6 +44,33 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
},
|
||||
};
|
||||
|
||||
const mockServiceNowBasic = {
|
||||
...mockServiceNowCommon,
|
||||
config: {
|
||||
apiUrl: 'www.servicenowisinkibanaactions.com',
|
||||
usesTableApi: false,
|
||||
},
|
||||
secrets: {
|
||||
password: 'elastic',
|
||||
username: 'changeme',
|
||||
},
|
||||
};
|
||||
const mockServiceNowOAuth = {
|
||||
...mockServiceNowCommon,
|
||||
config: {
|
||||
apiUrl: 'www.servicenowisinkibanaactions.com',
|
||||
usesTableApi: false,
|
||||
isOAuth: true,
|
||||
clientId: 'abc',
|
||||
userIdentifierValue: 'elastic',
|
||||
jwtKeyId: 'def',
|
||||
},
|
||||
secrets: {
|
||||
clientSecret: 'xyz',
|
||||
privateKey: '-----BEGIN RSA PRIVATE KEY-----\nddddddd\n-----END RSA PRIVATE KEY-----',
|
||||
},
|
||||
};
|
||||
|
||||
describe('ServiceNow ITSM', () => {
|
||||
let simulatedActionId = '';
|
||||
let serviceNowSimulatorURL: string = '';
|
||||
|
@ -82,7 +102,7 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
describe('ServiceNow ITSM - Action Creation', () => {
|
||||
it('should return 200 when creating a servicenow action successfully', async () => {
|
||||
it('should return 200 when creating a servicenow Basic Auth connector successfully', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
|
@ -93,7 +113,7 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
apiUrl: serviceNowSimulatorURL,
|
||||
usesTableApi: false,
|
||||
},
|
||||
secrets: mockServiceNow.secrets,
|
||||
secrets: mockServiceNowBasic.secrets,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
|
@ -107,6 +127,10 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
config: {
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
usesTableApi: false,
|
||||
isOAuth: false,
|
||||
clientId: null,
|
||||
jwtKeyId: null,
|
||||
userIdentifierValue: null,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -124,6 +148,64 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
config: {
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
usesTableApi: false,
|
||||
isOAuth: false,
|
||||
clientId: null,
|
||||
jwtKeyId: null,
|
||||
userIdentifierValue: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 when creating a servicenow OAuth connector successfully', async () => {
|
||||
const { body: createdConnector } = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A servicenow action',
|
||||
connector_type_id: '.servicenow',
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
},
|
||||
secrets: mockServiceNowOAuth.secrets,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(createdConnector).to.eql({
|
||||
id: createdConnector.id,
|
||||
is_preconfigured: false,
|
||||
is_deprecated: false,
|
||||
name: 'A servicenow action',
|
||||
connector_type_id: '.servicenow',
|
||||
is_missing_secrets: false,
|
||||
config: {
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
usesTableApi: false,
|
||||
isOAuth: true,
|
||||
clientId: mockServiceNowOAuth.config.clientId,
|
||||
jwtKeyId: mockServiceNowOAuth.config.jwtKeyId,
|
||||
userIdentifierValue: mockServiceNowOAuth.config.userIdentifierValue,
|
||||
},
|
||||
});
|
||||
|
||||
const { body: fetchedConnector } = await supertest
|
||||
.get(`/api/actions/connector/${createdConnector.id}`)
|
||||
.expect(200);
|
||||
|
||||
expect(fetchedConnector).to.eql({
|
||||
id: fetchedConnector.id,
|
||||
is_preconfigured: false,
|
||||
is_deprecated: false,
|
||||
name: 'A servicenow action',
|
||||
connector_type_id: '.servicenow',
|
||||
is_missing_secrets: false,
|
||||
config: {
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
usesTableApi: false,
|
||||
isOAuth: true,
|
||||
clientId: mockServiceNowOAuth.config.clientId,
|
||||
jwtKeyId: mockServiceNowOAuth.config.jwtKeyId,
|
||||
userIdentifierValue: mockServiceNowOAuth.config.userIdentifierValue,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -138,7 +220,7 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
config: {
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
},
|
||||
secrets: mockServiceNow.secrets,
|
||||
secrets: mockServiceNowBasic.secrets,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
|
@ -149,7 +231,7 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
expect(fetchedAction.config.usesTableApi).to.be(true);
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow action with no apiUrl', async () => {
|
||||
it('should respond with a 400 Bad Request when creating a servicenow Basic Auth connector with no apiUrl', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
|
@ -169,7 +251,30 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow action with a not present in allowedHosts apiUrl', async () => {
|
||||
it('should respond with a 400 Bad Request when creating a servicenow OAuth connector with no apiUrl', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A servicenow action',
|
||||
connector_type_id: '.servicenow',
|
||||
config: {
|
||||
isOAuth: true,
|
||||
},
|
||||
secrets: mockServiceNowOAuth.secrets,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow connector with a not present in allowedHosts apiUrl', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
|
@ -179,7 +284,7 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
config: {
|
||||
apiUrl: 'http://servicenow.mynonexistent.com',
|
||||
},
|
||||
secrets: mockServiceNow.secrets,
|
||||
secrets: mockServiceNowBasic.secrets,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
|
@ -192,7 +297,7 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow action without secrets', async () => {
|
||||
it('should respond with a 400 Bad Request when creating a servicenow Basic Auth connector without secrets', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
|
@ -209,10 +314,107 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type secrets: [password]: expected value of type [string] but got [undefined]',
|
||||
'error validating action type secrets: Either basic auth or OAuth credentials must be specified',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow OAuth connector without secrets', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A servicenow action',
|
||||
connector_type_id: '.servicenow',
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
},
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type secrets: Either basic auth or OAuth credentials must be specified',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow OAuth connector with missing fields', async () => {
|
||||
const badConfigs = [
|
||||
{
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
clientId: null,
|
||||
},
|
||||
secrets: mockServiceNowOAuth.secrets,
|
||||
errorMessage: `error validating action type config: clientId must be provided when isOAuth = true`,
|
||||
},
|
||||
{
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
userIdentifierValue: null,
|
||||
},
|
||||
secrets: mockServiceNowOAuth.secrets,
|
||||
errorMessage: `error validating action type config: userIdentifierValue must be provided when isOAuth = true`,
|
||||
},
|
||||
{
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
jwtKeyId: null,
|
||||
},
|
||||
secrets: mockServiceNowOAuth.secrets,
|
||||
errorMessage: `error validating action type config: jwtKeyId must be provided when isOAuth = true`,
|
||||
},
|
||||
{
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
},
|
||||
secrets: {
|
||||
...mockServiceNowOAuth.secrets,
|
||||
clientSecret: null,
|
||||
},
|
||||
errorMessage: `error validating action type secrets: clientSecret and privateKey must both be specified`,
|
||||
},
|
||||
{
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
},
|
||||
secrets: {
|
||||
...mockServiceNowOAuth.secrets,
|
||||
privateKey: null,
|
||||
},
|
||||
errorMessage: `error validating action type secrets: clientSecret and privateKey must both be specified`,
|
||||
},
|
||||
];
|
||||
|
||||
await asyncForEach(badConfigs, async (badConfig) => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A servicenow action',
|
||||
connector_type_id: '.servicenow',
|
||||
config: badConfig.config,
|
||||
secrets: badConfig.secrets,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: badConfig.errorMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ServiceNow ITSM - Executor', () => {
|
||||
|
@ -227,7 +429,7 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
apiUrl: serviceNowSimulatorURL,
|
||||
usesTableApi: false,
|
||||
},
|
||||
secrets: mockServiceNow.secrets,
|
||||
secrets: mockServiceNowBasic.secrets,
|
||||
});
|
||||
simulatedActionId = body.id;
|
||||
});
|
||||
|
@ -289,7 +491,7 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
...mockServiceNow.params,
|
||||
...mockServiceNowBasic.params,
|
||||
subActionParams: {
|
||||
savedObjectId: 'success',
|
||||
},
|
||||
|
@ -312,10 +514,10 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
...mockServiceNow.params,
|
||||
...mockServiceNowBasic.params,
|
||||
subActionParams: {
|
||||
incident: {
|
||||
...mockServiceNow.params.subActionParams.incident,
|
||||
...mockServiceNowBasic.params.subActionParams.incident,
|
||||
short_description: 'success',
|
||||
},
|
||||
comments: [{ comment: 'boo' }],
|
||||
|
@ -339,10 +541,10 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
...mockServiceNow.params,
|
||||
...mockServiceNowBasic.params,
|
||||
subActionParams: {
|
||||
incident: {
|
||||
...mockServiceNow.params.subActionParams.incident,
|
||||
...mockServiceNowBasic.params.subActionParams.incident,
|
||||
short_description: 'success',
|
||||
},
|
||||
comments: [{ commentId: 'success' }],
|
||||
|
@ -393,9 +595,9 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
...mockServiceNow.params,
|
||||
...mockServiceNowBasic.params,
|
||||
subActionParams: {
|
||||
incident: mockServiceNow.params.subActionParams.incident,
|
||||
incident: mockServiceNowBasic.params.subActionParams.incident,
|
||||
comments: [],
|
||||
},
|
||||
},
|
||||
|
@ -429,7 +631,7 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
apiUrl: serviceNowSimulatorURL,
|
||||
usesTableApi: true,
|
||||
},
|
||||
secrets: mockServiceNow.secrets,
|
||||
secrets: mockServiceNowBasic.secrets,
|
||||
});
|
||||
simulatedActionId = body.id;
|
||||
});
|
||||
|
@ -440,9 +642,9 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
...mockServiceNow.params,
|
||||
...mockServiceNowBasic.params,
|
||||
subActionParams: {
|
||||
incident: mockServiceNow.params.subActionParams.incident,
|
||||
incident: mockServiceNowBasic.params.subActionParams.incident,
|
||||
comments: [],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import httpProxy from 'http-proxy';
|
||||
import expect from '@kbn/expect';
|
||||
import { asyncForEach } from '@kbn/std';
|
||||
import getPort from 'get-port';
|
||||
import http from 'http';
|
||||
|
||||
|
@ -19,7 +20,7 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
|
|||
const supertest = getService('supertest');
|
||||
const configService = getService('config');
|
||||
|
||||
const mockServiceNow = {
|
||||
const mockServiceNowCommon = {
|
||||
config: {
|
||||
apiUrl: 'www.servicenowisinkibanaactions.com',
|
||||
usesTableApi: false,
|
||||
|
@ -55,6 +56,33 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
|
|||
},
|
||||
};
|
||||
|
||||
const mockServiceNowBasic = {
|
||||
...mockServiceNowCommon,
|
||||
config: {
|
||||
apiUrl: 'www.servicenowisinkibanaactions.com',
|
||||
usesTableApi: false,
|
||||
},
|
||||
secrets: {
|
||||
password: 'elastic',
|
||||
username: 'changeme',
|
||||
},
|
||||
};
|
||||
const mockServiceNowOAuth = {
|
||||
...mockServiceNowCommon,
|
||||
config: {
|
||||
apiUrl: 'www.servicenowisinkibanaactions.com',
|
||||
usesTableApi: false,
|
||||
isOAuth: true,
|
||||
clientId: 'abc',
|
||||
userIdentifierValue: 'elastic',
|
||||
jwtKeyId: 'def',
|
||||
},
|
||||
secrets: {
|
||||
clientSecret: 'xyz',
|
||||
privateKey: '-----BEGIN RSA PRIVATE KEY-----\nddddddd\n-----END RSA PRIVATE KEY-----',
|
||||
},
|
||||
};
|
||||
|
||||
describe('ServiceNow SIR', () => {
|
||||
let simulatedActionId = '';
|
||||
let serviceNowSimulatorURL: string = '';
|
||||
|
@ -86,7 +114,7 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
describe('ServiceNow SIR - Action Creation', () => {
|
||||
it('should return 200 when creating a servicenow action successfully', async () => {
|
||||
it('should return 200 when creating a servicenow Basic Auth connector successfully', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
|
@ -97,7 +125,7 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
|
|||
apiUrl: serviceNowSimulatorURL,
|
||||
usesTableApi: false,
|
||||
},
|
||||
secrets: mockServiceNow.secrets,
|
||||
secrets: mockServiceNowBasic.secrets,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
|
@ -111,6 +139,10 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
|
|||
config: {
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
usesTableApi: false,
|
||||
isOAuth: false,
|
||||
clientId: null,
|
||||
jwtKeyId: null,
|
||||
userIdentifierValue: null,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -128,6 +160,64 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
|
|||
config: {
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
usesTableApi: false,
|
||||
isOAuth: false,
|
||||
clientId: null,
|
||||
jwtKeyId: null,
|
||||
userIdentifierValue: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 when creating a servicenow OAuth connector successfully', async () => {
|
||||
const { body: createdConnector } = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A servicenow action',
|
||||
connector_type_id: '.servicenow-sir',
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
},
|
||||
secrets: mockServiceNowOAuth.secrets,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(createdConnector).to.eql({
|
||||
id: createdConnector.id,
|
||||
is_preconfigured: false,
|
||||
is_deprecated: false,
|
||||
name: 'A servicenow action',
|
||||
connector_type_id: '.servicenow-sir',
|
||||
is_missing_secrets: false,
|
||||
config: {
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
usesTableApi: false,
|
||||
isOAuth: true,
|
||||
clientId: mockServiceNowOAuth.config.clientId,
|
||||
jwtKeyId: mockServiceNowOAuth.config.jwtKeyId,
|
||||
userIdentifierValue: mockServiceNowOAuth.config.userIdentifierValue,
|
||||
},
|
||||
});
|
||||
|
||||
const { body: fetchedConnector } = await supertest
|
||||
.get(`/api/actions/connector/${createdConnector.id}`)
|
||||
.expect(200);
|
||||
|
||||
expect(fetchedConnector).to.eql({
|
||||
id: fetchedConnector.id,
|
||||
is_preconfigured: false,
|
||||
is_deprecated: false,
|
||||
name: 'A servicenow action',
|
||||
connector_type_id: '.servicenow-sir',
|
||||
is_missing_secrets: false,
|
||||
config: {
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
usesTableApi: false,
|
||||
isOAuth: true,
|
||||
clientId: mockServiceNowOAuth.config.clientId,
|
||||
jwtKeyId: mockServiceNowOAuth.config.jwtKeyId,
|
||||
userIdentifierValue: mockServiceNowOAuth.config.userIdentifierValue,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -142,7 +232,7 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
|
|||
config: {
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
},
|
||||
secrets: mockServiceNow.secrets,
|
||||
secrets: mockServiceNowBasic.secrets,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
|
@ -153,7 +243,7 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
|
|||
expect(fetchedAction.config.usesTableApi).to.be(true);
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow action with no apiUrl', async () => {
|
||||
it('should respond with a 400 Bad Request when creating a servicenow Basic Auth connector with no apiUrl', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
|
@ -173,7 +263,30 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow action with a not present in allowedHosts apiUrl', async () => {
|
||||
it('should respond with a 400 Bad Request when creating a servicenow OAuth connector with no apiUrl', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A servicenow action',
|
||||
connector_type_id: '.servicenow-sir',
|
||||
config: {
|
||||
isOAuth: true,
|
||||
},
|
||||
secrets: mockServiceNowOAuth.secrets,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow connector with a not present in allowedHosts apiUrl', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
|
@ -183,7 +296,7 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
|
|||
config: {
|
||||
apiUrl: 'http://servicenow.mynonexistent.com',
|
||||
},
|
||||
secrets: mockServiceNow.secrets,
|
||||
secrets: mockServiceNowBasic.secrets,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
|
@ -196,7 +309,7 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow action without secrets', async () => {
|
||||
it('should respond with a 400 Bad Request when creating a servicenow Basic Auth connector without secrets', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
|
@ -213,10 +326,107 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type secrets: [password]: expected value of type [string] but got [undefined]',
|
||||
'error validating action type secrets: Either basic auth or OAuth credentials must be specified',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow OAuth connector without secrets', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A servicenow action',
|
||||
connector_type_id: '.servicenow-sir',
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
},
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type secrets: Either basic auth or OAuth credentials must be specified',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow OAuth connector with missing fields', async () => {
|
||||
const badConfigs = [
|
||||
{
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
clientId: null,
|
||||
},
|
||||
secrets: mockServiceNowOAuth.secrets,
|
||||
errorMessage: `error validating action type config: clientId must be provided when isOAuth = true`,
|
||||
},
|
||||
{
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
userIdentifierValue: null,
|
||||
},
|
||||
secrets: mockServiceNowOAuth.secrets,
|
||||
errorMessage: `error validating action type config: userIdentifierValue must be provided when isOAuth = true`,
|
||||
},
|
||||
{
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
jwtKeyId: null,
|
||||
},
|
||||
secrets: mockServiceNowOAuth.secrets,
|
||||
errorMessage: `error validating action type config: jwtKeyId must be provided when isOAuth = true`,
|
||||
},
|
||||
{
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
},
|
||||
secrets: {
|
||||
...mockServiceNowOAuth.secrets,
|
||||
clientSecret: null,
|
||||
},
|
||||
errorMessage: `error validating action type secrets: clientSecret and privateKey must both be specified`,
|
||||
},
|
||||
{
|
||||
config: {
|
||||
...mockServiceNowOAuth.config,
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
},
|
||||
secrets: {
|
||||
...mockServiceNowOAuth.secrets,
|
||||
privateKey: null,
|
||||
},
|
||||
errorMessage: `error validating action type secrets: clientSecret and privateKey must both be specified`,
|
||||
},
|
||||
];
|
||||
|
||||
await asyncForEach(badConfigs, async (badConfig) => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A servicenow action',
|
||||
connector_type_id: '.servicenow-sir',
|
||||
config: badConfig.config,
|
||||
secrets: badConfig.secrets,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: badConfig.errorMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ServiceNow SIR - Executor', () => {
|
||||
|
@ -230,8 +440,9 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
|
|||
config: {
|
||||
apiUrl: serviceNowSimulatorURL,
|
||||
usesTableApi: false,
|
||||
isOAuth: false,
|
||||
},
|
||||
secrets: mockServiceNow.secrets,
|
||||
secrets: mockServiceNowBasic.secrets,
|
||||
});
|
||||
simulatedActionId = body.id;
|
||||
});
|
||||
|
@ -293,7 +504,7 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
|
|||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
...mockServiceNow.params,
|
||||
...mockServiceNowBasic.params,
|
||||
subActionParams: {
|
||||
savedObjectId: 'success',
|
||||
},
|
||||
|
@ -316,10 +527,10 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
|
|||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
...mockServiceNow.params,
|
||||
...mockServiceNowBasic.params,
|
||||
subActionParams: {
|
||||
incident: {
|
||||
...mockServiceNow.params.subActionParams.incident,
|
||||
...mockServiceNowBasic.params.subActionParams.incident,
|
||||
short_description: 'success',
|
||||
},
|
||||
comments: [{ comment: 'boo' }],
|
||||
|
@ -343,10 +554,10 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
|
|||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
...mockServiceNow.params,
|
||||
...mockServiceNowBasic.params,
|
||||
subActionParams: {
|
||||
incident: {
|
||||
...mockServiceNow.params.subActionParams.incident,
|
||||
...mockServiceNowBasic.params.subActionParams.incident,
|
||||
short_description: 'success',
|
||||
},
|
||||
comments: [{ commentId: 'success' }],
|
||||
|
@ -397,9 +608,9 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
|
|||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
...mockServiceNow.params,
|
||||
...mockServiceNowBasic.params,
|
||||
subActionParams: {
|
||||
incident: mockServiceNow.params.subActionParams.incident,
|
||||
incident: mockServiceNowBasic.params.subActionParams.incident,
|
||||
comments: [],
|
||||
},
|
||||
},
|
||||
|
@ -433,7 +644,7 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
|
|||
apiUrl: serviceNowSimulatorURL,
|
||||
usesTableApi: true,
|
||||
},
|
||||
secrets: mockServiceNow.secrets,
|
||||
secrets: mockServiceNowBasic.secrets,
|
||||
});
|
||||
simulatedActionId = body.id;
|
||||
});
|
||||
|
@ -444,9 +655,9 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
|
|||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
...mockServiceNow.params,
|
||||
...mockServiceNowBasic.params,
|
||||
subActionParams: {
|
||||
incident: mockServiceNow.params.subActionParams.incident,
|
||||
incident: mockServiceNowBasic.params.subActionParams.incident,
|
||||
comments: [],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { asyncForEach } from '@kbn/std';
|
||||
import { getUrlPrefix } from '../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
|
@ -83,6 +84,23 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
|||
expect(connectorWithoutService.body.config.service).to.eql('other');
|
||||
});
|
||||
|
||||
it('8.3.0 migrates service now connectors to have `isOAuth` property', async () => {
|
||||
const serviceNowConnectorIds = [
|
||||
'7d04bc30-c4c0-11ec-ae29-917aa31a5b75',
|
||||
'8a9331b0-c4c0-11ec-ae29-917aa31a5b75',
|
||||
'6d3a1250-c4c0-11ec-ae29-917aa31a5b75',
|
||||
];
|
||||
|
||||
await asyncForEach(serviceNowConnectorIds, async (serviceNowConnectorId) => {
|
||||
const connectorResponse = await supertest.get(
|
||||
`${getUrlPrefix(``)}/api/actions/action/${serviceNowConnectorId}`
|
||||
);
|
||||
|
||||
expect(connectorResponse.status).to.eql(200);
|
||||
expect(connectorResponse.body.config.isOAuth).to.eql(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('decryption error during migration', async () => {
|
||||
const badEmailConnector = await supertest.get(
|
||||
`${getUrlPrefix(``)}/api/actions/connector/0f8f2810-0a59-11ec-9a7c-fd0c2b83ff7d`
|
||||
|
|
|
@ -217,6 +217,23 @@ export const getServiceNowConnector = () => ({
|
|||
},
|
||||
});
|
||||
|
||||
export const getServiceNowOAuthConnector = () => ({
|
||||
name: 'ServiceNow OAuth Connector',
|
||||
connector_type_id: '.servicenow',
|
||||
secrets: {
|
||||
clientSecret: 'xyz',
|
||||
privateKey: '-----BEGIN RSA PRIVATE KEY-----\nddddddd\n-----END RSA PRIVATE KEY-----',
|
||||
},
|
||||
config: {
|
||||
apiUrl: 'http://some.non.existent.com',
|
||||
usesTableApi: false,
|
||||
isOAuth: true,
|
||||
clientId: 'abc',
|
||||
userIdentifierValue: 'elastic',
|
||||
jwtKeyId: 'def',
|
||||
},
|
||||
});
|
||||
|
||||
export const getJiraConnector = () => ({
|
||||
name: 'Jira Connector',
|
||||
connector_type_id: '.jira',
|
||||
|
@ -262,7 +279,7 @@ export const getResilientConnector = () => ({
|
|||
});
|
||||
|
||||
export const getServiceNowSIRConnector = () => ({
|
||||
name: 'ServiceNow Connector',
|
||||
name: 'ServiceNow SIR Connector',
|
||||
connector_type_id: '.servicenow-sir',
|
||||
secrets: {
|
||||
username: 'admin',
|
||||
|
|
|
@ -11,6 +11,7 @@ import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
|||
import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib';
|
||||
import {
|
||||
getServiceNowConnector,
|
||||
getServiceNowOAuthConnector,
|
||||
getJiraConnector,
|
||||
getResilientConnector,
|
||||
createConnector,
|
||||
|
@ -31,6 +32,10 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
|
||||
it('should return the correct connectors', async () => {
|
||||
const snConnector = await createConnector({ supertest, req: getServiceNowConnector() });
|
||||
const snOAuthConnector = await createConnector({
|
||||
supertest,
|
||||
req: getServiceNowOAuthConnector(),
|
||||
});
|
||||
const emailConnector = await createConnector({ supertest, req: getEmailConnector() });
|
||||
const jiraConnector = await createConnector({ supertest, req: getJiraConnector() });
|
||||
const resilientConnector = await createConnector({ supertest, req: getResilientConnector() });
|
||||
|
@ -38,13 +43,15 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
|
||||
actionsRemover.add('default', sir.id, 'action', 'actions');
|
||||
actionsRemover.add('default', snConnector.id, 'action', 'actions');
|
||||
actionsRemover.add('default', snOAuthConnector.id, 'action', 'actions');
|
||||
actionsRemover.add('default', emailConnector.id, 'action', 'actions');
|
||||
actionsRemover.add('default', jiraConnector.id, 'action', 'actions');
|
||||
actionsRemover.add('default', resilientConnector.id, 'action', 'actions');
|
||||
|
||||
const connectors = await getCaseConnectors({ supertest });
|
||||
const sortedConnectors = connectors.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
expect(connectors).to.eql([
|
||||
expect(sortedConnectors).to.eql([
|
||||
{
|
||||
id: jiraConnector.id,
|
||||
actionTypeId: '.jira',
|
||||
|
@ -90,6 +97,27 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
config: {
|
||||
apiUrl: 'http://some.non.existent.com',
|
||||
usesTableApi: false,
|
||||
isOAuth: false,
|
||||
clientId: null,
|
||||
jwtKeyId: null,
|
||||
userIdentifierValue: null,
|
||||
},
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isMissingSecrets: false,
|
||||
referencedByCount: 0,
|
||||
},
|
||||
{
|
||||
id: snOAuthConnector.id,
|
||||
actionTypeId: '.servicenow',
|
||||
name: 'ServiceNow OAuth Connector',
|
||||
config: {
|
||||
apiUrl: 'http://some.non.existent.com',
|
||||
usesTableApi: false,
|
||||
isOAuth: true,
|
||||
clientId: 'abc',
|
||||
userIdentifierValue: 'elastic',
|
||||
jwtKeyId: 'def',
|
||||
},
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
|
@ -99,10 +127,14 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
{
|
||||
id: sir.id,
|
||||
actionTypeId: '.servicenow-sir',
|
||||
name: 'ServiceNow Connector',
|
||||
name: 'ServiceNow SIR Connector',
|
||||
config: {
|
||||
apiUrl: 'http://some.non.existent.com',
|
||||
usesTableApi: false,
|
||||
isOAuth: false,
|
||||
clientId: null,
|
||||
jwtKeyId: null,
|
||||
userIdentifierValue: null,
|
||||
},
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
|
|
|
@ -11,6 +11,7 @@ import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
|||
import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib';
|
||||
import {
|
||||
getServiceNowConnector,
|
||||
getServiceNowOAuthConnector,
|
||||
getJiraConnector,
|
||||
getResilientConnector,
|
||||
createConnector,
|
||||
|
@ -39,7 +40,11 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
req: getServiceNowConnector(),
|
||||
auth: authSpace1,
|
||||
});
|
||||
|
||||
const snOAuthConnector = await createConnector({
|
||||
supertest,
|
||||
req: getServiceNowOAuthConnector(),
|
||||
auth: authSpace1,
|
||||
});
|
||||
const emailConnector = await createConnector({
|
||||
supertest,
|
||||
req: getEmailConnector(),
|
||||
|
@ -66,13 +71,15 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
|
||||
actionsRemover.add(space, sir.id, 'action', 'actions');
|
||||
actionsRemover.add(space, snConnector.id, 'action', 'actions');
|
||||
actionsRemover.add(space, snOAuthConnector.id, 'action', 'actions');
|
||||
actionsRemover.add(space, emailConnector.id, 'action', 'actions');
|
||||
actionsRemover.add(space, jiraConnector.id, 'action', 'actions');
|
||||
actionsRemover.add(space, resilientConnector.id, 'action', 'actions');
|
||||
|
||||
const connectors = await getCaseConnectors({ supertest, auth: authSpace1 });
|
||||
const sortedConnectors = connectors.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
expect(connectors).to.eql([
|
||||
expect(sortedConnectors).to.eql([
|
||||
{
|
||||
id: jiraConnector.id,
|
||||
actionTypeId: '.jira',
|
||||
|
@ -118,6 +125,27 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
config: {
|
||||
apiUrl: 'http://some.non.existent.com',
|
||||
usesTableApi: false,
|
||||
isOAuth: false,
|
||||
clientId: null,
|
||||
jwtKeyId: null,
|
||||
userIdentifierValue: null,
|
||||
},
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isMissingSecrets: false,
|
||||
referencedByCount: 0,
|
||||
},
|
||||
{
|
||||
id: snOAuthConnector.id,
|
||||
actionTypeId: '.servicenow',
|
||||
name: 'ServiceNow OAuth Connector',
|
||||
config: {
|
||||
apiUrl: 'http://some.non.existent.com',
|
||||
usesTableApi: false,
|
||||
isOAuth: true,
|
||||
clientId: 'abc',
|
||||
userIdentifierValue: 'elastic',
|
||||
jwtKeyId: 'def',
|
||||
},
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
|
@ -127,10 +155,14 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
{
|
||||
id: sir.id,
|
||||
actionTypeId: '.servicenow-sir',
|
||||
name: 'ServiceNow Connector',
|
||||
name: 'ServiceNow SIR Connector',
|
||||
config: {
|
||||
apiUrl: 'http://some.non.existent.com',
|
||||
usesTableApi: false,
|
||||
isOAuth: false,
|
||||
clientId: null,
|
||||
jwtKeyId: null,
|
||||
userIdentifierValue: null,
|
||||
},
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
|
@ -147,6 +179,12 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
auth: authSpace1,
|
||||
});
|
||||
|
||||
const snOAuthConnector = await createConnector({
|
||||
supertest,
|
||||
req: getServiceNowOAuthConnector(),
|
||||
auth: authSpace1,
|
||||
});
|
||||
|
||||
const emailConnector = await createConnector({
|
||||
supertest,
|
||||
req: getEmailConnector(),
|
||||
|
@ -173,6 +211,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
|
||||
actionsRemover.add(space, sir.id, 'action', 'actions');
|
||||
actionsRemover.add(space, snConnector.id, 'action', 'actions');
|
||||
actionsRemover.add(space, snOAuthConnector.id, 'action', 'actions');
|
||||
actionsRemover.add(space, emailConnector.id, 'action', 'actions');
|
||||
actionsRemover.add(space, jiraConnector.id, 'action', 'actions');
|
||||
actionsRemover.add(space, resilientConnector.id, 'action', 'actions');
|
||||
|
|
|
@ -205,4 +205,96 @@
|
|||
"updated_at": "2021-08-31T12:43:37.117Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"id": "action:7d04bc30-c4c0-11ec-ae29-917aa31a5b75",
|
||||
"index": ".kibana_1",
|
||||
"source": {
|
||||
"action": {
|
||||
"actionTypeId" : ".servicenow-sir",
|
||||
"name" : "test servicenow SecOps",
|
||||
"isMissingSecrets" : false,
|
||||
"config" : {
|
||||
"apiUrl": "https://devtestsecops.service-now.com",
|
||||
"usesTableApi": false
|
||||
},
|
||||
"secrets" : "kPp4tl4ueQ2ZNWSfATR3dFrbxd+NNBo4MY8izS6GJf358Lmeg/YaYjb2rIymrbPktR6HnPBRaVyXWlRTvBGstRicJc0LJHZbx3wNJlTRIj4UFlVqZLGQWQ/GcSqFLSZ1JQbKwgAvyfLtF6BhjAhGYEovK3/OLUNzGc3gvUOOHBiPWjiAY8A="
|
||||
},
|
||||
"migrationVersion": {
|
||||
"action": "8.0.0"
|
||||
},
|
||||
"coreMigrationVersion" : "8.2.0",
|
||||
"references": [
|
||||
],
|
||||
"namespaces": [
|
||||
"default"
|
||||
],
|
||||
"type": "action",
|
||||
"updated_at": "2022-04-25T17:52:35.201Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"id": "action:8a9331b0-c4c0-11ec-ae29-917aa31a5b75",
|
||||
"index": ".kibana_1",
|
||||
"source": {
|
||||
"action": {
|
||||
"actionTypeId" : ".servicenow-itom",
|
||||
"name" : "test servicenow ITOM",
|
||||
"isMissingSecrets" : false,
|
||||
"config" : {
|
||||
"apiUrl": "https://devtestsecops.service-now.com"
|
||||
},
|
||||
"secrets" : "yYThM4vbrSTIg5IjKWE+eMDrxzL7UO0JQIyh6FvEMgqoNREUxRrIavSo25v+DXQIX1DyfsvjjKg97pNPlZhvS3siCwDZZafSFrwkCKDl+S4KHORgIMX+slilcQeuEnzwit7bFxcY7Y/AcNF8Ks6jO0Gs1UR58ibSPUALXoK2VOlJnHSgtvE="
|
||||
},
|
||||
"migrationVersion": {
|
||||
"action": "8.0.0"
|
||||
},
|
||||
"coreMigrationVersion" : "8.2.0",
|
||||
"references": [
|
||||
],
|
||||
"namespaces": [
|
||||
"default"
|
||||
],
|
||||
"type": "action",
|
||||
"updated_at": "2022-04-25T17:52:35.201Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"id": "action:6d3a1250-c4c0-11ec-ae29-917aa31a5b75",
|
||||
"index": ".kibana_1",
|
||||
"source": {
|
||||
"action": {
|
||||
"actionTypeId" : ".servicenow",
|
||||
"name" : "test servicenow ITSM",
|
||||
"isMissingSecrets" : false,
|
||||
"config" : {
|
||||
"usesTableApi": false,
|
||||
"apiUrl": "https://devtestsecops.service-now.com"
|
||||
},
|
||||
"secrets" : "zfXUDtG0CyJkJUKnQ8rSqo75hb6ZhbRUWkV1NiFEjApM87b72Rcqz3Fv+sbm8eBDOO1Fdd9CVyK+Bfly4ZwVCgL2lR0qIbPzz34q36r267dnGVsaERyJIVv2WPy+EGdiRZKgfpy4XFbMNT1R3gyIsUkd4TT+McqGfVTont2XTFIpMW2A9y8="
|
||||
},
|
||||
"migrationVersion": {
|
||||
"action": "8.0.0"
|
||||
},
|
||||
"coreMigrationVersion" : "8.2.0",
|
||||
"references": [
|
||||
],
|
||||
"namespaces": [
|
||||
"default"
|
||||
],
|
||||
"type": "action",
|
||||
"updated_at": "2022-04-25T17:52:35.201Z"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue