Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
bab21d4
Adding new OAuth fields to ServiceNow ExternalIncidentServiceConfigur…
ymao1 Apr 21, 2022
e9628c4
Creating new function in ConnectorTokenClient for updating or replaci…
ymao1 Apr 21, 2022
4e86e06
Update servicenow executors to get Oauth access tokens if configured.…
ymao1 Apr 21, 2022
c39118b
Merge branch 'main' of https://github.com/elastic/kibana into connect…
ymao1 Apr 21, 2022
ada790c
Merge branch 'main' of https://github.com/elastic/kibana into connect…
ymao1 Apr 25, 2022
d30004f
Creating wrapper function for createService to only create one axios …
ymao1 Apr 25, 2022
f71c39a
Fixing translation check error
ymao1 Apr 25, 2022
75af7a5
Adding migration for adding isOAuth to service now connectors
ymao1 Apr 25, 2022
3f563ec
Fixing unit tests
ymao1 Apr 25, 2022
e2653e0
Fixing functional test
ymao1 Apr 25, 2022
32dbb7a
Merge branch 'main' of https://github.com/elastic/kibana into connect…
ymao1 Apr 25, 2022
53272c6
Not requiring privateKeyPassword
ymao1 Apr 25, 2022
5b2101e
Merge branch 'main' of https://github.com/elastic/kibana into connect…
ymao1 Apr 26, 2022
8cbdecc
Fixing tests
ymao1 Apr 26, 2022
b16ec16
Merge branch 'main' of https://github.com/elastic/kibana into connect…
ymao1 Apr 26, 2022
f039cf5
Adding functional tests for connector creation
ymao1 Apr 27, 2022
5f2f7e0
Adding functional tests
ymao1 Apr 27, 2022
8c8db33
Merging in main
ymao1 Apr 27, 2022
5d6f1b2
Fixing functional test
ymao1 Apr 27, 2022
6497f84
PR feedback
ymao1 Apr 28, 2022
efb10fc
Merge branch 'main' of https://github.com/elastic/kibana into connect…
ymao1 Apr 28, 2022
8b6db2b
Adding route for requesting access token using OAuth credentials
ymao1 Apr 28, 2022
e06f3e7
Fixing test
ymao1 Apr 28, 2022
eb944c8
Merge branch 'main' of https://github.com/elastic/kibana into connect…
ymao1 Apr 28, 2022
361bf20
Merge branch 'connectors/servicenow-itom-oauth' into connectors/servi…
ymao1 Apr 28, 2022
26852d8
Adding functional test
ymao1 Apr 28, 2022
df6b834
Fixing functional test
ymao1 Apr 28, 2022
aadaf8a
Fixing checks
ymao1 Apr 28, 2022
c9b5cb1
Merging in main
ymao1 Apr 29, 2022
ebf4704
Merge branch 'main' into connectors/servicenow-token-route
kibanamachine May 2, 2022
eabe4c5
Merge branch 'main' of https://github.com/elastic/kibana into connect…
ymao1 May 3, 2022
d3422a0
Using existing private key
ymao1 May 3, 2022
dc65c55
Refactoring get access token utilities to be more generic
ymao1 May 3, 2022
68af17d
Merge branch 'main' into connectors/servicenow-token-route
kibanamachine May 3, 2022
44824aa
Checking tokenurl against allowlist
ymao1 May 4, 2022
b051242
Merge branch 'main' of https://github.com/elastic/kibana into connect…
ymao1 May 4, 2022
b065a5c
Merge branch 'connectors/servicenow-token-route' of https://github.co…
ymao1 May 4, 2022
b6714be
Merging in main
ymao1 May 6, 2022
79962f5
Restricting access to users with ability to update connectors
ymao1 May 6, 2022
769176f
Adding slashesDenotesHost parameter to url.parse
ymao1 May 6, 2022
b02b197
Removing ability to specify custom claims for jwt assertion
ymao1 May 6, 2022
52b94de
Verifying that token url contains hostname and uses https
ymao1 May 6, 2022
a32003d
Allowing http
ymao1 May 6, 2022
1b0263c
Merge branch 'main' of https://github.com/elastic/kibana into connect…
ymao1 May 6, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -436,5 +436,113 @@ describe('utils', () => {
`Not able to update ServiceNow connector token for connectorId: 123 due to error: updateOrReplace error`
);
});

test('gets access token if connectorId is not provided', async () => {
(createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion');
(requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({
tokenType: 'access_token',
accessToken: 'brandnewaccesstoken',
expiresIn: 1000,
});

const accessToken = await getAccessToken({
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,
});

expect(connectorTokenClient.get).not.toHaveBeenCalled();
expect(connectorTokenClient.updateOrReplace).not.toHaveBeenCalled();
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
);
});

test('gets access token if connectorTokenClient is not provided', async () => {
(createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion');
(requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({
tokenType: 'access_token',
accessToken: 'brandnewaccesstoken',
expiresIn: 1000,
});

const accessToken = await getAccessToken({
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',
});

expect(connectorTokenClient.get).not.toHaveBeenCalled();
expect(connectorTokenClient.updateOrReplace).not.toHaveBeenCalled();
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
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ 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 { ConnectorToken, ConnectorTokenClientContract } from '../../types';
import { createJWTAssertion } from '../lib/create_jwt_assertion';
import { requestOAuthJWTToken } from '../lib/request_oauth_jwt_token';

Expand Down Expand Up @@ -84,12 +84,12 @@ export const throwIfSubActionIsNotSupported = ({
};

export interface GetAccessTokenAndAxiosInstanceOpts {
connectorId: string;
connectorId?: string;
logger: Logger;
configurationUtilities: ActionsConfigurationUtilities;
credentials: ExternalServiceCredentials;
snServiceUrl: string;
connectorTokenClient: ConnectorTokenClientContract;
connectorTokenClient?: ConnectorTokenClientContract;
}

export const getAxiosInstance = ({
Expand Down Expand Up @@ -151,9 +151,17 @@ export const getAccessToken = async ({
credentials.secrets as ServiceNowSecretConfigurationType;

let accessToken: string;
let connectorToken: ConnectorToken | null = null;
let hasErrors: boolean = false;

// Check if there is a token stored for this connector
const { connectorToken, hasErrors } = await connectorTokenClient.get({ connectorId });
if (connectorId && connectorTokenClient) {
// Check if there is a token stored for this connector
const { connectorToken: token, hasErrors: errors } = await connectorTokenClient.get({
connectorId,
});
connectorToken = token;
hasErrors = errors;
}

if (connectorToken === null || Date.parse(connectorToken.expiresAt) <= Date.now()) {
// generate a new assertion
Expand Down Expand Up @@ -189,18 +197,20 @@ export const getAccessToken = async ({
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}`
);
if (connectorId && connectorTokenClient) {
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
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/actions/server/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ describe('Actions Plugin', () => {
httpServerMock.createKibanaRequest(),
httpServerMock.createResponseFactory()
)) as unknown as ActionsApiRequestHandlerContext;
actionsContextHandler!.getActionsClient();
expect(actionsContextHandler!.getActionsClient()).toBeDefined();
});

it('should throw error when ESO plugin is missing encryption key', async () => {
Expand Down
11 changes: 6 additions & 5 deletions x-pack/plugins/actions/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,12 +311,13 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
}

// Routes
defineRoutes(
core.http.createRouter<ActionsRequestHandlerContext>(),
this.licenseState,
defineRoutes({
router: core.http.createRouter<ActionsRequestHandlerContext>(),
licenseState: this.licenseState,
logger: this.logger,
actionsConfigUtils,
this.usageCounter
);
usageCounter: this.usageCounter,
});

// Cleanup failed execution task definition
if (this.actionsConfig.cleanupFailedExecutionsTask.enabled) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* 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 { getServiceNowAccessToken } from './get_service_now_access_token';
import { Logger } from '@kbn/core/server';
import { httpServiceMock, loggingSystemMock } from '@kbn/core/server/mocks';
import { licenseStateMock } from '../lib/license_state.mock';
import { mockHandlerArguments } from './legacy/_mock_handler_arguments';
import { verifyAccessAndContext } from './verify_access_and_context';
import { getAccessToken } from '../builtin_action_types/servicenow/utils';
import { actionsConfigMock } from '../actions_config.mock';

jest.mock('./verify_access_and_context', () => ({
verifyAccessAndContext: jest.fn(),
}));
jest.mock('../builtin_action_types/servicenow/utils', () => ({
getAccessToken: jest.fn(),
}));

const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
const configurationUtilities = actionsConfigMock.create();

beforeEach(() => {
jest.resetAllMocks();
(verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler);
(getAccessToken as jest.Mock).mockResolvedValue(`Bearer tokentokentoken`);
});

describe('getServiceNowAccessToken', () => {
it('returns access token for given oauth config', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();

getServiceNowAccessToken(router, licenseState, logger, configurationUtilities);

const [config, handler] = router.post.mock.calls[0];

expect(config.path).toMatchInlineSnapshot(
`"/internal/actions/connector/_servicenow_access_token"`
);

const [context, req, res] = mockHandlerArguments(
{},
{
body: {
apiUrl: 'https://testurl.service-now.com/',
config: {
clientId: 'abc',
jwtKeyId: 'def',
userIdentifierValue: 'userA',
},
secrets: {
clientSecret: 'iamasecret',
privateKey: 'xyz',
},
},
},
['ok']
);

expect(await handler(context, req, res)).toMatchInlineSnapshot(`
Object {
"body": Object {
"accessToken": "Bearer tokentokentoken",
},
}
`);

expect(getAccessToken as jest.Mock).toHaveBeenCalledWith({
logger,
configurationUtilities,
credentials: {
config: {
clientId: 'abc',
jwtKeyId: 'def',
userIdentifierValue: 'userA',
isOAuth: true,
},
secrets: {
clientSecret: 'iamasecret',
privateKey: 'xyz',
},
},
snServiceUrl: 'https://testurl.service-now.com/',
});

expect(res.ok).toHaveBeenCalledWith({
body: {
accessToken: 'Bearer tokentokentoken',
},
});
});

it('ensures the license allows getting servicenow access token', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();

getServiceNowAccessToken(router, licenseState, logger, configurationUtilities);

const [config, handler] = router.post.mock.calls[0];

expect(config.path).toMatchInlineSnapshot(
`"/internal/actions/connector/_servicenow_access_token"`
);

const [context, req, res] = mockHandlerArguments(
{},
{
body: {
apiUrl: 'https://testurl.service-now.com/',
config: {
clientId: 'abc',
jwtKeyId: 'def',
userIdentifierValue: 'userA',
},
secrets: {
clientSecret: 'iamasecret',
privateKey: 'xyz',
},
},
},
['ok']
);

await handler(context, req, res);

expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function));
});

it('ensures the license check prevents getting service now access token', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();

(verifyAccessAndContext as jest.Mock).mockImplementation(() => async () => {
throw new Error('OMG');
});

getServiceNowAccessToken(router, licenseState, logger, configurationUtilities);

const [config, handler] = router.post.mock.calls[0];

expect(config.path).toMatchInlineSnapshot(
`"/internal/actions/connector/_servicenow_access_token"`
);

const [context, req, res] = mockHandlerArguments(
{},
{
body: {
apiUrl: 'https://testurl.service-now.com/',
config: {
clientId: 'abc',
jwtKeyId: 'def',
userIdentifierValue: 'userA',
},
secrets: {
clientSecret: 'iamasecret',
privateKey: 'xyz',
},
},
},
['ok']
);

expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`);

expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function));
});
});
Loading