From 49c5b2a54160bf5467fc0949cf95a69760fa3ad2 Mon Sep 17 00:00:00 2001
From: thiloho <123883702+thiloho@users.noreply.github.com>
Date: Sun, 1 Sep 2024 16:51:21 +0200
Subject: [PATCH] Run specific test suites in serial matter and restrict file
uploads
---
...0240901135831_restrict_file_operations.sql | 76 +++++++++++++++++++
web-app/.gitignore | 3 +
.../src/routes/(authenticated)/+page.svelte | 16 +++-
.../website/[websiteId]/articles/+page.svelte | 16 +++-
web-app/tests/website.spec.ts | 8 +-
5 files changed, 109 insertions(+), 10 deletions(-)
create mode 100644 rest-api/db/migrations/20240901135831_restrict_file_operations.sql
diff --git a/rest-api/db/migrations/20240901135831_restrict_file_operations.sql b/rest-api/db/migrations/20240901135831_restrict_file_operations.sql
new file mode 100644
index 0000000..98ffa23
--- /dev/null
+++ b/rest-api/db/migrations/20240901135831_restrict_file_operations.sql
@@ -0,0 +1,76 @@
+-- migrate:up
+CREATE OR REPLACE FUNCTION api.upload_file (BYTEA, OUT file_id UUID)
+AS $$
+DECLARE
+ _headers JSON := CURRENT_SETTING('request.headers', TRUE)::JSON;
+ _website_id UUID := (_headers ->> 'x-website-id')::UUID;
+ _mimetype TEXT := _headers ->> 'x-mimetype';
+ _original_filename TEXT := _headers ->> 'x-original-filename';
+ _allowed_mimetypes TEXT[] := ARRAY['image/png', 'image/jpeg', 'image/webp'];
+ _max_file_size INT := 5 * 1024 * 1024;
+ _has_access BOOLEAN;
+BEGIN
+ _has_access = internal.user_has_website_access (_website_id, 20);
+ IF OCTET_LENGTH($1) = 0 THEN
+ RAISE invalid_parameter_value
+ USING message = 'No file data was provided';
+ END IF;
+ IF _mimetype IS NULL OR _mimetype NOT IN (
+ SELECT
+ UNNEST(_allowed_mimetypes)) THEN
+ RAISE invalid_parameter_value
+ USING message = 'Invalid MIME type. Allowed types are: png, jpg, webp';
+ END IF;
+ IF OCTET_LENGTH($1) > _max_file_size THEN
+ RAISE program_limit_exceeded
+ USING message = FORMAT('File size exceeds the maximum limit of %s MB', _max_file_size / (1024 * 1024));
+ END IF;
+ INSERT INTO internal.media (website_id, blob, mimetype, original_name)
+ VALUES (_website_id, $1, _mimetype, _original_filename)
+ RETURNING
+ id INTO file_id;
+END;
+$$
+LANGUAGE plpgsql
+SECURITY DEFINER;
+
+GRANT EXECUTE ON FUNCTION api.upload_file (BYTEA) TO authenticated_user;
+
+-- migrate:down
+DROP FUNCTION api.upload_file (BYTEA);
+
+CREATE FUNCTION api.upload_file (BYTEA, OUT file_id UUID)
+AS $$
+DECLARE
+ _headers JSON := CURRENT_SETTING('request.headers', TRUE)::JSON;
+ _website_id UUID := (_headers ->> 'x-website-id')::UUID;
+ _mimetype TEXT := _headers ->> 'x-mimetype';
+ _original_filename TEXT := _headers ->> 'x-original-filename';
+ _allowed_mimetypes TEXT[] := ARRAY['image/png', 'image/jpeg', 'image/webp'];
+ _max_file_size INT := 5 * 1024 * 1024;
+BEGIN
+ IF OCTET_LENGTH($1) = 0 THEN
+ RAISE invalid_parameter_value
+ USING message = 'No file data was provided';
+ END IF;
+ IF _mimetype IS NULL OR _mimetype NOT IN (
+ SELECT
+ UNNEST(_allowed_mimetypes)) THEN
+ RAISE invalid_parameter_value
+ USING message = 'Invalid MIME type. Allowed types are: png, jpg, webp';
+ END IF;
+ IF OCTET_LENGTH($1) > _max_file_size THEN
+ RAISE program_limit_exceeded
+ USING message = FORMAT('File size exceeds the maximum limit of %s MB', _max_file_size / (1024 * 1024));
+ END IF;
+ INSERT INTO internal.media (website_id, blob, mimetype, original_name)
+ VALUES (_website_id, $1, _mimetype, _original_filename)
+ RETURNING
+ id INTO file_id;
+END;
+$$
+LANGUAGE plpgsql
+SECURITY DEFINER;
+
+GRANT EXECUTE ON FUNCTION api.upload_file (BYTEA) TO authenticated_user;
+
diff --git a/web-app/.gitignore b/web-app/.gitignore
index 79518f7..b549aa9 100644
--- a/web-app/.gitignore
+++ b/web-app/.gitignore
@@ -19,3 +19,6 @@ Thumbs.db
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+
+# Playwright
+test-results
\ No newline at end of file
diff --git a/web-app/src/routes/(authenticated)/+page.svelte b/web-app/src/routes/(authenticated)/+page.svelte
index cff2465..7ac662b 100644
--- a/web-app/src/routes/(authenticated)/+page.svelte
+++ b/web-app/src/routes/(authenticated)/+page.svelte
@@ -77,9 +77,19 @@
diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/articles/+page.svelte b/web-app/src/routes/(authenticated)/website/[websiteId]/articles/+page.svelte
index ba790d9..23d3e12 100644
--- a/web-app/src/routes/(authenticated)/website/[websiteId]/articles/+page.svelte
+++ b/web-app/src/routes/(authenticated)/website/[websiteId]/articles/+page.svelte
@@ -66,9 +66,19 @@
diff --git a/web-app/tests/website.spec.ts b/web-app/tests/website.spec.ts
index 58557ff..2c3916a 100644
--- a/web-app/tests/website.spec.ts
+++ b/web-app/tests/website.spec.ts
@@ -125,7 +125,7 @@ test.describe("Blog", () => {
});
});
- test.describe("Articles", () => {
+ test.describe.serial("Articles", () => {
test("Create article", async ({ authenticatedPage: page }) => {
await page.getByRole("link", { name: "Blog" }).click();
await page.getByRole("link", { name: "Articles" }).click();
@@ -171,7 +171,7 @@ test.describe("Blog", () => {
});
});
- test.describe("Collaborators", () => {
+ test.describe.serial("Collaborators", () => {
test("Add collaborator", async ({ authenticatedPage: page }) => {
await page.getByRole("link", { name: "Blog" }).click();
await page.getByRole("link", { name: "Collaborators" }).click();
@@ -202,7 +202,7 @@ test.describe("Blog", () => {
});
test.describe("Docs", () => {
- test.describe("Categories", () => {
+ test.describe.serial("Categories", () => {
test("Create category", async ({ authenticatedPage: page }) => {
await page.getByRole("link", { name: "Documentation" }).click();
await page.getByRole("link", { name: "Categories" }).click();
@@ -299,7 +299,7 @@ test("Delete websites", async ({ authenticatedPage: page }) => {
await expect(page.getByRole("link", { name: "All websites" })).toBeHidden();
});
-test("Delete account", async ({ authenticatedPage: page }) => {
+test("Delete accounts", async ({ authenticatedPage: page }) => {
await page.getByRole("link", { name: "Account" }).click();
await page.getByRole("button", { name: "Delete account" }).click();
await page.getByLabel("Password:").click();