[Obs AI Assistant] Add scoped privileges to readUser and writeUser (#183592)

Related to https://github.com/elastic/kibana/issues/183588
This commit is contained in:
Søren Louv-Jansen 2024-05-22 08:43:11 +02:00 committed by GitHub
parent b8777aa07c
commit 97de948dc5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 210 additions and 100 deletions

View file

@ -6,15 +6,15 @@
*/
import { Config, FtrConfigProviderContext } from '@kbn/test';
import supertest from 'supertest';
import { format, UrlObject } from 'url';
import { UrlObject } from 'url';
import { ObservabilityAIAssistantFtrConfigName } from '../configs';
import { getApmSynthtraceEsClient } from './create_synthtrace_client';
import { InheritedFtrProviderContext, InheritedServices } from './ftr_provider_context';
import {
createObservabilityAIAssistantApiClient,
getScopedApiClient,
ObservabilityAIAssistantAPIClient,
} from './observability_ai_assistant_api_client';
import { editorUser, viewerUser } from './users/users';
export interface ObservabilityAIAssistantFtrConfig {
name: ObservabilityAIAssistantFtrConfigName;
@ -22,10 +22,6 @@ export interface ObservabilityAIAssistantFtrConfig {
kibanaConfig?: Record<string, any>;
}
async function getObservabilityAIAssistantAPIClient(kibanaServerUrl: string) {
return createObservabilityAIAssistantApiClient(supertest(kibanaServerUrl));
}
export type CreateTestConfig = ReturnType<typeof createTestConfig>;
export interface CreateTest {
@ -33,8 +29,9 @@ export interface CreateTest {
servers: any;
services: InheritedServices & {
observabilityAIAssistantAPIClient: () => Promise<{
readUser: ObservabilityAIAssistantAPIClient;
writeUser: ObservabilityAIAssistantAPIClient;
adminUser: ObservabilityAIAssistantAPIClient;
viewerUser: ObservabilityAIAssistantAPIClient;
editorUser: ObservabilityAIAssistantAPIClient;
}>;
};
junit: { reportName: string };
@ -56,7 +53,6 @@ export function createObservabilityAIAssistantAPIConfig({
const services = config.get('services') as InheritedServices;
const servers = config.get('servers');
const kibanaServer = servers.kibana as UrlObject;
const kibanaServerUrl = format(kibanaServer);
const apmSynthtraceKibanaClient = services.apmSynthtraceKibanaClient();
const createTest: Omit<CreateTest, 'testFiles'> = {
@ -68,8 +64,9 @@ export function createObservabilityAIAssistantAPIConfig({
getApmSynthtraceEsClient(context, apmSynthtraceKibanaClient),
observabilityAIAssistantAPIClient: async () => {
return {
readUser: await getObservabilityAIAssistantAPIClient(kibanaServerUrl),
writeUser: await getObservabilityAIAssistantAPIClient(kibanaServerUrl),
adminUser: await getScopedApiClient(kibanaServer, 'elastic'),
viewerUser: await getScopedApiClient(kibanaServer, viewerUser.username),
editorUser: await getScopedApiClient(kibanaServer, editorUser.username),
};
},
},

View file

@ -12,8 +12,20 @@ import type {
} from '@kbn/observability-ai-assistant-plugin/public';
import { formatRequest } from '@kbn/server-route-repository';
import supertest from 'supertest';
import { format } from 'url';
import { Subtract } from 'utility-types';
import { format, UrlObject } from 'url';
import { kbnTestConfig } from '@kbn/test';
import { User } from './users/users';
export async function getScopedApiClient(kibanaServer: UrlObject, username: User['username']) {
const { password } = kbnTestConfig.getUrlParts();
const baseUrlWithAuth = format({
...kibanaServer,
auth: `${username}:${password}`,
});
return createObservabilityAIAssistantApiClient(supertest(baseUrlWithAuth));
}
export function createObservabilityAIAssistantApiClient(st: supertest.Agent) {
return <TEndpoint extends ObservabilityAIAssistantAPIEndpoint>(

View file

@ -0,0 +1,33 @@
/*
* 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 { InheritedFtrProviderContext } from '../ftr_provider_context';
import { allUsers } from './users';
import { allRoles } from './roles';
export async function createUsersAndRoles(getService: InheritedFtrProviderContext['getService']) {
const security = getService('security');
const log = getService('log');
// create roles
await Promise.all(
allRoles.map(({ name, privileges }) => {
return security.role.create(name, privileges);
})
);
// create users
await Promise.all(
allUsers.map((user) => {
log.info(`Creating user: ${user.username} with roles: ${user.roles.join(', ')}`);
return security.user.create(user.username, {
password: user.password,
roles: user.roles,
});
})
);
}

View file

@ -0,0 +1,52 @@
/*
* 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.
*/
// Example role:
// export const allAccessRole: Role = {
// name: 'all_access',
// privileges: {
// elasticsearch: {
// indices: [
// {
// names: ['*'],
// privileges: ['all'],
// },
// ],
// },
// kibana: [
// {
// feature: {
// apm: ['all'],
// actions: ['all'],
// },
// spaces: ['*'],
// },
// ],
// },
// };
export interface Role {
name: string;
privileges: {
elasticsearch?: {
cluster?: string[];
indices?: Array<{
names: string[];
privileges: string[];
}>;
};
kibana?: Array<{
spaces: string[];
base?: string[];
feature?: {
[featureId: string]: string[];
};
}>;
};
}
export const allRoles = [];

View file

@ -0,0 +1,29 @@
/*
* 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 { kbnTestConfig } from '@kbn/test';
const password = kbnTestConfig.getUrlParts().password!;
export interface User {
username: 'elastic' | 'editor' | 'viewer';
password: string;
roles: string[];
}
export const editorUser: User = {
username: 'editor',
password,
roles: ['editor'],
};
export const viewerUser: User = {
username: 'viewer',
password,
roles: ['viewer'],
};
export const allUsers = [editorUser, viewerUser];

View file

@ -302,7 +302,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
)[0]?.conversation.id;
await observabilityAIAssistantAPIClient
.writeUser({
.adminUser({
endpoint: 'DELETE /internal/observability_ai_assistant/conversation/{conversationId}',
params: {
path: {
@ -378,7 +378,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
).to.eql(0);
const conversations = await observabilityAIAssistantAPIClient
.writeUser({
.editorUser({
endpoint: 'POST /internal/observability_ai_assistant/conversations',
})
.expect(200);
@ -422,7 +422,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
.complete();
const createResponse = await observabilityAIAssistantAPIClient
.writeUser({
.editorUser({
endpoint: 'POST /internal/observability_ai_assistant/chat/complete',
params: {
body: {
@ -440,7 +440,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
conversationCreatedEvent = getConversationCreatedEvent(createResponse.body);
const conversationId = conversationCreatedEvent.conversation.id;
const fullConversation = await observabilityAIAssistantAPIClient.readUser({
const fullConversation = await observabilityAIAssistantAPIClient.editorUser({
endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}',
params: {
path: {
@ -454,7 +454,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
.complete();
const updatedResponse = await observabilityAIAssistantAPIClient
.writeUser({
.editorUser({
endpoint: 'POST /internal/observability_ai_assistant/chat/complete',
params: {
body: {
@ -484,7 +484,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
after(async () => {
await observabilityAIAssistantAPIClient
.writeUser({
.editorUser({
endpoint: 'DELETE /internal/observability_ai_assistant/conversation/{conversationId}',
params: {
path: {

View file

@ -24,14 +24,14 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('Returns a 2xx for enterprise license', async () => {
await observabilityAIAssistantAPIClient
.readUser({
.editorUser({
endpoint: 'GET /internal/observability_ai_assistant/connectors',
})
.expect(200);
});
it('returns an empty list of connectors', async () => {
const res = await observabilityAIAssistantAPIClient.readUser({
const res = await observabilityAIAssistantAPIClient.editorUser({
endpoint: 'GET /internal/observability_ai_assistant/connectors',
});
@ -55,7 +55,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
})
.expect(200);
const res = await observabilityAIAssistantAPIClient.readUser({
const res = await observabilityAIAssistantAPIClient.editorUser({
endpoint: 'GET /internal/observability_ai_assistant/connectors',
});

View file

@ -48,7 +48,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
describe('without conversations', () => {
it('returns no conversations when listing', async () => {
const response = await observabilityAIAssistantAPIClient
.readUser({
.editorUser({
endpoint: 'POST /internal/observability_ai_assistant/conversations',
})
.expect(200);
@ -58,7 +58,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('returns a 404 for updating conversations', async () => {
await observabilityAIAssistantAPIClient
.writeUser({
.editorUser({
endpoint: 'PUT /internal/observability_ai_assistant/conversation/{conversationId}',
params: {
path: {
@ -74,7 +74,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('returns a 404 for retrieving a conversation', async () => {
await observabilityAIAssistantAPIClient
.readUser({
.editorUser({
endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}',
params: {
path: {
@ -92,7 +92,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
>;
before(async () => {
createResponse = await observabilityAIAssistantAPIClient
.writeUser({
.editorUser({
endpoint: 'POST /internal/observability_ai_assistant/conversation',
params: {
body: {
@ -105,7 +105,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
after(async () => {
await observabilityAIAssistantAPIClient
.writeUser({
.editorUser({
endpoint: 'DELETE /internal/observability_ai_assistant/conversation/{conversationId}',
params: {
path: {
@ -116,7 +116,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
.expect(200);
await observabilityAIAssistantAPIClient
.writeUser({
.editorUser({
endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}',
params: {
path: {
@ -141,14 +141,14 @@ export default function ApiTest({ getService }: FtrProviderContext) {
namespace: 'default',
public: conversationCreate.public,
user: {
name: 'elastic',
name: 'editor',
},
});
});
it('returns a 404 for updating a non-existing conversation', async () => {
await observabilityAIAssistantAPIClient
.writeUser({
.editorUser({
endpoint: 'PUT /internal/observability_ai_assistant/conversation/{conversationId}',
params: {
path: {
@ -164,7 +164,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('returns a 404 for retrieving a non-existing conversation', async () => {
await observabilityAIAssistantAPIClient
.readUser({
.editorUser({
endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}',
params: {
path: {
@ -177,7 +177,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('returns the conversation that was created', async () => {
const response = await observabilityAIAssistantAPIClient
.readUser({
.editorUser({
endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}',
params: {
path: {
@ -192,7 +192,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('returns the created conversation when listing', async () => {
const response = await observabilityAIAssistantAPIClient
.readUser({
.editorUser({
endpoint: 'POST /internal/observability_ai_assistant/conversations',
})
.expect(200);
@ -210,7 +210,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
before(async () => {
updateResponse = await observabilityAIAssistantAPIClient
.writeUser({
.editorUser({
endpoint: 'PUT /internal/observability_ai_assistant/conversation/{conversationId}',
params: {
path: {
@ -234,7 +234,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('returns the updated conversation after get', async () => {
const updateAfterCreateResponse = await observabilityAIAssistantAPIClient
.writeUser({
.editorUser({
endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}',
params: {
path: {

View file

@ -6,6 +6,7 @@
*/
import globby from 'globby';
import path from 'path';
import { createUsersAndRoles } from '../common/users/create_users_and_roles';
import { FtrProviderContext } from '../common/ftr_provider_context';
const cwd = path.join(__dirname);
@ -18,6 +19,11 @@ export default function observabilityAIAssistantApiIntegrationTests({
const filePattern = '**/*.spec.ts';
const tests = globby.sync(filePattern, { cwd });
// Creates roles and users before running tests
before(async () => {
await createUsersAndRoles(getService);
});
tests.forEach((testName) => {
describe(testName, () => {
loadTestFile(require.resolve(`./${testName}`));

View file

@ -7,10 +7,13 @@
import { FtrConfigProviderContext } from '@kbn/test';
import { merge } from 'lodash';
import supertest from 'supertest';
import { format, UrlObject } from 'url';
import { UrlObject } from 'url';
import type { EBTHelpersContract } from '@kbn/analytics-ftr-helpers-plugin/common/types';
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import {
editorUser,
viewerUser,
} from '../../observability_ai_assistant_api_integration/common/users/users';
import {
KibanaEBTServerProvider,
KibanaEBTUIProvider,
@ -21,7 +24,7 @@ import {
createObservabilityAIAssistantAPIConfig,
} from '../../observability_ai_assistant_api_integration/common/config';
import {
createObservabilityAIAssistantApiClient,
getScopedApiClient,
ObservabilityAIAssistantAPIClient,
} from '../../observability_ai_assistant_api_integration/common/observability_ai_assistant_api_client';
import { InheritedFtrProviderContext, InheritedServices } from '../ftr_provider_context';
@ -33,11 +36,11 @@ export interface TestConfig extends CreateTestAPI {
observabilityAIAssistantUI: (
context: InheritedFtrProviderContext
) => Promise<ObservabilityAIAssistantUIService>;
observabilityAIAssistantAPIClient: () => Promise<
Awaited<ReturnType<CreateTestAPI['services']['observabilityAIAssistantAPIClient']>> & {
testUser: ObservabilityAIAssistantAPIClient;
}
>;
observabilityAIAssistantAPIClient: () => Promise<{
adminUser: ObservabilityAIAssistantAPIClient;
viewerUser: ObservabilityAIAssistantAPIClient;
editorUser: ObservabilityAIAssistantAPIClient;
}>;
kibana_ebt_server: (context: InheritedFtrProviderContext) => EBTHelpersContract;
kibana_ebt_ui: (context: InheritedFtrProviderContext) => EBTHelpersContract;
apmSynthtraceEsClient: (
@ -63,6 +66,8 @@ export function createTestConfig(
kibanaConfig,
});
const kibanaServer = baseConfig.servers.kibana as UrlObject;
return merge(
{
services: testConfig.get('services'),
@ -74,17 +79,10 @@ export function createTestConfig(
observabilityAIAssistantUI: (context: InheritedFtrProviderContext) =>
ObservabilityAIAssistantUIProvider(context),
observabilityAIAssistantAPIClient: async (context: InheritedFtrProviderContext) => {
const otherUsers = await baseConfig.services.observabilityAIAssistantAPIClient();
return {
...otherUsers,
testUser: createObservabilityAIAssistantApiClient(
supertest(
format({
...(baseConfig.servers.kibana as UrlObject),
auth: `test_user:changeme`,
})
)
),
adminUser: await getScopedApiClient(kibanaServer, 'elastic'),
viewerUser: await getScopedApiClient(kibanaServer, viewerUser.username),
editorUser: await getScopedApiClient(kibanaServer, editorUser.username),
};
},
kibana_ebt_server: KibanaEBTServerProvider,

View file

@ -6,17 +6,16 @@
*/
import type { PathsOf, TypeAsArgs, TypeOf } from '@kbn/typed-react-router-config';
import { kbnTestConfig } from '@kbn/test';
import type { ObservabilityAIAssistantRoutes } from '@kbn/observability-ai-assistant-app-plugin/public/routes/config';
import qs from 'query-string';
import type { Role } from '@kbn/security-plugin-types-common';
import { OBSERVABILITY_AI_ASSISTANT_FEATURE_ID } from '@kbn/observability-ai-assistant-plugin/common/feature';
import { APM_SERVER_FEATURE_ID } from '@kbn/apm-plugin/server';
import { User } from '../../../observability_ai_assistant_api_integration/common/users/users';
import type { InheritedFtrProviderContext } from '../../ftr_provider_context';
export interface ObservabilityAIAssistantUIService {
pages: typeof pages;
auth: {
login: () => Promise<void>;
login: (username: User['username']) => Promise<void>;
logout: () => Promise<void>;
};
router: {
@ -54,42 +53,20 @@ export async function ObservabilityAIAssistantUIProvider({
getPageObjects,
getService,
}: InheritedFtrProviderContext): Promise<ObservabilityAIAssistantUIService> {
const browser = getService('browser');
const deployment = getService('deployment');
const security = getService('security');
const pageObjects = getPageObjects(['common']);
const roleDefinition: Role = {
name: 'observability-ai-assistant-functional-test-role',
elasticsearch: {
cluster: [],
indices: [],
run_as: [],
},
kibana: [
{
spaces: ['*'],
base: [],
feature: {
actions: ['all'],
[APM_SERVER_FEATURE_ID]: ['all'],
[OBSERVABILITY_AI_ASSISTANT_FEATURE_ID]: ['all'],
},
},
],
};
const pageObjects = getPageObjects(['common', 'security']);
return {
pages,
auth: {
login: async () => {
await browser.navigateTo(deployment.getHostPort());
await security.role.create(roleDefinition.name, roleDefinition);
await security.testUser.setRoles([roleDefinition.name, 'apm_user', 'viewer']); // performs a page reload
login: async (username: string) => {
const { password } = kbnTestConfig.getUrlParts();
await pageObjects.security.login(username, password, {
expectSpaceSelector: false,
});
},
logout: async () => {
await security.role.delete(roleDefinition.name);
await security.testUser.restoreDefaults();
await pageObjects.security.forceLogout();
},
},
router: {

View file

@ -101,7 +101,7 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte
await Promise.all([
createSynthtraceErrors(), // create synthtrace
ui.auth.login(), // login
ui.auth.login('editor'), // login
]);
});

View file

@ -31,17 +31,17 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte
const toasts = getService('toasts');
const { header } = getPageObjects(['header', 'common']);
const { header } = getPageObjects(['header', 'security']);
const flyoutService = getService('flyout');
async function deleteConversations() {
const response = await observabilityAIAssistantAPIClient.testUser({
const response = await observabilityAIAssistantAPIClient.editorUser({
endpoint: 'POST /internal/observability_ai_assistant/conversations',
});
for (const conversation of response.body.conversations) {
await observabilityAIAssistantAPIClient.testUser({
await observabilityAIAssistantAPIClient.editorUser({
endpoint: `DELETE /internal/observability_ai_assistant/conversation/{conversationId}`,
params: {
path: {
@ -53,7 +53,7 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte
}
async function deleteConnectors() {
const response = await observabilityAIAssistantAPIClient.testUser({
const response = await observabilityAIAssistantAPIClient.editorUser({
endpoint: 'GET /internal/observability_ai_assistant/connectors',
});
@ -66,7 +66,7 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte
}
async function createOldConversation() {
await observabilityAIAssistantAPIClient.testUser({
await observabilityAIAssistantAPIClient.editorUser({
endpoint: 'POST /internal/observability_ai_assistant/conversation',
params: {
body: {
@ -150,7 +150,7 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte
proxy = await createLlmProxy(log);
await ui.auth.login();
await ui.auth.login('editor');
await ui.router.goto('/conversations/new', { path: {}, query: {} });
});
@ -204,7 +204,7 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte
});
it('creates a connector', async () => {
const response = await observabilityAIAssistantAPIClient.testUser({
const response = await observabilityAIAssistantAPIClient.editorUser({
endpoint: 'GET /internal/observability_ai_assistant/connectors',
});
@ -264,7 +264,7 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte
});
it('creates a conversation and updates the URL', async () => {
const response = await observabilityAIAssistantAPIClient.testUser({
const response = await observabilityAIAssistantAPIClient.editorUser({
endpoint: 'POST /internal/observability_ai_assistant/conversations',
});
@ -331,7 +331,7 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte
});
it('does not create another conversation', async () => {
const response = await observabilityAIAssistantAPIClient.testUser({
const response = await observabilityAIAssistantAPIClient.editorUser({
endpoint: 'POST /internal/observability_ai_assistant/conversations',
});
@ -339,7 +339,7 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte
});
it('appends to the existing one', async () => {
const response = await observabilityAIAssistantAPIClient.testUser({
const response = await observabilityAIAssistantAPIClient.editorUser({
endpoint: 'POST /internal/observability_ai_assistant/conversations',
});
@ -398,7 +398,7 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte
expect(conversation.conversation.title).to.eql('My title');
expect(conversation.namespace).to.eql('default');
expect(conversation.public).to.eql(false);
expect(conversation.user?.name).to.eql('test_user');
expect(conversation.user?.name).to.eql('editor');
const { messages } = conversation;
@ -475,7 +475,7 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte
expect(conversation.conversation.title).to.eql('My old conversation');
expect(conversation.namespace).to.eql('default');
expect(conversation.public).to.eql(false);
expect(conversation.user?.name).to.eql('test_user');
expect(conversation.user?.name).to.eql('editor');
const { messages } = conversation;

View file

@ -7,6 +7,7 @@
import globby from 'globby';
import path from 'path';
import { createUsersAndRoles } from '../../observability_ai_assistant_api_integration/common/users/create_users_and_roles';
import { FtrProviderContext } from '../../observability_ai_assistant_api_integration/common/ftr_provider_context';
const cwd = path.join(__dirname);
@ -19,6 +20,11 @@ export default function observabilityAIAssistantFunctionalTests({
const filePattern = '**/*.spec.ts';
const tests = globby.sync(filePattern, { cwd });
// Creates roles and users before running tests
before(async () => {
await createUsersAndRoles(getService);
});
tests.forEach((testName) => {
describe(testName, () => {
loadTestFile(require.resolve(`./${testName}`));