Skip to content

Commit

Permalink
add permission support in lightspeed-backend
Browse files Browse the repository at this point in the history
Signed-off-by: Karthik <[email protected]>
  • Loading branch information
karthikjeeyar committed Nov 22, 2024
1 parent 1967890 commit 2dc7f6d
Show file tree
Hide file tree
Showing 8 changed files with 292 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
"backstage": {
"role": "backend-plugin",
"pluginId": "lightspeed",
"pluginPackage": "@red-hat-developer-hub/backstage-plugin-lightspeed-backend",
"pluginPackages": [
"@red-hat-developer-hub/backstage-plugin-lightspeed",
"@red-hat-developer-hub/backstage-plugin-lightspeed-backend"
"@red-hat-developer-hub/backstage-plugin-lightspeed-backend",
"@red-hat-developer-hub/backstage-plugin-lightspeed-common"
],
"supported-versions": "1.32.5"
},
Expand Down Expand Up @@ -44,8 +46,12 @@
"dependencies": {
"@backstage/backend-defaults": "^0.5.2",
"@backstage/backend-plugin-api": "^1.0.1",
"@backstage/errors": "^1.2.4",
"@backstage/plugin-permission-common": "^0.8.1",
"@backstage/plugin-permission-node": "^0.8.5",
"@langchain/core": "^0.2.30",
"@langchain/openai": "^0.2.8",
"@red-hat-developer-hub/backstage-plugin-lightspeed-common": "workspace:^",
"express": "^4.21.1",
"http-proxy-middleware": "^3.0.2"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,16 @@ export const lightspeedPlugin = createBackendPlugin({
http: coreServices.httpRouter,
httpAuth: coreServices.httpAuth,
userInfo: coreServices.userInfo,
permissions: coreServices.permissions,
},
async init({ logger, config, http, httpAuth, userInfo }) {
async init({ logger, config, http, httpAuth, userInfo, permissions }) {
http.use(
await createRouter({
config: config,
logger: logger,
httpAuth: httpAuth,
userInfo: userInfo,
permissions,
}),
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { mockCredentials } from '@backstage/backend-test-utils';
import {
AuthorizeResult,
createPermission,
} from '@backstage/plugin-permission-common';

import { userPermissionAuthorization } from './permission';

describe('userPermissionAuthorization', () => {
let mockPermissionsService: any;
const mockPermission = createPermission({
name: 'test.permission',
attributes: { action: 'read' },
});

beforeEach(() => {
mockPermissionsService = {
authorize: jest.fn(),
};
});

it('should not throw NotAllowedError', async () => {
mockPermissionsService.authorize.mockResolvedValueOnce([
{ result: AuthorizeResult.ALLOW },
]);

const { authorizeUser } = userPermissionAuthorization(
mockPermissionsService,
);

const result = await authorizeUser(mockPermission, mockCredentials.user());
expect(() => result).not.toThrow();
});

it('should throw NotAllowedError when authorization is denied', async () => {
mockPermissionsService.authorize.mockResolvedValueOnce([
{ result: AuthorizeResult.DENY },
]);

const { authorizeUser } = userPermissionAuthorization(
mockPermissionsService,
);

await expect(
authorizeUser(mockPermission, mockCredentials.user()),
).rejects.toThrow('Unauthorized');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
BackstageCredentials,
PermissionsService,
} from '@backstage/backend-plugin-api';
import { NotAllowedError } from '@backstage/errors';
import {
AuthorizeResult,
BasicPermission,
} from '@backstage/plugin-permission-common';

export function userPermissionAuthorization(
permissionsService: PermissionsService,
) {
const permissions = permissionsService;

return {
async authorizeUser(
permission: BasicPermission,
credentials: BackstageCredentials,
): Promise<void> {
const decision = (
await permissions.authorize(
[
{
permission,
},
],
{ credentials },
)
)[0];

if (decision.result !== AuthorizeResult.ALLOW) {
throw new NotAllowedError('Unauthorized');
}
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
mockServices,
startTestBackend,
} from '@backstage/backend-test-utils';
import { AuthorizeResult } from '@backstage/plugin-permission-common';

import { AIMessage, HumanMessage } from '@langchain/core/messages';
import { ChatPromptTemplate } from '@langchain/core/prompts';
Expand Down Expand Up @@ -129,7 +130,10 @@ describe('lightspeed router tests', () => {
server.resetHandlers();
});

async function startBackendServer(config?: Record<PropertyKey, unknown>) {
async function startBackendServer(
config?: Record<PropertyKey, unknown>,
authorizeResult?: AuthorizeResult.DENY | AuthorizeResult.ALLOW,
) {
const features: (BackendFeature | Promise<{ default: BackendFeature }>)[] =
[
lightspeedPlugin,
Expand All @@ -140,6 +144,11 @@ describe('lightspeed router tests', () => {
mockServices.httpAuth.factory({
defaultCredentials: mockCredentials.user(mockUserId),
}),
mockServices.permissions.mock({
authorize: async () => [
{ result: authorizeResult ?? AuthorizeResult.ALLOW },
],
}).factory,
mockServices.userInfo.factory(),
];
return (await startTestBackend({ features })).server;
Expand Down Expand Up @@ -182,6 +191,14 @@ describe('lightspeed router tests', () => {
});

describe('POST /conversations', () => {
it('should fail with unauthorized error while creating new conversation_id', async () => {
const backendServer = await startBackendServer({}, AuthorizeResult.DENY);
const response = await request(backendServer).post(
`/api/lightspeed/conversations`,
);

expect(response.statusCode).toEqual(403);
});
it('generate new conversation_id', async () => {
const backendServer = await startBackendServer();
const response = await request(backendServer).post(
Expand Down Expand Up @@ -244,6 +261,14 @@ describe('lightspeed router tests', () => {
expect(responseData[1].kwargs?.response_metadata.model).toBe(mockModel);
});

it('should fail with unauthorized error while fetching conversation history', async () => {
const backendServer = await startBackendServer({}, AuthorizeResult.DENY);
const response = await request(backendServer).get(
`/api/lightspeed/conversations/${encodedConversationId}`,
);
expect(response.statusCode).toEqual(403);
});

it('delete history', async () => {
// delete request
const backendServer = await startBackendServer();
Expand All @@ -253,6 +278,15 @@ describe('lightspeed router tests', () => {
expect(deleteResponse.statusCode).toEqual(200);
});

it('should fail with unauthorized error while deleting a conversation history', async () => {
// delete request
const backendServer = await startBackendServer({}, AuthorizeResult.DENY);
const deleteResponse = await request(backendServer).delete(
`/api/lightspeed/conversations/${encodedConversationId}`,
);
expect(deleteResponse.statusCode).toEqual(403);
});

it('load history with deleted conversation_id', async () => {
await deleteHistory(mockConversationId);
const backendServer = await startBackendServer();
Expand Down Expand Up @@ -344,6 +378,19 @@ describe('lightspeed router tests', () => {
expect(receivedData).toEqual(expectedData);
});

it('should fail with unauthorized error in chat completion API', async () => {
const backendServer = await startBackendServer({}, AuthorizeResult.DENY);
const chatCompletionResponse = await request(backendServer)
.post('/api/lightspeed/v1/query')
.send({
model: mockModel,
conversation_id: mockConversationId,
query: 'Hello',
serverURL: LOCAL_AI_ADDR,
});
expect(chatCompletionResponse.statusCode).toEqual(403);
});

it('should not have any history for the initial conversation', async () => {
const mockStream = jest.fn().mockImplementation(async function* stream() {
yield { content: 'Chunk 1', response_metadata: {} };
Expand Down
Loading

0 comments on commit 2dc7f6d

Please sign in to comment.