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:
Larry Gregory 2020-10-01 08:26:26 -04:00 committed by GitHub
parent d793040082
commit 4525f0cfab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 160 additions and 9 deletions

View file

@ -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']);
});
});

View file

@ -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));

View file

@ -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);
});
});
});
}

View file

@ -30,6 +30,13 @@
}
},
"type": "text"
},
"runtime_customer_ssn": {
"type": "runtime",
"runtime_type": "keyword",
"script": {
"source": "emit(doc['customer_ssn'].value + ' calculated at runtime')"
}
}
}
},