diff --git a/schema/schema.definition.sql b/schema/schema.definition.sql index d35a250..31087be 100644 --- a/schema/schema.definition.sql +++ b/schema/schema.definition.sql @@ -993,21 +993,33 @@ COMMENT ON FUNCTION maevsi.events_organized() IS 'Add a function that returns al CREATE FUNCTION maevsi.invitation_claim_array() RETURNS uuid[] LANGUAGE plpgsql STABLE STRICT AS $$ +DECLARE + _arr UUID[]; + _result_arr UUID[] := ARRAY[]::UUID[]; + _id UUID; BEGIN - RETURN string_to_array(replace(btrim(current_setting('jwt.claims.invitations', true), '[]'), '"', ''), ',')::UUID[]; + _arr := string_to_array(replace(btrim(current_setting('jwt.claims.invitations', true), '[]'), '"', ''), ',')::UUID[]; + FOREACH _id IN ARRAY arr + LOOP + -- omit invitations authored by a blocked account + IF NOT EXISTS( + SELECT 1 + FROM maevsi.invitation i + JOIN maevsi.contact c ON i.contact_id = c.contact_id + JOIN maevsi.account_block b ON c.author_account_id = b.blocked_account_id + WHERE i.id = _id and b.author_account_id = current_setting('jwt.claims.account_id', true) + ) THEN + _result_arr := append_array(result_arr, _id); + END IF; + END LOOP; + + RETURN _result_arr; END $$; ALTER FUNCTION maevsi.invitation_claim_array() OWNER TO postgres; --- --- Name: FUNCTION invitation_claim_array(); Type: COMMENT; Schema: maevsi; Owner: postgres --- - -COMMENT ON FUNCTION maevsi.invitation_claim_array() IS 'Returns the current invitation claims as UUID array.'; - - -- -- Name: invitation_contact_ids(); Type: FUNCTION; Schema: maevsi; Owner: postgres -- @@ -1019,7 +1031,17 @@ BEGIN RETURN QUERY SELECT invitation.contact_id FROM maevsi.invitation WHERE id = ANY (maevsi.invitation_claim_array()) - OR event_id IN (SELECT maevsi.events_organized()); + OR ( + event_id IN (SELECT maevsi.events_organized()) + AND + -- omit contacts authored by a blocked account or referring to a blocked account + contact_id NOT IN ( + SELECT c.id + FROM maevsi.contact c + JOIN maevsi.account_block b ON c.author_account_id = b.blocked_account_id OR c.account_id = b.blocked_account_id + WHERE b.author_account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID + ) + ); END; $$; @@ -1319,7 +1341,7 @@ DECLARE whitelisted_cols TEXT[] := ARRAY['feedback', 'feedback_paper']; BEGIN IF - TG_OP = 'UPDATE' + TG_OP = 'UPDATE' AND ( -- Invited. OLD.id = ANY (maevsi.invitation_claim_array()) OR @@ -1521,17 +1543,25 @@ BEGIN jwt_account_id := NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID; RETURN QUERY - SELECT invitation.event_id FROM maevsi.invitation + SELECT event_id FROM maevsi.invitation WHERE - invitation.contact_id IN ( + ( + contact_id IN ( SELECT id FROM maevsi.contact WHERE - jwt_account_id IS NOT NULL - AND - contact.account_id = jwt_account_id - ) -- The contact selection does not return rows where account_id "IS" null due to the equality comparison. - OR invitation.id = ANY (maevsi.invitation_claim_array()); + account_id = jwt_account_id + -- The contact selection does not return rows where account_id "IS" null due to the equality comparison. + AND + -- contact not created by a blocked account + author_account_id NOT IN ( + SELECT account_block_id + FROM maevsi.account_block + WHERE b.author_account_id = jwt_account_id + ) + ) + ) + OR id = ANY (maevsi.invitation_claim_array()); END $$; @@ -1579,6 +1609,58 @@ COMMENT ON COLUMN maevsi.account.id IS 'The account''s internal id.'; COMMENT ON COLUMN maevsi.account.username IS 'The account''s username.'; +-- +-- Name: account_block; Type: TABLE; Schema: maevsi; Owner: postgres +-- + +CREATE TABLE maevsi.account_block ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + author_account_id uuid NOT NULL, + blocked_account_id uuid NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT account_block_check CHECK ((author_account_id <> blocked_account_id)) +); + + +ALTER TABLE maevsi.account_block OWNER TO postgres; + +-- +-- Name: TABLE account_block; Type: COMMENT; Schema: maevsi; Owner: postgres +-- + +COMMENT ON TABLE maevsi.account_block IS '@omit update,delete +Blocking of an account by another account.'; + + +-- +-- Name: COLUMN account_block.id; Type: COMMENT; Schema: maevsi; Owner: postgres +-- + +COMMENT ON COLUMN maevsi.account_block.id IS '@omit create\nThe blocking''s internal id.'; + + +-- +-- Name: COLUMN account_block.author_account_id; Type: COMMENT; Schema: maevsi; Owner: postgres +-- + +COMMENT ON COLUMN maevsi.account_block.author_account_id IS 'The id of the user who created the blocking.'; + + +-- +-- Name: COLUMN account_block.blocked_account_id; Type: COMMENT; Schema: maevsi; Owner: postgres +-- + +COMMENT ON COLUMN maevsi.account_block.blocked_account_id IS 'The id of the account to be blocked.'; + + +-- +-- Name: COLUMN account_block.created_at; Type: COMMENT; Schema: maevsi; Owner: postgres +-- + +COMMENT ON COLUMN maevsi.account_block.created_at IS '@omit update,delete +The timestamp when the blocking was created.'; + + -- -- Name: achievement; Type: TABLE; Schema: maevsi; Owner: postgres -- @@ -2877,6 +2959,22 @@ COMMENT ON COLUMN sqitch.tags.planner_name IS 'Name of the user who planed the t COMMENT ON COLUMN sqitch.tags.planner_email IS 'Email address of the user who planned the tag.'; +-- +-- Name: account_block account_block_author_account_id_blocked_account_id_key; Type: CONSTRAINT; Schema: maevsi; Owner: postgres +-- + +ALTER TABLE ONLY maevsi.account_block + ADD CONSTRAINT account_block_author_account_id_blocked_account_id_key UNIQUE (author_account_id, blocked_account_id); + + +-- +-- Name: account_block account_block_pkey; Type: CONSTRAINT; Schema: maevsi; Owner: postgres +-- + +ALTER TABLE ONLY maevsi.account_block + ADD CONSTRAINT account_block_pkey PRIMARY KEY (id); + + -- -- Name: account account_pkey; Type: CONSTRAINT; Schema: maevsi; Owner: postgres -- @@ -3322,6 +3420,22 @@ CREATE TRIGGER maevsi_private_account_email_address_verification_valid_until BEF CREATE TRIGGER maevsi_private_account_password_reset_verification_valid_until BEFORE INSERT OR UPDATE OF password_reset_verification ON maevsi_private.account FOR EACH ROW EXECUTE FUNCTION maevsi_private.account_password_reset_verification_valid_until(); +-- +-- Name: account_block account_block_author_account_id_fkey; Type: FK CONSTRAINT; Schema: maevsi; Owner: postgres +-- + +ALTER TABLE ONLY maevsi.account_block + ADD CONSTRAINT account_block_author_account_id_fkey FOREIGN KEY (author_account_id) REFERENCES maevsi.account(id); + + +-- +-- Name: account_block account_block_blocked_account_id_fkey; Type: FK CONSTRAINT; Schema: maevsi; Owner: postgres +-- + +ALTER TABLE ONLY maevsi.account_block + ADD CONSTRAINT account_block_blocked_account_id_fkey FOREIGN KEY (blocked_account_id) REFERENCES maevsi.account(id); + + -- -- Name: account account_id_fkey; Type: FK CONSTRAINT; Schema: maevsi; Owner: postgres -- @@ -3528,6 +3642,26 @@ ALTER TABLE ONLY sqitch.tags ALTER TABLE maevsi.account ENABLE ROW LEVEL SECURITY; +-- +-- Name: account_block; Type: ROW SECURITY; Schema: maevsi; Owner: postgres +-- + +ALTER TABLE maevsi.account_block ENABLE ROW LEVEL SECURITY; + +-- +-- Name: account_block account_block_insert; Type: POLICY; Schema: maevsi; Owner: postgres +-- + +CREATE POLICY account_block_insert ON maevsi.account_block FOR INSERT WITH CHECK ((((NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid IS NOT NULL) AND (author_account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid))); + + +-- +-- Name: account_block account_block_select; Type: POLICY; Schema: maevsi; Owner: postgres +-- + +CREATE POLICY account_block_select ON maevsi.account_block FOR SELECT USING ((((NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid IS NOT NULL) AND (author_account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid))); + + -- -- Name: account account_select; Type: POLICY; Schema: maevsi; Owner: postgres -- @@ -3565,21 +3699,29 @@ CREATE POLICY contact_delete ON maevsi.contact FOR DELETE USING ((((NULLIF(curre -- Name: contact contact_insert; Type: POLICY; Schema: maevsi; Owner: postgres -- -CREATE POLICY contact_insert ON maevsi.contact FOR INSERT WITH CHECK ((((NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid IS NOT NULL) AND (author_account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid))); +CREATE POLICY contact_insert ON maevsi.contact FOR INSERT WITH CHECK (((author_account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid) AND (NOT (account_id IN ( SELECT account_block.blocked_account_id + FROM maevsi.account_block + WHERE (account_block.author_account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid)))))); -- -- Name: contact contact_select; Type: POLICY; Schema: maevsi; Owner: postgres -- -CREATE POLICY contact_select ON maevsi.contact FOR SELECT USING (((((NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid IS NOT NULL) AND ((account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid) OR (author_account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid))) OR (id IN ( SELECT maevsi.invitation_contact_ids() AS invitation_contact_ids)))); +CREATE POLICY contact_select ON maevsi.contact FOR SELECT USING ((((account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid) AND (NOT (author_account_id IN ( SELECT account_block.blocked_account_id + FROM maevsi.account_block + WHERE (account_block.author_account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid))))) OR ((author_account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid) AND (NOT (account_id IN ( SELECT account_block.blocked_account_id + FROM maevsi.account_block + WHERE (account_block.author_account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid))))) OR (id IN ( SELECT maevsi.invitation_contact_ids() AS invitation_contact_ids)))); -- -- Name: contact contact_update; Type: POLICY; Schema: maevsi; Owner: postgres -- -CREATE POLICY contact_update ON maevsi.contact FOR UPDATE USING ((((NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid IS NOT NULL) AND (author_account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid))); +CREATE POLICY contact_update ON maevsi.contact FOR UPDATE USING (((author_account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid) AND (NOT (account_id IN ( SELECT account_block.blocked_account_id + FROM maevsi.account_block + WHERE (account_block.author_account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid)))))); -- @@ -3611,7 +3753,9 @@ CREATE POLICY event_insert ON maevsi.event FOR INSERT WITH CHECK ((((NULLIF(curr -- Name: event event_select; Type: POLICY; Schema: maevsi; Owner: postgres -- -CREATE POLICY event_select ON maevsi.event FOR SELECT USING ((((visibility = 'public'::maevsi.event_visibility) AND ((invitee_count_maximum IS NULL) OR (invitee_count_maximum > maevsi.invitee_count(id)))) OR (((NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid IS NOT NULL) AND (author_account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid)) OR (id IN ( SELECT maevsi_private.events_invited() AS events_invited)))); +CREATE POLICY event_select ON maevsi.event FOR SELECT USING ((((visibility = 'public'::maevsi.event_visibility) AND ((invitee_count_maximum IS NULL) OR (invitee_count_maximum > maevsi.invitee_count(id))) AND (NOT (author_account_id IN ( SELECT account_block.blocked_account_id + FROM maevsi.account_block + WHERE (account_block.author_account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid))))) OR (author_account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid) OR (id IN ( SELECT maevsi_private.events_invited() AS events_invited)))); -- @@ -3638,27 +3782,48 @@ CREATE POLICY invitation_delete ON maevsi.invitation FOR DELETE USING ((event_id -- Name: invitation invitation_insert; Type: POLICY; Schema: maevsi; Owner: postgres -- -CREATE POLICY invitation_insert ON maevsi.invitation FOR INSERT WITH CHECK (((event_id IN ( SELECT maevsi.events_organized() AS events_organized)) AND ((maevsi.event_invitee_count_maximum(event_id) IS NULL) OR (maevsi.event_invitee_count_maximum(event_id) > maevsi.invitee_count(event_id))) AND (((NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid IS NOT NULL) AND (contact_id IN ( SELECT contact.id +CREATE POLICY invitation_insert ON maevsi.invitation FOR INSERT WITH CHECK (((event_id IN ( SELECT maevsi.events_organized() AS events_organized)) AND ((maevsi.event_invitee_count_maximum(event_id) IS NULL) OR (maevsi.event_invitee_count_maximum(event_id) > maevsi.invitee_count(event_id))) AND (contact_id IN ( SELECT contact.id FROM maevsi.contact - WHERE (contact.author_account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid)))))); + WHERE (contact.author_account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid) +EXCEPT + SELECT c.id + FROM (maevsi.contact c + JOIN maevsi.account_block b ON (((c.account_id = b.blocked_account_id) AND (c.author_account_id = b.author_account_id)))) + WHERE (c.author_account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid))))); -- -- Name: invitation invitation_select; Type: POLICY; Schema: maevsi; Owner: postgres -- -CREATE POLICY invitation_select ON maevsi.invitation FOR SELECT USING (((id = ANY (maevsi.invitation_claim_array())) OR (((NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid IS NOT NULL) AND (contact_id IN ( SELECT contact.id +CREATE POLICY invitation_select ON maevsi.invitation FOR SELECT USING (((id = ANY (maevsi.invitation_claim_array())) OR (contact_id IN ( SELECT contact.id FROM maevsi.contact - WHERE (contact.account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid)))) OR (event_id IN ( SELECT maevsi.events_organized() AS events_organized)))); + WHERE (contact.account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid) +EXCEPT + SELECT c.id + FROM (maevsi.contact c + JOIN maevsi.account_block b ON (((c.account_id = b.author_account_id) AND (c.author_account_id = b.blocked_account_id)))) + WHERE (c.account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid))) OR ((event_id IN ( SELECT maevsi.events_organized() AS events_organized)) AND (NOT (contact_id IN ( SELECT c.id + FROM (maevsi.contact c + JOIN maevsi.account_block b ON (((c.author_account_id = b.blocked_account_id) OR (c.account_id = b.blocked_account_id)))) + WHERE (b.author_account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid))))))); -- -- Name: invitation invitation_update; Type: POLICY; Schema: maevsi; Owner: postgres -- -CREATE POLICY invitation_update ON maevsi.invitation FOR UPDATE USING (((id = ANY (maevsi.invitation_claim_array())) OR (((NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid IS NOT NULL) AND (contact_id IN ( SELECT contact.id +CREATE POLICY invitation_update ON maevsi.invitation FOR UPDATE USING (((id = ANY (maevsi.invitation_claim_array())) OR (contact_id IN ( SELECT contact.id FROM maevsi.contact - WHERE (contact.account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid)))) OR (event_id IN ( SELECT maevsi.events_organized() AS events_organized)))); + WHERE (contact.account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid) +EXCEPT + SELECT c.id + FROM (maevsi.contact c + JOIN maevsi.account_block b ON (((c.account_id = b.author_account_id) AND (c.author_account_id = b.blocked_account_id)))) + WHERE (c.account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid))) OR ((event_id IN ( SELECT maevsi.events_organized() AS events_organized)) AND (NOT (contact_id IN ( SELECT c.id + FROM (maevsi.contact c + JOIN maevsi.account_block b ON (((c.author_account_id = b.blocked_account_id) OR (c.account_id = b.blocked_account_id)))) + WHERE (b.author_account_id = (NULLIF(current_setting('jwt.claims.account_id'::text, true), ''::text))::uuid))))))); -- @@ -4321,6 +4486,13 @@ GRANT SELECT ON TABLE maevsi.account TO maevsi_account; GRANT SELECT ON TABLE maevsi.account TO maevsi_anonymous; +-- +-- Name: TABLE account_block; Type: ACL; Schema: maevsi; Owner: postgres +-- + +GRANT SELECT,INSERT ON TABLE maevsi.account_block TO maevsi_account; + + -- -- Name: TABLE achievement; Type: ACL; Schema: maevsi; Owner: postgres -- diff --git a/src/deploy/function_events_invited.sql b/src/deploy/function_events_invited.sql index 7fff04b..b07fc5c 100644 --- a/src/deploy/function_events_invited.sql +++ b/src/deploy/function_events_invited.sql @@ -4,32 +4,43 @@ -- requires: schema_public -- requires: table_invitation -- requires: table_contact +-- requires: table_account_block -- requires: role_account -- requires: role_anonymous BEGIN; -CREATE FUNCTION maevsi_private.events_invited() -RETURNS TABLE (event_id UUID) AS $$ +CREATE OR REPLACE FUNCTION maevsi_private.events_invited() + RETURNS TABLE(event_id uuid) +AS $$ DECLARE jwt_account_id UUID; BEGIN jwt_account_id := NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID; RETURN QUERY - SELECT invitation.event_id FROM maevsi.invitation + SELECT event_id FROM maevsi.invitation WHERE - invitation.contact_id IN ( + ( + contact_id IN ( SELECT id FROM maevsi.contact WHERE - jwt_account_id IS NOT NULL - AND - contact.account_id = jwt_account_id - ) -- The contact selection does not return rows where account_id "IS" null due to the equality comparison. - OR invitation.id = ANY (maevsi.invitation_claim_array()); + account_id = jwt_account_id + -- The contact selection does not return rows where account_id "IS" null due to the equality comparison. + AND + -- contact not created by a blocked account + author_account_id NOT IN ( + SELECT account_block_id + FROM maevsi.account_block + WHERE b.author_account_id = jwt_account_id + ) + ) + ) + OR id = ANY (maevsi.invitation_claim_array()); END -$$ LANGUAGE PLPGSQL STRICT STABLE SECURITY DEFINER; +$$ LANGUAGE plpgsql STABLE STRICT SECURITY DEFINER +; COMMENT ON FUNCTION maevsi_private.events_invited() IS 'Add a function that returns all event ids for which the invoker is invited.'; diff --git a/src/deploy/function_invitation_claim_array.sql b/src/deploy/function_invitation_claim_array.sql index 05a122a..365833a 100644 --- a/src/deploy/function_invitation_claim_array.sql +++ b/src/deploy/function_invitation_claim_array.sql @@ -1,20 +1,40 @@ -- Deploy maevsi:function_invitation_claim_array to pg -- requires: privilege_execute_revoke -- requires: schema_public +-- requires: table_invitation +-- requires: table_contact +-- requires: table_account_block -- requires: role_account -- requires: role_anonymous BEGIN; -CREATE FUNCTION maevsi.invitation_claim_array() +CREATE OR REPLACE FUNCTION maevsi.invitation_claim_array() RETURNS UUID[] AS $$ +DECLARE + _arr UUID[]; + _result_arr UUID[] := ARRAY[]::UUID[]; + _id UUID; BEGIN - RETURN string_to_array(replace(btrim(current_setting('jwt.claims.invitations', true), '[]'), '"', ''), ',')::UUID[]; + _arr := string_to_array(replace(btrim(current_setting('jwt.claims.invitations', true), '[]'), '"', ''), ',')::UUID[]; + FOREACH _id IN ARRAY arr + LOOP + -- omit invitations authored by a blocked account + IF NOT EXISTS( + SELECT 1 + FROM maevsi.invitation i + JOIN maevsi.contact c ON i.contact_id = c.contact_id + JOIN maevsi.account_block b ON c.author_account_id = b.blocked_account_id + WHERE i.id = _id and b.author_account_id = current_setting('jwt.claims.account_id', true) + ) THEN + _result_arr := append_array(result_arr, _id); + END IF; + END LOOP; + + RETURN _result_arr; END $$ LANGUAGE PLPGSQL STRICT STABLE SECURITY INVOKER; -COMMENT ON FUNCTION maevsi.invitation_claim_array() IS 'Returns the current invitation claims as UUID array.'; - GRANT EXECUTE ON FUNCTION maevsi.invitation_claim_array() TO maevsi_account, maevsi_anonymous; COMMIT; diff --git a/src/deploy/function_invitation_contact_ids.sql b/src/deploy/function_invitation_contact_ids.sql index 11bd513..e3f0f9b 100644 --- a/src/deploy/function_invitation_contact_ids.sql +++ b/src/deploy/function_invitation_contact_ids.sql @@ -2,6 +2,8 @@ -- requires: privilege_execute_revoke -- requires: schema_public -- requires: table_invitation +-- requires: table_contact +-- requires: table_account_block -- requires: function_invitation_claim_array -- requires: function_events_organized -- requires: role_account @@ -9,13 +11,23 @@ BEGIN; -CREATE FUNCTION maevsi.invitation_contact_ids() +CREATE OR REPLACE FUNCTION maevsi.invitation_contact_ids() RETURNS TABLE (contact_id UUID) AS $$ BEGIN RETURN QUERY SELECT invitation.contact_id FROM maevsi.invitation WHERE id = ANY (maevsi.invitation_claim_array()) - OR event_id IN (SELECT maevsi.events_organized()); + OR ( + event_id IN (SELECT maevsi.events_organized()) + AND + -- omit contacts authored by a blocked account or referring to a blocked account + contact_id NOT IN ( + SELECT c.id + FROM maevsi.contact c + JOIN maevsi.account_block b ON c.author_account_id = b.blocked_account_id OR c.account_id = b.blocked_account_id + WHERE b.author_account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID + ) + ); END; $$ LANGUAGE PLPGSQL STRICT STABLE SECURITY DEFINER; diff --git a/src/deploy/table_account_block.sql b/src/deploy/table_account_block.sql new file mode 100644 index 0000000..8182dbe --- /dev/null +++ b/src/deploy/table_account_block.sql @@ -0,0 +1,22 @@ +-- Deploy maevsi:table_account_block to pg +-- requires: schema_public +-- requires: table_account_public + +BEGIN; + +CREATE TABLE maevsi.account_block ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + author_account_id UUID NOT NULL REFERENCES maevsi.account(id), + blocked_account_id UUID NOT NULL REFERENCES maevsi.account(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE (author_account_id, blocked_account_id), + CHECK (author_account_id != blocked_account_id) +); + +COMMENT ON TABLE maevsi.account_block IS E'@omit update,delete\nBlocking of an account by another account.'; +COMMENT ON COLUMN maevsi.account_block.id IS '@omit create\nThe blocking''s internal id.'; +COMMENT ON COLUMN maevsi.account_block.author_account_id IS 'The id of the user who created the blocking.'; +COMMENT ON COLUMN maevsi.account_block.blocked_account_id IS 'The id of the account to be blocked.'; +COMMENT ON COLUMN maevsi.account_block.created_at IS E'@omit update,delete\nThe timestamp when the blocking was created.'; + +COMMIT; diff --git a/src/deploy/table_account_block_policy.sql b/src/deploy/table_account_block_policy.sql new file mode 100644 index 0000000..c2d45c4 --- /dev/null +++ b/src/deploy/table_account_block_policy.sql @@ -0,0 +1,26 @@ +-- Deploy maevsi:table_account_block_policy to pg +-- requires: schema_public +-- requires: table_account_block +-- requires: role_account + +BEGIN; + +GRANT INSERT, SELECT ON TABLE maevsi.account_block TO maevsi_account; + +ALTER TABLE maevsi.account_block ENABLE ROW LEVEL SECURITY; + +-- Only allow inserts for blocked accounts authored by the current user. +CREATE POLICY account_block_insert ON maevsi.account_block FOR INSERT WITH CHECK ( + NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID IS NOT NULL + AND + author_account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID +); + +-- Only allow selects for blocked accounts authored by the current user. +CREATE POLICY account_block_select ON maevsi.account_block FOR SELECT USING ( + NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID IS NOT NULL + AND + author_account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID +); + +COMMIT; diff --git a/src/deploy/table_contact_policy.sql b/src/deploy/table_contact_policy.sql index 5be8989..8652d14 100644 --- a/src/deploy/table_contact_policy.sql +++ b/src/deploy/table_contact_policy.sql @@ -1,5 +1,6 @@ -- Deploy maevsi:table_contact_policy to pg -- requires: schema_public +-- requires: table_account_block -- requires: table_contact -- requires: role_account -- requires: role_anonymous @@ -12,34 +13,52 @@ GRANT INSERT, UPDATE, DELETE ON TABLE maevsi.contact TO maevsi_account; ALTER TABLE maevsi.contact ENABLE ROW LEVEL SECURITY; --- Only display contacts referencing the invoker's account. --- Only display contacts authored by the invoker's account. --- Only display contacts for which an accessible invitation exists. +-- Only display contacts referencing the invoker's account, omit contacts authored by a blocked account. +-- Only display contacts authored by the invoker's account, omit contacts referring to a blocked account. +-- Only display contacts for which an accessible invitation exists, omit contacts auhored by a blocked account or referring to a blocked account. CREATE POLICY contact_select ON maevsi.contact FOR SELECT USING ( ( - NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID IS NOT NULL - AND ( - account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID - OR + account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID + AND + author_account_id NOT IN ( + SELECT blocked_account_id + FROM maevsi.account_block + WHERE author_account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID + ) + ) + OR + ( author_account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID - ) - ) - OR - id IN (SELECT maevsi.invitation_contact_ids()) + AND + account_id NOT IN ( + SELECT blocked_account_id + FROM maevsi.account_block + WHERE author_account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID + ) + ) + OR id IN (SELECT maevsi.invitation_contact_ids()) ); --- Only allow inserts for contacts authored by the invoker's account. +-- Only allow inserts for contacts authored by the invoker's account +-- No blocked account can be invited CREATE POLICY contact_insert ON maevsi.contact FOR INSERT WITH CHECK ( - NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID IS NOT NULL - AND author_account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID + AND account_id NOT IN ( + SELECT blocked_account_id + FROM maevsi.account_block + WHERE author_account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID + ) ); -- Only allow updates for contacts authored by the invoker's account. +-- No contact referring to a blocked account can be updated CREATE POLICY contact_update ON maevsi.contact FOR UPDATE USING ( - NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID IS NOT NULL - AND author_account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID + AND account_id NOT IN ( + SELECT blocked_account_id + FROM maevsi.account_block + WHERE author_account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID + ) ); -- Only allow deletes for contacts authored by the invoker's account except for the own account's contact. diff --git a/src/deploy/table_event_policy.sql b/src/deploy/table_event_policy.sql index 5c63988..63f1f0f 100644 --- a/src/deploy/table_event_policy.sql +++ b/src/deploy/table_event_policy.sql @@ -1,6 +1,7 @@ -- Deploy maevsi:table_event_policy to pg -- requires: schema_public -- requires: table_event +-- requires: table_account_block -- requires: role_account -- requires: role_anonymous -- requires: schema_private @@ -13,26 +14,31 @@ GRANT INSERT, UPDATE, DELETE ON TABLE maevsi.event TO maevsi_account; ALTER TABLE maevsi.event ENABLE ROW LEVEL SECURITY; --- Only display events that are public and not full. +-- Only display events that are public and not full and not organized by a blocked account -- Only display events that are organized by oneself. --- Only display events to which oneself is invited. +-- Only display events to which oneself is invited, but not by an invitation authored by a blocked account CREATE POLICY event_select ON maevsi.event FOR SELECT USING ( -- Below copied to function `maevsi.event_invitee_count_maximum`. - ( - visibility = 'public' - AND - ( - invitee_count_maximum IS NULL - OR - invitee_count_maximum > (maevsi.invitee_count(id)) -- Using the function here is required as there would otherwise be infinite recursion. - ) - ) - OR ( - NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID IS NOT NULL + ( + visibility = 'public' AND - author_account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID + ( + invitee_count_maximum IS NULL + OR + invitee_count_maximum > (maevsi.invitee_count(id)) -- Using the function here is required as there would otherwise be infinite recursion. + ) + AND author_account_id NOT IN ( + SELECT blocked_account_id + FROM maevsi.account_block + WHERE author_account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID + ) ) - OR id IN (SELECT maevsi_private.events_invited()) + OR ( + author_account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID + ) + OR ( + id IN (SELECT maevsi_private.events_invited()) + ) ); -- Only allow inserts for events authored by the current user. diff --git a/src/deploy/table_invitation_policy.sql b/src/deploy/table_invitation_policy.sql index c333693..4f61ea6 100644 --- a/src/deploy/table_invitation_policy.sql +++ b/src/deploy/table_invitation_policy.sql @@ -1,6 +1,8 @@ -- Deploy maevsi:table_invitation_policy to pg -- requires: schema_public -- requires: table_invitation +-- requires: table_contact +-- requires: table_account_block -- requires: role_account -- requires: role_anonymous -- requires: function_invitation_claim_array @@ -14,62 +16,99 @@ GRANT INSERT, DELETE ON TABLE maevsi.invitation TO maevsi_account; ALTER TABLE maevsi.invitation ENABLE ROW LEVEL SECURITY; --- Only display invitations issued to oneself through invitation claims. --- Only display invitations issued to oneself through the account. --- Only display invitations to events organized by oneself. +-- Only display invitations issued to oneself through invitation claims, omit invitations authored by a blocked user. +-- Only display invitations issued to oneself through the account and not authored by a blocked user. +-- Only display invitations to events organized by oneself, omit invitations authored by a blocked user and invitations issued to a blocked user. CREATE POLICY invitation_select ON maevsi.invitation FOR SELECT USING ( - id = ANY (maevsi.invitation_claim_array()) + id = ANY (maevsi.invitation_claim_array()) OR ( - NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID IS NOT NULL - AND contact_id IN ( SELECT id FROM maevsi.contact - WHERE contact.account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID + WHERE account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID + + EXCEPT + + -- contacts to oneself authored by a blocked account + SELECT c.id + FROM maevsi.contact c + JOIN maevsi.account_block b ON c.account_id = b.author_account_id AND c.author_account_id = b.blocked_account_id + WHERE c.account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID ) ) - OR event_id IN (SELECT maevsi.events_organized()) + OR ( + event_id IN (SELECT maevsi.events_organized()) + AND + contact_id NOT IN ( + SELECT c.id + FROM maevsi.contact c + JOIN maevsi.account_block b + -- contact authored by a blocked account OR referring to a blocked account + ON c.author_account_id = b.blocked_account_id OR c.account_id = b.blocked_account_id + WHERE b.author_account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID + ) + ) ); -- Only allow inserts for invitations to events organized by oneself. -- Only allow inserts for invitations to events for which the maximum invitee count is not yet reached. --- Only allow inserts for invitations issued to a contact that was created by oneself. +-- Only allow inserts for invitations issued to a contact that was created by oneself +-- Do not allow inserts for invitations issued to a contact referring a blocked account CREATE POLICY invitation_insert ON maevsi.invitation FOR INSERT WITH CHECK ( - event_id IN (SELECT maevsi.events_organized()) + event_id IN (SELECT maevsi.events_organized()) AND ( maevsi.event_invitee_count_maximum(event_id) IS NULL OR maevsi.event_invitee_count_maximum(event_id) > maevsi.invitee_count(event_id) ) AND - ( - NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID IS NOT NULL - AND contact_id IN ( SELECT id FROM maevsi.contact - WHERE contact.author_account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID - ) - ) + WHERE author_account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID + + EXCEPT + + SELECT c.id + FROM maevsi.contact c + JOIN maevsi.account_block b ON c.account_id = b.blocked_account_id and c.author_account_id = b.author_account_id + WHERE c.author_account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID + ) ); -- Only allow updates to invitations issued to oneself through invitation claims. --- Only allow updates to invitations issued to oneself through the account. --- Only allow updates to invitations to events organized by oneself. +-- Only allow updates to invitations issued to oneself through the account, but not invitations auhored by a blocked account +-- Only allow updates to invitations to events organized by oneself, but not invitations issued to a blocked account or issued by a blocked account CREATE POLICY invitation_update ON maevsi.invitation FOR UPDATE USING ( id = ANY (maevsi.invitation_claim_array()) OR ( - NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID IS NOT NULL - AND contact_id IN ( SELECT id FROM maevsi.contact - WHERE contact.account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID + WHERE account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID + + EXCEPT + + SELECT c.id + FROM maevsi.contact c + JOIN maevsi.account_block b ON c.account_id = b.author_account_id and c.author_account_id = b.blocked_account_id + WHERE c.account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID + ) + ) + OR + ( + event_id IN (SELECT maevsi.events_organized()) + AND + -- omit contacts authored by a blocked account or referring to a blocked account + contact_id NOT IN ( + SELECT c.id + FROM maevsi.contact c + JOIN maevsi.account_block b ON c.author_account_id = b.blocked_account_id OR c.account_id = b.blocked_account_id + WHERE b.author_account_id = NULLIF(current_setting('jwt.claims.account_id', true), '')::UUID ) ) - OR event_id IN (SELECT maevsi.events_organized()) ); -- Only allow deletes for invitations to events organized by oneself. @@ -82,7 +121,7 @@ DECLARE whitelisted_cols TEXT[] := ARRAY['feedback', 'feedback_paper']; BEGIN IF - TG_OP = 'UPDATE' + TG_OP = 'UPDATE' AND ( -- Invited. OLD.id = ANY (maevsi.invitation_claim_array()) OR diff --git a/src/revert/table_account_block.sql b/src/revert/table_account_block.sql new file mode 100644 index 0000000..77d764a --- /dev/null +++ b/src/revert/table_account_block.sql @@ -0,0 +1,7 @@ +-- Revert maevsi:table_account_block from pg + +BEGIN; + +DROP TABLE maevsi.account_block; + +COMMIT; diff --git a/src/revert/table_account_block_policy.sql b/src/revert/table_account_block_policy.sql new file mode 100644 index 0000000..a2efadb --- /dev/null +++ b/src/revert/table_account_block_policy.sql @@ -0,0 +1,8 @@ +-- Revert maevsi:table_account_block_policy from pg + +BEGIN; + +DROP POLICY account_block_insert ON maevsi.account_block; +DROP POLICY account_block_select ON maevsi.account_block; + +COMMIT; diff --git a/src/sqitch.plan b/src/sqitch.plan index 4a6e450..7f3d006 100644 --- a/src/sqitch.plan +++ b/src/sqitch.plan @@ -15,6 +15,8 @@ enum_event_visibility [schema_public] 1970-01-01T00:00:00Z Jonas Thelemann # Notifications that are sent via pg_notify. table_account_private [schema_private schema_public] 1970-01-01T00:00:00Z Jonas Thelemann # Add private table account. table_account_public [schema_public schema_private table_account_private] 1970-01-01T00:00:00Z Jonas Thelemann # Add public table account. +table_account_block [schema_public table_account_public] 1970-01-01T00:00:00Z Sven Thelemann # Blocking of an account by another account. +table_account_block_policy [schema_public table_account_block role_account] 1970-01-01T00:00:00Z Sven Thelemann # Policy for table account block. table_event_group [schema_public role_account role_anonymous table_account_public enum_event_visibility] 1970-01-01T00:00:00Z Jonas Thelemann # Add table event_group. index_event_group_author_username [table_event_group] 1970-01-01T00:00:00Z Jonas Thelemann # Add an index to the event group table's author_username field. table_event [schema_public role_account role_anonymous table_account_public] 1970-01-01T00:00:00Z Jonas Thelemann # Add table event. diff --git a/src/verify/table_account_block.sql b/src/verify/table_account_block.sql new file mode 100644 index 0000000..378bac1 --- /dev/null +++ b/src/verify/table_account_block.sql @@ -0,0 +1,12 @@ + +-- Verify maevsi:table_account_block on pg + +BEGIN; + +SELECT id, + author_account_id, + blocked_account_id, + created_at +FROM maevsi.account_block WHERE FALSE; + +ROLLBACK; diff --git a/src/verify/table_account_block_policy.sql b/src/verify/table_account_block_policy.sql new file mode 100644 index 0000000..6d0d9e1 --- /dev/null +++ b/src/verify/table_account_block_policy.sql @@ -0,0 +1,21 @@ +-- Verify maevsi:table_account_block_policy on pg + +BEGIN; + +DO $$ +BEGIN + ASSERT (SELECT pg_catalog.has_table_privilege('maevsi_account', 'maevsi.account_block', 'INSERT')); + ASSERT (SELECT pg_catalog.has_table_privilege('maevsi_account', 'maevsi.account_block', 'SELECT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('maevsi_account', 'maevsi.account_block', 'UPDATE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('maevsi_account', 'maevsi.account_block', 'DELETE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('maevsi_anonymous', 'maevsi.account_block', 'SELECT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('maevsi_anonymous', 'maevsi.account_block', 'INSERT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('maevsi_anonymous', 'maevsi.account_block', 'UPDATE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('maevsi_anonymous', 'maevsi.account_block', 'DELETE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('maevsi_tusd', 'maevsi.account_block', 'SELECT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('maevsi_tusd', 'maevsi.account_block', 'INSERT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('maevsi_tusd', 'maevsi.account_block', 'UPDATE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('maevsi_tusd', 'maevsi.account_block', 'DELETE')); +END $$; + +ROLLBACK;