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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@
"@kbn/config": "link:src/platform/packages/shared/kbn-config",
"@kbn/config-mocks": "link:src/platform/packages/private/kbn-config-mocks",
"@kbn/config-schema": "link:src/platform/packages/shared/kbn-config-schema",
"@kbn/connector-events-bridge-plugin": "link:x-pack/platform/plugins/shared/connector_events_bridge",
"@kbn/connector-schemas": "link:src/platform/packages/shared/kbn-connector-schemas",
"@kbn/connector-specs": "link:src/platform/packages/shared/kbn-connector-specs",
"@kbn/console-plugin": "link:src/platform/plugins/shared/console",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { FC, PropsWithChildren } from 'react';
import { waitFor, renderHook } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@kbn/react-query';
import { docLinksServiceMock } from '@kbn/core/public/mocks';
import { connectorsSpecs } from '@kbn/connector-specs';
import * as connectorsSpecs from '@kbn/connector-specs/src/all_specs';
import { serializeConnectorSpec } from '@kbn/connector-specs/src/lib/serialize_connector_spec';
import { actionTypeRegistryMock } from '../test_utils/action_type_registry.mock';
import type { ActionTypeModel } from '../types';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { httpServiceMock, docLinksServiceMock } from '@kbn/core/public/mocks';
import { connectorsSpecs } from '@kbn/connector-specs';
import * as connectorsSpecs from '@kbn/connector-specs/src/all_specs';
import { serializeConnectorSpec } from '@kbn/connector-specs/src/lib/serialize_connector_spec';
import type { ConnectorSpecWireResponse } from '../apis/fetch_connector_spec';
import {
Expand Down
21 changes: 13 additions & 8 deletions src/platform/packages/shared/kbn-connector-specs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export * as connectorsSpecs from './src/all_specs';
export type * from './src/connector_spec';
export type * from './src/connector_spec_events';
export { defineConnectorEvent } from './src/define_connector_event';
export {
buildConnectorEventId,
buildConnectorIngressEventsPath,
connectorTypeToEventNamespace,
normalizeConnectorTypeId,
} from './src/connector_event_type_id';
Expand All @@ -30,14 +30,19 @@ export {
type JwtAlgorithm,
} from './src/auth_types/oauth_client_credentials_private_key_jwt';

export { getConnectorSpec } from './src/get_connector_spec';
export {
listConnectorEventInfos,
listConnectorEventInfosForType,
type ConnectorEventInfo,
} from './src/list_connector_event_infos';
export { resolveRegisteredConnectorEventByEventId } from './src/resolve_registered_connector_event_by_event_id';
export { inboundWebhookReceivedEventSchema } from './src/inbound_webhook_received_event_schema';
export {
INBOUND_WEBHOOK_CONNECTOR_TYPE_ID,
INBOUND_WEBHOOK_RECEIVED_EVENT_ID,
INBOUND_WEBHOOK_RECEIVED_EVENT_KEY,
} from './src/inbound_webhook_constants';
export {
getInboundWebhookAuthorizationHeader,
INBOUND_WEBHOOK_DELEGATED_API_KEY_ID_CONFIG,
INBOUND_WEBHOOK_DELEGATED_API_KEY_SECRET,
INBOUND_WEBHOOK_DELEGATED_UIAM_API_KEY_ID_CONFIG,
INBOUND_WEBHOOK_DELEGATED_UIAM_API_KEY_SECRET,
} from './src/inbound_webhook/delegated_execution_credentials';
export { isToolAction, TEST_CONNECTOR_SUB_ACTION } from './src/connector_spec';
export {
getConnectorActionErrorMeta,
Expand Down
15 changes: 15 additions & 0 deletions src/platform/packages/shared/kbn-connector-specs/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export { getConnectorSpec } from '../src/get_connector_spec';
export {
listConnectorEventInfosForType,
type ConnectorEventInfo,
} from '../src/list_connector_event_infos';
export { resolveRegisteredConnectorEventByEventId } from '../src/resolve_registered_connector_event_by_event_id';
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export * from './specs/zoom/zoom';
export * from './specs/zendesk/zendesk';
export * from './specs/amazon_s3/amazon_s3';
export * from './specs/hubspot/hubspot';
export * from './specs/inbound_webhook/inbound_webhook';
export * from './specs/google_cloud_storage/google_cloud_storage';
export * from './specs/sharepoint_server/sharepoint_server';
export * from './specs/microsoft_teams/microsoft_teams';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,11 @@ export const normalizeConnectorTypeId = (typeId: string): string =>

export const buildConnectorEventId = (connectorTypeId: string, eventKey: string): string =>
`${connectorTypeToEventNamespace(connectorTypeId)}.${eventKey}`;

export const buildConnectorIngressEventsPath = ({
connectorTypeId,
connectorId,
}: {
connectorTypeId: string;
connectorId: string;
}): string => `/api/events/v1/${connectorTypeToEventNamespace(connectorTypeId)}/${connectorId}`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

// eslint-disable-next-line import/no-nodejs-modules -- server-side ingress credential hashing
import { createHash } from 'node:crypto';

export const computeIngestTokenHash = ({
connectorId,
spaceId,
token,
}: {
connectorId: string;
spaceId: string;
token: string;
}): string => createHash('sha256').update(`${connectorId}|${spaceId}|${token}`).digest('hex');
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

/**
* Secret/config field names for delegated Task Manager execution credentials
* granted when an inbound webhook connector is created (see feature/inbound-webhook).
*/
export const INBOUND_WEBHOOK_DELEGATED_API_KEY_SECRET = 'delegatedApiKey' as const;
export const INBOUND_WEBHOOK_DELEGATED_UIAM_API_KEY_SECRET = 'delegatedUiamApiKey' as const;
export const INBOUND_WEBHOOK_DELEGATED_API_KEY_ID_CONFIG = 'delegatedApiKeyId' as const;
export const INBOUND_WEBHOOK_DELEGATED_UIAM_API_KEY_ID_CONFIG = 'delegatedUiamApiKeyId' as const;

/**
* Builds an `ApiKey …` Authorization header from inbound-webhook connector secrets.
* Returns undefined when delegated credentials have not been granted yet.
*/
export function getInboundWebhookAuthorizationHeader(
secrets: Record<string, unknown>
): string | undefined {
const uiamApiKey = secrets[INBOUND_WEBHOOK_DELEGATED_UIAM_API_KEY_SECRET];
if (typeof uiamApiKey === 'string' && uiamApiKey.length > 0) {
const parts = Buffer.from(uiamApiKey, 'base64').toString().split(':');
const value = parts[1];
if (value) {
return `ApiKey ${value}`;
}
}

const apiKey = secrets[INBOUND_WEBHOOK_DELEGATED_API_KEY_SECRET];
if (typeof apiKey === 'string' && apiKey.length > 0) {
return `ApiKey ${apiKey}`;
}
return undefined;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

const SENSITIVE_HEADERS = new Set([
'authorization',
'cookie',
'host',
'proxy-authorization',
'x-forwarded-for',
'x-forwarded-host',
'x-forwarded-proto',
'x-inbound-query',
]);

export const filterInboundHeaders = (
headers: Record<string, string | string[] | undefined>
): Record<string, string> =>
Object.fromEntries(
Object.entries(headers).flatMap(([name, value]) => {
const normalizedName = name.toLowerCase();
if (
SENSITIVE_HEADERS.has(normalizedName) ||
(!normalizedName.startsWith('x-') &&
normalizedName !== 'content-type' &&
normalizedName !== 'user-agent')
) {
return [];
}
const normalizedValue = Array.isArray(value) ? value.join(',') : value;
return typeof normalizedValue === 'string' ? [[normalizedName, normalizedValue]] : [];
})
);
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export const InboundWebhookParamsFields = () => null;
export const INBOUND_WEBHOOK_CONNECTOR_TYPE_ID = '.inboundWebhook' as const;
export const INBOUND_WEBHOOK_RECEIVED_EVENT_ID = 'inboundWebhook.received' as const;
export const INBOUND_WEBHOOK_RECEIVED_EVENT_KEY = 'received' as const;
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,22 @@ import { z } from '@kbn/zod/v4';

/**
* Payload shape for inbound webhook `received` connector events.
* Describes the emitted event data; connector binding is resolved separately at ingress.
* Hub adds `connectorId` and `connectorTypeId` before emit; handleEvents supplies the rest.
*/
export const inboundWebhookReceivedEventSchema = z.object({
connectorId: z.string().describe('Saved connector instance id.'),
connectorTypeId: z.string().describe('Connector type id (e.g. .inboundWebhook).'),
body: z.unknown().describe('Parsed request body.'),
headers: z
.record(z.string(), z.string())
.describe('Non-sensitive request headers forwarded with the event.'),
query: z
.record(z.string(), z.union([z.string(), z.array(z.string())]))
.optional()
.describe('Query string parameters from the inbound request.'),
receivedAt: z.string().describe('ISO timestamp when the event was received.'),
correlationKey: z
.string()
.optional()
.describe('Idempotency / deduplication key for this ingress request.'),
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*/

import { isString } from 'lodash';
import * as allSpecs from '../all_specs';
import { EARS_AUTH_ID } from '../auth_types/ears';
import type { AuthTypeDef } from '../connector_spec';

Expand All @@ -17,11 +16,12 @@ export const isEarsExperimentalAuthType = (
): authType is AuthTypeDef =>
!isString(authType) && authType.type === EARS_AUTH_ID && authType.isExperimental === true;

const experimentalEarsConnectorIds = new Set(
Object.values(allSpecs)
.filter((spec) => spec.auth?.types.some(isEarsExperimentalAuthType))
.map((spec) => spec.metadata.id)
);
/** Connector type IDs that use experimental EARS auth — kept explicit so public UI can tree-shake all_specs. */
const EXPERIMENTAL_EARS_CONNECTOR_TYPE_IDS = new Set([
'.gmail',
'.google_calendar',
'.google_drive',
]);

export const isEarsExperimentalConnector = (connectorTypeId: string): boolean =>
experimentalEarsConnectorIds.has(connectorTypeId);
EXPERIMENTAL_EARS_CONNECTOR_TYPE_IDS.has(connectorTypeId);
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { loggingSystemMock } from '@kbn/core/server/mocks';

import { INBOUND_WEBHOOK_RECEIVED_EVENT_ID } from '@kbn/connector-specs';
import { InboundWebhookConnector } from './inbound_webhook';

describe('InboundWebhookConnector.handleEvents', () => {
it('returns inboundWebhook.received payload', async () => {
const result = await InboundWebhookConnector.events!.handleEvents({
connectorId: 'sales-ingress',
connectorTypeId: '.inboundWebhook',
spaceId: 'default',
config: {},
secrets: {},
rawBody: { eventType: 'order.created' },
headers: {
'content-type': 'application/json',
authorization: 'Bearer secret',
'x-custom': 'value',
'x-inbound-query': JSON.stringify({ source: 'shopify' }),
},
log: loggingSystemMock.create().get(),
});

expect(result.httpResponse).toBeUndefined();
expect(result.events).toHaveLength(1);
expect(result.events?.[0]?.eventId).toBe(INBOUND_WEBHOOK_RECEIVED_EVENT_ID);
expect(result.events?.[0]?.payload.body).toEqual({ eventType: 'order.created' });
expect(result.events?.[0]?.payload.headers).toEqual({
'content-type': 'application/json',
'x-custom': 'value',
});
expect(result.events?.[0]?.payload.query).toEqual({ source: 'shopify' });
});
});
Loading