-- -- MULTI_MULTIUSERS -- -- Test user permissions. -- SET citus.next_shard_id TO 1420000; SET citus.shard_replication_factor TO 1; CREATE TABLE test (id integer, val integer); SELECT create_distributed_table('test', 'id'); create_distributed_table --------------------------------------------------------------------- (1 row) CREATE TABLE test_coloc (id integer, val integer); SELECT create_distributed_table('test_coloc', 'id', colocate_with := 'test'); create_distributed_table --------------------------------------------------------------------- (1 row) SET citus.shard_count TO 1; CREATE TABLE singleshard (id integer, val integer); SELECT create_distributed_table('singleshard', 'id'); create_distributed_table --------------------------------------------------------------------- (1 row) -- turn off propagation to avoid Enterprise processing the following section CREATE USER full_access; CREATE USER usage_access; CREATE USER read_access; CREATE USER no_access; CREATE ROLE some_role; GRANT some_role TO full_access; GRANT some_role TO read_access; SET citus.enable_ddl_propagation TO off; GRANT ALL ON TABLE test TO full_access; GRANT SELECT ON TABLE test TO read_access; CREATE SCHEMA full_access_user_schema; REVOKE ALL ON SCHEMA full_access_user_schema FROM PUBLIC; GRANT ALL ON SCHEMA full_access_user_schema TO full_access; GRANT USAGE ON SCHEMA full_access_user_schema TO usage_access; SET citus.enable_ddl_propagation TO DEFAULT; \c - - - :worker_1_port GRANT ALL ON TABLE test_1420000 TO full_access; GRANT SELECT ON TABLE test_1420000 TO read_access; GRANT ALL ON TABLE test_1420002 TO full_access; GRANT SELECT ON TABLE test_1420002 TO read_access; \c - - - :worker_2_port GRANT ALL ON TABLE test_1420001 TO full_access; GRANT SELECT ON TABLE test_1420001 TO read_access; GRANT ALL ON TABLE test_1420003 TO full_access; GRANT SELECT ON TABLE test_1420003 TO read_access; \c - - - :master_port SET citus.shard_replication_factor TO 1; -- create prepare tests PREPARE prepare_insert AS INSERT INTO test VALUES ($1); PREPARE prepare_select AS SELECT count(*) FROM test; -- check full permission SET ROLE full_access; EXECUTE prepare_insert(1); EXECUTE prepare_select; count --------------------------------------------------------------------- 1 (1 row) INSERT INTO test VALUES (2); SELECT count(*) FROM test; count --------------------------------------------------------------------- 2 (1 row) SELECT count(*) FROM test WHERE id = 1; count --------------------------------------------------------------------- 1 (1 row) SET citus.enable_repartition_joins to ON; SELECT count(*), min(current_user) FROM test; count | min --------------------------------------------------------------------- 2 | full_access (1 row) -- test re-partition query (needs to transmit intermediate results) SELECT count(*) FROM test a JOIN test b ON (a.val = b.val) WHERE a.id = 1 AND b.id = 2; count --------------------------------------------------------------------- 0 (1 row) SET citus.enable_repartition_joins TO true; SELECT count(*) FROM test a JOIN test b ON (a.val = b.val) WHERE a.id = 1 AND b.id = 2; count --------------------------------------------------------------------- 0 (1 row) -- check read permission SET ROLE read_access; -- should be allowed to run commands, as the current user SELECT result FROM run_command_on_workers($$SELECT current_user$$); result --------------------------------------------------------------------- read_access read_access (2 rows) SELECT result FROM run_command_on_placements('test', $$SELECT current_user$$); result --------------------------------------------------------------------- read_access read_access read_access read_access (4 rows) SELECT result FROM run_command_on_colocated_placements('test', 'test_coloc', $$SELECT current_user$$); result --------------------------------------------------------------------- read_access read_access read_access read_access (4 rows) EXECUTE prepare_insert(1); ERROR: permission denied for table test EXECUTE prepare_select; count --------------------------------------------------------------------- 2 (1 row) INSERT INTO test VALUES (2); ERROR: permission denied for table test SELECT count(*) FROM test; count --------------------------------------------------------------------- 2 (1 row) SELECT count(*) FROM test WHERE id = 1; count --------------------------------------------------------------------- 1 (1 row) SET citus.enable_repartition_joins to ON; SELECT count(*), min(current_user) FROM test; count | min --------------------------------------------------------------------- 2 | read_access (1 row) -- test re-partition query (needs to transmit intermediate results) SELECT count(*) FROM test a JOIN test b ON (a.val = b.val) WHERE a.id = 1 AND b.id = 2; count --------------------------------------------------------------------- 0 (1 row) SET citus.enable_repartition_joins TO true; SELECT count(*) FROM test a JOIN test b ON (a.val = b.val) WHERE a.id = 1 AND b.id = 2; count --------------------------------------------------------------------- 0 (1 row) -- should not be allowed to take aggressive locks on table BEGIN; SELECT lock_relation_if_exists('test', 'ACCESS SHARE'); lock_relation_if_exists --------------------------------------------------------------------- t (1 row) SELECT lock_relation_if_exists('test', 'EXCLUSIVE'); ERROR: permission denied for table test ABORT; -- test creating columnar tables and accessing to columnar metadata tables via unprivileged user -- all below 5 commands should throw no permission errors -- read columnar metadata table SELECT * FROM columnar.stripe; relation | storage_id | stripe_num | file_offset | data_length | column_count | chunk_row_count | row_count | chunk_group_count | first_row_number --------------------------------------------------------------------- (0 rows) -- alter a columnar setting SET columnar.chunk_group_row_limit = 1050; -- create columnar table CREATE TABLE columnar_table (a int) USING columnar; -- alter a columnar table that is created by that unprivileged user ALTER TABLE columnar_table SET (columnar.chunk_group_row_limit = 2000); -- insert some data and read INSERT INTO columnar_table VALUES (1), (1); SELECT * FROM columnar_table; a --------------------------------------------------------------------- 1 1 (2 rows) -- Fail to alter a columnar table that is created by a different user SET ROLE full_access; ALTER TABLE columnar_table SET (columnar.chunk_group_row_limit = 2000); ERROR: must be owner of table columnar_table -- Fail to reset a columnar table value created by a different user ALTER TABLE columnar_table RESET (columnar.chunk_group_row_limit); ERROR: must be owner of table columnar_table SET ROLE read_access; -- and drop it DROP TABLE columnar_table; -- cannot modify columnar metadata table as unprivileged user INSERT INTO columnar_internal.stripe VALUES(99); ERROR: permission denied for schema columnar_internal -- Cannot drop columnar metadata table as unprivileged user. -- Privileged user also cannot drop but with a different error message. -- (since citus extension has a dependency to it) DROP TABLE columnar_internal.chunk; ERROR: permission denied for schema columnar_internal SELECT * FROM columnar.chunk; relation | storage_id | stripe_num | attr_num | chunk_group_num | minimum_value | maximum_value | value_stream_offset | value_stream_length | exists_stream_offset | exists_stream_length | value_compression_type | value_compression_level | value_decompressed_length | value_count --------------------------------------------------------------------- (0 rows) -- test whether a read-only user can read from citus_tables view SELECT distribution_column FROM citus_tables WHERE table_name = 'test'::regclass; distribution_column --------------------------------------------------------------------- id (1 row) -- check no permission SET ROLE no_access; EXECUTE prepare_insert(1); ERROR: permission denied for table test EXECUTE prepare_select; ERROR: permission denied for table test INSERT INTO test VALUES (2); ERROR: permission denied for table test SELECT count(*) FROM test; ERROR: permission denied for table test SELECT count(*) FROM test WHERE id = 1; ERROR: permission denied for table test SET citus.enable_repartition_joins to ON; SELECT count(*), min(current_user) FROM test; ERROR: permission denied for table test -- test re-partition query SELECT count(*) FROM test a JOIN test b ON (a.val = b.val) WHERE a.id = 1 AND b.id = 2; ERROR: permission denied for table test SET citus.enable_repartition_joins TO true; SELECT count(*) FROM test a JOIN test b ON (a.val = b.val) WHERE a.id = 1 AND b.id = 2; ERROR: permission denied for table test -- should be able to use intermediate results as any user BEGIN; SELECT create_intermediate_result('topten', 'SELECT s FROM generate_series(1,10) s'); create_intermediate_result --------------------------------------------------------------------- 10 (1 row) SELECT * FROM read_intermediate_result('topten', 'binary'::citus_copy_format) AS res (s int) ORDER BY s; s --------------------------------------------------------------------- 1 2 3 4 5 6 7 8 9 10 (10 rows) END; -- as long as we don't read from a table BEGIN; SELECT create_intermediate_result('topten', 'SELECT count(*) FROM test'); ERROR: permission denied for table test ABORT; SELECT * FROM citus_stat_statements_reset(); ERROR: permission denied for function citus_stat_statements_reset -- should not be allowed to co-located tables SELECT update_distributed_table_colocation('test', colocate_with => 'test_coloc'); ERROR: must be owner of table test -- should not be allowed to take any locks BEGIN; SELECT lock_relation_if_exists('test', 'ACCESS SHARE'); ERROR: permission denied for table test ABORT; BEGIN; SELECT lock_relation_if_exists('test', 'EXCLUSIVE'); ERROR: permission denied for table test ABORT; -- table owner should be the same on the shards, even when distributing the table as superuser SET ROLE full_access; CREATE TABLE my_table (id integer, val integer); RESET ROLE; SELECT create_distributed_table('my_table', 'id'); create_distributed_table --------------------------------------------------------------------- (1 row) SELECT result FROM run_command_on_workers($$SELECT tableowner FROM pg_tables WHERE tablename LIKE 'my_table_%' LIMIT 1$$); result --------------------------------------------------------------------- full_access full_access (2 rows) -- table should be distributable by super user when it has data in there SET ROLE full_access; CREATE TABLE my_table_with_data (id integer, val integer); INSERT INTO my_table_with_data VALUES (1,2); RESET ROLE; SELECT create_distributed_table('my_table_with_data', 'id'); NOTICE: Copying data from local table... NOTICE: copying the data has completed DETAIL: The local data in the table is no longer visible, but is still on disk. HINT: To remove the local data, run: SELECT truncate_local_data_after_distributing_table($$public.my_table_with_data$$) create_distributed_table --------------------------------------------------------------------- (1 row) SELECT count(*) FROM my_table_with_data; count --------------------------------------------------------------------- 1 (1 row) -- table that is owned by a role should be distributable by a user that has that role granted -- while it should not be if the user has the role not granted SET ROLE full_access; CREATE TABLE my_role_table_with_data (id integer, val integer); ALTER TABLE my_role_table_with_data OWNER TO some_role; INSERT INTO my_role_table_with_data VALUES (1,2); RESET ROLE; -- we first try to distribute it with a user that does not have the role so we can reuse the table SET ROLE no_access; SELECT create_distributed_table('my_role_table_with_data', 'id'); ERROR: must be owner of table my_role_table_with_data RESET ROLE; -- then we try to distribute it with a user that has the role but different then the one creating SET ROLE read_access; SELECT create_distributed_table('my_role_table_with_data', 'id'); NOTICE: Copying data from local table... NOTICE: copying the data has completed DETAIL: The local data in the table is no longer visible, but is still on disk. HINT: To remove the local data, run: SELECT truncate_local_data_after_distributing_table($$public.my_role_table_with_data$$) create_distributed_table --------------------------------------------------------------------- (1 row) RESET ROLE; -- lastly we want to verify the table owner is set to the role, not the user that distributed SELECT result FROM run_command_on_workers($cmd$ SELECT tableowner FROM pg_tables WHERE tablename LIKE 'my_role_table_with_data%' LIMIT 1; $cmd$); result --------------------------------------------------------------------- some_role some_role (2 rows) -- we want to verify a user without CREATE access cannot distribute its table, but can get -- its table distributed by the super user -- we want to make sure the schema and user are setup in such a way they can't create a -- table SET ROLE usage_access; CREATE TABLE full_access_user_schema.t1 (id int); ERROR: permission denied for schema full_access_user_schema RESET ROLE; -- now we create the table for the user CREATE TABLE full_access_user_schema.t1 (id int); ALTER TABLE full_access_user_schema.t1 OWNER TO usage_access; -- make sure we can insert data SET ROLE usage_access; INSERT INTO full_access_user_schema.t1 VALUES (1),(2),(3); -- creating the table should fail with a failure on the worker machine since the user is -- not allowed to create a table SELECT create_distributed_table('full_access_user_schema.t1', 'id'); ERROR: permission denied for schema full_access_user_schema CONTEXT: while executing command on localhost:xxxxx RESET ROLE; SET ROLE usage_access; CREATE TYPE usage_access_type AS ENUM ('a', 'b'); CREATE FUNCTION usage_access_func(x usage_access_type, variadic v int[]) RETURNS int[] LANGUAGE plpgsql AS 'begin return v; end;'; SET ROLE no_access; SELECT create_distributed_function('usage_access_func(usage_access_type,int[])'); ERROR: must be owner of function usage_access_func SET ROLE usage_access; SELECT create_distributed_function('usage_access_func(usage_access_type,int[])'); NOTICE: procedure public.usage_access_func is already distributed DETAIL: Citus distributes procedures with CREATE [PROCEDURE|FUNCTION|AGGREGATE] commands create_distributed_function --------------------------------------------------------------------- (1 row) SELECT typowner::regrole FROM pg_type WHERE typname = 'usage_access_type'; typowner --------------------------------------------------------------------- usage_access (1 row) SELECT proowner::regrole FROM pg_proc WHERE proname = 'usage_access_func'; proowner --------------------------------------------------------------------- usage_access (1 row) SELECT run_command_on_workers($$SELECT typowner::regrole FROM pg_type WHERE typname = 'usage_access_type'$$); run_command_on_workers --------------------------------------------------------------------- (localhost,57637,t,usage_access) (localhost,57638,t,usage_access) (2 rows) SELECT run_command_on_workers($$SELECT proowner::regrole FROM pg_proc WHERE proname = 'usage_access_func'$$); run_command_on_workers --------------------------------------------------------------------- (localhost,57637,t,usage_access) (localhost,57638,t,usage_access) (2 rows) SELECT wait_until_metadata_sync(30000); wait_until_metadata_sync --------------------------------------------------------------------- (1 row) CREATE TABLE colocation_table(id text); SELECT create_distributed_table('colocation_table','id'); create_distributed_table --------------------------------------------------------------------- (1 row) -- now, make sure that the user can use the function -- created in the transaction BEGIN; CREATE FUNCTION usage_access_func_second(key int, variadic v int[]) RETURNS text LANGUAGE plpgsql AS 'begin return current_user; end;'; SELECT create_distributed_function('usage_access_func_second(int,int[])', '$1', colocate_with := 'colocation_table'); create_distributed_function --------------------------------------------------------------------- (1 row) SELECT usage_access_func_second(1, 2,3,4,5) FROM full_access_user_schema.t1 LIMIT 1; usage_access_func_second --------------------------------------------------------------------- usage_access (1 row) ROLLBACK; CREATE FUNCTION usage_access_func_third(key int, variadic v int[]) RETURNS text LANGUAGE plpgsql AS 'begin return current_user; end;'; -- connect back as super user \c - - - :master_port -- show that the current user is a super user SELECT usesuper FROM pg_user where usename IN (SELECT current_user); usesuper --------------------------------------------------------------------- t (1 row) -- superuser creates the distributed function that is owned by a regular user SELECT create_distributed_function('usage_access_func_third(int,int[])', '$1', colocate_with := 'colocation_table'); create_distributed_function --------------------------------------------------------------------- (1 row) SELECT proowner::regrole FROM pg_proc WHERE proname = 'usage_access_func_third'; proowner --------------------------------------------------------------------- usage_access (1 row) SELECT run_command_on_workers($$SELECT proowner::regrole FROM pg_proc WHERE proname = 'usage_access_func_third'$$); run_command_on_workers --------------------------------------------------------------------- (localhost,57637,t,usage_access) (localhost,57638,t,usage_access) (2 rows) RESET ROLE; -- now we distribute the table as super user SELECT create_distributed_table('full_access_user_schema.t1', 'id'); NOTICE: Copying data from local table... NOTICE: copying the data has completed DETAIL: The local data in the table is no longer visible, but is still on disk. HINT: To remove the local data, run: SELECT truncate_local_data_after_distributing_table($$full_access_user_schema.t1$$) create_distributed_table --------------------------------------------------------------------- (1 row) -- verify the owner of the shards for the distributed tables SELECT result FROM run_command_on_workers($cmd$ SELECT tableowner FROM pg_tables WHERE true AND schemaname = 'full_access_user_schema' AND tablename LIKE 't1_%' LIMIT 1; $cmd$); result --------------------------------------------------------------------- usage_access usage_access (2 rows) -- a user with all privileges on a schema should be able to distribute tables SET ROLE full_access; CREATE TABLE full_access_user_schema.t2(id int); SELECT create_distributed_table('full_access_user_schema.t2', 'id'); create_distributed_table --------------------------------------------------------------------- (1 row) RESET ROLE; DROP SCHEMA full_access_user_schema CASCADE; NOTICE: drop cascades to 2 other objects DETAIL: drop cascades to table full_access_user_schema.t1 drop cascades to table full_access_user_schema.t2 DROP TABLE my_table, my_table_with_data, my_role_table_with_data, singleshard, test, test_coloc, colocation_table; DROP USER full_access; DROP USER read_access; DROP USER no_access; DROP ROLE some_role;