Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions x-pack/plugins/actions/server/actions_client.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const createActionsClientMock = () => {
update: jest.fn(),
getAll: jest.fn(),
getBulk: jest.fn(),
getOAuthAccessToken: jest.fn(),
execute: jest.fn(),
enqueueExecution: jest.fn(),
ephemeralEnqueuedExecution: jest.fn(),
Expand Down
310 changes: 307 additions & 3 deletions x-pack/plugins/actions/server/actions_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ import { actionsConfigMock } from './actions_config.mock';
import { getActionsConfigurationUtilities } from './actions_config';
import { licenseStateMock } from './lib/license_state.mock';
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
import { httpServerMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
httpServerMock,
loggingSystemMock,
elasticsearchServiceMock,
savedObjectsClientMock,
} from '@kbn/core/server/mocks';
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock';

import { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks';
import { actionExecutorMock } from './lib/action_executor.mock';
import uuid from 'uuid';
import { ActionsAuthorization } from './authorization/actions_authorization';
Expand All @@ -37,6 +40,9 @@ import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/s
import { Logger } from '@kbn/core/server';
import { connectorTokenClientMock } from './builtin_action_types/lib/connector_token_client.mock';
import { inMemoryMetricsMock } from './monitoring/in_memory_metrics.mock';
import { getOAuthJwtAccessToken } from './builtin_action_types/lib/get_oauth_jwt_access_token';
import { getOAuthClientCredentialsAccessToken } from './builtin_action_types/lib/get_oauth_client_credentials_access_token';
import { OAuthParams } from './routes/get_oauth_access_token';

jest.mock('@kbn/core/server/saved_objects/service/lib/utils', () => ({
SavedObjectsUtils: {
Expand All @@ -60,6 +66,13 @@ jest.mock('./authorization/get_authorization_mode_by_source', () => {
};
});

jest.mock('./builtin_action_types/lib/get_oauth_jwt_access_token', () => ({
getOAuthJwtAccessToken: jest.fn(),
}));
jest.mock('./builtin_action_types/lib/get_oauth_client_credentials_access_token', () => ({
getOAuthClientCredentialsAccessToken: jest.fn(),
}));

const defaultKibanaIndex = '.kibana';
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
Expand All @@ -73,6 +86,7 @@ const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract();
const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
const mockTaskManager = taskManagerMock.createSetup();
const configurationUtilities = actionsConfigMock.create();

let actionsClient: ActionsClient;
let mockedLicenseState: jest.Mocked<ILicenseState>;
Expand Down Expand Up @@ -115,6 +129,10 @@ beforeEach(() => {
usageCounter: mockUsageCounter,
connectorTokenClient,
});
(getOAuthJwtAccessToken as jest.Mock).mockResolvedValue(`Bearer jwttokentokentoken`);
(getOAuthClientCredentialsAccessToken as jest.Mock).mockResolvedValue(
`Bearer clienttokentokentoken`
);
});

describe('create()', () => {
Expand Down Expand Up @@ -1274,6 +1292,292 @@ describe('getBulk()', () => {
});
});

describe('getOAuthAccessToken()', () => {
function getOAuthAccessToken(
requestBody: OAuthParams
): ReturnType<ActionsClient['getOAuthAccessToken']> {
actionsClient = new ActionsClient({
actionTypeRegistry,
unsecuredSavedObjectsClient,
scopedClusterClient,
defaultKibanaIndex,
actionExecutor,
executionEnqueuer,
ephemeralExecutionEnqueuer,
request,
authorization: authorization as unknown as ActionsAuthorization,
preconfiguredActions: [
{
id: 'testPreconfigured',
actionTypeId: '.slack',
secrets: {},
isPreconfigured: true,
isDeprecated: false,
name: 'test',
config: {
foo: 'bar',
},
},
],
connectorTokenClient: connectorTokenClientMock.create(),
});
return actionsClient.getOAuthAccessToken(requestBody, logger, configurationUtilities);
}

describe('authorization', () => {
test('ensures user is authorised to get the type of action', async () => {
await getOAuthAccessToken({
type: 'jwt',
options: {
tokenUrl: 'https://testurl.service-now.com/oauth_token.do',
config: {
clientId: 'abc',
jwtKeyId: 'def',
userIdentifierValue: 'userA',
},
secrets: {
clientSecret: 'iamasecret',
privateKey: 'xyz',
},
},
});
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
});

test('throws when user is not authorised to create the type of action', async () => {
authorization.ensureAuthorized.mockRejectedValue(new Error(`Unauthorized to update actions`));

await expect(
getOAuthAccessToken({
type: 'jwt',
options: {
tokenUrl: 'https://testurl.service-now.com/oauth_token.do',
config: {
clientId: 'abc',
jwtKeyId: 'def',
userIdentifierValue: 'userA',
},
secrets: {
clientSecret: 'iamasecret',
privateKey: 'xyz',
},
},
})
).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to update actions]`);

expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
});
});

test('throws when tokenUrl is not using http or https', async () => {
await expect(
getOAuthAccessToken({
type: 'jwt',
options: {
tokenUrl: 'ftp://testurl.service-now.com/oauth_token.do',
config: {
clientId: 'abc',
jwtKeyId: 'def',
userIdentifierValue: 'userA',
},
secrets: {
clientSecret: 'iamasecret',
privateKey: 'xyz',
},
},
})
).rejects.toMatchInlineSnapshot(`[Error: Token URL must use http or https]`);

expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
});

test('throws when tokenUrl does not contain hostname', async () => {
await expect(
getOAuthAccessToken({
type: 'jwt',
options: {
tokenUrl: '/path/to/myfile',
config: {
clientId: 'abc',
jwtKeyId: 'def',
userIdentifierValue: 'userA',
},
secrets: {
clientSecret: 'iamasecret',
privateKey: 'xyz',
},
},
})
).rejects.toMatchInlineSnapshot(`[Error: Token URL must contain hostname]`);

expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
});

test('throws when tokenUrl is not in allowed hosts', async () => {
configurationUtilities.ensureUriAllowed.mockImplementationOnce(() => {
throw new Error('URI not allowed');
});

await expect(
getOAuthAccessToken({
type: 'jwt',
options: {
tokenUrl: 'https://testurl.service-now.com/oauth_token.do',
config: {
clientId: 'abc',
jwtKeyId: 'def',
userIdentifierValue: 'userA',
},
secrets: {
clientSecret: 'iamasecret',
privateKey: 'xyz',
},
},
})
).rejects.toMatchInlineSnapshot(`[Error: URI not allowed]`);

expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
expect(configurationUtilities.ensureUriAllowed).toHaveBeenCalledWith(
`https://testurl.service-now.com/oauth_token.do`
);
});

test('calls getOAuthJwtAccessToken when type="jwt"', async () => {
const result = await getOAuthAccessToken({
type: 'jwt',
options: {
tokenUrl: 'https://testurl.service-now.com/oauth_token.do',
config: {
clientId: 'abc',
jwtKeyId: 'def',
userIdentifierValue: 'userA',
},
secrets: {
clientSecret: 'iamasecret',
privateKey: 'xyz',
},
},
});
expect(result).toEqual({
accessToken: 'Bearer jwttokentokentoken',
});
expect(getOAuthJwtAccessToken as jest.Mock).toHaveBeenCalledWith({
logger,
configurationUtilities,
credentials: {
config: {
clientId: 'abc',
jwtKeyId: 'def',
userIdentifierValue: 'userA',
},
secrets: {
clientSecret: 'iamasecret',
privateKey: 'xyz',
},
},
tokenUrl: 'https://testurl.service-now.com/oauth_token.do',
});
expect(getOAuthClientCredentialsAccessToken).not.toHaveBeenCalled();
expect(logger.debug).toHaveBeenCalledWith(
`Successfully retrieved access token using JWT OAuth with tokenUrl https://testurl.service-now.com/oauth_token.do and config {\"clientId\":\"abc\",\"jwtKeyId\":\"def\",\"userIdentifierValue\":\"userA\"}`
);
});

test('calls getOAuthClientCredentialsAccessToken when type="client"', async () => {
const result = await getOAuthAccessToken({
type: 'client',
options: {
tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token',
scope: 'https://graph.microsoft.com/.default',
config: {
clientId: 'abc',
tenantId: 'def',
},
secrets: {
clientSecret: 'iamasecret',
},
},
});
expect(result).toEqual({
accessToken: 'Bearer clienttokentokentoken',
});
expect(getOAuthClientCredentialsAccessToken as jest.Mock).toHaveBeenCalledWith({
logger,
configurationUtilities,
credentials: {
config: {
clientId: 'abc',
tenantId: 'def',
},
secrets: {
clientSecret: 'iamasecret',
},
},
tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token',
oAuthScope: 'https://graph.microsoft.com/.default',
});
expect(getOAuthJwtAccessToken).not.toHaveBeenCalled();
expect(logger.debug).toHaveBeenCalledWith(
`Successfully retrieved access token using Client Credentials OAuth with tokenUrl https://login.microsoftonline.com/98765/oauth2/v2.0/token, scope https://graph.microsoft.com/.default and config {\"clientId\":\"abc\",\"tenantId\":\"def\"}`
);
});

test('throws when getOAuthJwtAccessToken throws error', async () => {
(getOAuthJwtAccessToken as jest.Mock).mockRejectedValue(new Error(`Something went wrong!`));

await expect(
getOAuthAccessToken({
type: 'jwt',
options: {
tokenUrl: 'https://testurl.service-now.com/oauth_token.do',
config: {
clientId: 'abc',
jwtKeyId: 'def',
userIdentifierValue: 'userA',
},
secrets: {
clientSecret: 'iamasecret',
privateKey: 'xyz',
},
},
})
).rejects.toMatchInlineSnapshot(`[Error: Failed to retrieve access token]`);

expect(getOAuthJwtAccessToken as jest.Mock).toHaveBeenCalled();
expect(logger.debug).toHaveBeenCalledWith(
`Failed to retrieve access token using JWT OAuth with tokenUrl https://testurl.service-now.com/oauth_token.do and config {\"clientId\":\"abc\",\"jwtKeyId\":\"def\",\"userIdentifierValue\":\"userA\"} - Something went wrong!`
);
});

test('throws when getOAuthClientCredentialsAccessToken throws error', async () => {
(getOAuthClientCredentialsAccessToken as jest.Mock).mockRejectedValue(
new Error(`Something went wrong!`)
);

await expect(
getOAuthAccessToken({
type: 'client',
options: {
tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token',
scope: 'https://graph.microsoft.com/.default',
config: {
clientId: 'abc',
tenantId: 'def',
},
secrets: {
clientSecret: 'iamasecret',
},
},
})
).rejects.toMatchInlineSnapshot(`[Error: Failed to retrieve access token]`);

expect(getOAuthClientCredentialsAccessToken as jest.Mock).toHaveBeenCalled();
expect(logger.debug).toHaveBeenCalledWith(
`Failed to retrieved access token using Client Credentials OAuth with tokenUrl https://login.microsoftonline.com/98765/oauth2/v2.0/token, scope https://graph.microsoft.com/.default and config {\"clientId\":\"abc\",\"tenantId\":\"def\"} - Something went wrong!`
);
});
});

describe('delete()', () => {
describe('authorization', () => {
test('ensures user is authorised to delete actions', async () => {
Expand Down
Loading