Skip to content
This repository has been archived by the owner on Feb 15, 2025. It is now read-only.

Commit

Permalink
Merge remote-tracking branch 'origin/main' into 750-epic-ironbank-lea…
Browse files Browse the repository at this point in the history
…pfrogai-hardening
  • Loading branch information
justinthelaw committed Aug 16, 2024
2 parents e066ac3 + b83ef04 commit 3768f78
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 25 deletions.
2 changes: 1 addition & 1 deletion src/leapfrogai_api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ dependencies = [
"python-multipart >= 0.0.7", #indirect dep of FastAPI to receive form data for file uploads
"watchfiles >= 0.21.0",
"leapfrogai_sdk",
"supabase >= 2.5.1",
"supabase == 2.6.0",
"langchain >= 0.2.1",
"langchain-community >= 0.2.1",
"unstructured[md,xlsx,pptx] >= 0.15.3", # Only specify necessary filetypes to prevent package bloat (e.g. 130MB vs 6GB)
Expand Down
2 changes: 1 addition & 1 deletion src/leapfrogai_ui/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ LEAPFROGAI_API_BASE_URL=https://leapfrogai-api.uds.dev #for OpenAI it would be:
SUPABASE_AUTH_EXTERNAL_KEYCLOAK_URL=https://sso.uds.dev/realms/uds
SUPABASE_AUTH_KEYCLOAK_CLIENT_ID=uds-supabase
SUPABASE_AUTH_KEYCLOAK_SECRET=<secret>
ORIGIN=http://localhost:5137
#ORIGIN=http://localhost:3000 # set if running in Docker locally (variable is also used in deployment)

#If specified, app will use OpenAI instead of Leapfrog
OPENAI_API_KEY=
Expand Down
33 changes: 32 additions & 1 deletion src/leapfrogai_ui/src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,35 @@ const authGuard: Handle = async ({ event, resolve }) => {
return resolve(event);
};

export const handle: Handle = sequence(supabase, authGuard);
const csp: Handle = async ({ event, resolve }) => {
const response = await resolve(event);
const directives = {
'default-src': ["'none'"],
'base-uri': ["'self'"],
'object-src': ["'none'"], // typically used for legacy content, such as Flash files or Java applets
'style-src': ["'self'", "'unsafe-inline'"],
'font-src': ["'self'"],
'manifest-src': ["'self'"],
'img-src': ["'self'", `data: 'self' ${process.env.PUBLIC_SUPABASE_URL}`, `blob: 'self'`],
'media-src': ["'self'"],
'form-action': ["'self'"],
'connect-src': [
"'self'",
process.env.LEAPFROGAI_API_BASE_URL,
process.env.PUBLIC_SUPABASE_URL,
process.env.SUPABASE_AUTH_EXTERNAL_KEYCLOAK_URL
],
'child-src': ["'none'"], // note - this will break the annotations story and will need to updated to allow the correct resource
'frame-ancestors': ["'none'"]
};

const CSP = Object.entries(directives)
.map(([key, arr]) => key + ' ' + arr.join(' '))
.join('; ');
// We use Sveltekits generated CSP for script-src to get the nonce
const svelteKitGeneratedCSPWithNonce = response.headers.get('Content-Security-Policy');
response.headers.set('Content-Security-Policy', `${CSP}; ${svelteKitGeneratedCSPWithNonce}`);
return response;
};

export const handle: Handle = sequence(csp, supabase, authGuard);
4 changes: 4 additions & 0 deletions src/leapfrogai_ui/src/lib/mocks/file-mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,7 @@ export const mockDeleteCheck = (assistantsToReturn: LFAssistant[]) => {
})
);
};

export const mockDownloadError = (id: string) => {
server.use(http.get(`api/files/${id}`, () => new HttpResponse(null, { status: 500 })));
};
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import LFFileUploadBtn from '$components/LFFileUploadBtn.svelte';
import ConfirmFilesDeleteModal from '$components/modals/ConfirmFilesDeleteModal.svelte';
import { allFilesAndPendingUploads } from '$stores/filesStore';
import { browser } from '$app/environment';
export let data;
Expand Down Expand Up @@ -190,6 +191,41 @@
submit(); //upload all files
};
const handleDownload = async () => {
let currentFilename;
if (browser) {
try {
for (const id of $filesStore.selectedFileManagementFileIds) {
const res = await fetch(`/api/files/${id}`);
if (!res.ok) {
throw new Error(`Failed to fetch file with id ${id}`);
}
currentFilename = $filesStore.files.find((f) => f.id === id)?.filename;
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = currentFilename || `file_${id}`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}
toastStore.addToast({
kind: 'success',
title: `File${$filesStore.selectedFileManagementFileIds.length > 1 ? 's' : ''} Downloaded`
});
filesStore.setSelectedFileManagementFileIds([]); // deselect all
} catch {
toastStore.addToast({
kind: 'error',
title: 'Download Failed',
subtitle: currentFilename && `Download of file ${currentFilename} failed.`
});
}
}
};
afterNavigate(() => {
// Remove files with "uploading" status from store and invalidate the route so files are re-fetched
// when the page is loaded again
Expand Down Expand Up @@ -219,6 +255,7 @@
<div class="h-[42px]">
{#if editMode}
<div in:fade={{ duration: 150 }} class="flex items-center gap-2">
<Button color="blue" on:click={handleDownload}>Download</Button>
{#if deleting}
<Button color="red" disabled>
<Spinner class="me-3" size="4" color="white" />Deleting...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
mockDeleteCheck,
mockDeleteFile,
mockDeleteFileWithDelay,
mockDownloadError,
mockGetFiles
} from '$lib/mocks/file-mocks';
import { beforeEach, vi } from 'vitest';
Expand Down Expand Up @@ -172,6 +173,34 @@ describe('file management', () => {
screen.queryByText(/this will affect the following assistants/i)
).not.toBeInTheDocument();
});

it('displays an error toast when there is an error downloading a file', async () => {
vi.mock('$app/environment', () => ({
browser: true
}));
const toastSpy = vi.spyOn(toastStore, 'addToast');
mockDeleteCheck([]); // no assistants affected
mockGetFiles(files);
for (const file of files) {
mockDownloadError(file.id);
}

const checkbox = screen.getByRole('checkbox', {
name: /select all rows/i
});
await fireEvent.click(checkbox);

const downloadBtn = screen.getByRole('button', { name: /download/i });

await userEvent.click(downloadBtn);
files.forEach(() => {
expect(toastSpy).toHaveBeenCalledWith({
kind: 'error',
title: 'Download Failed',
subtitle: undefined // currentFilename is undefined since we don't get that far
});
});
});
});

// TODO - The API Keys table also uses this pagination logic, but we are only testing it here on the files table
Expand Down
25 changes: 3 additions & 22 deletions src/leapfrogai_ui/svelte.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,10 @@ const config = {
$testUtils: 'testUtils'
},
csp: {
// Remainder of the CSP is set in hooks.server.ts, we partially define here for the nonce generation provided
// by Sveltekit
directives: {
'default-src': ['none'],
'base-uri': ['self'],
'script-src': ['self', 'strict-dynamic'],
'object-src': ['none'], // typically used for legacy content, such as Flash files or Java applets
'style-src': ['self', 'unsafe-inline'],
'font-src': ['self'],
'manifest-src': ['self'],
'img-src': [
'self',
`data: ${process.env.ORIGIN} ${process.env.PUBLIC_SUPABASE_URL}`,
`blob: ${process.env.ORIGIN}`
],
'media-src': ['self'],
'form-action': ['self'],
'connect-src': [
'self',
process.env.LEAPFROGAI_API_BASE_URL || '',
process.env.PUBLIC_SUPABASE_URL || '',
process.env.SUPABASE_AUTH_EXTERNAL_KEYCLOAK_URL || ''
],
'child-src': [`blob: ${process.env.ORIGIN}`],
'frame-ancestors': ['none']
'script-src': ['self', 'strict-dynamic']
}
}
}
Expand Down
26 changes: 26 additions & 0 deletions src/leapfrogai_ui/tests/file-management.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,29 @@ test('it shows toast when there is an error submitting the form', async ({
deleteFixtureFile(filename);
await deleteFileByName(filename, openAIClient);
});

test('it can download a file', async ({ page, openAIClient }) => {
await loadFileManagementPage(page);

const filename = await createPDF();

await uploadFile(page, filename);
await expect(page.getByText(`${filename} imported successfully`)).toBeVisible();
await expect(page.getByText(`${filename} imported successfully`)).not.toBeVisible(); // wait for upload to finish

const row = await getTableRow(page, filename, 'file-management-table');
await row.getByRole('checkbox').check();

const downloadPromise = page.waitForEvent('download');
const downloadBtn = page.getByRole('button', { name: 'Download' });
await downloadBtn.click();
const download = await downloadPromise;

expect(download.suggestedFilename()).toEqual(filename.replace(/:/g, '_'));
await expect(page.getByText('File Downloaded')).toBeVisible();
await expect(downloadBtn).not.toBeVisible(); // all items deselected

// Cleanup
deleteFixtureFile(filename);
await deleteFileByName(filename, openAIClient);
});
4 changes: 4 additions & 0 deletions src/leapfrogai_ui/tests/global.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { delay } from 'msw';
const authFile = 'playwright/.auth/user.json';

setup('authenticate', async ({ page }) => {
page.on('pageerror', (err) => {
console.log(err.message);
});

await page.goto('/'); // go to the home page
await delay(2000); // allow page to fully hydrate
if (process.env.PUBLIC_DISABLE_KEYCLOAK === 'true') {
Expand Down

0 comments on commit 3768f78

Please sign in to comment.