diff options
author | Marko Kreen | 2012-05-02 13:45:00 +0000 |
---|---|---|
committer | Marko Kreen | 2012-05-10 18:19:24 +0000 |
commit | 128f094b0852944366856bdc56badb1b27dad40c (patch) | |
tree | 70e051b590ee966e7fa2f735a6914684fc4048ff | |
parent | f82bdd553aabbdee0b44fc2c91e182baa8928d87 (diff) |
Group-access roles for SQL functions (draft)
This is attempt for fine-grained access rights for all
Skytools SQL schemas. As it still needs review,
the rights are not activated by default, instead
following sql files are generated:
newgrants_<schema>.sql - applies new rights, drop old public access
oldgrants_<schema>.sql - restores old rights - public execute
privilege to all functions
Only thing that is active by default is creation of new
groups in upgrade functions.
New access roles:
pgq_reader
Can consume queues (source-side)
pgq_writer
Can write into queues (source-side / dest-side)
Can use pgq_node/pgq_ext schema as regular
consumer (dest-side)
pgq_admin
Admin operations on queues, required for CascadedWorker on dest-side.
Member of pgq_reader and pgq_writer.
londiste_reader
Member of pgq_reader, needs additional read access to tables.
(source-side)
londiste_writer
Member of pgq_admin, needs additional write access to tables.
(dest-side)
29 files changed, 935 insertions, 31 deletions
diff --git a/doc/Makefile b/doc/Makefile index d62a7355..3937572d 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -12,6 +12,7 @@ DOCHTML = \ TODO.html pgq-sql.html pgq-nodupes.html \ faq.html set.notes.html skytools3.html devnotes.html pgqd.html \ londiste3.html walmgr3.html qadmin.html scriptmgr.html \ + sql-grants.html \ skytools_upgrade.html queue_mover.html queue_splitter.html \ howto/londiste3_cascaded_rep_howto.html \ howto/londiste3_merge_howto.html \ diff --git a/doc/sql-grants.txt b/doc/sql-grants.txt new file mode 100644 index 00000000..f388910e --- /dev/null +++ b/doc/sql-grants.txt @@ -0,0 +1,40 @@ + += SQL permissions (draft) = + +== Setup == + +Currently following no-login roles are created during upgrade: +`pgq_reader`, `pgq_writer`, `pgq_admin`, `londiste_reader`, `londiste_writer`. + +Actual grants are not applied to functions, instead default +`public:execute` grants are kept. New grants can be applied +manually: + +newgrants_<schema>.sql:: + applies new rights, drop old public access + +oldgrants_<schema>.sql:: + restores old rights - public execute privilege to all functions + +== New roles == + +pgq_reader:: + Can consume queues (source-side) + +pgq_writer:: + Can write into queues (source-side / dest-side) + Can use `pgq_node`/`pgq_ext` schema as regular + consumer (dest-side) + +pgq_admin:: + Admin operations on queues, required for CascadedWorker on dest-side. + Member of `pgq_reader` and `pgq_writer`. + +londiste_reader:: + Member of `pgq_reader`, needs additional read access to tables. + (source-side) + +londiste_writer:: + Member of `pgq_admin`, needs additional write access to tables. + (dest-side) + diff --git a/scripts/grantfu.py b/scripts/grantfu.py new file mode 100755 index 00000000..5d89d559 --- /dev/null +++ b/scripts/grantfu.py @@ -0,0 +1,331 @@ +#! /usr/bin/env python + +# GrantFu - GRANT/REVOKE generator for Postgres +# +# Copyright (c) 2005 Marko Kreen +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +"""Generator for PostgreSQL permissions. + +Loads config where roles, objects and their mapping is described +and generates grants based on them. + +ConfigParser docs: http://docs.python.org/lib/module-ConfigParser.html + +Example: +-------------------------------------------------------------------- +[DEFAULT] +users = user1, user2 # users to handle +groups = group1, group2 # groups to handle +auto_seq = 0 # dont handle seqs (default) + # '!' after a table negates this setting for a table +seq_name = id # the name for serial field (default: id) +seq_usage = 0 # should we grant "usage" or "select, update" + # for automatically handled sequences + +# section names can be random, but if you want to see them +# in same order as in config file, then order them alphabetically +[1.section] +on.tables = testtbl, testtbl_id_seq, # here we handle seq by hand + table_with_seq! # handle seq automatically + # (table_with_seq_id_seq) +user1 = select +group1 = select, insert, update + +# instead of 'tables', you may use 'functions', 'languages', +# 'schemas', 'tablespaces' +--------------------------------------------------------------------- +""" + +import sys, os, getopt +from ConfigParser import SafeConfigParser + +__version__ = "1.0" + +R_NEW = 0x01 +R_DEFS = 0x02 +G_DEFS = 0x04 +R_ONLY = 0x80 + +def usage(err): + sys.stderr.write("usage: %s [-r|-R] CONF_FILE\n" % sys.argv[0]) + sys.stderr.write(" -r Generate also REVOKE commands\n") + sys.stderr.write(" -R Generate only REVOKE commands\n") + sys.stderr.write(" -d Also REVOKE default perms\n") + sys.stderr.write(" -D Only REVOKE default perms\n") + sys.stderr.write(" -o Generate default GRANTS\n") + sys.stderr.write(" -v Print program version\n") + sys.stderr.write(" -t Put everything in one big transaction\n") + sys.exit(err) + +class PConf(SafeConfigParser): + "List support for ConfigParser" + def __init__(self, defaults = None): + SafeConfigParser.__init__(self, defaults) + + def get_list(self, sect, key): + str = self.get(sect, key).strip() + res = [] + if not str: + return res + for val in str.split(","): + res.append(val.strip()) + return res + +class GrantFu: + def __init__(self, cf_file, revoke): + self.revoke = revoke + + # load config + self.cf = PConf() + self.cf.read(cf_file) + if not self.cf.has_section("GrantFu"): + print "Incorrect config file, GrantFu sction missing" + sys.exit(1) + + # avoid putting grantfu vars into defaults, thus into every section + self.group_list = [] + self.user_list = [] + self.auto_seq = 0 + self.seq_name = "id" + self.seq_usage = 0 + if self.cf.has_option('GrantFu', 'groups'): + self.group_list = self.cf.get_list('GrantFu', 'groups') + if self.cf.has_option('GrantFu', 'users'): + self.user_list += self.cf.get_list('GrantFu', 'users') + if self.cf.has_option('GrantFu', 'roles'): + self.user_list += self.cf.get_list('GrantFu', 'roles') + if self.cf.has_option('GrantFu', 'auto_seq'): + self.auto_seq = self.cf.getint('GrantFu', 'auto_seq') + if self.cf.has_option('GrantFu', 'seq_name'): + self.seq_name = self.cf.get('GrantFu', 'seq_name') + if self.cf.has_option('GrantFu', 'seq_usage'): + self.seq_usage = self.cf.getint('GrantFu', 'seq_usage') + + # make string of all subjects + tmp = [] + for g in self.group_list: + tmp.append("group " + g) + for u in self.user_list: + tmp.append(u) + self.all_subjs = ", ".join(tmp) + + # per-section vars + self.sect = None + self.seq_list = [] + self.seq_allowed = [] + + def process(self): + if len(self.user_list) == 0 and len(self.group_list) == 0: + return + + sect_list = self.cf.sections() + sect_list.sort() + for self.sect in sect_list: + if self.sect == "GrantFu": + continue + print "\n-- %s --" % self.sect + + self.handle_tables() + self.handle_other('on.databases', 'DATABASE') + self.handle_other('on.functions', 'FUNCTION') + self.handle_other('on.languages', 'LANGUAGE') + self.handle_other('on.schemas', 'SCHEMA') + self.handle_other('on.tablespaces', 'TABLESPACE') + self.handle_other('on.sequences', 'SEQUENCE') + self.handle_other('on.types', 'TYPE') + self.handle_other('on.domains', 'DOMAIN') + + def handle_other(self, listname, obj_type): + """Handle grants for all objects except tables.""" + + if not self.sect_hasvar(listname): + return + + # don't parse list, as in case of functions it may be complicated + obj_str = obj_type + " " + self.sect_var(listname) + + if self.revoke & R_NEW: + self.gen_revoke(obj_str) + + if self.revoke & R_DEFS: + self.gen_revoke_defs(obj_str, obj_type) + + if not self.revoke & R_ONLY: + self.gen_one_type(obj_str) + + if self.revoke & G_DEFS: + self.gen_defs(obj_str, obj_type) + + def handle_tables(self): + """Handle grants for tables and sequences. + + The tricky part here is the automatic handling of sequences.""" + + if not self.sect_hasvar('on.tables'): + return + + cleaned_list = [] + table_list = self.sect_list('on.tables') + for table in table_list: + if table[-1] == '!': + table = table[:-1] + if not self.auto_seq: + self.seq_list.append("%s_%s_seq" % (table, self.seq_name)) + else: + if self.auto_seq: + self.seq_list.append("%s_%s_seq" % (table, self.seq_name)) + cleaned_list.append(table) + obj_str = "TABLE " + ", ".join(cleaned_list) + + if self.revoke & R_NEW: + self.gen_revoke(obj_str) + if self.revoke & R_DEFS: + self.gen_revoke_defs(obj_str, "TABLE") + if not self.revoke & R_ONLY: + self.gen_one_type(obj_str) + if self.revoke & G_DEFS: + self.gen_defs(obj_str, "TABLE") + + # cleanup + self.seq_list = [] + self.seq_allowed = [] + + def gen_revoke(self, obj_str): + "Generate revoke for one section / subject type (user or group)" + + if len(self.seq_list) > 0: + obj_str += ", " + ", ".join(self.seq_list) + obj_str = obj_str.strip().replace('\n', '\n ') + print "REVOKE ALL ON %s\n FROM %s CASCADE;" % (obj_str, self.all_subjs) + + def gen_revoke_defs(self, obj_str, obj_type): + "Generate revoke defaults for one section" + + # process only things that have default grants to public + if obj_type not in ('FUNCTION', 'DATABASE', 'LANGUAGE', 'TYPE', 'DOMAIN'): + return + + defrole = 'public' + + # if the sections contains grants to 'public', dont drop + if self.sect_hasvar(defrole): + return + + obj_str = obj_str.strip().replace('\n', '\n ') + print "REVOKE ALL ON %s\n FROM %s CASCADE;" % (obj_str, defrole) + + def gen_defs(self, obj_str, obj_type): + "Generate defaults grants for one section" + + if obj_type == "FUNCTION": + defgrants = "execute" + elif obj_type == "DATABASE": + defgrants = "connect, temp" + elif obj_type in ("LANGUAGE", "TYPE", "DOMAIN"): + defgrants = "usage" + else: + return + + defrole = 'public' + + obj_str = obj_str.strip().replace('\n', '\n ') + print "GRANT %s ON %s\n TO %s;" % (defgrants, obj_str, defrole) + + def gen_one_subj(self, subj, fqsubj, obj_str): + if not self.sect_hasvar(subj): + return + obj_str = obj_str.strip().replace('\n', '\n ') + perm = self.sect_var(subj).strip() + if perm: + print "GRANT %s ON %s\n TO %s;" % (perm, obj_str, fqsubj) + + # check for seq perms + if len(self.seq_list) > 0: + loperm = perm.lower() + if loperm.find("insert") >= 0 or loperm.find("all") >= 0: + self.seq_allowed.append(fqsubj) + + def gen_one_type(self, obj_str): + "Generate GRANT for one section / one object type in section" + + for u in self.user_list: + self.gen_one_subj(u, u, obj_str) + for g in self.group_list: + self.gen_one_subj(g, "group " + g, obj_str) + + # if there was any seq perms, generate grants + if len(self.seq_allowed) > 0: + seq_str = ", ".join(self.seq_list) + subj_str = ", ".join(self.seq_allowed) + if self.seq_usage: + cmd = "GRANT usage ON SEQUENCE %s\n TO %s;" + else: + cmd = "GRANT select, update ON %s\n TO %s;" + print cmd % (seq_str, subj_str) + + def sect_var(self, name): + return self.cf.get(self.sect, name).strip() + + def sect_list(self, name): + return self.cf.get_list(self.sect, name) + + def sect_hasvar(self, name): + return self.cf.has_option(self.sect, name) + +def main(): + revoke = 0 + tx = False + + try: + opts, args = getopt.getopt(sys.argv[1:], "vhrRdDot") + except getopt.error, det: + print "getopt error:", det + usage(1) + + for o, v in opts: + if o == "-h": + usage(0) + elif o == "-r": + revoke |= R_NEW + elif o == "-R": + revoke |= R_NEW | R_ONLY + elif o == "-d": + revoke |= R_DEFS + elif o == "-D": + revoke |= R_DEFS | R_ONLY + elif o == "-o": + revoke |= G_DEFS + elif o == "-t": + tx = True + elif o == "-v": + print "GrantFu version", __version__ + sys.exit(0) + + if len(args) != 1: + usage(1) + + if tx: + print "begin;\n" + + g = GrantFu(args[0], revoke) + g.process() + + if tx: + print "\ncommit;\n" + +if __name__ == '__main__': + main() + diff --git a/sql/londiste/Makefile b/sql/londiste/Makefile index c81efb34..55a526dd 100644 --- a/sql/londiste/Makefile +++ b/sql/londiste/Makefile @@ -1,5 +1,7 @@ -DATA_built = londiste.sql londiste.upgrade.sql +DATA_built = londiste.sql londiste.upgrade.sql \ + structure/oldgrants_londiste.sql \ + structure/newgrants_londiste.sql SQLS = $(shell sed -e 's/^[^\\].*//' -e 's/\\i //' structure/install.sql) FUNCS = $(shell sed -e 's/^[^\\].*//' -e 's/\\i //' $(SQLS)) @@ -8,6 +10,7 @@ SRCS = $(SQLS) $(FUNCS) NDOC = NaturalDocs NDOCARGS = -r -o html docs/html -p docs -i docs/sql CATSQL = ../../scripts/catsql.py +GRANTFU = ../../scripts/grantfu.py REGRESS = londiste_install londiste_provider londiste_subscriber \ londiste_fkeys londiste_execute londiste_seqs londiste_merge \ @@ -27,6 +30,15 @@ londiste.sql: $(SRCS) londiste.upgrade.sql: $(SRCS) $(CATSQL) structure/upgrade.sql > $@ +structure/newgrants_londiste.sql: structure/grants.ini + $(GRANTFU) -r -d -t $< > $@ + +structure/oldgrants_londiste.sql: structure/grants.ini + echo "begin;" > $@ + $(GRANTFU) -R -o $< >> $@ + cat structure/grants.sql >> $@ + echo "commit;" >> $@ + test: londiste.sql $(MAKE) installcheck || { filterdiff --format=unified regression.diffs | less; exit 1; } diff --git a/sql/londiste/functions/londiste.upgrade_schema.sql b/sql/londiste/functions/londiste.upgrade_schema.sql index b982a0cc..9067ed7d 100644 --- a/sql/londiste/functions/londiste.upgrade_schema.sql +++ b/sql/londiste/functions/londiste.upgrade_schema.sql @@ -28,6 +28,18 @@ begin alter table londiste.table_info add column dest_table text; end if; + -- create roles + perform 1 from pg_catalog.pg_roles where rolname = 'londiste_writer'; + if not found then + create role londiste_writer in role pgq_admin; + cnt := cnt + 1; + end if; + perform 1 from pg_catalog.pg_roles where rolname = 'londiste_reader'; + if not found then + create role londiste_reader in role pgq_reader; + cnt := cnt + 1; + end if; + return cnt; end; $$ language plpgsql; diff --git a/sql/londiste/structure/grants.ini b/sql/londiste/structure/grants.ini new file mode 100644 index 00000000..26dff49a --- /dev/null +++ b/sql/londiste/structure/grants.ini @@ -0,0 +1,87 @@ + +[GrantFu] +# roles that we maintain in this file +roles = londiste_local, londiste_remote, public + + +[1.tables] +on.tables = londiste.table_info, londiste.seq_info, londiste.pending_fkeys, londiste.applied_execute + +londiste_local = select, insert, update, delete +londiste_remote = select + +# backwards compat, should be dropped? +public = select + + +[2.public.fns] +on.functions = %(londiste_public_fns)s +public = execute + + +[3.remote.node] +on.functions = %(londiste_remote_fns)s +londiste_remote = execute +londiste_local = execute + + +[3.local.node] +on.functions = %(londiste_local_fns)s, %(londiste_internal_fns)s +londiste_local = execute + + +# define various groups of functions +[DEFAULT] + +# can be executed by everybody, read-only, not secdef +londiste_public_fns = + londiste.find_column_types(text), + londiste.find_table_fkeys(text), + londiste.find_rel_oid(text, text), + londiste.find_table_oid(text), + londiste.find_seq_oid(text), + londiste.is_replica_func(oid), + londiste.quote_fqname(text), + londiste.make_fqname(text), + londiste.split_fqname(text), + londiste.version() + +# remote node uses those on provider, read local tables +londiste_remote_fns = + londiste.get_seq_list(text), + londiste.get_table_list(text), + londiste._coordinate_copy(text, text) + +# used by owner only +londiste_internal_fns = + londiste.upgrade_schema() + +# used by local worker, admin +londiste_local_fns = + londiste.local_show_missing(text), + londiste.local_add_seq(text, text), + londiste.local_add_table(text, text, text[], text, text), + londiste.local_add_table(text, text, text[], text), + londiste.local_add_table(text, text, text[]), + londiste.local_add_table(text, text), + londiste.local_remove_seq(text, text), + londiste.local_remove_table(text, text), + londiste.global_add_table(text, text), + londiste.global_remove_table(text, text), + londiste.global_update_seq(text, text, int8), + londiste.global_remove_seq(text, text), + londiste.get_table_pending_fkeys(text), + londiste.get_valid_pending_fkeys(text), + londiste.drop_table_fkey(text, text), + londiste.restore_table_fkey(text, text), + londiste.execute_start(text, text, text, boolean), + londiste.execute_finish(text, text), + londiste.root_check_seqs(text, int8), + londiste.root_check_seqs(text), + londiste.root_notify_change(text, text, text), + londiste.local_set_table_state(text, text, text, text), + londiste.local_set_table_attrs(text, text, text), + londiste.local_set_table_struct(text, text, text), + londiste.drop_table_triggers(text, text), + londiste.table_info_trigger() + diff --git a/sql/londiste/structure/install.sql b/sql/londiste/structure/install.sql index 00bf5638..fde88003 100644 --- a/sql/londiste/structure/install.sql +++ b/sql/londiste/structure/install.sql @@ -1,4 +1,4 @@ \i structure/tables.sql -\i structure/grants.sql \i structure/functions.sql \i structure/triggers.sql +\i structure/grants.sql diff --git a/sql/pgq/Makefile b/sql/pgq/Makefile index 87992bae..487f92c9 100644 --- a/sql/pgq/Makefile +++ b/sql/pgq/Makefile @@ -1,6 +1,6 @@ DOCS = README.pgq -DATA_built = pgq.sql pgq.upgrade.sql +DATA_built = pgq.sql pgq.upgrade.sql structure/oldgrants_pgq.sql structure/newgrants_pgq.sql DATA = structure/uninstall_pgq.sql # scripts that load other sql files @@ -9,7 +9,7 @@ FUNCS = $(shell sed -e 's/^[^\\].*//' -e 's/\\i //' $(LDRS)) SRCS = structure/tables.sql structure/grants.sql structure/install.sql \ structure/uninstall_pgq.sql $(FUNCS) -REGRESS = pgq_init pgq_core logutriga sqltriga trunctrg +REGRESS = pgq_init pgq_core pgq_perms logutriga sqltriga trunctrg REGRESS_OPTS = --load-language=plpgsql PG_CONFIG = pg_config @@ -19,6 +19,7 @@ include $(PGXS) NDOC = NaturalDocs NDOCARGS = -r -o html docs/html -p docs -i docs/sql CATSQL = ../../scripts/catsql.py +GRANTFU = ../../scripts/grantfu.py SUBDIRS = lowlevel triggers @@ -49,6 +50,15 @@ pgq.sql: $(SRCS) pgq.upgrade.sql: $(SRCS) $(CATSQL) structure/upgrade.sql > $@ +structure/newgrants_pgq.sql: structure/grants.ini + $(GRANTFU) -t -r -d $< > $@ + +structure/oldgrants_pgq.sql: structure/grants.ini structure/grants.sql + echo "begin;" > $@ + $(GRANTFU) -R -o $< >> $@ + cat structure/grants.sql >> $@ + echo "commit;" >> $@ + # # docs # @@ -79,3 +89,4 @@ test: pgq.sql ack: cp results/*.out expected/ +.PHONY: test ack upload cleandox dox diff --git a/sql/pgq/expected/pgq_perms.out b/sql/pgq/expected/pgq_perms.out new file mode 100644 index 00000000..83b6ac4d --- /dev/null +++ b/sql/pgq/expected/pgq_perms.out @@ -0,0 +1,42 @@ +\set ECHO off +drop role if exists pgq_test_producer; +drop role if exists pgq_test_consumer; +drop role if exists pgq_test_admin; +create role pgq_test_consumer with login in role pgq_reader; +create role pgq_test_producer with login in role pgq_writer; +create role pgq_test_admin with login in role pgq_admin; +\c - pgq_test_admin +select * from pgq.create_queue('pqueue'); -- ok + create_queue +-------------- + 1 +(1 row) + +\c - pgq_test_producer +select * from pgq.create_queue('pqueue'); -- fail +ERROR: permission denied for function create_queue +select * from pgq.insert_event('pqueue', 'test', 'data'); -- ok + insert_event +-------------- + 1 +(1 row) + +select * from pgq.register_consumer('pqueue', 'prod'); -- fail +ERROR: permission denied for function register_consumer +\c - pgq_test_consumer +select * from pgq.create_queue('pqueue'); -- fail +ERROR: permission denied for function create_queue +select * from pgq.insert_event('pqueue', 'test', 'data'); -- fail +ERROR: permission denied for function insert_event +select * from pgq.register_consumer('pqueue', 'cons'); -- ok + register_consumer +------------------- + 1 +(1 row) + +select * from pgq.next_batch('pqueue', 'cons'); -- ok + next_batch +------------ + +(1 row) + diff --git a/sql/pgq/functions/pgq.grant_perms.sql b/sql/pgq/functions/pgq.grant_perms.sql index afabfa5c..db8c6edf 100644 --- a/sql/pgq/functions/pgq.grant_perms.sql +++ b/sql/pgq/functions/pgq.grant_perms.sql @@ -14,8 +14,12 @@ returns integer as $$ declare q record; i integer; + pos integer; tbl_perms text; seq_perms text; + dst_schema text; + dst_table text; + part_table text; begin select * from pgq.queue into q where queue_name = x_queue_name; @@ -23,36 +27,72 @@ begin raise exception 'Queue not found'; end if; - if true then - -- safe, all access must go via functions - seq_perms := 'select'; - tbl_perms := 'select'; + -- split data table name to components + pos := position('.' in q.queue_data_pfx); + if pos > 0 then + dst_schema := substring(q.queue_data_pfx for pos - 1); + dst_table := substring(q.queue_data_pfx from pos + 1); else - -- allow ordinery users to directly insert - -- to event tables. dangerous. - seq_perms := 'select, update'; - tbl_perms := 'select, insert'; + dst_schema := 'public'; + dst_table := q.queue_data_pfx; end if; -- tick seq, normal users don't need to modify it - execute 'grant ' || seq_perms - || ' on ' || q.queue_tick_seq || ' to public'; + execute 'grant select on ' || q.queue_tick_seq || ' to public'; -- event seq - execute 'grant ' || seq_perms - || ' on ' || q.queue_event_seq || ' to public'; + execute 'grant select on ' || q.queue_event_seq || ' to public'; - -- parent table for events - execute 'grant select on ' || q.queue_data_pfx || ' to public'; + -- set grants on parent table + perform pgq._grant_perms_from('pgq', 'event_template', dst_schema, dst_table); - -- real event tables + -- set grants on real event tables for i in 0 .. q.queue_ntables - 1 loop - execute 'grant ' || tbl_perms - || ' on ' || q.queue_data_pfx || '_' || i::text - || ' to public'; + part_table := dst_table || '_' || i::text; + perform pgq._grant_perms_from('pgq', 'event_template', dst_schema, part_table); end loop; return 1; end; $$ language plpgsql security definer; + +create or replace function pgq._grant_perms_from(src_schema text, src_table text, dst_schema text, dst_table text) +returns integer as $$ +-- ---------------------------------------------------------------------- +-- Function: pgq.grant_perms_from(1) +-- +-- Copy grants from one table to another. +-- Workaround for missing GRANTS option for CREATE TABLE LIKE. +-- ---------------------------------------------------------------------- +declare + fq_table text; + sql text; + g record; + q_grantee text; +begin + fq_table := quote_ident(dst_schema) || '.' || quote_ident(dst_table); + + for g in + select grantor, grantee, privilege_type, is_grantable + from information_schema.table_privileges + where table_schema = src_schema + and table_name = src_table + loop + if g.grantee = 'PUBLIC' then + q_grantee = 'public'; + else + q_grantee = quote_ident(g.grantee); + end if; + sql := 'grant ' || g.privilege_type || ' on ' || fq_table + || ' to ' || q_grantee; + if g.is_grantable = 'YES' then + sql := sql || ' with grant option'; + end if; + execute sql; + end loop; + + return 1; +end; +$$ language plpgsql; + diff --git a/sql/pgq/functions/pgq.upgrade_schema.sql b/sql/pgq/functions/pgq.upgrade_schema.sql index 69d1bb17..8ddc8b32 100644 --- a/sql/pgq/functions/pgq.upgrade_schema.sql +++ b/sql/pgq/functions/pgq.upgrade_schema.sql @@ -19,6 +19,23 @@ begin cnt := cnt + 1; end if; + -- create roles + perform 1 from pg_catalog.pg_roles where rolname = 'pgq_reader'; + if not found then + create role pgq_reader; + cnt := cnt + 1; + end if; + perform 1 from pg_catalog.pg_roles where rolname = 'pgq_writer'; + if not found then + create role pgq_writer; + cnt := cnt + 1; + end if; + perform 1 from pg_catalog.pg_roles where rolname = 'pgq_admin'; + if not found then + create role pgq_admin in role pgq_reader, pgq_writer; + cnt := cnt + 1; + end if; + return cnt; end; $$ language plpgsql; diff --git a/sql/pgq/sql/pgq_perms.sql b/sql/pgq/sql/pgq_perms.sql new file mode 100644 index 00000000..c1962a1f --- /dev/null +++ b/sql/pgq/sql/pgq_perms.sql @@ -0,0 +1,39 @@ +\set ECHO off +\set VERBOSITY 'terse' +set client_min_messages = 'warning'; + +-- drop public perms +\i structure/newgrants_pgq.sql + +-- select proname, proacl from pg_proc p, pg_namespace n where n.nspname = 'pgq' and p.pronamespace = n.oid; + +\set ECHO all + +drop role if exists pgq_test_producer; +drop role if exists pgq_test_consumer; +drop role if exists pgq_test_admin; + +create role pgq_test_consumer with login in role pgq_reader; +create role pgq_test_producer with login in role pgq_writer; +create role pgq_test_admin with login in role pgq_admin; + + +\c - pgq_test_admin + +select * from pgq.create_queue('pqueue'); -- ok + +\c - pgq_test_producer + +select * from pgq.create_queue('pqueue'); -- fail + +select * from pgq.insert_event('pqueue', 'test', 'data'); -- ok + +select * from pgq.register_consumer('pqueue', 'prod'); -- fail + +\c - pgq_test_consumer + +select * from pgq.create_queue('pqueue'); -- fail +select * from pgq.insert_event('pqueue', 'test', 'data'); -- fail +select * from pgq.register_consumer('pqueue', 'cons'); -- ok +select * from pgq.next_batch('pqueue', 'cons'); -- ok + diff --git a/sql/pgq/structure/grants.ini b/sql/pgq/structure/grants.ini new file mode 100644 index 00000000..82cb8554 --- /dev/null +++ b/sql/pgq/structure/grants.ini @@ -0,0 +1,100 @@ + +[GrantFu] +roles = pgq_reader, pgq_writer, pgq_admin, public + +[1.public] +on.functions = %(pgq_generic_fns)s +public = execute + +[2.consumer] +on.functions = %(pgq_read_fns)s +pgq_reader = execute + +[3.producer] +on.functions = %(pgq_write_fns)s +pgq_writer = execute + +[4.admin] +on.functions = %(pgq_system_fns)s +pgq_admin = execute + +[5.meta.tables] +on.tables = + pgq.consumer, + pgq.queue, + pgq.tick, + pgq.subscription +pgq_reader = select +public = select + +[5.event.tables] +on.tables = pgq.event_template, pgq.retry_queue +pgq_reader = select + +# drop public access to events +public = + + +# +# define various groups of functions +# + +[DEFAULT] + +pgq_generic_fns = + pgq.seq_getval(text), + pgq.get_queue_info(), + pgq.get_queue_info(text), + pgq.get_consumer_info(), + pgq.get_consumer_info(text), + pgq.get_consumer_info(text, text), + pgq.version() + +pgq_read_fns = + pgq.batch_event_sql(bigint), + pgq.batch_event_tables(bigint), + pgq.find_tick_helper(int4, int8, timestamptz, int8, int8, interval), + pgq.register_consumer(text, text), + pgq.register_consumer_at(text, text, bigint), + pgq.unregister_consumer(text, text), + pgq.next_batch_info(text, text), + pgq.next_batch(text, text), + pgq.next_batch_custom(text, text, interval, int4, interval), + pgq.get_batch_events(bigint), + pgq.get_batch_info(bigint), + pgq.get_batch_cursor(bigint, text, int4, text), + pgq.get_batch_cursor(bigint, text, int4), + pgq.event_retry(bigint, bigint, timestamptz), + pgq.event_retry(bigint, bigint, integer), + pgq.batch_retry(bigint, integer), + pgq.finish_batch(bigint) + +pgq_write_fns = + pgq.insert_event(text, text, text), + pgq.insert_event(text, text, text, text, text, text, text), + pgq.current_event_table(text), + pgq.sqltriga(), + pgq.logutriga() + +pgq_system_fns = + pgq.ticker(text, bigint, timestamptz, bigint), + pgq.ticker(text), + pgq.ticker(), + pgq.maint_retry_events(), + pgq.maint_rotate_tables_step1(text), + pgq.maint_rotate_tables_step2(), + pgq.maint_tables_to_vacuum(), + pgq.maint_operations(), + pgq.upgrade_schema(), + pgq.grant_perms(text), + pgq._grant_perms_from(text,text,text,text), + pgq.tune_storage(text), + pgq.force_tick(text), + pgq.seq_setval(text, int8), + pgq.create_queue(text), + pgq.drop_queue(text, bool), + pgq.drop_queue(text), + pgq.set_queue_config(text, text, text), + pgq.insert_event_raw(text, bigint, timestamptz, integer, integer, text, text, text, text, text, text), + pgq.event_retry_raw(text, text, timestamptz, bigint, timestamptz, integer, text, text, text, text, text, text) + diff --git a/sql/pgq/structure/grants.sql b/sql/pgq/structure/grants.sql index d5f8ef1f..acbd484c 100644 --- a/sql/pgq/structure/grants.sql +++ b/sql/pgq/structure/grants.sql @@ -1,5 +1,8 @@ + grant usage on schema pgq to public; + +-- old default grants grant select on table pgq.consumer to public; grant select on table pgq.queue to public; grant select on table pgq.tick to public; @@ -7,3 +10,4 @@ grant select on table pgq.queue to public; grant select on table pgq.subscription to public; grant select on table pgq.event_template to public; grant select on table pgq.retry_queue to public; + diff --git a/sql/pgq/structure/install.sql b/sql/pgq/structure/install.sql index 511aaba7..747801ef 100644 --- a/sql/pgq/structure/install.sql +++ b/sql/pgq/structure/install.sql @@ -1,7 +1,7 @@ \i structure/tables.sql -\i structure/grants.sql \i structure/func_internal.sql \i structure/func_public.sql \i structure/triggers.sql +\i structure/grants.sql diff --git a/sql/pgq_coop/Makefile b/sql/pgq_coop/Makefile index 31c5d329..5561ef71 100644 --- a/sql/pgq_coop/Makefile +++ b/sql/pgq_coop/Makefile @@ -1,5 +1,7 @@ -DATA_built = pgq_coop.sql pgq_coop.upgrade.sql +DATA_built = pgq_coop.sql pgq_coop.upgrade.sql \ + structure/newgrants_pgq_coop.sql \ + structure/oldgrants_pgq_coop.sql SQL_FULL = structure/schema.sql structure/functions.sql structure/grants.sql @@ -16,6 +18,8 @@ include $(PGXS) NDOC = NaturalDocs NDOCARGS = -r -o html docs/html -p docs -i docs/sql CATSQL = ../../scripts/catsql.py +GRANTFU = ../../scripts/grantfu.py + # # combined SQL files @@ -27,6 +31,15 @@ pgq_coop.sql: $(SRCS) pgq_coop.upgrade.sql: $(SRCS) $(CATSQL) structure/upgrade.sql > $@ +structure/newgrants_pgq_coop.sql: structure/grants.ini + $(GRANTFU) -t -r -d $< > $@ + +structure/oldgrants_pgq_coop.sql: structure/grants.ini structure/grants.sql + echo "begin;" > $@ + $(GRANTFU) -R -o $< >> $@ + cat structure/grants.sql >> $@ + echo "commit;" >> $@ + # # docs # diff --git a/sql/pgq_coop/structure/grants.ini b/sql/pgq_coop/structure/grants.ini new file mode 100644 index 00000000..a1e98ea9 --- /dev/null +++ b/sql/pgq_coop/structure/grants.ini @@ -0,0 +1,21 @@ +[GrantFu] +roles = pgq_reader, pgq_writer, pgq_admin, public + +[1.consumer] +on.functions = %(pgq_coop_fns)s +pgq_reader = execute + +[2.public] +on.functions = pgq_coop.version() +public = execute + +[DEFAULT] +pgq_coop_fns = + pgq_coop.register_subconsumer(text, text, text), + pgq_coop.unregister_subconsumer(text, text, text, integer), + pgq_coop.next_batch(text, text, text), + pgq_coop.next_batch(text, text, text, interval), + pgq_coop.next_batch_custom(text, text, text, interval, int4, interval), + pgq_coop.next_batch_custom(text, text, text, interval, int4, interval, interval), + pgq_coop.finish_batch(bigint) + diff --git a/sql/pgq_coop/structure/grants.sql b/sql/pgq_coop/structure/grants.sql index b3f384cd..2ed2bd20 100644 --- a/sql/pgq_coop/structure/grants.sql +++ b/sql/pgq_coop/structure/grants.sql @@ -1,3 +1,3 @@ -grant usage on schema pgq_coop to public; +GRANT usage ON SCHEMA pgq_coop TO public; diff --git a/sql/pgq_ext/Makefile b/sql/pgq_ext/Makefile index 50381f6f..8fec6e61 100644 --- a/sql/pgq_ext/Makefile +++ b/sql/pgq_ext/Makefile @@ -1,12 +1,15 @@ DOCS = README.pgq_ext -DATA_built = pgq_ext.sql pgq_ext.upgrade.sql +DATA_built = pgq_ext.sql pgq_ext.upgrade.sql \ + structure/oldgrants_pgq_ext.sql \ + structure/newgrants_pgq_ext.sql SRCS = $(wildcard functions/*.sql structure/*.sql) REGRESS = test_pgq_ext test_upgrade REGRESS_OPTS = --load-language=plpgsql +GRANTFU = ../../scripts/grantfu.py CATSQL = ../../scripts/catsql.py NDOC = NaturalDocs NDOCARGS = -r -o html docs/html -p docs -i docs/sql @@ -21,6 +24,15 @@ pgq_ext.sql: $(SRCS) pgq_ext.upgrade.sql: $(SRCS) $(CATSQL) structure/upgrade.sql > $@ +structure/newgrants_pgq_ext.sql: structure/grants.ini + $(GRANTFU) -t -r -d $< > $@ + +structure/oldgrants_pgq_ext.sql: structure/grants.ini structure/grants.sql + echo "begin;" > $@ + $(GRANTFU) -R -o $< >> $@ + cat structure/grants.sql >> $@ + echo "commit;" >> $@ + test: pgq_ext.sql make installcheck || { less regression.diffs ; exit 1; } diff --git a/sql/pgq_ext/structure/grants.ini b/sql/pgq_ext/structure/grants.ini new file mode 100644 index 00000000..e9c6927b --- /dev/null +++ b/sql/pgq_ext/structure/grants.ini @@ -0,0 +1,28 @@ +[GrantFu] +roles = pgq_writer, public + +[1.public] +on.functions = pgq_ext.version() +public = execute + +[2.pgq_ext] +on.functions = %(pgq_ext_fns)s +pgq_writer = execute + + +[DEFAULT] +pgq_ext_fns = + pgq_ext.upgrade_schema(), + pgq_ext.is_batch_done(text, text, bigint), + pgq_ext.is_batch_done(text, bigint), + pgq_ext.set_batch_done(text, text, bigint), + pgq_ext.set_batch_done(text, bigint), + pgq_ext.is_event_done(text, text, bigint, bigint), + pgq_ext.is_event_done(text, bigint, bigint), + pgq_ext.set_event_done(text, text, bigint, bigint), + pgq_ext.set_event_done(text, bigint, bigint), + pgq_ext.get_last_tick(text, text), + pgq_ext.get_last_tick(text), + pgq_ext.set_last_tick(text, text, bigint), + pgq_ext.set_last_tick(text, bigint) + diff --git a/sql/pgq_ext/structure/grants.sql b/sql/pgq_ext/structure/grants.sql new file mode 100644 index 00000000..0b3db788 --- /dev/null +++ b/sql/pgq_ext/structure/grants.sql @@ -0,0 +1,3 @@ + +grant usage on schema pgq_ext to public; + diff --git a/sql/pgq_ext/structure/install.sql b/sql/pgq_ext/structure/install.sql index 973e35b8..4b9be2c5 100644 --- a/sql/pgq_ext/structure/install.sql +++ b/sql/pgq_ext/structure/install.sql @@ -1,3 +1,4 @@ \i structure/tables.sql \i structure/upgrade.sql +\i structure/grants.sql diff --git a/sql/pgq_ext/structure/tables.sql b/sql/pgq_ext/structure/tables.sql index c40368eb..78a1b234 100644 --- a/sql/pgq_ext/structure/tables.sql +++ b/sql/pgq_ext/structure/tables.sql @@ -52,7 +52,6 @@ set client_min_messages = 'warning'; set default_with_oids = 'off'; create schema pgq_ext; -grant usage on schema pgq_ext to public; -- diff --git a/sql/pgq_node/Makefile b/sql/pgq_node/Makefile index c3932ca9..c735587a 100644 --- a/sql/pgq_node/Makefile +++ b/sql/pgq_node/Makefile @@ -1,9 +1,12 @@ -DATA_built = pgq_node.sql pgq_node.upgrade.sql +DATA_built = pgq_node.sql pgq_node.upgrade.sql \ + structure/newgrants_pgq_node.sql \ + structure/oldgrants_pgq_node.sql LDRS = structure/functions.sql FUNCS = $(shell sed -e 's/^[^\\].*//' -e 's/\\i //' $(LDRS)) -SRCS = structure/tables.sql structure/functions.sql $(FUNCS) +SRCS = structure/tables.sql structure/functions.sql structure/grants.sql \ + $(FUNCS) REGRESS = pgq_node_test REGRESS_OPTS = --load-language=plpgsql @@ -15,6 +18,7 @@ include $(PGXS) NDOC = NaturalDocs NDOCARGS = -r -o html docs/html -p docs -i docs/sql CATSQL = ../../scripts/catsql.py +GRANTFU = ../../scripts/grantfu.py # # combined SQL files @@ -26,6 +30,16 @@ pgq_node.sql: $(SRCS) pgq_node.upgrade.sql: $(SRCS) $(CATSQL) structure/upgrade.sql > $@ +structure/newgrants_pgq_node.sql: structure/grants.ini + $(GRANTFU) -r -d -t $< > $@ + +structure/oldgrants_pgq_node.sql: structure/grants.ini structure/grants.sql + echo "begin;" > $@ + $(GRANTFU) -R -o $< >> $@ + cat structure/grants.sql >> $@ + echo "commit;" >> $@ + + # # docs # diff --git a/sql/pgq_node/functions/pgq_node.upgrade_schema.sql b/sql/pgq_node/functions/pgq_node.upgrade_schema.sql index d9c1b6f4..1678559f 100644 --- a/sql/pgq_node/functions/pgq_node.upgrade_schema.sql +++ b/sql/pgq_node/functions/pgq_node.upgrade_schema.sql @@ -19,4 +19,3 @@ begin end; $$ language plpgsql; - diff --git a/sql/pgq_node/structure/grants.ini b/sql/pgq_node/structure/grants.ini new file mode 100644 index 00000000..d1cc4558 --- /dev/null +++ b/sql/pgq_node/structure/grants.ini @@ -0,0 +1,73 @@ +[GrantFu] +roles = pgq_writer, pgq_admin, pgq_reader, public + +[1.public.fns] +on.functions = %(pgq_node_public_fns)s +public = execute + +# cascaded consumer, target side +[2.consumer.fns] +on.functions = %(pgq_node_consumer_fns)s +pgq_writer = execute +pgq_admin = execute + +# cascaded worker, target side +[3.worker.fns] +on.functions = %(pgq_node_worker_fns)s +pgq_admin = execute + +# cascaded consumer/worker, source side +[4.remote.fns] +on.functions = %(pgq_node_remote_fns)s +pgq_reader = execute +pgq_writer = execute +pgq_admin = execute + +# called by ticker, upgrade script +[4.admin.fns] +on.functions = %(pgq_node_admin_fns)s +pgq_admin = execute + +# define various groups of functions +[DEFAULT] + +pgq_node_remote_fns = + pgq_node.get_queue_locations(text), + pgq_node.get_node_info(text), + pgq_node.get_subscriber_info(text), + pgq_node.register_subscriber(text, text, text, int8), + pgq_node.unregister_subscriber(text, text), + pgq_node.set_subscriber_watermark(text, text, bigint) + +pgq_node_public_fns = + pgq_node.is_root_node(text), + pgq_node.is_leaf_node(text), + pgq_node.version() + +pgq_node_admin_fns = + pgq_node.upgrade_schema(), + pgq_node.maint_watermark(text) + +pgq_node_consumer_fns = + pgq_node.get_consumer_info(text), + pgq_node.get_consumer_state(text, text), + pgq_node.register_consumer(text, text, text, int8), + pgq_node.unregister_consumer(text, text), + pgq_node.change_consumer_provider(text, text, text), + pgq_node.set_consumer_uptodate(text, text, boolean), + pgq_node.set_consumer_paused(text, text, boolean), + pgq_node.set_consumer_completed(text, text, int8), + pgq_node.set_consumer_error(text, text, text) + +pgq_node_worker_fns = + pgq_node.register_location(text, text, text, boolean), + pgq_node.unregister_location(text, text), + pgq_node.create_node(text, text, text, text, text, bigint, text), + pgq_node.drop_node(text, text), + pgq_node.demote_root(text, int4, text), + pgq_node.promote_branch(text), + pgq_node.set_node_attrs(text, text), + pgq_node.get_worker_state(text), + pgq_node.set_global_watermark(text, bigint), + pgq_node.set_partition_watermark(text, text, bigint) + diff --git a/sql/pgq_node/structure/grants.sql b/sql/pgq_node/structure/grants.sql new file mode 100644 index 00000000..1efff292 --- /dev/null +++ b/sql/pgq_node/structure/grants.sql @@ -0,0 +1,3 @@ + +grant usage on schema pgq_node to public; + diff --git a/sql/pgq_node/structure/install.sql b/sql/pgq_node/structure/install.sql index a6d95cad..9a2e23e9 100644 --- a/sql/pgq_node/structure/install.sql +++ b/sql/pgq_node/structure/install.sql @@ -1,2 +1,5 @@ + \i structure/tables.sql \i structure/functions.sql +\i structure/grants.sql + diff --git a/sql/pgq_node/structure/tables.sql b/sql/pgq_node/structure/tables.sql index bd8d4e1d..464a4454 100644 --- a/sql/pgq_node/structure/tables.sql +++ b/sql/pgq_node/structure/tables.sql @@ -16,7 +16,6 @@ -- ---------------------------------------------------------------------- create schema pgq_node; -grant usage on schema pgq_node to public; -- ---------------------------------------------------------------------- -- Table: pgq_node.location |