mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `8.7`: - [[drift] Determine if trial is active (plus buffer) before showing chat. (#151548)](https://github.com/elastic/kibana/pull/151548) <!--- Backport version: 8.9.7 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Clint Andrew Hall","email":"clint.hall@elastic.co"},"sourceCommit":{"committedDate":"2023-02-22T21:34:44Z","message":"[drift] Determine if trial is active (plus buffer) before showing chat. (#151548)\n\n## Summary\r\n\r\nIn-app chat should only be enabled in Cloud if a trial is still active.\r\nhttps://github.com/elastic/kibana/pull/143002 added metadata including\r\n`trial_end_date`. This PR:\r\n\r\n- adds a config key to `cloud_integrations` for a `trialBuffer`, in\r\ndays, which defaults to ~~`30`~~ `60`.\r\n- adds logic to not display chat if the trial end date + buffer exceeds\r\nthe current date.\r\n- adds logic to not add a server route if the trial end date + buffer\r\nexceeds the current date.\r\n\r\n## Testing Locally\r\n\r\nAdd the following config to `kibana.dev.yml`:\r\n\r\n```\r\nxpack.cloud.id: \"some-id\"\r\nxpack.cloud.trial_end_date: \"2023-02-21T00:00:00.000Z\"\r\n\r\nxpack.cloud_integrations.chat.enabled: true\r\nxpack.cloud_integrations.chat.chatURL: \"https://elasticcloud-production-chat-us-east-1.s3.amazonaws.com/drift-iframe.html\"\r\nxpack.cloud_integrations.chat.chatIdentitySecret: \"some-secret\"\r\n```\r\n\r\nAnd start Kibana. You can optionally change the default of `30` days by\r\nadding `xpack.cloud_integrations.chat.trialBuffer`.\r\n\r\n## Storybook\r\n\r\nRun `yarn storybook cloud_chat`.\r\n\r\n## Testing in Cloud\r\n\r\nSet the same config keys as above on a Cloud deployment.","sha":"00ab82ecd7e6c0e33977d7d37b26c6d88e367fd6","branchLabelMapping":{"^v8.8.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["review","loe:hours","release_note:skip","impact:high","ci:cloud-deploy","v8.7.0","v8.8.0","Feature:Chat"],"number":151548,"url":"https://github.com/elastic/kibana/pull/151548","mergeCommit":{"message":"[drift] Determine if trial is active (plus buffer) before showing chat. (#151548)\n\n## Summary\r\n\r\nIn-app chat should only be enabled in Cloud if a trial is still active.\r\nhttps://github.com/elastic/kibana/pull/143002 added metadata including\r\n`trial_end_date`. This PR:\r\n\r\n- adds a config key to `cloud_integrations` for a `trialBuffer`, in\r\ndays, which defaults to ~~`30`~~ `60`.\r\n- adds logic to not display chat if the trial end date + buffer exceeds\r\nthe current date.\r\n- adds logic to not add a server route if the trial end date + buffer\r\nexceeds the current date.\r\n\r\n## Testing Locally\r\n\r\nAdd the following config to `kibana.dev.yml`:\r\n\r\n```\r\nxpack.cloud.id: \"some-id\"\r\nxpack.cloud.trial_end_date: \"2023-02-21T00:00:00.000Z\"\r\n\r\nxpack.cloud_integrations.chat.enabled: true\r\nxpack.cloud_integrations.chat.chatURL: \"https://elasticcloud-production-chat-us-east-1.s3.amazonaws.com/drift-iframe.html\"\r\nxpack.cloud_integrations.chat.chatIdentitySecret: \"some-secret\"\r\n```\r\n\r\nAnd start Kibana. You can optionally change the default of `30` days by\r\nadding `xpack.cloud_integrations.chat.trialBuffer`.\r\n\r\n## Storybook\r\n\r\nRun `yarn storybook cloud_chat`.\r\n\r\n## Testing in Cloud\r\n\r\nSet the same config keys as above on a Cloud deployment.","sha":"00ab82ecd7e6c0e33977d7d37b26c6d88e367fd6"}},"sourceBranch":"main","suggestedTargetBranches":["8.7"],"targetPullRequestStates":[{"branch":"8.7","label":"v8.7.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.8.0","labelRegex":"^v8.8.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/151548","number":151548,"mergeCommit":{"message":"[drift] Determine if trial is active (plus buffer) before showing chat. (#151548)\n\n## Summary\r\n\r\nIn-app chat should only be enabled in Cloud if a trial is still active.\r\nhttps://github.com/elastic/kibana/pull/143002 added metadata including\r\n`trial_end_date`. This PR:\r\n\r\n- adds a config key to `cloud_integrations` for a `trialBuffer`, in\r\ndays, which defaults to ~~`30`~~ `60`.\r\n- adds logic to not display chat if the trial end date + buffer exceeds\r\nthe current date.\r\n- adds logic to not add a server route if the trial end date + buffer\r\nexceeds the current date.\r\n\r\n## Testing Locally\r\n\r\nAdd the following config to `kibana.dev.yml`:\r\n\r\n```\r\nxpack.cloud.id: \"some-id\"\r\nxpack.cloud.trial_end_date: \"2023-02-21T00:00:00.000Z\"\r\n\r\nxpack.cloud_integrations.chat.enabled: true\r\nxpack.cloud_integrations.chat.chatURL: \"https://elasticcloud-production-chat-us-east-1.s3.amazonaws.com/drift-iframe.html\"\r\nxpack.cloud_integrations.chat.chatIdentitySecret: \"some-secret\"\r\n```\r\n\r\nAnd start Kibana. You can optionally change the default of `30` days by\r\nadding `xpack.cloud_integrations.chat.trialBuffer`.\r\n\r\n## Storybook\r\n\r\nRun `yarn storybook cloud_chat`.\r\n\r\n## Testing in Cloud\r\n\r\nSet the same config keys as above on a Cloud deployment.","sha":"00ab82ecd7e6c0e33977d7d37b26c6d88e367fd6"}}]}] BACKPORT--> Co-authored-by: Clint Andrew Hall <clint.hall@elastic.co>
This commit is contained in:
parent
12a539eb84
commit
ee7f50abd3
9 changed files with 183 additions and 15 deletions
|
@ -172,6 +172,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
|
|||
'xpack.cloud.is_elastic_staff_owned (boolean)',
|
||||
'xpack.cloud.trial_end_date (string)',
|
||||
'xpack.cloud_integrations.chat.chatURL (string)',
|
||||
'xpack.cloud_integrations.chat.trialBuffer (number)',
|
||||
// No PII. This is an escape patch to override LaunchDarkly's flag resolution mechanism for testing or quick fix.
|
||||
'xpack.cloud_integrations.experiments.flag_overrides (record)',
|
||||
// Commented because it's inside a schema conditional, and the test is not able to resolve it. But it's shared.
|
||||
|
|
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
|
||||
export const GET_CHAT_USER_DATA_ROUTE_PATH = '/internal/cloud/chat_user';
|
||||
export const DEFAULT_TRIAL_BUFFER = 60;
|
||||
|
|
19
x-pack/plugins/cloud_integrations/cloud_chat/common/util.ts
Normal file
19
x-pack/plugins/cloud_integrations/cloud_chat/common/util.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns true if today's date is within the an end date + buffer, false otherwise.
|
||||
*
|
||||
* @param endDate The end date of the trial.
|
||||
* @param buffer The number of days to add to the end date.
|
||||
* @returns true if today's date is within the an end date + buffer, false otherwise.
|
||||
*/
|
||||
export const isTodayInDateWindow = (endDate: Date, buffer: number) => {
|
||||
const endDateWithBuffer = new Date(endDate);
|
||||
endDateWithBuffer.setDate(endDateWithBuffer.getDate() + buffer);
|
||||
return endDateWithBuffer > new Date();
|
||||
};
|
|
@ -15,9 +15,12 @@ describe('Cloud Chat Plugin', () => {
|
|||
describe('#setup', () => {
|
||||
describe('setupChat', () => {
|
||||
let consoleMock: jest.SpyInstance<void, [message?: any, ...optionalParams: any[]]>;
|
||||
let newTrialEndDate: Date;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleMock = jest.spyOn(console, 'debug').mockImplementation(() => {});
|
||||
newTrialEndDate = new Date();
|
||||
newTrialEndDate.setDate(new Date().getDate() + 14);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -30,12 +33,14 @@ describe('Cloud Chat Plugin', () => {
|
|||
currentUserProps = {},
|
||||
isCloudEnabled = true,
|
||||
failHttp = false,
|
||||
trialEndDate = newTrialEndDate,
|
||||
}: {
|
||||
config?: Partial<CloudChatConfigType>;
|
||||
securityEnabled?: boolean;
|
||||
currentUserProps?: Record<string, any>;
|
||||
isCloudEnabled?: boolean;
|
||||
failHttp?: boolean;
|
||||
trialEndDate?: Date;
|
||||
}) => {
|
||||
const initContext = coreMock.createPluginInitializerContext(config);
|
||||
|
||||
|
@ -60,7 +65,7 @@ describe('Cloud Chat Plugin', () => {
|
|||
const cloud = cloudMock.createSetup();
|
||||
|
||||
plugin.setup(coreSetup, {
|
||||
cloud: { ...cloud, isCloudEnabled },
|
||||
cloud: { ...cloud, isCloudEnabled, trialEndDate },
|
||||
...(securityEnabled ? { security: securitySetup } : {}),
|
||||
});
|
||||
|
||||
|
@ -85,16 +90,27 @@ describe('Cloud Chat Plugin', () => {
|
|||
|
||||
it('chatConfig is not retrieved if internal API fails', async () => {
|
||||
const { coreSetup } = await setupPlugin({
|
||||
config: { chatURL: 'http://chat.elastic.co' },
|
||||
config: { chatURL: 'http://chat.elastic.co', trialBuffer: 30 },
|
||||
failHttp: true,
|
||||
});
|
||||
expect(coreSetup.http.get).toHaveBeenCalled();
|
||||
expect(consoleMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('chatConfig is retrieved if chat is enabled and url is provided', async () => {
|
||||
it('chatConfig is not retrieved if chat is enabled and url is provided but trial has expired', async () => {
|
||||
const date = new Date();
|
||||
date.setDate(new Date().getDate() - 44);
|
||||
const { coreSetup } = await setupPlugin({
|
||||
config: { chatURL: 'http://chat.elastic.co' },
|
||||
config: { chatURL: 'http://chat.elastic.co', trialBuffer: 30 },
|
||||
trialEndDate: date,
|
||||
});
|
||||
expect(coreSetup.http.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('chatConfig is retrieved if chat is enabled and url is provided and trial is active', async () => {
|
||||
const { coreSetup } = await setupPlugin({
|
||||
config: { chatURL: 'http://chat.elastic.co', trialBuffer: 30 },
|
||||
trialEndDate: new Date(),
|
||||
});
|
||||
expect(coreSetup.http.get).toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ import { ReplaySubject } from 'rxjs';
|
|||
import type { GetChatUserDataResponseBody } from '../common/types';
|
||||
import { GET_CHAT_USER_DATA_ROUTE_PATH } from '../common/constants';
|
||||
import { ChatConfig, ServicesProvider } from './services';
|
||||
import { isTodayInDateWindow } from '../common/util';
|
||||
|
||||
interface CloudChatSetupDeps {
|
||||
cloud: CloudSetup;
|
||||
|
@ -27,6 +28,7 @@ interface SetupChatDeps extends CloudChatSetupDeps {
|
|||
|
||||
interface CloudChatConfig {
|
||||
chatURL?: string;
|
||||
trialBuffer: number;
|
||||
}
|
||||
|
||||
export class CloudChatPlugin implements Plugin {
|
||||
|
@ -57,7 +59,16 @@ export class CloudChatPlugin implements Plugin {
|
|||
public stop() {}
|
||||
|
||||
private async setupChat({ cloud, http, security }: SetupChatDeps) {
|
||||
if (!cloud.isCloudEnabled || !security || !this.config.chatURL) {
|
||||
const { isCloudEnabled, trialEndDate } = cloud;
|
||||
const { chatURL, trialBuffer } = this.config;
|
||||
|
||||
if (
|
||||
!security ||
|
||||
!isCloudEnabled ||
|
||||
!chatURL ||
|
||||
!trialEndDate ||
|
||||
!isTodayInDateWindow(trialEndDate, trialBuffer)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -73,7 +84,7 @@ export class CloudChatPlugin implements Plugin {
|
|||
}
|
||||
|
||||
this.chatConfig$.next({
|
||||
chatURL: this.config.chatURL,
|
||||
chatURL,
|
||||
user: {
|
||||
email,
|
||||
id,
|
||||
|
|
|
@ -9,10 +9,13 @@ import { get, has } from 'lodash';
|
|||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { PluginConfigDescriptor } from '@kbn/core/server';
|
||||
|
||||
import { DEFAULT_TRIAL_BUFFER } from '../common/constants';
|
||||
|
||||
const configSchema = schema.object({
|
||||
enabled: schema.boolean({ defaultValue: false }),
|
||||
chatURL: schema.maybe(schema.string()),
|
||||
chatIdentitySecret: schema.maybe(schema.string()),
|
||||
trialBuffer: schema.number({ defaultValue: DEFAULT_TRIAL_BUFFER }),
|
||||
});
|
||||
|
||||
export type CloudChatConfigType = TypeOf<typeof configSchema>;
|
||||
|
@ -20,6 +23,7 @@ export type CloudChatConfigType = TypeOf<typeof configSchema>;
|
|||
export const config: PluginConfigDescriptor<CloudChatConfigType> = {
|
||||
exposeToBrowser: {
|
||||
chatURL: true,
|
||||
trialBuffer: true,
|
||||
},
|
||||
schema: configSchema,
|
||||
deprecations: () => [
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
|
||||
import { PluginInitializerContext, CoreSetup, Plugin } from '@kbn/core/server';
|
||||
|
||||
import { SecurityPluginSetup } from '@kbn/security-plugin/server';
|
||||
import { CloudSetup } from '@kbn/cloud-plugin/server';
|
||||
import type { SecurityPluginSetup } from '@kbn/security-plugin/server';
|
||||
import type { CloudSetup } from '@kbn/cloud-plugin/server';
|
||||
import { registerChatRoute } from './routes';
|
||||
import { CloudChatConfigType } from './config';
|
||||
import type { CloudChatConfigType } from './config';
|
||||
|
||||
interface CloudChatSetupDeps {
|
||||
cloud: CloudSetup;
|
||||
|
@ -27,10 +27,15 @@ export class CloudChatPlugin implements Plugin<void, void, CloudChatSetupDeps> {
|
|||
}
|
||||
|
||||
public setup(core: CoreSetup, { cloud, security }: CloudChatSetupDeps) {
|
||||
if (cloud.isCloudEnabled && this.config.chatIdentitySecret) {
|
||||
const { chatIdentitySecret, trialBuffer } = this.config;
|
||||
const { isCloudEnabled, trialEndDate } = cloud;
|
||||
|
||||
if (isCloudEnabled && chatIdentitySecret) {
|
||||
registerChatRoute({
|
||||
router: core.http.createRouter(),
|
||||
chatIdentitySecret: this.config.chatIdentitySecret,
|
||||
chatIdentitySecret,
|
||||
trialEndDate,
|
||||
trialBuffer,
|
||||
security,
|
||||
isDev: this.isDev,
|
||||
});
|
||||
|
|
|
@ -19,7 +19,7 @@ import { registerChatRoute } from './chat';
|
|||
describe('chat route', () => {
|
||||
test('do not add the route if security is not enabled', async () => {
|
||||
const router = httpServiceMock.createRouter();
|
||||
registerChatRoute({ router, isDev: false, chatIdentitySecret: 'secret' });
|
||||
registerChatRoute({ router, isDev: false, chatIdentitySecret: 'secret', trialBuffer: 60 });
|
||||
expect(router.get.mock.calls).toEqual([]);
|
||||
});
|
||||
|
||||
|
@ -28,7 +28,14 @@ describe('chat route', () => {
|
|||
security.authc.getCurrentUser.mockReturnValueOnce(null);
|
||||
|
||||
const router = httpServiceMock.createRouter();
|
||||
registerChatRoute({ router, security, isDev: false, chatIdentitySecret: 'secret' });
|
||||
registerChatRoute({
|
||||
router,
|
||||
security,
|
||||
isDev: false,
|
||||
chatIdentitySecret: 'secret',
|
||||
trialBuffer: 60,
|
||||
trialEndDate: new Date(),
|
||||
});
|
||||
|
||||
const [_config, handler] = router.get.mock.calls[0];
|
||||
|
||||
|
@ -44,6 +51,79 @@ describe('chat route', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
test('error if no trial end date specified', async () => {
|
||||
const security = securityMock.createSetup();
|
||||
const username = 'user.name';
|
||||
const email = 'user@elastic.co';
|
||||
|
||||
security.authc.getCurrentUser.mockReturnValueOnce({
|
||||
username,
|
||||
metadata: {
|
||||
saml_email: [email],
|
||||
},
|
||||
});
|
||||
|
||||
const router = httpServiceMock.createRouter();
|
||||
registerChatRoute({
|
||||
router,
|
||||
security,
|
||||
isDev: false,
|
||||
chatIdentitySecret: 'secret',
|
||||
trialBuffer: 2,
|
||||
});
|
||||
|
||||
const [_config, handler] = router.get.mock.calls[0];
|
||||
|
||||
await expect(handler({}, httpServerMock.createKibanaRequest(), kibanaResponseFactory)).resolves
|
||||
.toMatchInlineSnapshot(`
|
||||
KibanaResponse {
|
||||
"options": Object {
|
||||
"body": "Chat can only be started if a trial end date is specified",
|
||||
},
|
||||
"payload": "Chat can only be started if a trial end date is specified",
|
||||
"status": 400,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('error if not in trial window', async () => {
|
||||
const security = securityMock.createSetup();
|
||||
const username = 'user.name';
|
||||
const email = 'user@elastic.co';
|
||||
|
||||
security.authc.getCurrentUser.mockReturnValueOnce({
|
||||
username,
|
||||
metadata: {
|
||||
saml_email: [email],
|
||||
},
|
||||
});
|
||||
|
||||
const router = httpServiceMock.createRouter();
|
||||
const trialEndDate = new Date();
|
||||
trialEndDate.setDate(trialEndDate.getDate() - 30);
|
||||
registerChatRoute({
|
||||
router,
|
||||
security,
|
||||
isDev: false,
|
||||
chatIdentitySecret: 'secret',
|
||||
trialBuffer: 2,
|
||||
trialEndDate,
|
||||
});
|
||||
|
||||
const [_config, handler] = router.get.mock.calls[0];
|
||||
|
||||
await expect(handler({}, httpServerMock.createKibanaRequest(), kibanaResponseFactory)).resolves
|
||||
.toMatchInlineSnapshot(`
|
||||
KibanaResponse {
|
||||
"options": Object {
|
||||
"body": "Chat can only be started during trial and trial chat buffer",
|
||||
},
|
||||
"payload": "Chat can only be started during trial and trial chat buffer",
|
||||
"status": 400,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('returns user information taken from saml metadata and a token', async () => {
|
||||
const security = securityMock.createSetup();
|
||||
const username = 'user.name';
|
||||
|
@ -57,7 +137,14 @@ describe('chat route', () => {
|
|||
});
|
||||
|
||||
const router = httpServiceMock.createRouter();
|
||||
registerChatRoute({ router, security, isDev: false, chatIdentitySecret: 'secret' });
|
||||
registerChatRoute({
|
||||
router,
|
||||
security,
|
||||
isDev: false,
|
||||
chatIdentitySecret: 'secret',
|
||||
trialBuffer: 60,
|
||||
trialEndDate: new Date(),
|
||||
});
|
||||
const [_config, handler] = router.get.mock.calls[0];
|
||||
await expect(handler({}, httpServerMock.createKibanaRequest(), kibanaResponseFactory)).resolves
|
||||
.toMatchInlineSnapshot(`
|
||||
|
@ -87,7 +174,14 @@ describe('chat route', () => {
|
|||
security.authc.getCurrentUser.mockReturnValueOnce({});
|
||||
|
||||
const router = httpServiceMock.createRouter();
|
||||
registerChatRoute({ router, security, isDev: true, chatIdentitySecret: 'secret' });
|
||||
registerChatRoute({
|
||||
router,
|
||||
security,
|
||||
isDev: true,
|
||||
chatIdentitySecret: 'secret',
|
||||
trialBuffer: 60,
|
||||
trialEndDate: new Date(),
|
||||
});
|
||||
const [_config, handler] = router.get.mock.calls[0];
|
||||
await expect(handler({}, httpServerMock.createKibanaRequest(), kibanaResponseFactory)).resolves
|
||||
.toMatchInlineSnapshot(`
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { SecurityPluginSetup, AuthenticatedUser } from '@kbn/security-plugi
|
|||
import { GET_CHAT_USER_DATA_ROUTE_PATH } from '../../common/constants';
|
||||
import type { GetChatUserDataResponseBody } from '../../common/types';
|
||||
import { generateSignedJwt } from '../util/generate_jwt';
|
||||
import { isTodayInDateWindow } from '../../common/util';
|
||||
|
||||
type MetaWithSaml = AuthenticatedUser['metadata'] & {
|
||||
saml_name: [string];
|
||||
|
@ -21,11 +22,15 @@ type MetaWithSaml = AuthenticatedUser['metadata'] & {
|
|||
export const registerChatRoute = ({
|
||||
router,
|
||||
chatIdentitySecret,
|
||||
trialEndDate,
|
||||
trialBuffer,
|
||||
security,
|
||||
isDev,
|
||||
}: {
|
||||
router: IRouter;
|
||||
chatIdentitySecret: string;
|
||||
trialEndDate?: Date;
|
||||
trialBuffer: number;
|
||||
security?: SecurityPluginSetup;
|
||||
isDev: boolean;
|
||||
}) => {
|
||||
|
@ -61,6 +66,18 @@ export const registerChatRoute = ({
|
|||
});
|
||||
}
|
||||
|
||||
if (!trialEndDate) {
|
||||
return response.badRequest({
|
||||
body: 'Chat can only be started if a trial end date is specified',
|
||||
});
|
||||
}
|
||||
|
||||
if (!trialEndDate || !isTodayInDateWindow(trialEndDate, trialBuffer)) {
|
||||
return response.badRequest({
|
||||
body: 'Chat can only be started during trial and trial chat buffer',
|
||||
});
|
||||
}
|
||||
|
||||
const token = generateSignedJwt(userId, chatIdentitySecret);
|
||||
const body: GetChatUserDataResponseBody = {
|
||||
token,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue