Skip to content

Commit 4f9f40f

Browse files
fix(cases-analytics-v2): gate cases-templates SO references behind templates.enabled
`xpack.cases.templates.enabled` defaults to false. When it's off, the `cases-templates` SO type is not registered with core (see saved_object_types/index.ts). The v2 internal repository was naming `CASE_TEMPLATE_SAVED_OBJECT` unconditionally whenever v2 was on, so `createInternalRepository` threw "Missing mappings for saved objects types: 'cases-templates'" on boot and Kibana never came up. This is the exact failure mode `config_analytics_v2.ts` (FTR config with v2 on / templates off) triggers, and the latent production crash for any customer enabling v2 without templates. Mirrors the cases-attachments gating pattern from PR3: - plugin.ts: pass templatesEnabled to CasesAnalyticsV2Service and conditionally spread CASE_TEMPLATE_SAVED_OBJECT into the v2 internal repository via the same flag. Comment block on the repo opt-in expanded to document the gate. - cases_analytics_v2/service.ts: thread templatesEnabled through to CasesAnalyticsV2DataViewService at start. - cases_analytics_v2/data_view/service.ts: collectSnakeKeysForSpace short-circuits to [] when templatesEnabled is false. Per-space data views still bootstrap, with an empty runtime field overlay — the correct shape when there are no templates to project. - service.test.ts / data_view/service.test.ts: fixture updates plus a new regression test asserting the internal SO client is never queried for cases-templates when the flag is off. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 0716d5d commit 4f9f40f

5 files changed

Lines changed: 93 additions & 4 deletions

File tree

x-pack/platform/plugins/shared/cases/server/cases_analytics_v2/data_view/service.test.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ describe('CasesAnalyticsV2DataViewService', () => {
3535
* and returns the deps any test needs to call `ensureForSpace` /
3636
* `refreshForSpace`.
3737
*/
38-
const setup = (templates: Array<SavedObject<TemplateLike>> = []) => {
38+
const setup = (
39+
templates: Array<SavedObject<TemplateLike>> = [],
40+
{ templatesEnabled = true }: { templatesEnabled?: boolean } = {}
41+
) => {
3942
const internalSoClient = savedObjectsClientMock.create();
4043
stubFindOnePage(internalSoClient, templates);
4144

@@ -47,6 +50,7 @@ describe('CasesAnalyticsV2DataViewService', () => {
4750
logger,
4851
dataViewsService,
4952
internalSavedObjectsClient: internalSoClient,
53+
templatesEnabled,
5054
});
5155

5256
return {
@@ -145,6 +149,35 @@ describe('CasesAnalyticsV2DataViewService', () => {
145149
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('cluster unavailable'));
146150
});
147151

152+
/**
153+
* Regression guard for the "templates off → cases-templates SO type
154+
* not registered" path. When `xpack.cases.templates.enabled` is
155+
* false, `caseTemplateSavedObjectType` is not registered with core
156+
* (see `saved_object_types/index.ts`), so the internal SO client
157+
* would throw "Missing mappings for saved objects types:
158+
* 'cases-templates'" on any `find({ type: CASE_TEMPLATE_SAVED_OBJECT })`
159+
* call. The service must skip the template walk entirely in that
160+
* configuration — never touching the SO client for templates — and
161+
* bootstrap the per-space data view with an empty runtime field
162+
* overlay (the base data view is still useful for ES|QL against
163+
* the analytics index; only the extended-field projections are
164+
* absent).
165+
*/
166+
it('skips the cases-templates SO walk when templates feature flag is off', async () => {
167+
const { service, dvService, internalSoClient, deps } = setup(
168+
[makeTemplate('tpl-1', [{ name: 'risk', type: 'long', control: 'INPUT_NUMBER' }])],
169+
{ templatesEnabled: false }
170+
);
171+
stubMissingDataView(dvService);
172+
173+
await service.ensureForSpace(deps);
174+
175+
expect(internalSoClient.find).not.toHaveBeenCalled();
176+
expect(dvService.createAndSave).toHaveBeenCalledTimes(1);
177+
const [spec] = dvService.createAndSave.mock.calls[0];
178+
expect(Object.keys(spec.runtimeFieldMap ?? {})).toEqual([]);
179+
});
180+
148181
/**
149182
* Two parallel cases requests in a fresh-cache space both pass the
150183
* cache check, both see `dvService.get` return 404, and both call
@@ -490,6 +523,7 @@ describe('CasesAnalyticsV2DataViewService', () => {
490523
logger: parentLogger,
491524
dataViewsService: makeDataViewsPluginStart(dvService),
492525
internalSavedObjectsClient: internalSoClient,
526+
templatesEnabled: true,
493527
});
494528
// `logger.get('dataView')` is what the service holds for its
495529
// own log calls; the parent mock's `.get` returns a child mock

x-pack/platform/plugins/shared/cases/server/cases_analytics_v2/data_view/service.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,24 @@ interface CasesAnalyticsV2DataViewServiceDeps {
5858
* Internal (no-request) SO client used for template reads. Templates are
5959
* a hidden SO type, and the request-scoped client passed at ensure time
6060
* may not include them in `includedHiddenTypes`. The internal client is
61-
* opted in to both `cases` and `cases-templates` at plugin start.
61+
* opted in to both `cases` and `cases-templates` at plugin start —
62+
* the latter only when `templatesEnabled` is true (see below).
6263
*
6364
* `namespaces: [spaceId]` on the find call scopes results to a single
6465
* space even though the client itself is unscoped.
6566
*/
6667
internalSavedObjectsClient: SavedObjectsClientContract;
68+
/**
69+
* Resolved value of `xpack.cases.templates.enabled`. Gates the
70+
* per-space `cases-templates` SO walk inside `collectSnakeKeysForSpace`.
71+
* When false the SO type is not registered with core, so calling
72+
* `internalSavedObjectsClient.find({ type: 'cases-templates' })` would
73+
* throw "Missing mappings for saved objects types: 'cases-templates'";
74+
* the walk short-circuits to an empty list and the data view is
75+
* bootstrapped with no runtime field overlay (which is the correct
76+
* shape when there are no templates to project).
77+
*/
78+
templatesEnabled: boolean;
6779
}
6880

6981
/**
@@ -427,6 +439,13 @@ export class CasesAnalyticsV2DataViewService {
427439
* version.
428440
*/
429441
private async collectSnakeKeysForSpace(spaceId: string): Promise<string[]> {
442+
// Templates feature flag is off — the `cases-templates` SO type is not
443+
// registered with core, so naming it in `find({ type })` would throw
444+
// "Missing mappings for saved objects types: 'cases-templates'".
445+
// Returning empty here is also semantically correct: with no templates
446+
// there are no extended fields to project as runtime fields.
447+
if (!this.deps.templatesEnabled) return [];
448+
430449
const out: string[] = [];
431450
let page = 1;
432451

x-pack/platform/plugins/shared/cases/server/cases_analytics_v2/service.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ describe('CasesAnalyticsV2Service', () => {
2626
enableAdminRoutes: false,
2727
resetTaskTimeoutMinutes: 60,
2828
resetPageDelayMs: 0,
29+
templatesEnabled: true,
2930
});
3031
const proxy = service.getWriter();
3132

@@ -56,6 +57,7 @@ describe('CasesAnalyticsV2Service', () => {
5657
enableAdminRoutes: false,
5758
resetTaskTimeoutMinutes: 60,
5859
resetPageDelayMs: 0,
60+
templatesEnabled: true,
5961
});
6062
const refA = service.getDataViewRefresher();
6163
const refB = service.getDataViewRefresher();
@@ -72,6 +74,7 @@ describe('CasesAnalyticsV2Service', () => {
7274
enableAdminRoutes: false,
7375
resetTaskTimeoutMinutes: 60,
7476
resetPageDelayMs: 0,
77+
templatesEnabled: true,
7578
});
7679
const refresher = service.getDataViewRefresher();
7780

x-pack/platform/plugins/shared/cases/server/cases_analytics_v2/service.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ interface CasesAnalyticsV2ServiceDeps {
6666
* periodic ticks always use 0 (no throttle).
6767
*/
6868
resetPageDelayMs: number;
69+
/**
70+
* Resolved value of `xpack.cases.templates.enabled`. When false, the
71+
* `cases-templates` SO type is not registered with core, so the data
72+
* view sub-service must skip its per-space template read (otherwise
73+
* the internal SO client throws "Missing mappings for saved objects
74+
* types: 'cases-templates'"). Threaded through to the data view
75+
* sub-service at `start()` time; per-space data views are still
76+
* bootstrapped with an empty runtime field overlay when templates is
77+
* off, which is the correct shape (no templates → no extended-field
78+
* projections).
79+
*/
80+
templatesEnabled: boolean;
6981
}
7082

7183
/**
@@ -138,6 +150,7 @@ export class CasesAnalyticsV2Service {
138150
private readonly enableAdminRoutes: boolean;
139151
private readonly resetTaskTimeoutMinutes: number;
140152
private readonly resetPageDelayMs: number;
153+
private readonly templatesEnabled: boolean;
141154
/**
142155
* Active writer. Starts as `V2_NOOP_WRITER` so calls before `start()`
143156
* (or when v2 is disabled) silently no-op. Replaced with a real
@@ -209,6 +222,7 @@ export class CasesAnalyticsV2Service {
209222
this.enableAdminRoutes = deps.enableAdminRoutes;
210223
this.resetTaskTimeoutMinutes = deps.resetTaskTimeoutMinutes;
211224
this.resetPageDelayMs = deps.resetPageDelayMs;
225+
this.templatesEnabled = deps.templatesEnabled;
212226
}
213227

214228
/**
@@ -368,6 +382,7 @@ export class CasesAnalyticsV2Service {
368382
logger: this.logger,
369383
dataViewsService: deps.dataViewsService,
370384
internalSavedObjectsClient: deps.internalSavedObjectsClient,
385+
templatesEnabled: this.templatesEnabled,
371386
});
372387

373388
// Schedule the singleton reconciliation task. Idempotent and safe

x-pack/platform/plugins/shared/cases/server/plugin.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,16 @@ export class CasePlugin
160160
// within budget without blocking the HTTP request.
161161
resetTaskTimeoutMinutes: this.caseConfig.analyticsV2.resetTaskTimeoutMinutes,
162162
resetPageDelayMs: this.caseConfig.analyticsV2.resetPageDelayMs,
163+
// `xpack.cases.templates.enabled` gates whether `cases-templates`
164+
// is registered with core (see `saved_object_types/index.ts`). The
165+
// v2 data view sub-service reads template SOs to derive per-space
166+
// runtime field overlays; if templates is off there are no SOs to
167+
// read, and asking the SO client for the type would throw
168+
// "Missing mappings for saved objects types: 'cases-templates'".
169+
// Threading the flag through here lets the data view sub-service
170+
// short-circuit to an empty runtime field map (the base data view
171+
// is still bootstrapped — it just has no extended-field overlays).
172+
templatesEnabled: this.caseConfig.templates?.enabled === true,
163173
});
164174
this.casesAnalyticsV2Service.setup({ core, taskManager: plugins.taskManager });
165175

@@ -344,7 +354,15 @@ export class CasePlugin
344354
// `cases-user-actions` SOs (created-only, no `updated_at`
345355
// filter — see `reconciliation/activity_runner.ts`).
346356
// - The data view sub-service reads `cases-templates` SOs per-space
347-
// to derive runtime fields.
357+
// to derive runtime fields. Only included when templates is on
358+
// — `cases-templates` is registered with core only when
359+
// `xpack.cases.templates.enabled` is true (see
360+
// `saved_object_types/index.ts`), and naming it here when the
361+
// mapping isn't registered throws "Missing mappings for saved
362+
// objects types: 'cases-templates'" from
363+
// `createInternalRepository`. With templates off, the data view
364+
// sub-service short-circuits its template read and bootstraps
365+
// per-space data views with an empty runtime field overlay.
348366
// - The `/reset` admin route deletes per-space `index-pattern` SOs
349367
// across namespaces. A request-scoped SO client can't do this:
350368
// the spaces extension scopes `delete` to the request's namespace,
@@ -358,7 +376,7 @@ export class CasePlugin
358376
const v2InternalRepository = core.savedObjects.createInternalRepository([
359377
CASE_SAVED_OBJECT,
360378
CASE_USER_ACTION_SAVED_OBJECT,
361-
CASE_TEMPLATE_SAVED_OBJECT,
379+
...(this.caseConfig.templates?.enabled ? [CASE_TEMPLATE_SAVED_OBJECT] : []),
362380
'index-pattern',
363381
]);
364382
const v2InternalSavedObjectsClient = new SavedObjectsClient(v2InternalRepository);

0 commit comments

Comments
 (0)