Skip to content

Commit

Permalink
[Obs AI Assistant] Add route privilege tests for serverless (elastic#…
Browse files Browse the repository at this point in the history
  • Loading branch information
viduni94 committed Dec 30, 2024
1 parent 12608c3 commit d934aab
Show file tree
Hide file tree
Showing 10 changed files with 359 additions and 59 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* 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 class ForbiddenApiError extends Error {
status: number;

constructor(message: string = 'Forbidden') {
super(message);
this.name = 'ForbiddenApiError';
this.status = 403;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ export function getObservabilityAIAssistantApiClient({
}
}

type ObservabilityAIAssistantApiClientKey = 'slsAdmin' | 'slsEditor' | 'slsUser';
type ObservabilityAIAssistantApiClientKey =
| 'slsAdmin'
| 'slsEditor'
| 'slsUser'
| 'slsUnauthorized';

export type ObservabilityAIAssistantApiClient = Record<
ObservabilityAIAssistantApiClientKey,
Expand Down Expand Up @@ -195,18 +199,27 @@ export async function getObservabilityAIAssistantApiClientService({
const svlSharedConfig = getService('config');
const roleScopedSupertest = getService('roleScopedSupertest');

// admin user
const supertestAdminWithCookieCredentials: SupertestWithRoleScope =
await roleScopedSupertest.getSupertestWithRoleScope('admin', {
useCookieHeader: true,
withInternalHeaders: true,
});

// editor user
const supertestEditorWithCookieCredentials: SupertestWithRoleScope =
await roleScopedSupertest.getSupertestWithRoleScope('editor', {
useCookieHeader: true,
withInternalHeaders: true,
});

// unauthorized user
const supertestUnauthorizedWithCookieCredentials: SupertestWithRoleScope =
await roleScopedSupertest.getSupertestWithRoleScope('viewer', {
useCookieHeader: true,
withInternalHeaders: false, // No internal headers for unauthorized users
});

return {
// defaults to elastic_admin user when used without auth
slsUser: await getObservabilityAIAssistantApiClient({
Expand All @@ -222,5 +235,9 @@ export async function getObservabilityAIAssistantApiClientService({
svlSharedConfig,
supertestUserWithCookieCredentials: supertestEditorWithCookieCredentials,
}),
slsUnauthorized: await getObservabilityAIAssistantApiClient({
svlSharedConfig,
supertestUserWithCookieCredentials: supertestUnauthorizedWithCookieCredentials,
}),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import { SupertestWithRoleScope } from '@kbn/test-suites-xpack/api_integration/d
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { createProxyActionConnector, deleteActionConnector } from '../../common/action_connectors';
import type { InternalRequestHeader, RoleCredentials } from '../../../../../../shared/services';
import { ForbiddenApiError } from '../../common/forbidden_api_error';

export default function ApiTest({ getService }: FtrProviderContext) {
const supertestWithoutAuth = getService('supertestWithoutAuth');
const svlUserManager = getService('svlUserManager');
const svlCommonApi = getService('svlCommonApi');
const log = getService('log');
const roleScopedSupertest = getService('roleScopedSupertest');
const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient');

let supertestEditorWithCookieCredentials: SupertestWithRoleScope;

Expand Down Expand Up @@ -170,57 +172,26 @@ export default function ApiTest({ getService }: FtrProviderContext) {
]);
});

it.skip('returns a useful error if the request fails', async () => {
const interceptor = proxy.intercept('conversation', () => true);

const passThrough = new PassThrough();

supertestWithoutAuth
.post(CHAT_API_URL)
.set(roleAuthc.apiKeyHeader)
.set(internalReqHeader)
.set('kbn-xsrf', 'foo')
.send({
name: 'my_api_call',
messages,
connectorId,
functions: [],
scopes: ['all'],
})
.expect(200)
.pipe(passThrough);

let data: string = '';

passThrough.on('data', (chunk) => {
data += chunk.toString('utf-8');
describe('security roles and access privileges', () => {
it('should deny access for users without the ai_assistant privilege', async () => {
try {
await observabilityAIAssistantAPIClient.slsUnauthorized({
endpoint: `POST ${CHAT_API_URL}`,
params: {
body: {
name: 'my_api_call',
messages,
connectorId,
functions: [],
scopes: ['all'],
},
},
});
throw new ForbiddenApiError('Expected slsUnauthorized() to throw a 403 Forbidden error');
} catch (e) {
expect(e.status).to.be(403);
}
});

const simulator = await interceptor.waitForIntercept();

await simulator.status(400);

await simulator.rawWrite(
JSON.stringify({
error: {
code: 'context_length_exceeded',
message:
"This model's maximum context length is 8192 tokens. However, your messages resulted in 11036 tokens. Please reduce the length of the messages.",
param: 'messages',
type: 'invalid_request_error',
},
})
);

await simulator.rawEnd();

await new Promise<void>((resolve) => passThrough.on('end', () => resolve()));

const response = JSON.parse(data.trim());

expect(response.error.message).to.be(
`Token limit reached. Token limit is 8192, but the current conversation has 11036 tokens.`
);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
} from '../conversations/helpers';
import { createProxyActionConnector, deleteActionConnector } from '../../common/action_connectors';
import type { InternalRequestHeader, RoleCredentials } from '../../../../../../shared/services';
import { ForbiddenApiError } from '../../common/forbidden_api_error';

export default function ApiTest({ getService }: FtrProviderContext) {
const supertestWithoutAuth = getService('supertestWithoutAuth');
Expand Down Expand Up @@ -547,5 +548,27 @@ export default function ApiTest({ getService }: FtrProviderContext) {

// todo
it.skip('executes a function', async () => {});

describe('security roles and access privileges', () => {
it('should deny access for users without the ai_assistant privilege', async () => {
try {
await observabilityAIAssistantAPIClient.slsUnauthorized({
endpoint: 'POST /internal/observability_ai_assistant/chat/complete',
params: {
body: {
messages,
connectorId,
persist: false,
screenContexts: [],
scopes: ['all'],
},
},
});
throw new ForbiddenApiError('Expected slsUnauthorized() to throw a 403 Forbidden error');
} catch (e) {
expect(e.status).to.be(403);
}
});
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import type {
RoleCredentials,
SupertestWithoutAuthProviderType,
} from '../../../../../../shared/services';
import { ForbiddenApiError } from '../../common/forbidden_api_error';

const CONNECTOR_API_URL = '/internal/observability_ai_assistant/connectors';

export default function ApiTest({ getService }: FtrProviderContext) {
const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient');
Expand Down Expand Up @@ -47,14 +50,14 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('Returns a 2xx for enterprise license', async () => {
await observabilityAIAssistantAPIClient
.slsEditor({
endpoint: 'GET /internal/observability_ai_assistant/connectors',
endpoint: `GET ${CONNECTOR_API_URL}`,
})
.expect(200);
});

it('returns an empty list of connectors', async () => {
const res = await observabilityAIAssistantAPIClient.slsEditor({
endpoint: 'GET /internal/observability_ai_assistant/connectors',
endpoint: `GET ${CONNECTOR_API_URL}`,
});

expect(res.body.length).to.be(0);
Expand All @@ -70,7 +73,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});

const res = await observabilityAIAssistantAPIClient.slsEditor({
endpoint: 'GET /internal/observability_ai_assistant/connectors',
endpoint: `GET ${CONNECTOR_API_URL}`,
});

expect(res.body.length).to.be(1);
Expand All @@ -83,6 +86,19 @@ export default function ApiTest({ getService }: FtrProviderContext) {
roleAuthc,
});
});

describe('security roles and access privileges', () => {
it('should deny access for users without the ai_assistant privilege', async () => {
try {
await observabilityAIAssistantAPIClient.slsUnauthorized({
endpoint: `GET ${CONNECTOR_API_URL}`,
});
throw new ForbiddenApiError('Expected slsUnauthorized() to throw a 403 Forbidden error');
} catch (e) {
expect(e.status).to.be(403);
}
});
});
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '@kbn/observability-ai-assistant-plugin/common/types';
import type { FtrProviderContext } from '../../common/ftr_provider_context';
import type { SupertestReturnType } from '../../common/observability_ai_assistant_api_client';
import { ForbiddenApiError } from '../../common/forbidden_api_error';

export default function ApiTest({ getService }: FtrProviderContext) {
const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient');
Expand Down Expand Up @@ -253,5 +254,128 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
});
});

describe('security roles and access privileges', () => {
describe('should deny access for users without the ai_assistant privilege', () => {
let createResponse: Awaited<
SupertestReturnType<'POST /internal/observability_ai_assistant/conversation'>
>;
before(async () => {
createResponse = await observabilityAIAssistantAPIClient
.slsEditor({
endpoint: 'POST /internal/observability_ai_assistant/conversation',
params: {
body: {
conversation: conversationCreate,
},
},
})
.expect(200);
});

after(async () => {
await observabilityAIAssistantAPIClient
.slsEditor({
endpoint: 'DELETE /internal/observability_ai_assistant/conversation/{conversationId}',
params: {
path: {
conversationId: createResponse.body.conversation.id,
},
},
})
.expect(200);
});

it('POST /internal/observability_ai_assistant/conversation', async () => {
try {
await observabilityAIAssistantAPIClient.slsUnauthorized({
endpoint: 'POST /internal/observability_ai_assistant/conversation',
params: {
body: {
conversation: conversationCreate,
},
},
});
// throw new ForbiddenApiError(
// 'Expected slsUnauthorized() to throw a 403 Forbidden error'
// );
} catch (e) {
expect(e.status).to.be(403);
}
});

it('POST /internal/observability_ai_assistant/conversations', async () => {
try {
await observabilityAIAssistantAPIClient.slsUnauthorized({
endpoint: 'POST /internal/observability_ai_assistant/conversations',
});
// throw new ForbiddenApiError(
// 'Expected slsUnauthorized() to throw a 403 Forbidden error'
// );
} catch (e) {
expect(e.status).to.be(403);
}
});

it('PUT /internal/observability_ai_assistant/conversation/{conversationId}', async () => {
try {
await observabilityAIAssistantAPIClient.slsUnauthorized({
endpoint: 'PUT /internal/observability_ai_assistant/conversation/{conversationId}',
params: {
path: {
conversationId: createResponse.body.conversation.id,
},
body: {
conversation: merge(omit(conversationUpdate, 'conversation.id'), {
conversation: { id: createResponse.body.conversation.id },
}),
},
},
});
// throw new ForbiddenApiError(
// 'Expected slsUnauthorized() to throw a 403 Forbidden error'
// );
} catch (e) {
expect(e.status).to.be(403);
}
});

it('GET /internal/observability_ai_assistant/conversation/{conversationId}', async () => {
try {
await observabilityAIAssistantAPIClient.slsUnauthorized({
endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}',
params: {
path: {
conversationId: createResponse.body.conversation.id,
},
},
});
// throw new ForbiddenApiError(
// 'Expected slsUnauthorized() to throw a 403 Forbidden error'
// );
} catch (e) {
expect(e.status).to.be(403);
}
});

it('DELETE /internal/observability_ai_assistant/conversation/{conversationId}', async () => {
try {
await observabilityAIAssistantAPIClient.slsUnauthorized({
endpoint: 'DELETE /internal/observability_ai_assistant/conversation/{conversationId}',
params: {
path: {
conversationId: createResponse.body.conversation.id,
},
},
});
throw new ForbiddenApiError(
'Expected slsUnauthorized() to throw a 403 Forbidden error'
);
} catch (e) {
expect(e.status).to.be(403);
}
});
});
});
});
}
Loading

0 comments on commit d934aab

Please sign in to comment.