mirror of
https://github.com/timescale/timescaledb.git
synced 2025-05-22 13:40:56 +08:00
Propagate grants when creating hypertables
When creating a hypertable, grants were not propagated to the table on the remote node, which causes later statements to fail when not executed as the owner of the table. This commit deparse grant statements from the table definition and add the grants to the deparsed statement to send when creating the table on the data node.
This commit is contained in:
parent
ef823a3060
commit
96dd266a0b
@ -6,6 +6,7 @@
|
||||
#include <postgres.h>
|
||||
#include <utils/rel.h>
|
||||
#include <lib/stringinfo.h>
|
||||
#include <utils/acl.h>
|
||||
#include <utils/builtins.h>
|
||||
#include <utils/lsyscache.h>
|
||||
#include <utils/relcache.h>
|
||||
@ -19,6 +20,7 @@
|
||||
#include <catalog/indexing.h>
|
||||
#include <catalog/pg_constraint.h>
|
||||
#include <catalog/pg_index.h>
|
||||
#include <catalog/pg_authid.h>
|
||||
#include <catalog/pg_proc.h>
|
||||
#include <catalog/namespace.h>
|
||||
#include <nodes/pg_list.h>
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,7 @@ typedef struct DeparsedHypertableCommands
|
||||
{
|
||||
const char *table_create_command;
|
||||
List *dimension_add_commands;
|
||||
List *grant_commands;
|
||||
} DeparsedHypertableCommands;
|
||||
|
||||
typedef struct Hypertable Hypertable;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
155
tsl/test/expected/dist_grant.out
Normal file
155
tsl/test/expected/dist_grant.out
Normal file
@ -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)
|
||||
|
@ -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
|
||||
|
52
tsl/test/sql/dist_grant.sql
Normal file
52
tsl/test/sql/dist_grant.sql
Normal file
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user