Prevent so type registration if schema declared directly (#177246)

Fix https://github.com/elastic/kibana/issues/176668

## Summary

After a switch to model versions, saved object registrations are blocked
if any schema for a higher version is declared when not coupled with a
model version.


### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Christiane (Tina) Heiligers 2024-02-22 09:21:00 -07:00 committed by GitHub
parent 6d44340e52
commit bf4b70ceb4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 167 additions and 4 deletions

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import { schema } from '@kbn/config-schema';
import { SavedObjectsType, SavedObjectsModelVersion } from '@kbn/core-saved-objects-server';
import { validateTypeMigrations } from './validate_migrations';
@ -50,6 +51,9 @@ describe('validateTypeMigrations', () => {
bar: jest.fn(),
'1.2.3': jest.fn(),
},
schemas: {
'1.2.3': schema.object({ bar: schema.string() }),
},
});
expect(() => validate({ type })).toThrow(/Expected all properties to be semvers/i);
@ -123,6 +127,23 @@ describe('validateTypeMigrations', () => {
`"Migration for type foo for version 8.10.0 registered after switchToModelVersionAt (8.9.0)"`
);
});
it('throws if a schema is specified for a version superior to switchToModelVersionAt', () => {
const type = createType({
name: 'foo',
switchToModelVersionAt: '8.9.0',
schemas: {
'8.10.0': schema.object({ name: schema.string() }),
},
});
expect(() =>
validate({ type, kibanaVersion: '8.10.0' })
).toThrowErrorMatchingInlineSnapshot(
`"Schema for type foo for version 8.10.0 registered after switchToModelVersionAt (8.9.0)"`
);
});
it('throws if a migration is specified for a version equal to switchToModelVersionAt', () => {
const type = createType({
name: 'foo',
@ -139,6 +160,22 @@ describe('validateTypeMigrations', () => {
);
});
it('throws if a schema is specified for a version equal to switchToModelVersionAt', () => {
const type = createType({
name: 'foo',
switchToModelVersionAt: '8.9.0',
schemas: {
'8.9.0': schema.object({ name: schema.string() }),
},
});
expect(() =>
validate({ type, kibanaVersion: '8.10.0' })
).toThrowErrorMatchingInlineSnapshot(
`"Schema for type foo for version 8.9.0 registered after switchToModelVersionAt (8.9.0)"`
);
});
it('does not throw if a migration is specified for a version inferior to switchToModelVersionAt', () => {
const type = createType({
name: 'foo',
@ -150,6 +187,18 @@ describe('validateTypeMigrations', () => {
expect(() => validate({ type, kibanaVersion: '8.10.0' })).not.toThrow();
});
it('does not throw if a schema is specified for a version inferior to switchToModelVersionAt', () => {
const type = createType({
name: 'foo',
switchToModelVersionAt: '8.9.0',
schemas: {
'8.7.0': schema.object({ name: schema.string() }),
},
});
expect(() => validate({ type, kibanaVersion: '8.10.0' })).not.toThrow();
});
});
});
@ -245,6 +294,71 @@ describe('validateTypeMigrations', () => {
});
});
describe('modelVersions with schemas', () => {
const baseSchema = schema.object({ name: schema.string() }, { unknowns: 'ignore' });
it('throws if used without specifying switchToModelVersionAt', () => {
const type = createType({
name: 'foo',
modelVersions: {
1: {
changes: [],
schemas: {
forwardCompatibility: baseSchema,
create: baseSchema,
},
},
},
mappings: {
properties: {
name: { type: 'text' },
},
},
});
expect(() => validate({ type, kibanaVersion: '3.2.3' })).toThrowErrorMatchingInlineSnapshot(
`"Type foo: Using modelVersions requires to specify switchToModelVersionAt"`
);
});
it('does not throw passing a model version schema map', () => {
const someModelVersionWithSchema = {
changes: [],
schemas: {
forwardCompatibility: baseSchema.extends({}, { unknowns: 'ignore' }),
create: baseSchema,
},
};
const type = createType({
name: 'foo',
switchToModelVersionAt: '3.1.0',
modelVersions: {
'1': someModelVersionWithSchema,
},
mappings: {
properties: {
name: { type: 'text' },
},
},
});
expect(() => validate({ type, kibanaVersion: '3.2.3' })).not.toThrow();
});
it('does not throw passing an empty model version schema map', () => {
const someModelVersionWithSchema = { changes: [], schemas: {} };
const type = createType({
name: 'foo',
switchToModelVersionAt: '3.1.0',
modelVersions: {
'1': someModelVersionWithSchema,
},
});
expect(() => validate({ type, kibanaVersion: '3.2.3' })).not.toThrow();
});
});
describe('modelVersions mapping additions', () => {
it('throws when registering mapping additions not present in the global mappings', () => {
const type = createType({
@ -274,7 +388,7 @@ describe('validateTypeMigrations', () => {
);
});
it('does not throw when registering mapping additions are present in the global mappings', () => {
it('does not throw when registering mapping additions are present in the global mappings with a schema', () => {
const type = createType({
name: 'foo',
switchToModelVersionAt: '8.8.0',
@ -288,6 +402,7 @@ describe('validateTypeMigrations', () => {
},
},
],
schemas: {},
},
'2': {
changes: [
@ -340,6 +455,30 @@ describe('validateTypeMigrations', () => {
`"Type foo: mappings added on model versions differs from the global mappings definition: field2.type"`
);
});
it('does not throw if a schema is specified for a modelVersion with no changes', () => {
const baseSchema = schema.object({ name: schema.string() });
const type = createType({
name: 'foo',
switchToModelVersionAt: '8.10.0',
modelVersions: {
1: {
changes: [],
schemas: {
forwardCompatibility: baseSchema.extends({}, { unknowns: 'ignore' }),
create: baseSchema,
},
},
},
mappings: {
properties: {
name: { type: 'text' },
},
},
});
expect(() => validate({ type, kibanaVersion: '3.2.3' })).not.toThrow();
});
});
describe('convertToMultiNamespaceTypeVersion', () => {

View file

@ -69,6 +69,23 @@ export function validateTypeMigrations({
});
}
if (type.schemas) {
const schemaMap = typeof type.schemas === 'object' ? type.schemas : {};
assertObject(
schemaMap,
`Schemas map for type ${type.name} should be an object like { '2.0.0': {schema} }.`
);
Object.entries(schemaMap).forEach(([version, schema]) => {
assertValidSemver(kibanaVersion, version, type.name);
if (type.switchToModelVersionAt && Semver.gte(version, type.switchToModelVersionAt)) {
throw new Error(
`Schema for type ${type.name} for version ${version} registered after switchToModelVersionAt (${type.switchToModelVersionAt})`
);
}
});
}
if (type.modelVersions) {
const modelVersionMap =
typeof type.modelVersions === 'function' ? type.modelVersions() : type.modelVersions ?? {};

View file

@ -76,7 +76,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"cases-connector-mappings": "f9d1ac57e484e69506c36a8051e4d61f4a8cfd25",
"cases-telemetry": "f219eb7e26772884342487fc9602cfea07b3cedc",
"cases-user-actions": "483f10db9b3bd1617948d7032a98b7791bf87414",
"cloud-security-posture-settings": "675e47dd958fbce6c70a20baac12af3145e7c0ef",
"cloud-security-posture-settings": "e0f61c68bbb5e4cfa46ce8994fa001e417df51ca",
"config": "179b3e2bc672626aafce3cf92093a113f456af38",
"config-global": "8e8a134a2952df700d7d4ec51abb794bbd4cf6da",
"connector_token": "5a9ac29fe9c740eb114e9c40517245c71706b005",

View file

@ -16,8 +16,15 @@ export const cspSettings: SavedObjectsType = {
indexPattern: SECURITY_SOLUTION_SAVED_OBJECT_INDEX,
hidden: true,
namespaceType: 'agnostic',
schemas: {
'8.12.0': cspSettingsSchema,
modelVersions: {
1: {
changes: [],
schemas: {
forwardCompatibility: cspSettingsSchema.extends({}, { unknowns: 'ignore' }),
create: cspSettingsSchema,
},
},
},
schemas: {},
mappings: cspSettingsSavedObjectMapping,
};