Skip to content

Commit

Permalink
[Obs AI Assistant] Add scoped privileges to readUser and writeUser (e…
Browse files Browse the repository at this point in the history
  • Loading branch information
sorenlouv authored May 22, 2024
1 parent b8777aa commit 97de948
Show file tree
Hide file tree
Showing 14 changed files with 210 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,32 @@
*/

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;
license: 'basic' | 'trial';
kibanaConfig?: Record<string, any>;
}

async function getObservabilityAIAssistantAPIClient(kibanaServerUrl: string) {
return createObservabilityAIAssistantApiClient(supertest(kibanaServerUrl));
}

export type CreateTestConfig = ReturnType<typeof createTestConfig>;

export interface CreateTest {
testFiles: string[];
servers: any;
services: InheritedServices & {
observabilityAIAssistantAPIClient: () => Promise<{
readUser: ObservabilityAIAssistantAPIClient;
writeUser: ObservabilityAIAssistantAPIClient;
adminUser: ObservabilityAIAssistantAPIClient;
viewerUser: ObservabilityAIAssistantAPIClient;
editorUser: ObservabilityAIAssistantAPIClient;
}>;
};
junit: { reportName: string };
Expand All @@ -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'> = {
Expand All @@ -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),
};
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>(
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
});
})
);
}
Original file line number Diff line number Diff line change
@@ -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 = [];
Original file line number Diff line number Diff line change
@@ -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];
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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: {
Expand All @@ -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: {
Expand All @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});

Expand All @@ -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',
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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: {
Expand All @@ -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: {
Expand All @@ -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: {
Expand All @@ -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: {
Expand All @@ -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: {
Expand All @@ -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: {
Expand All @@ -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: {
Expand All @@ -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: {
Expand All @@ -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);
Expand All @@ -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: {
Expand All @@ -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: {
Expand Down
Loading

0 comments on commit 97de948

Please sign in to comment.