diff --git a/changelogs/fragments/50877-postgresql_privs_add-support-for-target_role.yaml b/changelogs/fragments/50877-postgresql_privs_add-support-for-target_role.yaml new file mode 100644 index 0000000000..ccd0cad3d4 --- /dev/null +++ b/changelogs/fragments/50877-postgresql_privs_add-support-for-target_role.yaml @@ -0,0 +1,3 @@ +--- +minor_changes: + - "postgresql_privs - introduces support to postgresql_privs to use 'FOR { ROLE | USER } target_role' in 'ALTER DEFAULT PRIVILEGES'. (https://github.com/ansible/ansible/issues/50877)" diff --git a/lib/ansible/modules/database/postgresql/postgresql_privs.py b/lib/ansible/modules/database/postgresql/postgresql_privs.py index b77727ad79..3de6a17fa0 100644 --- a/lib/ansible/modules/database/postgresql/postgresql_privs.py +++ b/lib/ansible/modules/database/postgresql/postgresql_privs.py @@ -84,6 +84,12 @@ options: description: | Switch to session_role after connecting. The specified session_role must be a role that the current login_user is a member of. Permissions checking for SQL commands is carried out as though the session_role were the one that had logged in originally. + target_roles: + description: + - A list of existing role (user/group) names to set as the + default permissions for database objects subsequently created by them. + - Parameter I(target_roles) is only available with C(type=default_privs). + version_added: '2.8' grant_option: description: - Whether C(role) may grant/revoke the specified privileges/group @@ -302,6 +308,36 @@ EXAMPLES = """ roles: caller objs: ALL_IN_SCHEMA schema: common + +# Available since version 2.8 +# ALTER DEFAULT PRIVILEGES FOR ROLE librarian IN SCHEMA library GRANT SELECT ON TABLES TO reader +# GRANT SELECT privileges for new TABLES objects created by librarian as +# default to the role reader. +# For specific +- postgresql_privs: + db: library + schema: library + objs: TABLES + privs: SELECT + type: default_privs + role: reader + target_roles: librarian + +# Available since version 2.8 +# ALTER DEFAULT PRIVILEGES FOR ROLE librarian IN SCHEMA library REVOKE SELECT ON TABLES FROM reader +# REVOKE SELECT privileges for new TABLES objects created by librarian as +# default from the role reader. +# For specific +- postgresql_privs: + db: library + state: absent + schema: library + objs: TABLES + privs: SELECT + type: default_privs + role: reader + target_roles: librarian + """ import traceback @@ -538,7 +574,7 @@ class Connection(object): # Manipulating privileges - def manipulate_privs(self, obj_type, privs, objs, roles, + def manipulate_privs(self, obj_type, privs, objs, roles, target_roles, state, grant_option, schema_qualifier=None, fail_on_role=True): """Manipulate database object privileges. @@ -550,6 +586,8 @@ class Connection(object): privileges for. :param roles: Either a list of role names or "PUBLIC" for the implicitly defined "PUBLIC" group + :param target_roles: List of role names to grant/revoke + default privileges as. :param state: "present" to grant privileges, "absent" to revoke. :param grant_option: Only for state "present": If True, set grant/admin option. If False, revoke it. @@ -639,12 +677,18 @@ class Connection(object): for_whom = ','.join(for_whom) + # as_who: + as_who = None + if target_roles: + as_who = ','.join(pg_quote_identifier(r, 'role') for r in target_roles) + status_before = get_status(objs) query = QueryBuilder(state) \ .for_objtype(obj_type) \ .with_grant_option(grant_option) \ .for_whom(for_whom) \ + .as_who(as_who) \ .for_schema(schema_qualifier) \ .set_what(set_what) \ .for_objs(objs) \ @@ -659,6 +703,7 @@ class QueryBuilder(object): def __init__(self, state): self._grant_option = None self._for_whom = None + self._as_who = None self._set_what = None self._obj_type = None self._state = state @@ -682,6 +727,10 @@ class QueryBuilder(object): self._for_whom = who return self + def as_who(self, target_roles): + self._as_who = target_roles + return self + def set_what(self, what): self._set_what = what return self @@ -701,9 +750,15 @@ class QueryBuilder(object): def add_default_revoke(self): for obj in self._objs: - self.query.append( - 'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} REVOKE ALL ON {1} FROM {2};'.format(self._schema, obj, - self._for_whom)) + 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)) + else: + self.query.append( + 'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} REVOKE ALL ON {1} FROM {2};'.format(self._schema, obj, + self._for_whom)) def add_grant_option(self): if self._grant_option: @@ -720,13 +775,28 @@ class QueryBuilder(object): def add_default_priv(self): for obj in self._objs: - 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)) + 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)) + 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)) self.add_grant_option() - self.query.append( - 'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} GRANT USAGE ON TYPES TO {1}'.format(self._schema, self._for_whom)) + 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)) + else: + self.query.append( + 'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} GRANT USAGE ON TYPES TO {1}'.format(self._schema, self._for_whom)) self.add_grant_option() def build_present(self): @@ -741,9 +811,15 @@ class QueryBuilder(object): if self._obj_type == 'default_privs': self.query = [] for obj in ['TABLES', 'SEQUENCES', 'TYPES']: - self.query.append( - 'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} REVOKE ALL ON {1} FROM {2};'.format(self._schema, obj, - self._for_whom)) + 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)) + else: + self.query.append( + 'ALTER DEFAULT PRIVILEGES IN SCHEMA {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)) @@ -770,6 +846,7 @@ def main(): schema=dict(required=False), roles=dict(required=True, aliases=['role']), session_role=dict(required=False), + target_roles=dict(required=False), grant_option=dict(required=False, type='bool', aliases=['admin_option']), host=dict(default='', aliases=['login_host']), @@ -884,11 +961,23 @@ def main(): else: module.warn("Role '%s' does not exist, nothing to do" % roles[0].strip()) + # check if target_roles is set with type: default_privs + if p.target_roles and not p.type == 'default_privs': + module.warn('"target_roles" will be ignored ' + 'Argument "type: default_privs" is required for usage of "target_roles".') + + # target roles + if p.target_roles: + target_roles = p.target_roles.split(',') + else: + target_roles = None + changed = conn.manipulate_privs( obj_type=p.type, privs=privs, objs=objs, roles=roles, + target_roles=target_roles, state=p.state, grant_option=p.grant_option, schema_qualifier=p.schema, diff --git a/test/integration/targets/postgresql/tasks/main.yml b/test/integration/targets/postgresql/tasks/main.yml index 396fff2aad..8665d305b0 100644 --- a/test/integration/targets/postgresql/tasks/main.yml +++ b/test/integration/targets/postgresql/tasks/main.yml @@ -787,6 +787,10 @@ # Test postgresql_facts module: - include: postgresql_facts.yml +# Test default_privs with target_role +- include: test_target_role.yml + when: postgres_version_resp.stdout is version('9.1', '>=') + # dump/restore tests per format # ============================================================ - include: state_dump_restore.yml test_fixture=user file=dbdata.sql diff --git a/test/integration/targets/postgresql/tasks/test_target_role.yml b/test/integration/targets/postgresql/tasks/test_target_role.yml new file mode 100644 index 0000000000..26d2f02f17 --- /dev/null +++ b/test/integration/targets/postgresql/tasks/test_target_role.yml @@ -0,0 +1,94 @@ +--- + +# Setup +- name: Create DB + become_user: "{{ pg_user }}" + become: yes + postgresql_db: + state: present + name: "{{ db_name }}" + owner: "{{ db_user1 }}" + login_user: "{{ pg_user }}" + +- name: Create a user to be given permissions and other tests + postgresql_user: + name: "{{ db_user2 }}" + state: present + encrypted: yes + password: password + role_attr_flags: LOGIN + db: "{{ db_name }}" + login_user: "{{ pg_user }}" + +####################################### +# Test default_privs with target_role # +####################################### + +# Test +- name: Grant default privileges for new table objects + become_user: "{{ pg_user }}" + become: yes + postgresql_privs: + db: "{{ db_name }}" + objs: TABLES + privs: SELECT + type: default_privs + role: "{{ db_user2 }}" + target_roles: "{{ db_user1 }}" + login_user: "{{ pg_user }}" + register: result + +# Checks +- assert: + that: result.changed == true + +- name: Check that default privileges are set + become: yes + become_user: "{{ pg_user }}" + shell: psql {{ db_name }} -c "SELECT defaclrole, defaclobjtype, defaclacl FROM pg_default_acl a JOIN pg_roles b ON a.defaclrole=b.oid;" -t + register: result + +- assert: + that: "'{{ db_user2 }}=r/{{ db_user1 }}' in '{{ result.stdout_lines[0] }}'" + +# Test +- name: Revoke default privileges for new table objects + become_user: "{{ pg_user }}" + become: yes + postgresql_privs: + db: "{{ db_name }}" + state: absent + objs: TABLES + privs: SELECT + type: default_privs + role: "{{ db_user2 }}" + target_roles: "{{ db_user1 }}" + login_user: "{{ pg_user }}" + register: result + +# Checks +- assert: + that: result.changed == true + +# Cleanup +- name: Remove user given permissions + postgresql_user: + name: "{{ db_user2 }}" + state: absent + db: "{{ db_name }}" + login_user: "{{ pg_user }}" + +- name: Remove user owner of objects + postgresql_user: + name: "{{ db_user3 }}" + state: absent + db: "{{ db_name }}" + login_user: "{{ pg_user }}" + +- name: Destroy DB + become_user: "{{ pg_user }}" + become: yes + postgresql_db: + state: absent + name: "{{ db_name }}" + login_user: "{{ pg_user }}"