diff --git a/changelogs/fragments/332-postgresql_privs_def_privs_schemas.yml b/changelogs/fragments/332-postgresql_privs_def_privs_schemas.yml new file mode 100644 index 00000000..0a82e007 --- /dev/null +++ b/changelogs/fragments/332-postgresql_privs_def_privs_schemas.yml @@ -0,0 +1,3 @@ +bugfixes: +- postgresql_privs - add support for alter default privileges grant usage on schemas (https://github.com/ansible-collections/community.postgresql/issues/332). +- postgresql_privs - cannot grant select on objects in all schemas; add the ``not-specified`` value to the ``schema`` parameter to make this possible (https://github.com/ansible-collections/community.postgresql/issues/332). diff --git a/plugins/modules/postgresql_privs.py b/plugins/modules/postgresql_privs.py index 9760f423..bc62f036 100644 --- a/plugins/modules/postgresql_privs.py +++ b/plugins/modules/postgresql_privs.py @@ -72,6 +72,7 @@ or C(default_privs). Defaults to C(public) in these cases. - Pay attention, for embedded types when I(type=type) I(schema) can be C(pg_catalog) or C(information_schema) respectively. + - If not specified, uses C(public). Not to pass any schema, use C(not-specified). type: str roles: description: @@ -424,6 +425,14 @@ objs: numeric schema: pg_catalog db: acme + +- name: Alter default privileges grant usage on schemas to datascience + community.postgresql.postgresql_privs: + database: test + type: default_privs + privs: usage + objs: schemas + role: datascience ''' RETURN = r''' @@ -455,11 +464,12 @@ VALID_PRIVS = frozenset(('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'REFERENCES', 'TRIGGER', 'CREATE', 'CONNECT', - 'TEMPORARY', 'TEMP', 'EXECUTE', 'USAGE', 'ALL', 'USAGE')) + 'TEMPORARY', 'TEMP', 'EXECUTE', 'USAGE', 'ALL')) VALID_DEFAULT_OBJS = {'TABLES': ('ALL', 'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'REFERENCES', 'TRIGGER'), 'SEQUENCES': ('ALL', 'SELECT', 'UPDATE', 'USAGE'), 'FUNCTIONS': ('ALL', 'EXECUTE'), - 'TYPES': ('ALL', 'USAGE')} + 'TYPES': ('ALL', 'USAGE'), + 'SCHEMAS': ('CREATE', 'USAGE'), } executed_queries = [] @@ -555,53 +565,70 @@ def schema_exists(self, schema): return self.cursor.fetchone()[0] > 0 def get_all_tables_in_schema(self, schema): - if not self.schema_exists(schema): - raise Error('Schema "%s" does not exist.' % schema) - query = """SELECT relname - FROM pg_catalog.pg_class c - JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace - WHERE nspname = %s AND relkind in ('r', 'v', 'm', 'p')""" - self.cursor.execute(query, (schema,)) + if schema: + if not self.schema_exists(schema): + raise Error('Schema "%s" does not exist.' % schema) + + query = """SELECT relname + FROM pg_catalog.pg_class c + JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + WHERE nspname = %s AND relkind in ('r', 'v', 'm', 'p')""" + self.cursor.execute(query, (schema,)) + else: + query = ("SELECT relname FROM pg_catalog.pg_class " + "WHERE relkind in ('r', 'v', 'm', 'p')") + self.cursor.execute(query) return [t[0] for t in self.cursor.fetchall()] def get_all_sequences_in_schema(self, schema): - if not self.schema_exists(schema): - raise Error('Schema "%s" does not exist.' % schema) - query = """SELECT relname - FROM pg_catalog.pg_class c - JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace - WHERE nspname = %s AND relkind = 'S'""" - self.cursor.execute(query, (schema,)) + if schema: + if not self.schema_exists(schema): + raise Error('Schema "%s" does not exist.' % schema) + query = """SELECT relname + FROM pg_catalog.pg_class c + JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + WHERE nspname = %s AND relkind = 'S'""" + self.cursor.execute(query, (schema,)) + else: + self.cursor.execute("SELECT relname FROM pg_catalog.pg_class WHERE relkind = 'S'") return [t[0] for t in self.cursor.fetchall()] def get_all_functions_in_schema(self, schema): - if not self.schema_exists(schema): - raise Error('Schema "%s" does not exist.' % schema) + if schema: + if not self.schema_exists(schema): + raise Error('Schema "%s" does not exist.' % schema) - query = ("SELECT p.proname, oidvectortypes(p.proargtypes) " - "FROM pg_catalog.pg_proc p " - "JOIN pg_namespace n ON n.oid = p.pronamespace " - "WHERE nspname = %s") + query = ("SELECT p.proname, oidvectortypes(p.proargtypes) " + "FROM pg_catalog.pg_proc p " + "JOIN pg_namespace n ON n.oid = p.pronamespace " + "WHERE nspname = %s") - if self.pg_version >= 110000: - query += " and p.prokind = 'f'" + if self.pg_version >= 110000: + query += " and p.prokind = 'f'" - self.cursor.execute(query, (schema,)) + self.cursor.execute(query, (schema,)) + else: + self.cursor.execute("SELECT p.proname, oidvectortypes(p.proargtypes) FROM pg_catalog.pg_proc p") return ["%s(%s)" % (t[0], t[1]) for t in self.cursor.fetchall()] def get_all_procedures_in_schema(self, schema): if self.pg_version < 110000: raise Error("PostgreSQL verion must be >= 11 for type=procedure. Exit") - if not self.schema_exists(schema): - raise Error('Schema "%s" does not exist.' % schema) + if schema: + if not self.schema_exists(schema): + raise Error('Schema "%s" does not exist.' % schema) - query = ("SELECT p.proname, oidvectortypes(p.proargtypes) " - "FROM pg_catalog.pg_proc p " - "JOIN pg_namespace n ON n.oid = p.pronamespace " - "WHERE nspname = %s and p.prokind = 'p'") + query = ("SELECT p.proname, oidvectortypes(p.proargtypes) " + "FROM pg_catalog.pg_proc p " + "JOIN pg_namespace n ON n.oid = p.pronamespace " + "WHERE nspname = %s and p.prokind = 'p'") - self.cursor.execute(query, (schema,)) + self.cursor.execute(query, (schema,)) + else: + query = ("SELECT p.proname, oidvectortypes(p.proargtypes) " + "FROM pg_catalog.pg_proc p WHERE p.prokind = 'p'") + self.cursor.execute(query) return ["%s(%s)" % (t[0], t[1]) for t in self.cursor.fetchall()] # Methods for getting access control lists and group membership info @@ -613,31 +640,47 @@ def get_all_procedures_in_schema(self, schema): # The same should apply to group membership information. def get_table_acls(self, schema, tables): - query = """SELECT relacl - FROM pg_catalog.pg_class c - JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace - WHERE nspname = %s AND relkind in ('r','p','v','m') AND relname = ANY (%s) - ORDER BY relname""" - self.cursor.execute(query, (schema, tables)) + if schema: + query = """SELECT relacl + FROM pg_catalog.pg_class c + JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + WHERE nspname = %s AND relkind in ('r','p','v','m') AND relname = ANY (%s) + ORDER BY relname""" + self.cursor.execute(query, (schema, tables)) + else: + query = ("SELECT relacl FROM pg_catalog.pg_class " + "WHERE relkind in ('r','p','v','m') AND relname = ANY (%s) " + "ORDER BY relname") + self.cursor.execute(query) return [t[0] for t in self.cursor.fetchall()] def get_sequence_acls(self, schema, sequences): - query = """SELECT relacl - FROM pg_catalog.pg_class c - JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace - WHERE nspname = %s AND relkind = 'S' AND relname = ANY (%s) - ORDER BY relname""" - self.cursor.execute(query, (schema, sequences)) + if schema: + query = """SELECT relacl + FROM pg_catalog.pg_class c + JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + WHERE nspname = %s AND relkind = 'S' AND relname = ANY (%s) + ORDER BY relname""" + self.cursor.execute(query, (schema, sequences)) + else: + query = ("SELECT relacl FROM pg_catalog.pg_class " + "WHERE relkind = 'S' AND relname = ANY (%s) ORDER BY relname") + self.cursor.execute(query) return [t[0] for t in self.cursor.fetchall()] def get_function_acls(self, schema, function_signatures): funcnames = [f.split('(', 1)[0] for f in function_signatures] - query = """SELECT proacl - FROM pg_catalog.pg_proc p - JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace - WHERE nspname = %s AND proname = ANY (%s) - ORDER BY proname, proargtypes""" - self.cursor.execute(query, (schema, funcnames)) + if schema: + query = """SELECT proacl + FROM pg_catalog.pg_proc p + JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace + WHERE nspname = %s AND proname = ANY (%s) + ORDER BY proname, proargtypes""" + self.cursor.execute(query, (schema, funcnames)) + else: + query = ("SELECT proacl FROM pg_catalog.pg_proc WHERE proname = ANY (%s) " + "ORDER BY proname, proargtypes") + self.cursor.execute(query) return [t[0] for t in self.cursor.fetchall()] def get_schema_acls(self, schemas): @@ -674,11 +717,14 @@ def get_group_memberships(self, groups): return self.cursor.fetchall() def get_default_privs(self, schema, *args): - query = """SELECT defaclacl - FROM pg_default_acl a - JOIN pg_namespace b ON a.defaclnamespace=b.oid - WHERE b.nspname = %s;""" - self.cursor.execute(query, (schema,)) + if schema: + query = """SELECT defaclacl + FROM pg_default_acl a + JOIN pg_namespace b ON a.defaclnamespace=b.oid + WHERE b.nspname = %s;""" + self.cursor.execute(query, (schema,)) + else: + self.cursor.execute("SELECT defaclacl FROM pg_default_acl;") return [t[0] for t in self.cursor.fetchall()] def get_foreign_data_wrapper_acls(self, fdws): @@ -694,10 +740,14 @@ def get_foreign_server_acls(self, fs): return [t[0] for t in self.cursor.fetchall()] def get_type_acls(self, schema, types): - query = """SELECT t.typacl FROM pg_catalog.pg_type t - JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace - WHERE n.nspname = %s AND t.typname = ANY (%s) ORDER BY typname""" - self.cursor.execute(query, (schema, types)) + if schema: + query = """SELECT t.typacl FROM pg_catalog.pg_type t + JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace + WHERE n.nspname = %s AND t.typname = ANY (%s) ORDER BY typname""" + self.cursor.execute(query, (schema, types)) + else: + query = "SELECT typacl FROM pg_catalog.pg_type WHERE typname = ANY (%s) ORDER BY typname" + self.cursor.execute(query) return [t[0] for t in self.cursor.fetchall()] # Manipulating privileges @@ -869,7 +919,7 @@ def for_objs(self, objs): return self def for_schema(self, schema): - self._schema = schema + self._schema = ' IN SCHEMA %s' % schema if schema is not None else '' return self def with_grant_option(self, option): @@ -909,13 +959,13 @@ def add_default_revoke(self): for obj in self._objs: if self._as_who: self.query.append( - 'ALTER DEFAULT PRIVILEGES FOR ROLE {0} IN SCHEMA {1} REVOKE ALL ON {2} FROM {3};'.format(self._as_who, - self._schema, obj, - self._for_whom)) + 'ALTER DEFAULT PRIVILEGES FOR ROLE {0}{1} REVOKE ALL ON {2} FROM {3};'.format(self._as_who, + self._schema, obj, + self._for_whom)) else: self.query.append( - 'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} REVOKE ALL ON {1} FROM {2};'.format(self._schema, obj, - self._for_whom)) + 'ALTER DEFAULT PRIVILEGES{0} REVOKE ALL ON {1} FROM {2};'.format(self._schema, obj, + self._for_whom)) def add_grant_option(self): if self._grant_option: @@ -936,28 +986,28 @@ def add_default_priv(self): for obj in self._objs: if self._as_who: self.query.append( - 'ALTER DEFAULT PRIVILEGES FOR ROLE {0} IN SCHEMA {1} GRANT {2} ON {3} TO {4}'.format(self._as_who, - self._schema, - self._set_what, - obj, - self._for_whom)) + 'ALTER DEFAULT PRIVILEGES FOR ROLE {0}{1} GRANT {2} ON {3} TO {4}'.format(self._as_who, + self._schema, + self._set_what, + obj, + self._for_whom)) else: self.query.append( - 'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} GRANT {1} ON {2} TO {3}'.format(self._schema, - self._set_what, - obj, - self._for_whom)) + 'ALTER DEFAULT PRIVILEGES{0} GRANT {1} ON {2} TO {3}'.format(self._schema, + self._set_what, + obj, + self._for_whom)) self.add_grant_option() if self._usage_on_types: if self._as_who: self.query.append( - 'ALTER DEFAULT PRIVILEGES FOR ROLE {0} IN SCHEMA {1} GRANT USAGE ON TYPES TO {2}'.format(self._as_who, - self._schema, - self._for_whom)) + 'ALTER DEFAULT PRIVILEGES FOR ROLE {0}{1} GRANT USAGE ON TYPES TO {2}'.format(self._as_who, + self._schema, + self._for_whom)) else: self.query.append( - 'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} GRANT USAGE ON TYPES TO {1}'.format(self._schema, self._for_whom)) + 'ALTER DEFAULT PRIVILEGES{0} GRANT USAGE ON TYPES TO {1}'.format(self._schema, self._for_whom)) self.add_grant_option() def build_present(self): @@ -974,13 +1024,13 @@ def build_absent(self): for obj in ['TABLES', 'SEQUENCES', 'TYPES']: if self._as_who: self.query.append( - 'ALTER DEFAULT PRIVILEGES FOR ROLE {0} IN SCHEMA {1} REVOKE ALL ON {2} FROM {3};'.format(self._as_who, - self._schema, obj, - self._for_whom)) + 'ALTER DEFAULT PRIVILEGES FOR ROLE {0}{1} REVOKE ALL ON {2} FROM {3};'.format(self._as_who, + self._schema, obj, + self._for_whom)) else: self.query.append( - 'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} REVOKE ALL ON {1} FROM {2};'.format(self._schema, obj, - self._for_whom)) + 'ALTER DEFAULT PRIVILEGES{0} REVOKE ALL ON {1} FROM {2};'.format(self._schema, obj, + self._for_whom)) else: self.query.append('REVOKE {0} FROM {1};'.format(self._set_what, self._for_whom)) @@ -1033,7 +1083,10 @@ def main(): p = type('Params', (), module.params) # param "schema": default, allowed depends on param "type" if p.type in ['table', 'sequence', 'function', 'procedure', 'type', 'default_privs']: - p.schema = p.schema or 'public' + if p.objs == 'schemas' or p.schema == 'not-specified': + p.schema = None + else: + p.schema = p.schema or 'public' elif p.schema: module.fail_json(msg='Argument "schema" is not allowed ' 'for type "%s".' % p.type) diff --git a/tests/integration/targets/postgresql_privs/tasks/postgresql_privs_general.yml b/tests/integration/targets/postgresql_privs/tasks/postgresql_privs_general.yml index b0e80af2..b6c999f6 100644 --- a/tests/integration/targets/postgresql_privs/tasks/postgresql_privs_general.yml +++ b/tests/integration/targets/postgresql_privs/tasks/postgresql_privs_general.yml @@ -1476,6 +1476,74 @@ that: - result is not changed +############## +# Issue https://github.com/ansible-collections/community.postgresql/issues/332 +- name: Test community.postgresql issue 332 grant usage + become: yes + become_user: "{{ pg_user }}" + postgresql_privs: + login_user: "{{ pg_user }}" + login_db: "{{ db_name }}" + roles: "{{ db_user3 }}" + objs: schemas + type: default_privs + privs: usage + register: result + +- assert: + that: + - result is changed + +- name: Test community.postgresql issue 332 grant usage, run again + become: yes + become_user: "{{ pg_user }}" + postgresql_privs: + login_user: "{{ pg_user }}" + login_db: "{{ db_name }}" + roles: "{{ db_user3 }}" + objs: schemas + type: default_privs + privs: usage + register: result + +- assert: + that: + - result is not changed + +- name: Test community.postgresql issue 333 grant usage + become: yes + become_user: "{{ pg_user }}" + postgresql_privs: + login_user: "{{ pg_user }}" + login_db: "{{ db_name }}" + roles: "{{ db_user3 }}" + objs: tables + type: default_privs + schema: not-specified + privs: select + register: result + +- assert: + that: + - result is changed + +- name: Test community.postgresql issue 333 grant usage, run again + become: yes + become_user: "{{ pg_user }}" + postgresql_privs: + login_user: "{{ pg_user }}" + login_db: "{{ db_name }}" + roles: "{{ db_user3 }}" + objs: tables + type: default_privs + schema: not-specified + privs: select + register: result + +- assert: + that: + - result is not changed + # Cleanup - name: Remove privs become: true @@ -1503,6 +1571,13 @@ - "{{ db_user2 }}" - "{{ db_user3 }}" +- name: Drop a role for which the default privileges have been altered + become: yes + become_user: "{{ pg_user }}" + postgresql_query: + login_db: "{{ db_name }}" + query: "DROP OWNED BY {{ db_user3 }};" + - name: Remove user given permissions become: true become_user: "{{ pg_user }}"