Skip to content

Commit

Permalink
[Search][Onboarding] Empty State page endpoints (#191229)
Browse files Browse the repository at this point in the history
## Summary

This PR introduces two endpoints for the `search_indices` plugin that
will be used by the start (empty state) page to determine if the user
has an indices and what their permissions are.

### 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
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
TattdCodeMonkey and kibanamachine authored Aug 26, 2024
1 parent 9ea2adb commit 486df8c
Show file tree
Hide file tree
Showing 17 changed files with 623 additions and 3 deletions.
2 changes: 2 additions & 0 deletions x-pack/plugins/search_indices/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@

export const PLUGIN_ID = 'searchIndices';
export const PLUGIN_NAME = 'searchIndices';

export type { IndicesStatusResponse, UserStartPrivilegesResponse } from './types';
17 changes: 17 additions & 0 deletions x-pack/plugins/search_indices/common/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export interface IndicesStatusResponse {
indexNames: string[];
}

export interface UserStartPrivilegesResponse {
privileges: {
canCreateApiKeys: boolean;
canCreateIndex: boolean;
};
}
231 changes: 231 additions & 0 deletions x-pack/plugins/search_indices/server/lib/status.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type {
IndicesGetResponse,
SecurityHasPrivilegesResponse,
} from '@elastic/elasticsearch/lib/api/types';
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import type { Logger } from '@kbn/logging';
import { fetchIndicesStatus, fetchUserStartPrivileges } from './status';

const mockLogger = {
warn: jest.fn(),
error: jest.fn(),
};
const logger: Logger = mockLogger as unknown as Logger;

const mockClient = {
indices: {
get: jest.fn(),
},
security: {
hasPrivileges: jest.fn(),
},
};
const client = mockClient as unknown as ElasticsearchClient;

describe('status api lib', function () {
beforeEach(() => {
jest.clearAllMocks();
});

describe('fetchIndicesStatus', function () {
it('should return results from get', async () => {
const mockResult: IndicesGetResponse = {};
mockClient.indices.get.mockResolvedValue(mockResult);

await expect(fetchIndicesStatus(client, logger)).resolves.toEqual({ indexNames: [] });
expect(mockClient.indices.get).toHaveBeenCalledTimes(1);
expect(mockClient.indices.get).toHaveBeenCalledWith({
expand_wildcards: ['open'],
features: ['settings'],
index: '*',
});
});
it('should return index names', async () => {
const mockResult: IndicesGetResponse = {
'unit-test-index': {
settings: {},
},
};
mockClient.indices.get.mockResolvedValue(mockResult);

await expect(fetchIndicesStatus(client, logger)).resolves.toEqual({
indexNames: ['unit-test-index'],
});
});
it('should not return hidden indices', async () => {
const mockResult: IndicesGetResponse = {
'unit-test-index': {
settings: {},
},
'hidden-index': {
settings: {
index: {
hidden: true,
},
},
},
};
mockClient.indices.get.mockResolvedValue(mockResult);

await expect(fetchIndicesStatus(client, logger)).resolves.toEqual({
indexNames: ['unit-test-index'],
});

mockResult['hidden-index']!.settings!.index!.hidden = 'true';
await expect(fetchIndicesStatus(client, logger)).resolves.toEqual({
indexNames: ['unit-test-index'],
});
});
it('should not return closed indices', async () => {
const mockResult: IndicesGetResponse = {
'unit-test-index': {
settings: {},
},
'closed-index': {
settings: {
index: {
verified_before_close: true,
},
},
},
};
mockClient.indices.get.mockResolvedValue(mockResult);

await expect(fetchIndicesStatus(client, logger)).resolves.toEqual({
indexNames: ['unit-test-index'],
});

mockResult['closed-index']!.settings!.index!.verified_before_close = 'true';
await expect(fetchIndicesStatus(client, logger)).resolves.toEqual({
indexNames: ['unit-test-index'],
});
});
it('should raise exceptions', async () => {
const error = new Error('boom');
mockClient.indices.get.mockRejectedValue(error);

await expect(fetchIndicesStatus(client, logger)).rejects.toThrow(error);
});
});

describe('fetchUserStartPrivileges', function () {
it('should return privileges true', async () => {
const result: SecurityHasPrivilegesResponse = {
application: {},
cluster: {
manage_api_key: true,
},
has_all_requested: true,
index: {
'test-index-name': {
create_index: true,
},
},
username: 'unit-test',
};
mockClient.security.hasPrivileges.mockResolvedValue(result);

await expect(fetchUserStartPrivileges(client, logger)).resolves.toEqual({
privileges: {
canCreateIndex: true,
canCreateApiKeys: true,
},
});

expect(mockClient.security.hasPrivileges).toHaveBeenCalledTimes(1);
expect(mockClient.security.hasPrivileges).toHaveBeenCalledWith({
cluster: ['manage_api_key'],
index: [
{
names: ['test-index-name'],
privileges: ['create_index'],
},
],
});
});
it('should return privileges false', async () => {
const result: SecurityHasPrivilegesResponse = {
application: {},
cluster: {
manage_api_key: false,
},
has_all_requested: false,
index: {
'test-index-name': {
create_index: false,
},
},
username: 'unit-test',
};
mockClient.security.hasPrivileges.mockResolvedValue(result);

await expect(fetchUserStartPrivileges(client, logger)).resolves.toEqual({
privileges: {
canCreateIndex: false,
canCreateApiKeys: false,
},
});
});
it('should return mixed privileges', async () => {
const result: SecurityHasPrivilegesResponse = {
application: {},
cluster: {
manage_api_key: false,
},
has_all_requested: false,
index: {
'test-index-name': {
create_index: true,
},
},
username: 'unit-test',
};
mockClient.security.hasPrivileges.mockResolvedValue(result);

await expect(fetchUserStartPrivileges(client, logger)).resolves.toEqual({
privileges: {
canCreateIndex: true,
canCreateApiKeys: false,
},
});
});
it('should handle malformed responses', async () => {
const result: SecurityHasPrivilegesResponse = {
application: {},
cluster: {},
has_all_requested: true,
index: {
'test-index-name': {
create_index: true,
},
},
username: 'unit-test',
};
mockClient.security.hasPrivileges.mockResolvedValue(result);

await expect(fetchUserStartPrivileges(client, logger)).resolves.toEqual({
privileges: {
canCreateIndex: true,
canCreateApiKeys: false,
},
});
});
it('should default privileges on exceptions', async () => {
mockClient.security.hasPrivileges.mockRejectedValue(new Error('Boom!!'));

await expect(fetchUserStartPrivileges(client, logger)).resolves.toEqual({
privileges: {
canCreateIndex: false,
canCreateApiKeys: false,
},
});
});
});
});
70 changes: 70 additions & 0 deletions x-pack/plugins/search_indices/server/lib/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import type { Logger } from '@kbn/logging';

import type { IndicesStatusResponse, UserStartPrivilegesResponse } from '../../common/types';

import { isHidden, isClosed } from '../utils/index_utils';

export async function fetchIndicesStatus(
client: ElasticsearchClient,
logger: Logger
): Promise<IndicesStatusResponse> {
const indexMatches = await client.indices.get({
expand_wildcards: ['open'],
// for better performance only compute settings of indices but not mappings
features: ['settings'],
index: '*',
});

const indexNames = Object.keys(indexMatches).filter(
(indexName) =>
indexMatches[indexName] &&
!isHidden(indexMatches[indexName]) &&
!isClosed(indexMatches[indexName])
);

return {
indexNames,
};
}

export async function fetchUserStartPrivileges(
client: ElasticsearchClient,
logger: Logger,
indexName: string = 'test-index-name'
): Promise<UserStartPrivilegesResponse> {
try {
const securityCheck = await client.security.hasPrivileges({
cluster: ['manage_api_key'],
index: [
{
names: [indexName],
privileges: ['create_index'],
},
],
});

return {
privileges: {
canCreateIndex: securityCheck?.index?.[indexName]?.create_index ?? false,
canCreateApiKeys: securityCheck?.cluster?.manage_api_key ?? false,
},
};
} catch (e) {
logger.error(`Error checking user privileges for searchIndices elasticsearch start`);
logger.error(e);
return {
privileges: {
canCreateIndex: false,
canCreateApiKeys: false,
},
};
}
}
2 changes: 1 addition & 1 deletion x-pack/plugins/search_indices/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class SearchIndicesPlugin
const router = core.http.createRouter();

// Register server side APIs
defineRoutes(router);
defineRoutes(router, this.logger);

return {};
}
Expand Down
7 changes: 6 additions & 1 deletion x-pack/plugins/search_indices/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,10 @@
*/

import type { IRouter } from '@kbn/core/server';
import type { Logger } from '@kbn/logging';

export function defineRoutes(router: IRouter) {}
import { registerStatusRoutes } from './status';

export function defineRoutes(router: IRouter, logger: Logger) {
registerStatusRoutes(router, logger);
}
Loading

0 comments on commit 486df8c

Please sign in to comment.