Group-access roles for SQL functions (draft)
authorMarko Kreen <markokr@gmail.com>
Wed, 2 May 2012 13:45:00 +0000 (16:45 +0300)
committerMarko Kreen <markokr@gmail.com>
Thu, 10 May 2012 18:19:24 +0000 (21:19 +0300)
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:
doc/Makefile
doc/sql-grants.txt [new file with mode: 0644]
scripts/grantfu.py [new file with mode: 0755]
sql/londiste/Makefile
sql/londiste/functions/londiste.upgrade_schema.sql
sql/londiste/structure/grants.ini [new file with mode: 0644]
sql/londiste/structure/install.sql
sql/pgq/Makefile
sql/pgq/expected/pgq_perms.out [new file with mode: 0644]
sql/pgq/functions/pgq.grant_perms.sql
sql/pgq/functions/pgq.upgrade_schema.sql
sql/pgq/sql/pgq_perms.sql [new file with mode: 0644]
sql/pgq/structure/grants.ini [new file with mode: 0644]
sql/pgq/structure/grants.sql
sql/pgq/structure/install.sql
sql/pgq_coop/Makefile
sql/pgq_coop/structure/grants.ini [new file with mode: 0644]
sql/pgq_coop/structure/grants.sql
sql/pgq_ext/Makefile
sql/pgq_ext/structure/grants.ini [new file with mode: 0644]
sql/pgq_ext/structure/grants.sql [new file with mode: 0644]
sql/pgq_ext/structure/install.sql
sql/pgq_ext/structure/tables.sql
sql/pgq_node/Makefile
sql/pgq_node/functions/pgq_node.upgrade_schema.sql
sql/pgq_node/structure/grants.ini [new file with mode: 0644]
sql/pgq_node/structure/grants.sql [new file with mode: 0644]
sql/pgq_node/structure/install.sql
sql/pgq_node/structure/tables.sql

index d62a73555bc21ca766a91fe34f14e59125c4255e..3937572d5839520268909c589bf822a694a84592 100644 (file)
@@ -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 (file)
index 0000000..f388910
--- /dev/null
@@ -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 (executable)
index 0000000..5d89d55
--- /dev/null
@@ -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()
+
index c81efb34eebac75115beda422019705d25ead405..55a526dd60ce30957e857b4bcc5968cbb1aac222 100644 (file)
@@ -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; }
 
index b982a0cc1b213e00645ef613f1588bb8ef08742f..9067ed7d7d3e89dfdcaeb6c1b6016e4a464f9bf2 100644 (file)
@@ -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 (file)
index 0000000..26dff49
--- /dev/null
@@ -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()
+
index 00bf56386c2898135cd5df3999c2a78b1ceb9d64..fde88003f705837b5548b3f36ee51a15f433a317 100644 (file)
@@ -1,4 +1,4 @@
 \i structure/tables.sql
-\i structure/grants.sql
 \i structure/functions.sql
 \i structure/triggers.sql
+\i structure/grants.sql
index 87992bae255e0137e5b757a88b6f671ee2f74529..487f92c990185f68d1f827f6483d83959ee0a6c5 100644 (file)
@@ -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 (file)
index 0000000..83b6ac4
--- /dev/null
@@ -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)
+
index afabfa5c70bd103afd6ca46bf0e0e6a39dbfe3a8..db8c6edfb74c39125552018e3c71578d11d8fc27 100644 (file)
@@ -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;
+
index 69d1bb17bfcd3c7f992f0b939328b4677da8d11b..8ddc8b32a271b43c5d9fadd9337492c67d6dbc54 100644 (file)
@@ -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 (file)
index 0000000..c1962a1
--- /dev/null
@@ -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 (file)
index 0000000..82cb855
--- /dev/null
@@ -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)
+
index d5f8ef1f5f2279a8473357b1b818f82a793cf614..acbd484c6900c91bd5aa57f99843e8f674fd3712 100644 (file)
@@ -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;
+
index 511aaba72fae4a006be989f51bc9daf5e779bfaf..747801ef475f5d0c767827a9da875ed019ba4398 100644 (file)
@@ -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
 
index 31c5d32927f75f246d616d4bcfc507fa0b32ff58..5561ef71880416b2c6ed8538a9ea42f3d52e811e 100644 (file)
@@ -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 (file)
index 0000000..a1e98ea
--- /dev/null
@@ -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)
+
index b3f384cd6c7b7ff926f7c37bc487cc3af3cf04e3..2ed2bd200b762d55738131bcf01c28018786202b 100644 (file)
@@ -1,3 +1,3 @@
 
-grant usage on schema pgq_coop to public;
+GRANT usage ON SCHEMA pgq_coop TO public;
 
index 50381f6ff5a60f18426de5cdbb9ee3fad990849d..8fec6e613a498fb99891da6dd17a20bdc26d1979 100644 (file)
@@ -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 (file)
index 0000000..e9c6927
--- /dev/null
@@ -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 (file)
index 0000000..0b3db78
--- /dev/null
@@ -0,0 +1,3 @@
+
+grant usage on schema pgq_ext to public;
+
index 973e35b80d3d29534dc48b9fca9e0e5a6668c2af..4b9be2c50748de2a1890ce5066bffc2715735046 100644 (file)
@@ -1,3 +1,4 @@
 \i structure/tables.sql
 \i structure/upgrade.sql
+\i structure/grants.sql
 
index c40368eb0c6a3970a5686588d5696f8079e440df..78a1b2340bda0e3377014047d4c837b847bd4340 100644 (file)
@@ -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;
 
 
 --
index c3932ca9c0b874b4487bee0a87a5116b651eb672..c735587a0d62b5d996931f9e60a76970d20f2f8c 100644 (file)
@@ -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
 #
index d9c1b6f42c2277c5949c47eee8e399a0204f9b48..1678559f4e62cd0997682bfd3a00d17cc0bd502d 100644 (file)
@@ -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 (file)
index 0000000..d1cc455
--- /dev/null
@@ -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 (file)
index 0000000..1efff29
--- /dev/null
@@ -0,0 +1,3 @@
+
+grant usage on schema pgq_node to public;
+
index a6d95cad28aca9b8a7076906e4c455dbb4ea79eb..9a2e23e9d5a2bb3c1d9c078c137e48aed4c35f2d 100644 (file)
@@ -1,2 +1,5 @@
+
 \i structure/tables.sql
 \i structure/functions.sql
+\i structure/grants.sql
+
index bd8d4e1d9f3a146a12cf7183484442a70b589041..464a44547be9dfebcf60fe556ba8a3566d21ae80 100644 (file)
@@ -16,7 +16,6 @@
 -- ----------------------------------------------------------------------
 
 create schema pgq_node;
-grant usage on schema pgq_node to public;
 
 -- ----------------------------------------------------------------------
 -- Table: pgq_node.location