Skip to content

Commit

Permalink
Omit runtime fields from FLS suggestions (#78330)
Browse files Browse the repository at this point in the history
Co-authored-by: Aleh Zasypkin <[email protected]>
Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
3 people authored Oct 1, 2020
1 parent d793040 commit 4525f0c
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 9 deletions.
58 changes: 58 additions & 0 deletions x-pack/plugins/security/server/routes/indices/get_fields.test.ts
Original file line number Diff line number Diff line change
@@ -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']);
});
});
46 changes: 37 additions & 9 deletions x-pack/plugins/security/server/routes/indices/get_fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
{
Expand All @@ -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));
Expand Down
58 changes: 58 additions & 0 deletions x-pack/test/api_integration/apis/security/index_fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
});
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@
}
},
"type": "text"
},
"runtime_customer_ssn": {
"type": "runtime",
"runtime_type": "keyword",
"script": {
"source": "emit(doc['customer_ssn'].value + ' calculated at runtime')"
}
}
}
},
Expand Down

0 comments on commit 4525f0c

Please sign in to comment.