Skip to content

Commit

Permalink
Merge branch 'main' into update-dashboard-intial-save-button-disabled
Browse files Browse the repository at this point in the history
  • Loading branch information
paulinashakirova authored Oct 16, 2024
2 parents 18be2de + e6569f0 commit f3392ef
Show file tree
Hide file tree
Showing 36 changed files with 263 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const browser = getService('browser');
const security = getService('security');

describe('discover data grid context tests', () => {
// Failing: See https://github.com/elastic/kibana/issues/196120
describe.skip('discover data grid context tests', () => {
before(async () => {
await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']);
await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import { ConfigSchema } from '../config';
import { encryptionKeyRotationServiceMock } from '../crypto/index.mock';

export const routeDefinitionParamsMock = {
create: (config: Record<string, unknown> = {}) => ({
create: (config: Record<string, unknown> = {}, buildFlavor: BuildFlavor = 'traditional') => ({
router: httpServiceMock.createRouter(),
logger: loggingSystemMock.create().get(),
config: ConfigSchema.validate(config) as ConfigType,
encryptionKeyRotationService: encryptionKeyRotationServiceMock.create(),
buildFlavor: 'traditional' as BuildFlavor,
buildFlavor,
}),
};
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ describe('Key rotation routes', () => {

it('correctly defines route.', () => {
expect(routeConfig.options).toEqual({
access: 'public',
tags: ['access:rotateEncryptionKey', 'oas-tag:saved objects'],
summary: `Rotate a key for encrypted saved objects`,
description: `If a saved object cannot be decrypted using the primary encryption key, Kibana attempts to decrypt it using the specified decryption-only keys. In most of the cases this overhead is negligible, but if you're dealing with a large number of saved objects and experiencing performance issues, you may want to rotate the encryption key.
Expand Down Expand Up @@ -83,6 +84,25 @@ describe('Key rotation routes', () => {
);
});

it('defines route as internal when build flavor is serverless', () => {
const routeParamsMock = routeDefinitionParamsMock.create(
{ keyRotation: { decryptionOnlyKeys: ['b'.repeat(32)] } },
'serverless'
);
defineKeyRotationRoutes(routeParamsMock);
const [config] = routeParamsMock.router.post.mock.calls.find(
([{ path }]) => path === '/api/encrypted_saved_objects/_rotate_key'
)!;

expect(config.options).toEqual({
access: 'internal',
tags: ['access:rotateEncryptionKey', 'oas-tag:saved objects'],
summary: `Rotate a key for encrypted saved objects`,
description: `If a saved object cannot be decrypted using the primary encryption key, Kibana attempts to decrypt it using the specified decryption-only keys. In most of the cases this overhead is negligible, but if you're dealing with a large number of saved objects and experiencing performance issues, you may want to rotate the encryption key.
NOTE: Bulk key rotation can consume a considerable amount of resources and hence only user with a superuser role can trigger it.`,
});
});

it('returns 400 if decryption only keys are not specified.', async () => {
const routeParamsMock = routeDefinitionParamsMock.create();
defineKeyRotationRoutes(routeParamsMock);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function defineKeyRotationRoutes({
},
options: {
tags: ['access:rotateEncryptionKey', 'oas-tag:saved objects'],
access: buildFlavor === 'serverless' ? 'internal' : undefined,
access: buildFlavor === 'serverless' ? 'internal' : 'public',
summary: `Rotate a key for encrypted saved objects`,
description: `If a saved object cannot be decrypted using the primary encryption key, Kibana attempts to decrypt it using the specified decryption-only keys. In most of the cases this overhead is negligible, but if you're dealing with a large number of saved objects and experiencing performance issues, you may want to rotate the encryption key.
NOTE: Bulk key rotation can consume a considerable amount of resources and hence only user with a superuser role can trigger it.`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export const SearchBar: FC<SearchBarProps> = (opts) => {
reportEvent.searchRequest();
}

const rawParams = parseSearchParams(searchValue.toLowerCase());
const rawParams = parseSearchParams(searchValue.toLowerCase(), searchableTypes);
let tagIds: string[] | undefined;
if (taggingApi && rawParams.filters.tags) {
tagIds = rawParams.filters.tags.map(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,33 @@ import { parseSearchParams } from './parse_search_params';

describe('parseSearchParams', () => {
it('returns the correct term', () => {
const searchParams = parseSearchParams('tag:(my-tag OR other-tag) hello');
const searchParams = parseSearchParams('tag:(my-tag OR other-tag) hello', []);
expect(searchParams.term).toEqual('hello');
});

it('returns the raw query as `term` in case of parsing error', () => {
const searchParams = parseSearchParams('tag:((()^invalid');
const searchParams = parseSearchParams('tag:((()^invalid', []);
expect(searchParams).toEqual({
term: 'tag:((()^invalid',
filters: {},
});
});

it('returns `undefined` term if query only contains field clauses', () => {
const searchParams = parseSearchParams('tag:(my-tag OR other-tag)');
const searchParams = parseSearchParams('tag:(my-tag OR other-tag)', []);
expect(searchParams.term).toBeUndefined();
});

it('returns correct filters when no field clause is defined', () => {
const searchParams = parseSearchParams('hello');
const searchParams = parseSearchParams('hello', []);
expect(searchParams.filters).toEqual({
tags: undefined,
types: undefined,
});
});

it('returns correct filters when field clauses are present', () => {
const searchParams = parseSearchParams('tag:foo type:bar hello tag:dolly');
const searchParams = parseSearchParams('tag:foo type:bar hello tag:dolly', []);
expect(searchParams).toEqual({
term: 'hello',
filters: {
Expand All @@ -46,7 +46,7 @@ describe('parseSearchParams', () => {
});

it('considers unknown field clauses to be part of the raw search term', () => {
const searchParams = parseSearchParams('tag:foo unknown:bar hello');
const searchParams = parseSearchParams('tag:foo unknown:bar hello', []);
expect(searchParams).toEqual({
term: 'unknown:bar hello',
filters: {
Expand All @@ -56,7 +56,7 @@ describe('parseSearchParams', () => {
});

it('handles aliases field clauses', () => {
const searchParams = parseSearchParams('tag:foo tags:bar type:dash types:board hello');
const searchParams = parseSearchParams('tag:foo tags:bar type:dash types:board hello', []);
expect(searchParams).toEqual({
term: 'hello',
filters: {
Expand All @@ -67,7 +67,7 @@ describe('parseSearchParams', () => {
});

it('converts boolean and number values to string for known filters', () => {
const searchParams = parseSearchParams('tag:42 tags:true type:69 types:false hello');
const searchParams = parseSearchParams('tag:42 tags:true type:69 types:false hello', []);
expect(searchParams).toEqual({
term: 'hello',
filters: {
Expand All @@ -76,4 +76,74 @@ describe('parseSearchParams', () => {
},
});
});

it('converts multiword searchable types to phrases so they get picked up as types', () => {
const mockSearchableMultiwordTypes = ['canvas-workpad', 'enterprise search'];
const searchParams = parseSearchParams(
'type:canvas workpad types:canvas-workpad hello type:enterprise search type:not multiword',
mockSearchableMultiwordTypes
);
expect(searchParams).toEqual({
term: 'hello multiword',
filters: {
types: ['canvas workpad', 'enterprise search', 'not'],
},
});
});

it('parses correctly when multiword types are already quoted', () => {
const mockSearchableMultiwordTypes = ['canvas-workpad'];
const searchParams = parseSearchParams(
`type:"canvas workpad" hello type:"dashboard"`,
mockSearchableMultiwordTypes
);
expect(searchParams).toEqual({
term: 'hello',
filters: {
types: ['canvas workpad', 'dashboard'],
},
});
});

it('parses correctly when there is whitespace between type keyword and value', () => {
const mockSearchableMultiwordTypes = ['canvas-workpad'];
const searchParams = parseSearchParams(
'type: canvas workpad hello type: dashboard',
mockSearchableMultiwordTypes
);
expect(searchParams).toEqual({
term: 'hello',
filters: {
types: ['canvas workpad', 'dashboard'],
},
});
});

it('dedupes duplicate types', () => {
const mockSearchableMultiwordTypes = ['canvas-workpad'];
const searchParams = parseSearchParams(
'type:canvas workpad hello type:dashboard type:canvas-workpad type:canvas workpad type:dashboard',
mockSearchableMultiwordTypes
);
expect(searchParams).toEqual({
term: 'hello',
filters: {
types: ['canvas workpad', 'dashboard'],
},
});
});

it('handles whitespace removal even if there are no multiword types', () => {
const mockSearchableMultiwordTypes: string[] = [];
const searchParams = parseSearchParams(
'hello type: dashboard',
mockSearchableMultiwordTypes
);
expect(searchParams).toEqual({
term: 'hello',
filters: {
types: ['dashboard'],
},
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,54 @@ const aliasMap = {
type: ['types'],
};

export const parseSearchParams = (term: string): ParsedSearchParams => {
// Converts multiword types to phrases by wrapping them in quotes and trimming whitespace after type keyword. Example: type: canvas workpad -> type:"canvas workpad". If the type is already wrapped in quotes or is a single word, it will only trim whitespace after type keyword.
const convertMultiwordTypesToPhrasesAndTrimWhitespace = (
term: string,
multiWordTypes: string[]
): string => {
if (!multiWordTypes.length) {
return term.replace(
/(type:|types:)\s*([^"']*?)\b([^"'\s]+)/gi,
(_, typeKeyword, whitespace, typeValue) => `${typeKeyword}${whitespace.trim()}${typeValue}`
);
}

const typesPattern = multiWordTypes.join('|');
const termReplaceRegex = new RegExp(
`(type:|types:)\\s*([^"']*?)\\b((${typesPattern})\\b|[^\\s"']+)`,
'gi'
);

return term.replace(termReplaceRegex, (_, typeKeyword, whitespace, typeValue) => {
const trimmedTypeKeyword = `${typeKeyword}${whitespace.trim()}`;

// If the type value is already wrapped in quotes, leave it as is
return /['"]/.test(typeValue)
? `${trimmedTypeKeyword}${typeValue}`
: `${trimmedTypeKeyword}"${typeValue}"`;
});
};

const dedupeTypes = (types: FilterValues<string>): FilterValues<string> => [
...new Set(types.map((item) => item.replace(/[-\s]+/g, ' ').trim())),
];

export const parseSearchParams = (term: string, searchableTypes: string[]): ParsedSearchParams => {
const recognizedFields = knownFilters.concat(...Object.values(aliasMap));
let query: Query;

// Finds all multiword types that are separated by whitespace or hyphens
const multiWordSearchableTypesWhitespaceSeperated = searchableTypes
.filter((item) => /[ -]/.test(item))
.map((item) => item.replace(/-/g, ' '));

const modifiedTerm = convertMultiwordTypesToPhrasesAndTrimWhitespace(
term,
multiWordSearchableTypesWhitespaceSeperated
);

try {
query = Query.parse(term, {
query = Query.parse(modifiedTerm, {
schema: { recognizedFields },
});
} catch (e) {
Expand All @@ -42,7 +84,7 @@ export const parseSearchParams = (term: string): ParsedSearchParams => {
term: searchTerm,
filters: {
tags: tags ? valuesToString(tags) : undefined,
types: types ? valuesToString(types) : undefined,
types: types ? dedupeTypes(valuesToString(types)) : undefined,
},
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { EntityManagerUnauthorizedError } from '@kbn/entityManager-plugin/public';
import type { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
import { TechnicalPreviewBadge } from '@kbn/observability-shared-plugin/public';
import { useKibana } from '../../hooks/use_kibana';
import { Unauthorized } from './unauthorized_modal';

Expand Down Expand Up @@ -57,6 +58,8 @@ export function EnableEntityModelButton({ onSuccess }: { onSuccess: () => void }
data-test-subj="inventoryInventoryPageTemplateFilledButton"
fill
onClick={handleEnablement}
iconType={() => <TechnicalPreviewBadge />}
iconSide="right"
>
{i18n.translate('xpack.inventory.noData.card.button', {
defaultMessage: 'Enable',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,20 @@ export function getEntityManagerEnablement({
description: (
<FormattedMessage
id="xpack.inventory.noData.card.description"
defaultMessage="The inventory uses the {link} to show all of your observed entities in one place."
defaultMessage="The {inventoryLink} uses the {link} to show all of your observed entities in one place."
values={{
inventoryLink: (
<EuiLink
data-test-subj="inventoryNoDataCardInventoryLink"
href="https://ela.st/docs-entity-inventory"
external
target="_blank"
>
{i18n.translate('xpack.inventory.noData.card.description.inventory', {
defaultMessage: 'Inventory',
})}
</EuiLink>
),
link: (
<EuiLink
data-test-subj="inventoryNoDataCardLink"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ describe('Common authentication routes', () => {
access: 'public',
authRequired: false,
tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW],
excludeFromOAS: true,
});
expect(routeConfig.validate).toEqual({
body: undefined,
Expand Down Expand Up @@ -170,7 +171,7 @@ describe('Common authentication routes', () => {
});

it('correctly defines route.', async () => {
expect(routeConfig.options).toBeUndefined();
expect(routeConfig.options).toEqual({ access: 'internal' });
expect(routeConfig.validate).toBe(false);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function defineCommonRoutes({
validate: { query: schema.object({}, { unknowns: 'allow' }) },
options: {
access: 'public',
excludeFromOAS: true,
authRequired: false,
tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW],
},
Expand Down Expand Up @@ -89,10 +90,11 @@ export function defineCommonRoutes({
'/internal/security/me',
...(buildFlavor !== 'serverless' ? ['/api/security/v1/me'] : []),
]) {
const deprecated = path === '/api/security/v1/me';
router.get(
{ path, validate: false },
{ path, validate: false, options: { access: deprecated ? 'public' : 'internal' } },
createLicensedRouteHandler(async (context, request, response) => {
if (path === '/api/security/v1/me') {
if (deprecated) {
logger.warn(
`The "${basePath.serverBasePath}${path}" endpoint is deprecated and will be removed in the next major version.`,
{ tags: ['deprecation'] }
Expand Down
Loading

0 comments on commit f3392ef

Please sign in to comment.