From 837729c83c376e6a71e3e4f53a6877f438624adc Mon Sep 17 00:00:00 2001 From: thiloho <123883702+thiloho@users.noreply.github.com> Date: Thu, 8 Aug 2024 16:30:01 +0200 Subject: [PATCH] Update policies for collaborators for stricter rules --- .../migrations/20240719071602_main_tables.sql | 18 +++---- ...240720074103_user_management_roles_jwt.sql | 2 +- ...20240720132802_exposed_views_functions.sql | 2 +- .../20240724191017_row_level_security.sql | 51 +++++++++++++++++-- .../20240803163047_website_overview_view.sql | 2 +- .../20240808141708_collaborator_not_owner.sql | 24 +++++++++ .../[websiteId]/publish/+page.server.ts | 10 +--- .../website/[websiteId]/publish/+page.svelte | 2 +- 8 files changed, 85 insertions(+), 26 deletions(-) create mode 100644 rest-api/db/migrations/20240808141708_collaborator_not_owner.sql diff --git a/rest-api/db/migrations/20240719071602_main_tables.sql b/rest-api/db/migrations/20240719071602_main_tables.sql index 67cc7f5..e7a5a32 100644 --- a/rest-api/db/migrations/20240719071602_main_tables.sql +++ b/rest-api/db/migrations/20240719071602_main_tables.sql @@ -22,9 +22,9 @@ CREATE TABLE internal.user ( CREATE TABLE internal.website ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - owner_id UUID REFERENCES internal.user(id) ON DELETE CASCADE NOT NULL DEFAULT (current_setting('request.jwt.claims', true)::JSON->>'user_id')::UUID, + user_id UUID REFERENCES internal.user(id) ON DELETE CASCADE NOT NULL DEFAULT (current_setting('request.jwt.claims', true)::JSON->>'user_id')::UUID, content_type VARCHAR(10) CHECK (content_type IN ('Blog', 'Docs')) NOT NULL, - title VARCHAR(50) NOT NULL CHECK (trim(title) <> ''), + title VARCHAR(50) NOT NULL CHECK (trim(title) != ''), created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_by UUID REFERENCES internal.user(id) ON DELETE SET NULL @@ -56,14 +56,14 @@ CREATE TABLE internal.header ( last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_by UUID REFERENCES internal.user(id) ON DELETE SET NULL, CONSTRAINT logo_content_check CHECK ( - (logo_type = 'text' AND logo_text IS NOT NULL AND trim(logo_text) <> '') OR + (logo_type = 'text' AND logo_text IS NOT NULL AND trim(logo_text) != '') OR (logo_type = 'image' AND logo_image IS NOT NULL) ) ); CREATE TABLE internal.home ( website_id UUID PRIMARY KEY REFERENCES internal.website(id) ON DELETE CASCADE, - main_content TEXT NOT NULL CHECK (trim(main_content) <> ''), + main_content TEXT NOT NULL CHECK (trim(main_content) != ''), last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_by UUID REFERENCES internal.user(id) ON DELETE SET NULL ); @@ -72,12 +72,12 @@ CREATE TABLE internal.article ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), website_id UUID REFERENCES internal.website(id) ON DELETE CASCADE NOT NULL, user_id UUID REFERENCES internal.user(id) ON DELETE SET NULL, - title VARCHAR(100) NOT NULL CHECK (trim(title) <> ''), - meta_description VARCHAR(250) CHECK (trim(meta_description) <> ''), - meta_author VARCHAR(100) CHECK (trim(meta_author) <> ''), + title VARCHAR(100) NOT NULL CHECK (trim(title) != ''), + meta_description VARCHAR(250) CHECK (trim(meta_description) != ''), + meta_author VARCHAR(100) CHECK (trim(meta_author) != ''), cover_image UUID REFERENCES internal.media(id) ON DELETE SET NULL, publication_date DATE NOT NULL DEFAULT CURRENT_DATE, - main_content TEXT CHECK (trim(main_content) <> ''), + main_content TEXT CHECK (trim(main_content) != ''), created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_by UUID REFERENCES internal.user(id) ON DELETE SET NULL @@ -85,7 +85,7 @@ CREATE TABLE internal.article ( CREATE TABLE internal.footer ( website_id UUID PRIMARY KEY REFERENCES internal.website(id) ON DELETE CASCADE, - additional_text VARCHAR(250) NOT NULL CHECK (trim(additional_text) <> ''), + additional_text VARCHAR(250) NOT NULL CHECK (trim(additional_text) != ''), last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_by UUID REFERENCES internal.user(id) ON DELETE SET NULL ); diff --git a/rest-api/db/migrations/20240720074103_user_management_roles_jwt.sql b/rest-api/db/migrations/20240720074103_user_management_roles_jwt.sql index 1b4bed5..fe906e9 100644 --- a/rest-api/db/migrations/20240720074103_user_management_roles_jwt.sql +++ b/rest-api/db/migrations/20240720074103_user_management_roles_jwt.sql @@ -23,7 +23,7 @@ EXECUTE FUNCTION internal.check_role_exists(); CREATE FUNCTION internal.encrypt_pass() RETURNS TRIGGER AS $$ BEGIN - IF TG_OP = 'INSERT' OR NEW.password_hash <> OLD.password_hash THEN + IF TG_OP = 'INSERT' OR NEW.password_hash != OLD.password_hash THEN NEW.password_hash = crypt(NEW.password_hash, gen_salt('bf')); END IF; RETURN NEW; diff --git a/rest-api/db/migrations/20240720132802_exposed_views_functions.sql b/rest-api/db/migrations/20240720132802_exposed_views_functions.sql index 3145115..36635e7 100644 --- a/rest-api/db/migrations/20240720132802_exposed_views_functions.sql +++ b/rest-api/db/migrations/20240720132802_exposed_views_functions.sql @@ -17,7 +17,7 @@ WITH (security_invoker = on) AS SELECT id, - owner_id, + user_id, content_type, title, created_at, diff --git a/rest-api/db/migrations/20240724191017_row_level_security.sql b/rest-api/db/migrations/20240724191017_row_level_security.sql index e4e5f48..945df50 100644 --- a/rest-api/db/migrations/20240724191017_row_level_security.sql +++ b/rest-api/db/migrations/20240724191017_row_level_security.sql @@ -20,7 +20,7 @@ BEGIN SELECT EXISTS ( SELECT 1 FROM internal.website - WHERE id = website_id AND owner_id = _user_id + WHERE id = website_id AND user_id = _user_id ) INTO _has_access; IF _has_access THEN @@ -103,7 +103,15 @@ USING (internal.user_has_website_access(website_id, 20)); CREATE POLICY delete_article ON internal.article FOR DELETE -USING (internal.user_has_website_access(website_id, 30)); +USING ( + internal.user_has_website_access(website_id, 30) + OR + ( + internal.user_has_website_access(website_id, 20) + AND + user_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID + ) +); CREATE POLICY insert_article ON internal.article FOR INSERT @@ -125,15 +133,48 @@ USING (internal.user_has_website_access(website_id, 10)); CREATE POLICY insert_collaborations ON internal.collab FOR INSERT -WITH CHECK (internal.user_has_website_access(website_id, 30)); +WITH CHECK ( + CASE + WHEN internal.user_has_website_access(website_id, 40) THEN + true + WHEN internal.user_has_website_access(website_id, 30) THEN + (user_id != (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID) + AND + (permission_level < 30) + ELSE + false + END +); CREATE POLICY update_collaborations ON internal.collab FOR UPDATE -USING (internal.user_has_website_access(website_id, 30)); +USING ( + CASE + WHEN internal.user_has_website_access(website_id, 40) THEN + true + WHEN internal.user_has_website_access(website_id, 30) THEN + (user_id != (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID) + AND + (permission_level < 30) + ELSE + false + END +); CREATE POLICY delete_collaborations ON internal.collab FOR DELETE -USING (internal.user_has_website_access(website_id, 30)); +USING ( + CASE + WHEN internal.user_has_website_access(website_id, 40) THEN + TRUE + WHEN internal.user_has_website_access(website_id, 30) THEN + (user_id != (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID) + AND + (permission_level < 30) + ELSE + FALSE + END +); -- migrate:down diff --git a/rest-api/db/migrations/20240803163047_website_overview_view.sql b/rest-api/db/migrations/20240803163047_website_overview_view.sql index ca62d1c..03d5395 100644 --- a/rest-api/db/migrations/20240803163047_website_overview_view.sql +++ b/rest-api/db/migrations/20240803163047_website_overview_view.sql @@ -4,7 +4,7 @@ WITH (security_invoker = on) AS SELECT w.id, - w.owner_id, + w.user_id, w.content_type, w.title, s.accent_color_light_theme, diff --git a/rest-api/db/migrations/20240808141708_collaborator_not_owner.sql b/rest-api/db/migrations/20240808141708_collaborator_not_owner.sql new file mode 100644 index 0000000..8ec291a --- /dev/null +++ b/rest-api/db/migrations/20240808141708_collaborator_not_owner.sql @@ -0,0 +1,24 @@ +-- migrate:up +CREATE FUNCTION internal.check_user_not_website_owner() +RETURNS TRIGGER AS $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM internal.website + WHERE id = NEW.website_id AND user_id = NEW.user_id + ) THEN + RAISE foreign_key_violation USING MESSAGE = 'User cannot be added as a collaborator to their own website'; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +CREATE CONSTRAINT TRIGGER check_user_not_website_owner +AFTER INSERT ON internal.collab +FOR EACH ROW +EXECUTE FUNCTION internal.check_user_not_website_owner(); + +-- migrate:down +DROP TRIGGER check_user_not_website_owner ON internal.collab; +DROP FUNCTION internal.check_user_not_website_owner(); diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/publish/+page.server.ts b/web-app/src/routes/(authenticated)/website/[websiteId]/publish/+page.server.ts index c430fb3..cf5c75e 100644 --- a/web-app/src/routes/(authenticated)/website/[websiteId]/publish/+page.server.ts +++ b/web-app/src/routes/(authenticated)/website/[websiteId]/publish/+page.server.ts @@ -81,15 +81,9 @@ const generateStaticFiles = async (websiteData: any, isPreview: boolean = true) let uploadDir = ""; if (isPreview) { - uploadDir = join( - process.cwd(), - "static", - "user-websites", - websiteData.owner_id, - websiteData.id - ); + uploadDir = join(process.cwd(), "static", "user-websites", websiteData.user_id, websiteData.id); } else { - uploadDir = join("/", "var", "www", "archtika-websites", websiteData.owner_id, websiteData.id); + uploadDir = join("/", "var", "www", "archtika-websites", websiteData.user_id, websiteData.id); } await mkdir(uploadDir, { recursive: true }); diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/publish/+page.svelte b/web-app/src/routes/(authenticated)/website/[websiteId]/publish/+page.svelte index 64b12c6..a38f9a6 100644 --- a/web-app/src/routes/(authenticated)/website/[websiteId]/publish/+page.svelte +++ b/web-app/src/routes/(authenticated)/website/[websiteId]/publish/+page.svelte @@ -12,7 +12,7 @@