From cc89528a11c1861e4389c58a44a4a3026b549629 Mon Sep 17 00:00:00 2001 From: Ronan Dunklau Date: Wed, 1 Apr 2020 12:01:23 +0200 Subject: [PATCH 1/2] Handle PG >= 11 --- pgbedrock/context.py | 7 +++---- tests/conftest.py | 15 ++++++++++++--- tests/test_context.py | 9 ++++++--- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/pgbedrock/context.py b/pgbedrock/context.py index 0c7833d..35305a1 100644 --- a/pgbedrock/context.py +++ b/pgbedrock/context.py @@ -209,10 +209,9 @@ Q_GET_VERSIONS = """ SELECT - substring(version from 'PostgreSQL ([0-9.]*) ') AS postgres_version, - substring(version from 'Redshift ([0-9.]*)') AS redshift_version, - version LIKE '%Redshift%' AS is_redshift - FROM version() + current_setting('server_version_num')::int as postgres_version, + substring(version() from 'Redshift ([0-9.]*)') AS redshift_version, + version() LIKE '%Redshift%' AS is_redshift ; """ diff --git a/tests/conftest.py b/tests/conftest.py index 4da78df..a967cfb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -67,7 +67,9 @@ def drop_users_and_objects(cursor): WHERE rolname NOT IN ( 'test_user', 'postgres', 'pg_signal_backend', -- Roles introduced in Postgres 10: - 'pg_monitor', 'pg_read_all_settings', 'pg_read_all_stats', 'pg_stat_scan_tables' + 'pg_monitor', 'pg_read_all_settings', 'pg_read_all_stats', 'pg_stat_scan_tables', + -- Roles introduced in Postgres 11: + 'pg_execute_server_program', 'pg_read_server_files', 'pg_write_server_files' ); """) users = [u[0] for u in cursor.fetchall()] @@ -111,9 +113,9 @@ def base_spec(cursor): """) # Postgres 10 introduces several new roles that we have to account for - cursor.execute("SELECT substring(version from 'PostgreSQL ([0-9.]*) ') FROM version()") + cursor.execute("SELECT current_setting('server_version_num')::int") pg_version = cursor.fetchone()[0] - if pg_version.startswith('10.'): + if pg_version >= 100000: spec += dedent(""" pg_read_all_settings: @@ -128,7 +130,14 @@ def base_spec(cursor): - pg_stat_scan_tables - pg_read_all_stats """) + if pg_version >= 110000: + spec += dedent(""" + pg_execute_server_program: + + pg_read_server_files: + pg_write_server_files: + """) return spec diff --git a/tests/test_context.py b/tests/test_context.py index 73247a8..36ad62f 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -325,11 +325,14 @@ def test_get_all_role_attributes(cursor): expected = set(['test_user', 'postgres', ROLES[0], ROLES[1]]) pg_version = dbcontext.get_version_info().postgres_version # Postgres 10 introduces several new roles that we have to account for - if pg_version.startswith('10.'): + if pg_version >= 100000: expected.update(set([ 'pg_read_all_settings', 'pg_stat_scan_tables', 'pg_read_all_stats', 'pg_monitor'] )) - + if pg_version >= 110000: + expected.update(set([ + 'pg_execute_server_program', 'pg_read_server_files', 'pg_write_server_files'] + )) actual = dbcontext.get_all_role_attributes() assert set(actual.keys()) == expected @@ -422,7 +425,7 @@ def test_get_all_memberships(cursor): expected = set([('role1', 'role0'), ('role2', 'role1')]) pg_version = dbcontext.get_version_info().postgres_version # Postgres 10 introduces several new roles and memberships that we have to account for - if pg_version.startswith('10.'): + if pg_version >= 100000: expected.update(set([ ('pg_monitor', 'pg_stat_scan_tables'), ('pg_monitor', 'pg_read_all_stats'), From 50a3592e9602dc228395d93b4cbb0fc8a2cb5ef2 Mon Sep 17 00:00:00 2001 From: Ronan Dunklau Date: Wed, 1 Apr 2020 12:06:17 +0200 Subject: [PATCH 2/2] Add support for function objects --- pgbedrock/common.py | 36 ++++++++++++++++++++----- pgbedrock/context.py | 52 +++++++++++++++++++++++++++++++++---- pgbedrock/spec_inspector.py | 6 +++-- tests/conftest.py | 3 +++ tests/test_context.py | 6 +++++ tests/test_core_generate.py | 10 +++++++ tests/test_ownerships.py | 13 +++++++++- tests/test_privileges.py | 28 ++++++++++++++++++++ 8 files changed, 139 insertions(+), 15 deletions(-) diff --git a/pgbedrock/common.py b/pgbedrock/common.py index 1ece7f1..5a6d43b 100644 --- a/pgbedrock/common.py +++ b/pgbedrock/common.py @@ -10,6 +10,7 @@ import click import psycopg2 +import re logger = logging.getLogger(__name__) @@ -19,6 +20,7 @@ FAILED_QUERY_MSG = 'Failed to execute query "{}": {}' UNSUPPORTED_CHAR_MSG = 'Role "{}" contains an unsupported character: \' or "' PROGRESS_TEMPLATE = '%(label)s [%(bar)s] %(info)s' +FUNCTION_PARSING_RE = re.compile('^("[^"]+"|[^(]+)((.*))$') def check_name(name): @@ -76,11 +78,12 @@ class ObjectName(object): * Be sure that when we get the fully-qualified name it will be double quoted properly, i.e. "myschema"."mytable" """ - def __init__(self, schema, unqualified_name=None): + def __init__(self, schema, unqualified_name=None, object_args=None): # Make sure schema and table are both stored without double quotes around # them; we add these when ObjectName.qualified_name is called self._schema = self._unquoted_item(schema) self._unqualified_name = self._unquoted_item(unqualified_name) + self._object_args = object_args or '' if self._unqualified_name and self._unqualified_name == '*': self._qualified_name = '{}.{}'.format(self.schema, self.unqualified_name) @@ -88,12 +91,16 @@ def __init__(self, schema, unqualified_name=None): # Note that if we decide to support "schema"."table" within YAML that we'll need to # add a custom constructor since otherwise YAML gets confused unless you do # '"schema"."table"' - self._qualified_name = '{}."{}"'.format(self.schema, self.unqualified_name) + self._qualified_name = '{}."{}"{}'.format(self.schema, + self.unqualified_name, + self.object_args) else: self._qualified_name = '{}'.format(self.schema) def __eq__(self, other): - return (self.schema == other.schema) and (self.unqualified_name == other.unqualified_name) + return ((self.schema == other.schema) and + (self.unqualified_name == other.unqualified_name) and + (self.object_args == other.object_args)) def __hash__(self): return hash(self.qualified_name) @@ -103,12 +110,17 @@ def __lt__(self, other): def __repr__(self): if self.unqualified_name: - return "ObjectName('{}', '{}')".format(self.schema, self.unqualified_name) - + if self.object_args: + return "ObjectName('{}', '{}', '{}')".format(self.schema, + self.unqualified_name, + self.object_args) + else: + return "ObjectName('{}', '{}')".format(self.schema, + self.unqualified_name) return "ObjectName('{}')".format(self.schema) @classmethod - def from_str(cls, text): + def from_str(cls, text, kind=None): """ Convert a text representation of a qualified object name into an ObjectName instance For example, 'foo.bar', '"foo".bar', '"foo"."bar"', etc. will be converted an object with @@ -123,8 +135,14 @@ def from_str(cls, text): # If there are multiple periods we assume that the first one delineates the schema from # the rest of the object, i.e. foo.bar.baz means schema foo and object "bar.baz" schema, unqualified_name = text.split('.', 1) + object_args = None + if kind == 'functions' and unqualified_name != '*': + groups = FUNCTION_PARSING_RE.match(unqualified_name).groups() + if len(groups) == 2: + unqualified_name, object_args = groups # Don't worry about removing double quotes as that happens in __init__ - return cls(schema=schema, unqualified_name=unqualified_name) + return cls(schema=schema, unqualified_name=unqualified_name, + object_args=object_args) def only_schema(self): """ Return an ObjectName instance for the schema associated with the current object """ @@ -134,6 +152,10 @@ def only_schema(self): def schema(self): return self._schema + @property + def object_args(self): + return self._object_args + @property def unqualified_name(self): return self._unqualified_name diff --git a/pgbedrock/context.py b/pgbedrock/context.py index 35305a1..49ac946 100644 --- a/pgbedrock/context.py +++ b/pgbedrock/context.py @@ -59,6 +59,7 @@ SELECT nsp.nspname AS schema, c.relname AS unqualified_name, + NULL::text AS object_args, map.objkind, (aclexplode(c.relacl)).grantee AS grantee_oid, t_owner.rolname AS owner, @@ -78,6 +79,7 @@ SELECT nsp.nspname AS schema, NULL::TEXT AS unqualified_name, + NULL::text AS object_args, 'schemas'::TEXT AS objkind, (aclexplode(nsp.nspacl)).grantee AS grantee_oid, t_owner.rolname AS owner, @@ -85,18 +87,34 @@ FROM pg_namespace nsp JOIN pg_authid t_owner ON nsp.nspowner = t_owner.OID + ), functions AS ( + SELECT + nsp.nspname AS schema, + proname AS unqualified_name, + '(' || pg_get_function_identity_arguments(p.oid) || ')' AS object_args, + 'functions'::TEXT as objkind, + (aclexplode(p.proacl)).grantee AS grantee_oid, + t_owner.rolname AS owner, + (aclexplode(p.proacl)).privilege_type + FROM pg_proc p + JOIN pg_namespace nsp on nsp.oid = pronamespace + JOIN pg_authid t_owner ON p.proowner = t_owner.oid ), combined AS ( SELECT * FROM tables_and_sequences UNION ALL SELECT * FROM schemas + UNION ALL + SELECT * + FROM functions ) SELECT t_grantee.rolname AS grantee, combined.objkind, combined.schema, combined.unqualified_name, + combined.object_args, combined.privilege_type FROM combined @@ -149,6 +167,7 @@ map.kind, nsp.nspname AS schema, c.relname AS unqualified_name, + NULL::TEXT AS object_args, c.relowner AS owner_id, -- Auto-dependency means that a sequence is linked to a table. Ownership of -- that sequence automatically derives from the table's ownership @@ -174,20 +193,35 @@ 'schemas'::TEXT AS kind, nsp.nspname AS schema, NULL::TEXT AS unqualified_name, + NULL::TEXT AS object_args, nsp.nspowner AS owner_id, FALSE AS is_dependent FROM pg_namespace nsp + ), functions AS ( + SELECT + 'functions'::TEXT as kind, + nsp.nspname AS schema, + proname as unqualified_name, + '(' || pg_get_function_identity_arguments(p.oid) || ')' as object_args, + p.proowner as owner_id, + FALSE AS is_dependent + FROM pg_proc p + JOIN pg_namespace nsp on nsp.oid = pronamespace ), combined AS ( SELECT * FROM tables_and_sequences UNION ALL SELECT * FROM schemas + UNION ALL + SELECT * + FROM functions ) SELECT co.kind, co.schema, co.unqualified_name, + co.object_args, t_owner.rolname AS owner, co.is_dependent FROM combined AS co @@ -230,6 +264,9 @@ {'read': ('USAGE', ), 'write': ('CREATE', ) }, + 'functions': + {'read': ('EXECUTE', ), + 'write': ()} } ObjectInfo = namedtuple('ObjectInfo', ['kind', 'objname', 'owner', 'is_dependent']) @@ -375,7 +412,8 @@ def get_all_current_nondefaults(self): This will not include privileges granted by this role to itself """ NamedRow = namedtuple('NamedRow', - ['grantee', 'objkind', 'schema', 'unqualified_name', 'privilege']) + ['grantee', 'objkind', 'schema', + 'unqualified_name', 'object_args', 'privilege']) common.run_query(self.cursor, self.verbose, Q_GET_ALL_CURRENT_NONDEFAULTS) current_nondefaults = defaultdict(dict) @@ -391,8 +429,9 @@ def get_all_current_nondefaults(self): 'read': set(), 'write': set(), } - - objname = common.ObjectName(schema=row.schema, unqualified_name=row.unqualified_name) + objname = common.ObjectName(schema=row.schema, + unqualified_name=row.unqualified_name, + object_args=row.object_args) entry = (objname, row.privilege) role_nondefaults[row.objkind][access_key].add(entry) @@ -435,10 +474,13 @@ def get_all_raw_object_attributes(self): """ common.run_query(self.cursor, self.verbose, Q_GET_ALL_RAW_OBJECT_ATTRIBUTES) results = [] - NamedRow = namedtuple('NamedRow', ['kind', 'schema', 'unqualified_name', 'owner', 'is_dependent']) + NamedRow = namedtuple('NamedRow', ['kind', 'schema', 'unqualified_name', + 'object_args', 'owner', 'is_dependent']) for i in self.cursor.fetchall(): row = NamedRow(*i) - objname = common.ObjectName(schema=row.schema, unqualified_name=row.unqualified_name) + objname = common.ObjectName(schema=row.schema, + unqualified_name=row.unqualified_name, + object_args=row.object_args) entry = ObjectAttributes(row.kind, row.schema, objname, row.owner, row.is_dependent) results.append(entry) return results diff --git a/pgbedrock/spec_inspector.py b/pgbedrock/spec_inspector.py index 156c021..fcb5e12 100644 --- a/pgbedrock/spec_inspector.py +++ b/pgbedrock/spec_inspector.py @@ -64,6 +64,7 @@ - schemas - tables - sequences + - functions valueschema: type: list schema: @@ -74,6 +75,7 @@ - schemas - sequences - tables + - functions valueschema: type: dict allowed: @@ -103,14 +105,14 @@ def convert_spec_to_objectnames(spec): for objkind, owned_items in config.get('owns', {}).items(): if not owned_items: continue - converted = [common.ObjectName.from_str(item) for item in owned_items] + converted = [common.ObjectName.from_str(item, objkind) for item in owned_items] config['owns'][objkind] = converted for objkind, perm_dicts in config.get('privileges', {}).items(): for priv_kind, granted_items in perm_dicts.items(): if not granted_items: continue - converted = [common.ObjectName.from_str(item) for item in granted_items] + converted = [common.ObjectName.from_str(item, objkind) for item in granted_items] config['privileges'][objkind][priv_kind] = converted return output_spec diff --git a/tests/conftest.py b/tests/conftest.py index a967cfb..8c11a32 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -98,6 +98,9 @@ def base_spec(cursor): tables: - information_schema.* - pg_catalog.* + functions: + - pg_catalog.* + - information_schema.* privileges: schemas: write: diff --git a/tests/test_context.py b/tests/test_context.py index 36ad62f..a384ce2 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -7,12 +7,14 @@ Q_CREATE_TABLE = 'SET ROLE {}; CREATE TABLE {}.{} AS (SELECT 1+1); RESET ROLE;' Q_CREATE_SEQUENCE = 'SET ROLE {}; CREATE SEQUENCE {}.{}; RESET ROLE;' +Q_CREATE_FUNCTION = 'SET ROLE {}; CREATE FUNCTION {}.{} RETURNS VOID AS $$$$ language SQL; RESET ROLE;' Q_HAS_PRIVILEGE = "SELECT has_table_privilege('{}', '{}', 'SELECT');" SCHEMAS = tuple('schema{}'.format(i) for i in range(4)) ROLES = tuple('role{}'.format(i) for i in range(4)) TABLES = tuple('table{}'.format(i) for i in range(6)) SEQUENCES = tuple('seq{}'.format(i) for i in range(6)) +FUNCTIONS = tuple(['func()', 'func(int, text)', 'func2(int)']) DUMMY = 'foo' @@ -32,6 +34,10 @@ # Grant default privileges to role0 from role3 for this schema; these should get # revoked in our test privs.Q_GRANT_DEFAULT.format(ROLES[3], SCHEMAS[0], 'SELECT', 'TABLES', ROLES[0]), + + # Add some functions + Q_CREATE_FUNCTION.format(ROLES[2], SCHEMAS[0], FUNCTIONS[0]), + Q_CREATE_FUNCTION.format(ROLES[2], SCHEMAS[0], FUNCTIONS[1]), ] ) def test_get_all_current_defaults(cursor): diff --git a/tests/test_core_generate.py b/tests/test_core_generate.py index 1326248..7dbe175 100644 --- a/tests/test_core_generate.py +++ b/tests/test_core_generate.py @@ -15,6 +15,7 @@ Q_CREATE_TABLE = 'SET ROLE {}; CREATE TABLE {}.{} AS (SELECT 1+1); RESET ROLE;' +Q_CREATE_FUNCTION = 'SET ROLE {}; CREATE FUNCTION {}.{} RETURNS VOID AS $$$$ language SQL; RESET ROLE;' Q_CREATE_SEQUENCE = 'SET ROLE {}; CREATE SEQUENCE {}.{}; RESET ROLE;' VALID_FOREVER_VALUES = ( @@ -389,8 +390,13 @@ def test_collapse_personal_schemas_empty_schema_with_default_priv(cursor): # Create objects in schema0 and grant write access on all of them to role0 Q_CREATE_TABLE.format('role1', 'schema0', 'table0'), Q_CREATE_TABLE.format('role1', 'schema0', 'table1'), + Q_CREATE_FUNCTION.format('role1', 'schema0', '"f1"(id integer)'), + Q_CREATE_FUNCTION.format('role1', 'schema1', '"f1"(id integer)'), + Q_CREATE_FUNCTION.format('role1', 'schema1', '"f2"(id integer)'), privs.Q_GRANT_NONDEFAULT.format('INSERT', 'TABLE', 'schema0.table0', 'role0'), privs.Q_GRANT_NONDEFAULT.format('INSERT', 'TABLE', 'schema0.table1', 'role0'), + privs.Q_GRANT_NONDEFAULT.format('EXECUTE', 'FUNCTION', 'schema0.f1(id integer)', 'role0'), + privs.Q_GRANT_NONDEFAULT.format('EXECUTE', 'FUNCTION', 'schema1.f1(id integer)', 'role0'), # Create one schema owned by role0 own.Q_CREATE_SCHEMA.format('schema3', 'role0'), @@ -411,6 +417,10 @@ def test_add_privileges(cursor): 'tables': { 'write': set([ObjectName('schema0', '*')]), }, + 'functions': { + 'read': set([ObjectName('schema0', '*'), + ObjectName('schema1', 'f1', '(id integer)')]) + } }, }, } diff --git a/tests/test_ownerships.py b/tests/test_ownerships.py index c64c5cf..6531451 100644 --- a/tests/test_ownerships.py +++ b/tests/test_ownerships.py @@ -7,11 +7,13 @@ Q_CREATE_SEQUENCE = 'SET ROLE {}; CREATE SEQUENCE {}.{}; RESET ROLE;' Q_CREATE_TABLE = 'SET ROLE {}; CREATE TABLE {}.{} AS (SELECT 1+1); RESET ROLE;' Q_SCHEMA_EXISTS = "SELECT schema_name FROM information_schema.schemata WHERE schema_name='{}';" +Q_CREATE_FUNCTION = 'SET ROLE {}; CREATE FUNCTION {}.{} RETURNS VOID AS $$$$ language SQL; RESET ROLE;' ROLES = tuple('role{}'.format(i) for i in range(3)) SCHEMAS = tuple('schema{}'.format(i) for i in range(3)) TABLES = tuple('table{}'.format(i) for i in range(4)) SEQUENCES = tuple('seq{}'.format(i) for i in range(4)) +FUNCTIONS = tuple(['"func"()', '"func"(integer, text)', '"func2"(integer)']) DUMMY = 'foo' @@ -59,6 +61,12 @@ def test_analyze_ownerships_create_schemas(cursor): Q_CREATE_TABLE.format(ROLES[1], SCHEMAS[0], TABLES[2]), Q_CREATE_TABLE.format(ROLES[0], SCHEMAS[0], TABLES[3]), + # Create functions in SCHEMAS[0], some of which aren't owned by ROLES[0] + Q_CREATE_FUNCTION.format(ROLES[0], SCHEMAS[0], FUNCTIONS[0]), + Q_CREATE_FUNCTION.format(ROLES[1], SCHEMAS[0], FUNCTIONS[1]), + Q_CREATE_FUNCTION.format(ROLES[0], SCHEMAS[0], FUNCTIONS[2]), + + # Create two sequences in SCHEMAS[1], one of which isn't owned by ROLES[1] Q_CREATE_SEQUENCE.format(ROLES[1], SCHEMAS[1], SEQUENCES[0]), Q_CREATE_SEQUENCE.format(ROLES[0], SCHEMAS[1], SEQUENCES[1]), @@ -67,7 +75,8 @@ def test_analyze_ownerships_nonschemas(cursor): spec = { ROLES[0]: { 'owns': { - 'tables': [ObjectName(SCHEMAS[0], '*')] + 'tables': [ObjectName(SCHEMAS[0], '*')], + 'functions': [ObjectName(SCHEMAS[0], '*')], }, }, ROLES[1]: { @@ -88,6 +97,8 @@ def test_analyze_ownerships_nonschemas(cursor): ROLES[0], ROLES[1]), own.Q_SET_OBJECT_OWNER.format('SEQUENCE', quoted_object(SCHEMAS[1], SEQUENCES[1]), ROLES[1], ROLES[0]), + own.Q_SET_OBJECT_OWNER.format('FUNCTION', '{}.{}'.format(SCHEMAS[0], FUNCTIONS[1]), + ROLES[0], ROLES[1]), ]) assert set(actual) == expected diff --git a/tests/test_privileges.py b/tests/test_privileges.py index 6d0a644..ae0170b 100644 --- a/tests/test_privileges.py +++ b/tests/test_privileges.py @@ -17,12 +17,14 @@ Q_CREATE_TABLE = 'SET ROLE {}; CREATE TABLE {}.{} AS (SELECT 1+1); RESET ROLE;' Q_CREATE_SEQUENCE = 'SET ROLE {}; CREATE SEQUENCE {}.{}; RESET ROLE;' +Q_CREATE_FUNCTION = 'SET ROLE {}; CREATE FUNCTION {}.{} RETURNS VOID AS $$$$ language SQL; RESET ROLE;' Q_HAS_PRIVILEGE = "SELECT has_table_privilege('{}', '{}', 'SELECT');" SCHEMAS = tuple('schema{}'.format(i) for i in range(4)) ROLES = tuple('role{}'.format(i) for i in range(5)) TABLES = tuple('table{}'.format(i) for i in range(6)) SEQUENCES = tuple('seq{}'.format(i) for i in range(6)) +FUNCTIONS = tuple(['"func"()', '"func"(integer, text)', '"func2"(integer)']) DUMMY = 'foo' @@ -56,9 +58,15 @@ Q_CREATE_SEQUENCE.format(ROLES[2], SCHEMAS[2], SEQUENCES[4]), Q_CREATE_SEQUENCE.format(ROLES[2], SCHEMAS[2], SEQUENCES[5]), + # Create functions + Q_CREATE_FUNCTION.format(ROLES[2], SCHEMAS[0], FUNCTIONS[0]), + Q_CREATE_FUNCTION.format(ROLES[3], SCHEMAS[1], FUNCTIONS[1]), + + # Grant a couple unwanted default privileges to assert that they will be revoked privs.Q_GRANT_DEFAULT.format(ROLES[3], SCHEMAS[1], 'SELECT', 'TABLES', ROLES[0]), privs.Q_GRANT_DEFAULT.format(ROLES[3], SCHEMAS[1], 'TRIGGER', 'TABLES', ROLES[0]), + privs.Q_GRANT_DEFAULT.format(ROLES[3], SCHEMAS[1], 'EXECUTE', 'FUNCTIONS', ROLES[0]), # Grant privileges that would come along with the above default privs (i.e. if default # SELECT table priv, then grant SELECT to all existing tables) @@ -67,6 +75,8 @@ privs.Q_GRANT_NONDEFAULT.format('TRIGGER', 'TABLE', '{}.{}'.format(SCHEMAS[1], TABLES[2]), ROLES[0]), privs.Q_GRANT_NONDEFAULT.format('TRIGGER', 'TABLE', '{}.{}'.format(SCHEMAS[1], TABLES[3]), ROLES[0]), + privs.Q_GRANT_NONDEFAULT.format('EXECUTE', 'FUNCTION', '{}.{}'.format(SCHEMAS[1], FUNCTIONS[1]), ROLES[0]), + # Grant a non-default privilege that will be subsumed by a default privilege grant privs.Q_GRANT_NONDEFAULT.format('SELECT', 'TABLE', '{}.{}'.format(SCHEMAS[0], TABLES[0]), ROLES[0]), @@ -125,6 +135,9 @@ def test_analyze_privileges(cursor): write: - {schema0}.* - {schema1}.{table2} + functions: + read: + - {schema0}.* {role1}: privileges: sequences: @@ -164,16 +177,31 @@ def test_analyze_privileges(cursor): privs.Q_REVOKE_NONDEFAULT.format('SELECT', 'TABLE', quoted_object(SCHEMAS[1], TABLES[3]), ROLES[0]), privs.Q_REVOKE_NONDEFAULT.format('TRIGGER', 'TABLE', quoted_object(SCHEMAS[1], TABLES[3]), ROLES[0]), + # Revoke EXECUTE on function + privs.Q_REVOKE_NONDEFAULT.format('EXECUTE', 'FUNCTION', '{}.{}'.format(SCHEMAS[1], FUNCTIONS[1]), ROLES[0]), + # Revoke default SELECT and TRIGGER privs on tables in schema1 from role0 (granted by role3) privs.Q_REVOKE_DEFAULT.format(ROLES[3], SCHEMAS[1], 'SELECT', 'TABLES', ROLES[0]), privs.Q_REVOKE_DEFAULT.format(ROLES[3], SCHEMAS[1], 'TRIGGER', 'TABLES', ROLES[0]), + # Revoke default EXECUTE on functions in schema1 from role0 (granted by role3) + privs.Q_REVOKE_DEFAULT.format(ROLES[3], SCHEMAS[1], 'EXECUTE', 'FUNCTIONS', ROLES[0]), + # Grant default read on tables in schema0 to role0 from role3 and role2 (both own objects) privs.Q_GRANT_DEFAULT.format(ROLES[3], SCHEMAS[0], 'SELECT', 'TABLES', ROLES[0]), privs.Q_GRANT_DEFAULT.format(ROLES[2], SCHEMAS[0], 'SELECT', 'TABLES', ROLES[0]), + # Grant default EXECUTE on functions in schema0 from role0 (granted by + # role2 and role3) + privs.Q_GRANT_DEFAULT.format(ROLES[2], SCHEMAS[0], 'EXECUTE', 'FUNCTIONS', ROLES[0]), + privs.Q_GRANT_DEFAULT.format(ROLES[3], SCHEMAS[0], 'EXECUTE', 'FUNCTIONS', ROLES[0]), + + + # Grant read on all tables in schema0 except schema0.table0 (it already has read) privs.Q_GRANT_NONDEFAULT.format('SELECT', 'TABLE', quoted_object(SCHEMAS[0], TABLES[1]), ROLES[0]), + # Grant execute on all functions in schema 0 + privs.Q_GRANT_NONDEFAULT.format('EXECUTE', 'FUNCTION', '{}.{}'.format(SCHEMAS[0], FUNCTIONS[0]), ROLES[0]), ] # Grant write on schema1.table2 to role0 (already has SELECT and TRIGGER)