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:
Mats Kindahl 2019-11-11 13:44:58 +01:00 committed by Erik Nordström
parent ef823a3060
commit 96dd266a0b
7 changed files with 329 additions and 1 deletions

View File

@ -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;
}

View File

@ -32,6 +32,7 @@ typedef struct DeparsedHypertableCommands
{
const char *table_create_command;
List *dimension_add_commands;
List *grant_commands;
} DeparsedHypertableCommands;
typedef struct Hypertable Hypertable;

View File

@ -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;
}

View File

@ -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);
}

View 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)

View File

@ -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

View 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;