mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Omit runtime fields from FLS suggestions (#78330)
Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
d793040082
commit
4525f0cfab
4 changed files with 160 additions and 9 deletions
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { httpServerMock, elasticsearchServiceMock } from '../../../../../../src/core/server/mocks';
|
||||
import { kibanaResponseFactory } from '../../../../../../src/core/server';
|
||||
|
||||
import { routeDefinitionParamsMock } from '../index.mock';
|
||||
import { defineGetFieldsRoutes } from './get_fields';
|
||||
|
||||
const createFieldMapping = (field: string, type: string) => ({
|
||||
[field]: { mapping: { [field]: { type } } },
|
||||
});
|
||||
|
||||
const createEmptyFieldMapping = (field: string) => ({ [field]: { mapping: {} } });
|
||||
|
||||
const mockFieldMappingResponse = {
|
||||
foo: {
|
||||
mappings: {
|
||||
...createFieldMapping('fooField', 'keyword'),
|
||||
...createFieldMapping('commonField', 'keyword'),
|
||||
...createEmptyFieldMapping('emptyField'),
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
mappings: {
|
||||
...createFieldMapping('commonField', 'keyword'),
|
||||
...createFieldMapping('barField', 'keyword'),
|
||||
...createFieldMapping('runtimeField', 'runtime'),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('GET /internal/security/fields/{query}', () => {
|
||||
it('returns a list of deduplicated fields, omitting empty and runtime fields', async () => {
|
||||
const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
|
||||
|
||||
const scopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient();
|
||||
scopedClient.callAsCurrentUser.mockResolvedValue(mockFieldMappingResponse);
|
||||
mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(scopedClient);
|
||||
|
||||
defineGetFieldsRoutes(mockRouteDefinitionParams);
|
||||
|
||||
const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls;
|
||||
|
||||
const headers = { authorization: 'foo' };
|
||||
const mockRequest = httpServerMock.createKibanaRequest({
|
||||
method: 'get',
|
||||
path: `/internal/security/fields/foo`,
|
||||
headers,
|
||||
});
|
||||
const response = await handler({} as any, mockRequest, kibanaResponseFactory);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.payload).toEqual(['fooField', 'commonField', 'barField']);
|
||||
});
|
||||
});
|
|
@ -8,6 +8,20 @@ import { schema } from '@kbn/config-schema';
|
|||
import { RouteDefinitionParams } from '../index';
|
||||
import { wrapIntoCustomErrorResponse } from '../../errors';
|
||||
|
||||
interface FieldMappingResponse {
|
||||
[indexName: string]: {
|
||||
mappings: {
|
||||
[fieldName: string]: {
|
||||
mapping: {
|
||||
[fieldName: string]: {
|
||||
type: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function defineGetFieldsRoutes({ router, clusterClient }: RouteDefinitionParams) {
|
||||
router.get(
|
||||
{
|
||||
|
@ -23,21 +37,35 @@ export function defineGetFieldsRoutes({ router, clusterClient }: RouteDefinition
|
|||
fields: '*',
|
||||
allowNoIndices: false,
|
||||
includeDefaults: true,
|
||||
})) as Record<string, { mappings: Record<string, unknown> }>;
|
||||
})) as FieldMappingResponse;
|
||||
|
||||
// The flow is the following (see response format at https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html):
|
||||
// 1. Iterate over all matched indices.
|
||||
// 2. Extract all the field names from the `mappings` field of the particular index.
|
||||
// 3. Collect and flatten the list of the field names.
|
||||
// 3. Collect and flatten the list of the field names, omitting any fields without mappings, and any runtime fields
|
||||
// 4. Use `Set` to get only unique field names.
|
||||
const fields = Array.from(
|
||||
new Set(
|
||||
Object.values(indexMappings).flatMap((indexMapping) => {
|
||||
return Object.keys(indexMapping.mappings).filter((fieldName) => {
|
||||
const mappingValues = Object.values(indexMapping.mappings[fieldName].mapping);
|
||||
const hasMapping = mappingValues.length > 0;
|
||||
|
||||
const isRuntimeField = hasMapping && mappingValues[0]?.type === 'runtime';
|
||||
|
||||
// fields without mappings are internal fields such as `_routing` and `_index`,
|
||||
// and therefore don't make sense as autocomplete suggestions for FLS.
|
||||
|
||||
// Runtime fields are not securable via FLS.
|
||||
// Administrators should instead secure access to the fields which derive this information.
|
||||
return hasMapping && !isRuntimeField;
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return response.ok({
|
||||
body: Array.from(
|
||||
new Set(
|
||||
Object.values(indexMappings)
|
||||
.map((indexMapping) => Object.keys(indexMapping.mappings))
|
||||
.flat()
|
||||
)
|
||||
),
|
||||
body: fields,
|
||||
});
|
||||
} catch (error) {
|
||||
return response.customError(wrapIntoCustomErrorResponse(error));
|
||||
|
|
|
@ -7,10 +7,33 @@
|
|||
import expect from '@kbn/expect/expect.js';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
interface FLSFieldMappingResponse {
|
||||
flstest: {
|
||||
mappings: {
|
||||
[fieldName: string]: {
|
||||
mapping: {
|
||||
[fieldName: string]: {
|
||||
type: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('legacyEs');
|
||||
|
||||
describe('Index Fields', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load('security/flstest/data');
|
||||
});
|
||||
after(async () => {
|
||||
await esArchiver.unload('security/flstest/data');
|
||||
});
|
||||
|
||||
describe('GET /internal/security/fields/{query}', () => {
|
||||
it('should return a list of available index mapping fields', async () => {
|
||||
await supertest
|
||||
|
@ -30,6 +53,41 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
sampleOfExpectedFields.forEach((field) => expect(response.body).to.contain(field));
|
||||
});
|
||||
});
|
||||
|
||||
it('should not include runtime fields', async () => {
|
||||
// First, make sure the mapping actually includes a runtime field
|
||||
const fieldMapping = (await es.indices.getFieldMapping({
|
||||
index: 'flstest',
|
||||
fields: '*',
|
||||
includeDefaults: true,
|
||||
})) as FLSFieldMappingResponse;
|
||||
|
||||
expect(Object.keys(fieldMapping.flstest.mappings)).to.contain('runtime_customer_ssn');
|
||||
expect(
|
||||
fieldMapping.flstest.mappings.runtime_customer_ssn.mapping.runtime_customer_ssn.type
|
||||
).to.eql('runtime');
|
||||
|
||||
// Now, make sure it's not returned here
|
||||
const { body: actualFields } = (await supertest
|
||||
.get('/internal/security/fields/flstest')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send()
|
||||
.expect(200)) as { body: string[] };
|
||||
|
||||
const expectedFields = [
|
||||
'customer_ssn',
|
||||
'customer_ssn.keyword',
|
||||
'customer_region',
|
||||
'customer_region.keyword',
|
||||
'customer_name',
|
||||
'customer_name.keyword',
|
||||
];
|
||||
|
||||
actualFields.sort();
|
||||
expectedFields.sort();
|
||||
|
||||
expect(actualFields).to.eql(expectedFields);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -30,6 +30,13 @@
|
|||
}
|
||||
},
|
||||
"type": "text"
|
||||
},
|
||||
"runtime_customer_ssn": {
|
||||
"type": "runtime",
|
||||
"runtime_type": "keyword",
|
||||
"script": {
|
||||
"source": "emit(doc['customer_ssn'].value + ' calculated at runtime')"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue