diff --git a/tsl/src/deparse.c b/tsl/src/deparse.c index e034d86f8..9b9e1c8bb 100644 --- a/tsl/src/deparse.c +++ b/tsl/src/deparse.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -19,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -348,6 +350,115 @@ deparse_get_tabledef(TableInfo *table_info) return table_def; } +/* + * Append a privilege name to a string if the privilege is set. + * + * Parameters: + * buf: Buffer to append to. + * pfirst: Pointer to variable to remember if elements are already added. + * privs: Bitmap of privilege flags. + * mask: Mask for privilege to check. + * priv_name: String with name of privilege to add. + */ +static void +append_priv_if_set(StringInfo buf, bool *priv_added, uint32 privs, uint32 mask, + const char *priv_name) +{ + if (privs & mask) + { + if (*priv_added) + appendStringInfoString(buf, ", "); + else + *priv_added = true; + appendStringInfoString(buf, priv_name); + } +} + +static void +append_privs_as_text(StringInfo buf, uint32 privs) +{ + bool priv_added = false; + append_priv_if_set(buf, &priv_added, privs, ACL_INSERT, "INSERT"); + append_priv_if_set(buf, &priv_added, privs, ACL_SELECT, "SELECT"); + append_priv_if_set(buf, &priv_added, privs, ACL_UPDATE, "UPDATE"); + append_priv_if_set(buf, &priv_added, privs, ACL_DELETE, "DELETE"); + append_priv_if_set(buf, &priv_added, privs, ACL_TRUNCATE, "TRUNCATE"); + append_priv_if_set(buf, &priv_added, privs, ACL_REFERENCES, "REFERENCES"); + append_priv_if_set(buf, &priv_added, privs, ACL_TRIGGER, "TRIGGER"); +} + +/* + * Create grant statements for a relation. + * + * This will create a list of grant statements, one for each role. + */ +static List * +deparse_grant_commands_for_relid(Oid relid) +{ + HeapTuple reltup; + Form_pg_class pg_class_tuple; + List *cmds = NIL; + Datum acl_datum; + bool is_null; + Oid owner_id; + Acl *acl; + int i; + const AclItem *acldat; + + reltup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid)); + if (!HeapTupleIsValid(reltup)) + elog(ERROR, "cache lookup failed for relation %u", relid); + pg_class_tuple = (Form_pg_class) GETSTRUCT(reltup); + + if (pg_class_tuple->relkind != RELKIND_RELATION) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("\"%s\" is not an ordinary table", NameStr(pg_class_tuple->relname)))); + + owner_id = pg_class_tuple->relowner; + acl_datum = SysCacheGetAttr(RELOID, reltup, Anum_pg_class_relacl, &is_null); + + if (is_null) + acl = acldefault(OBJECT_TABLE, owner_id); + else + acl = DatumGetAclP(acl_datum); + + acldat = ACL_DAT(acl); + for (i = 0; i < ACL_NUM(acl); i++) + { + const AclItem *aclitem = &acldat[i]; + Oid role_id = aclitem->ai_grantee; + StringInfo grant_cmd; + HeapTuple utup; + + /* We skip the owner of the table since she automatically have all + * privileges on the table. */ + if (role_id == owner_id) + continue; + + grant_cmd = makeStringInfo(); + utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(role_id)); + + if (!HeapTupleIsValid(utup)) + continue; + + appendStringInfoString(grant_cmd, "GRANT "); + append_privs_as_text(grant_cmd, aclitem->ai_privs); + appendStringInfo(grant_cmd, + " ON TABLE %s.%s TO %s", + quote_identifier(get_namespace_name(pg_class_tuple->relnamespace)), + quote_identifier(NameStr(pg_class_tuple->relname)), + quote_identifier(NameStr(((Form_pg_authid) GETSTRUCT(utup))->rolname))); + + ReleaseSysCache(utup); + cmds = lappend(cmds, grant_cmd->data); + } + + ReleaseSysCache(reltup); + + return cmds; +} + List * deparse_get_tabledef_commands(Oid relid) { @@ -488,6 +599,8 @@ deparse_get_distributed_hypertable_create_command(Hypertable *ht) (char *) deparse_get_add_dimension_command(ht, &space->dimensions[i])); } + result->grant_commands = deparse_grant_commands_for_relid(ht->main_table_relid); + return result; } diff --git a/tsl/src/deparse.h b/tsl/src/deparse.h index 22c261619..b4307ff4a 100644 --- a/tsl/src/deparse.h +++ b/tsl/src/deparse.h @@ -32,6 +32,7 @@ typedef struct DeparsedHypertableCommands { const char *table_create_command; List *dimension_add_commands; + List *grant_commands; } DeparsedHypertableCommands; typedef struct Hypertable Hypertable; diff --git a/tsl/src/hypertable.c b/tsl/src/hypertable.c index 078eea14b..dd99d9d39 100644 --- a/tsl/src/hypertable.c +++ b/tsl/src/hypertable.c @@ -94,6 +94,9 @@ hypertable_create_backend_tables(int32 hypertable_id, List *data_nodes) foreach (cell, commands->dimension_add_commands) ts_dist_cmd_run_on_data_nodes(lfirst(cell), data_nodes); + foreach (cell, commands->grant_commands) + ts_dist_cmd_run_on_data_nodes(lfirst(cell), data_nodes); + return remote_ids; } diff --git a/tsl/src/remote/dist_commands.c b/tsl/src/remote/dist_commands.c index 397f30673..e11fd052e 100644 --- a/tsl/src/remote/dist_commands.c +++ b/tsl/src/remote/dist_commands.c @@ -97,10 +97,13 @@ ts_dist_cmd_invoke_on_data_nodes(const char *sql, List *data_nodes, bool transac foreach (lc, data_nodes) { const char *node_name = lfirst(lc); + AsyncRequest *req; TSConnection *connection = data_node_get_connection(node_name, REMOTE_TXN_NO_PREP_STMT, transactional); - AsyncRequest *req = async_request_send(connection, sql); + ereport(DEBUG2, (errmsg_internal("sending \"%s\" to data node \"%s\"", sql, node_name))); + + req = async_request_send(connection, sql); async_request_attach_user_data(req, (char *) node_name); requests = lappend(requests, req); } diff --git a/tsl/test/expected/dist_grant.out b/tsl/test/expected/dist_grant.out new file mode 100644 index 000000000..d0d3ce80a --- /dev/null +++ b/tsl/test/expected/dist_grant.out @@ -0,0 +1,155 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. +-- Need to be super user to create extension and add data nodes +\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER; +\unset ECHO +psql:include/remote_exec.sql:5: NOTICE: schema "test" already exists, skipping +DROP TABLE IF EXISTS conditions; +NOTICE: table "conditions" does not exist, skipping +SELECT * FROM add_data_node('data1', host => 'localhost', database => 'data1'); + node_name | host | port | database | node_created | database_created | extension_created +-----------+-----------+-------+----------+--------------+------------------+------------------- + data1 | localhost | 15432 | data1 | t | t | t +(1 row) + +SELECT * FROM add_data_node('data2', host => 'localhost', database => 'data2'); + node_name | host | port | database | node_created | database_created | extension_created +-----------+-----------+-------+----------+--------------+------------------+------------------- + data2 | localhost | 15432 | data2 | t | t | t +(1 row) + +SELECT * FROM add_data_node('data3', host => 'localhost', database => 'data3'); + node_name | host | port | database | node_created | database_created | extension_created +-----------+-----------+-------+----------+--------------+------------------+------------------- + data3 | localhost | 15432 | data3 | t | t | t +(1 row) + +CREATE TABLE conditions(time TIMESTAMPTZ NOT NULL, device INTEGER, temperature FLOAT, humidity FLOAT); +GRANT SELECT ON conditions TO :ROLE_1; +GRANT INSERT, DELETE ON conditions TO :ROLE_2; +SELECT relname, relacl FROM pg_class WHERE relname = 'conditions'; + relname | relacl +------------+-------------------------------------------------------------------------------------------------------------------- + conditions | {cluster_super_user=arwdDxt/cluster_super_user,test_role_1=r/cluster_super_user,test_role_2=ad/cluster_super_user} +(1 row) + +SELECT * FROM create_distributed_hypertable('conditions', 'time', 'device'); + hypertable_id | schema_name | table_name | created +---------------+-------------+------------+--------- + 1 | public | conditions | t +(1 row) + +SELECT has_table_privilege(:'ROLE_1', 'conditions', 'SELECT') AS "SELECT" + , has_table_privilege(:'ROLE_1', 'conditions', 'DELETE') AS "DELETE" + , has_table_privilege(:'ROLE_1', 'conditions', 'INSERT') AS "INSERT"; + SELECT | DELETE | INSERT +--------+--------+-------- + t | f | f +(1 row) + +SELECT * FROM test.remote_exec(NULL, format($$ + SELECT has_table_privilege('%s', 'conditions', 'SELECT') AS "SELECT" + , has_table_privilege('%s', 'conditions', 'DELETE') AS "DELETE" + , has_table_privilege('%s', 'conditions', 'INSERT') AS "INSERT"; +$$, :'ROLE_1', :'ROLE_1', :'ROLE_1')); +NOTICE: [data1]: + SELECT has_table_privilege('test_role_1', 'conditions', 'SELECT') AS "SELECT" + , has_table_privilege('test_role_1', 'conditions', 'DELETE') AS "DELETE" + , has_table_privilege('test_role_1', 'conditions', 'INSERT') AS "INSERT" +NOTICE: [data1]: +SELECT|DELETE|INSERT +------+------+------ +t |f |f +(1 row) + + +NOTICE: [data2]: + SELECT has_table_privilege('test_role_1', 'conditions', 'SELECT') AS "SELECT" + , has_table_privilege('test_role_1', 'conditions', 'DELETE') AS "DELETE" + , has_table_privilege('test_role_1', 'conditions', 'INSERT') AS "INSERT" +NOTICE: [data2]: +SELECT|DELETE|INSERT +------+------+------ +t |f |f +(1 row) + + +NOTICE: [data3]: + SELECT has_table_privilege('test_role_1', 'conditions', 'SELECT') AS "SELECT" + , has_table_privilege('test_role_1', 'conditions', 'DELETE') AS "DELETE" + , has_table_privilege('test_role_1', 'conditions', 'INSERT') AS "INSERT" +NOTICE: [data3]: +SELECT|DELETE|INSERT +------+------+------ +t |f |f +(1 row) + + + remote_exec +------------- + +(1 row) + +SELECT has_table_privilege(:'ROLE_2', 'conditions', 'SELECT') AS "SELECT" + , has_table_privilege(:'ROLE_2', 'conditions', 'DELETE') AS "DELETE" + , has_table_privilege(:'ROLE_2', 'conditions', 'INSERT') AS "INSERT"; + SELECT | DELETE | INSERT +--------+--------+-------- + f | t | t +(1 row) + +SELECT * FROM test.remote_exec(NULL, format($$ + SELECT has_table_privilege('%s', 'conditions', 'SELECT') AS "SELECT" + , has_table_privilege('%s', 'conditions', 'DELETE') AS "DELETE" + , has_table_privilege('%s', 'conditions', 'INSERT') AS "INSERT"; +$$, :'ROLE_2', :'ROLE_2', :'ROLE_2')); +NOTICE: [data1]: + SELECT has_table_privilege('test_role_2', 'conditions', 'SELECT') AS "SELECT" + , has_table_privilege('test_role_2', 'conditions', 'DELETE') AS "DELETE" + , has_table_privilege('test_role_2', 'conditions', 'INSERT') AS "INSERT" +NOTICE: [data1]: +SELECT|DELETE|INSERT +------+------+------ +f |t |t +(1 row) + + +NOTICE: [data2]: + SELECT has_table_privilege('test_role_2', 'conditions', 'SELECT') AS "SELECT" + , has_table_privilege('test_role_2', 'conditions', 'DELETE') AS "DELETE" + , has_table_privilege('test_role_2', 'conditions', 'INSERT') AS "INSERT" +NOTICE: [data2]: +SELECT|DELETE|INSERT +------+------+------ +f |t |t +(1 row) + + +NOTICE: [data3]: + SELECT has_table_privilege('test_role_2', 'conditions', 'SELECT') AS "SELECT" + , has_table_privilege('test_role_2', 'conditions', 'DELETE') AS "DELETE" + , has_table_privilege('test_role_2', 'conditions', 'INSERT') AS "INSERT" +NOTICE: [data3]: +SELECT|DELETE|INSERT +------+------+------ +f |t |t +(1 row) + + + remote_exec +------------- + +(1 row) + +INSERT INTO conditions +SELECT time, (random()*30)::int, random()*80 +FROM generate_series('2019-01-01 00:00:00'::timestamptz, '2019-02-01 00:00:00', '1 min') AS time; +-- Check that we can actually execute a select as non-owner +SET ROLE :ROLE_1; +SELECT COUNT(*) FROM conditions; + count +------- + 44641 +(1 row) + diff --git a/tsl/test/sql/CMakeLists.txt b/tsl/test/sql/CMakeLists.txt index 0a54d92b4..a13918e36 100644 --- a/tsl/test/sql/CMakeLists.txt +++ b/tsl/test/sql/CMakeLists.txt @@ -92,6 +92,7 @@ if (PG_VERSION_SUPPORTS_MULTINODE) deparse_fail.sql dist_commands.sql dist_ddl.sql + dist_grant.sql dist_partial_agg.sql dist_util.sql remote_connection.sql diff --git a/tsl/test/sql/dist_grant.sql b/tsl/test/sql/dist_grant.sql new file mode 100644 index 000000000..4bcd585ee --- /dev/null +++ b/tsl/test/sql/dist_grant.sql @@ -0,0 +1,52 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. + +-- Need to be super user to create extension and add data nodes +\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER; +\unset ECHO +\o /dev/null +\ir include/remote_exec.sql +\o +\set ECHO all + +DROP TABLE IF EXISTS conditions; + +SELECT * FROM add_data_node('data1', host => 'localhost', database => 'data1'); +SELECT * FROM add_data_node('data2', host => 'localhost', database => 'data2'); +SELECT * FROM add_data_node('data3', host => 'localhost', database => 'data3'); + +CREATE TABLE conditions(time TIMESTAMPTZ NOT NULL, device INTEGER, temperature FLOAT, humidity FLOAT); +GRANT SELECT ON conditions TO :ROLE_1; +GRANT INSERT, DELETE ON conditions TO :ROLE_2; +SELECT relname, relacl FROM pg_class WHERE relname = 'conditions'; + +SELECT * FROM create_distributed_hypertable('conditions', 'time', 'device'); +SELECT has_table_privilege(:'ROLE_1', 'conditions', 'SELECT') AS "SELECT" + , has_table_privilege(:'ROLE_1', 'conditions', 'DELETE') AS "DELETE" + , has_table_privilege(:'ROLE_1', 'conditions', 'INSERT') AS "INSERT"; + +SELECT * FROM test.remote_exec(NULL, format($$ + SELECT has_table_privilege('%s', 'conditions', 'SELECT') AS "SELECT" + , has_table_privilege('%s', 'conditions', 'DELETE') AS "DELETE" + , has_table_privilege('%s', 'conditions', 'INSERT') AS "INSERT"; +$$, :'ROLE_1', :'ROLE_1', :'ROLE_1')); + +SELECT has_table_privilege(:'ROLE_2', 'conditions', 'SELECT') AS "SELECT" + , has_table_privilege(:'ROLE_2', 'conditions', 'DELETE') AS "DELETE" + , has_table_privilege(:'ROLE_2', 'conditions', 'INSERT') AS "INSERT"; + +SELECT * FROM test.remote_exec(NULL, format($$ + SELECT has_table_privilege('%s', 'conditions', 'SELECT') AS "SELECT" + , has_table_privilege('%s', 'conditions', 'DELETE') AS "DELETE" + , has_table_privilege('%s', 'conditions', 'INSERT') AS "INSERT"; +$$, :'ROLE_2', :'ROLE_2', :'ROLE_2')); + +INSERT INTO conditions +SELECT time, (random()*30)::int, random()*80 +FROM generate_series('2019-01-01 00:00:00'::timestamptz, '2019-02-01 00:00:00', '1 min') AS time; + +-- Check that we can actually execute a select as non-owner +SET ROLE :ROLE_1; +SELECT COUNT(*) FROM conditions; +