1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

#50877: add support to postgresql_privs to use "FOR { ROLE | USER } target_role" in "ALTER DEFAULT PRIVILEGES" (#51073)

* #50877:
* add support to postgresql_privs to use "FOR { ROLE | USER } target_role"
   in "ALTER DEFAULT PRIVILEGES"

* fix sanity errors

* #50877: fix documentation and add a check for correct usage
of target_roles

*  #50877: fix missing absent option for default privs with target_role

* #50877: add clear description, when target_roles can be used

* #50877: fix conflicts, formatting, and add a changelog fragment

* #50877: fix sanity error E335

* #50877: swap conditions and fix error to warning msg

*  #50877: add tests for default privileges

* #50877: fix tests for default privileges

* #50877: fix tests for default privileges on centos 6
This commit is contained in:
tcraxs 2019-03-21 14:26:44 +01:00 committed by John R Barker
parent 7b44bc1ac9
commit bb61d7527f
4 changed files with 203 additions and 13 deletions

View file

@ -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)"

View file

@ -84,6 +84,12 @@ options:
description: | description: |
Switch to session_role after connecting. The specified session_role must be a role that the current login_user is a member of. 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. 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: grant_option:
description: description:
- Whether C(role) may grant/revoke the specified privileges/group - Whether C(role) may grant/revoke the specified privileges/group
@ -302,6 +308,36 @@ EXAMPLES = """
roles: caller roles: caller
objs: ALL_IN_SCHEMA objs: ALL_IN_SCHEMA
schema: common 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 import traceback
@ -538,7 +574,7 @@ class Connection(object):
# Manipulating privileges # 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): state, grant_option, schema_qualifier=None, fail_on_role=True):
"""Manipulate database object privileges. """Manipulate database object privileges.
@ -550,6 +586,8 @@ class Connection(object):
privileges for. privileges for.
:param roles: Either a list of role names or "PUBLIC" :param roles: Either a list of role names or "PUBLIC"
for the implicitly defined "PUBLIC" group 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 state: "present" to grant privileges, "absent" to revoke.
:param grant_option: Only for state "present": If True, set :param grant_option: Only for state "present": If True, set
grant/admin option. If False, revoke it. grant/admin option. If False, revoke it.
@ -639,12 +677,18 @@ class Connection(object):
for_whom = ','.join(for_whom) 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) status_before = get_status(objs)
query = QueryBuilder(state) \ query = QueryBuilder(state) \
.for_objtype(obj_type) \ .for_objtype(obj_type) \
.with_grant_option(grant_option) \ .with_grant_option(grant_option) \
.for_whom(for_whom) \ .for_whom(for_whom) \
.as_who(as_who) \
.for_schema(schema_qualifier) \ .for_schema(schema_qualifier) \
.set_what(set_what) \ .set_what(set_what) \
.for_objs(objs) \ .for_objs(objs) \
@ -659,6 +703,7 @@ class QueryBuilder(object):
def __init__(self, state): def __init__(self, state):
self._grant_option = None self._grant_option = None
self._for_whom = None self._for_whom = None
self._as_who = None
self._set_what = None self._set_what = None
self._obj_type = None self._obj_type = None
self._state = state self._state = state
@ -682,6 +727,10 @@ class QueryBuilder(object):
self._for_whom = who self._for_whom = who
return self return self
def as_who(self, target_roles):
self._as_who = target_roles
return self
def set_what(self, what): def set_what(self, what):
self._set_what = what self._set_what = what
return self return self
@ -701,6 +750,12 @@ class QueryBuilder(object):
def add_default_revoke(self): def add_default_revoke(self):
for obj in self._objs: 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))
else:
self.query.append( self.query.append(
'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} REVOKE ALL ON {1} FROM {2};'.format(self._schema, obj, 'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} REVOKE ALL ON {1} FROM {2};'.format(self._schema, obj,
self._for_whom)) self._for_whom))
@ -720,11 +775,26 @@ class QueryBuilder(object):
def add_default_priv(self): def add_default_priv(self):
for obj in self._objs: for obj in self._objs:
if self._as_who:
self.query.append( self.query.append(
'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} GRANT {1} ON {2} TO {3}'.format(self._schema, self._set_what, '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, obj,
self._for_whom)) self._for_whom))
self.add_grant_option() self.add_grant_option()
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( self.query.append(
'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} GRANT USAGE ON TYPES TO {1}'.format(self._schema, self._for_whom)) 'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} GRANT USAGE ON TYPES TO {1}'.format(self._schema, self._for_whom))
self.add_grant_option() self.add_grant_option()
@ -741,6 +811,12 @@ class QueryBuilder(object):
if self._obj_type == 'default_privs': if self._obj_type == 'default_privs':
self.query = [] self.query = []
for obj in ['TABLES', 'SEQUENCES', 'TYPES']: 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))
else:
self.query.append( self.query.append(
'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} REVOKE ALL ON {1} FROM {2};'.format(self._schema, obj, 'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} REVOKE ALL ON {1} FROM {2};'.format(self._schema, obj,
self._for_whom)) self._for_whom))
@ -770,6 +846,7 @@ def main():
schema=dict(required=False), schema=dict(required=False),
roles=dict(required=True, aliases=['role']), roles=dict(required=True, aliases=['role']),
session_role=dict(required=False), session_role=dict(required=False),
target_roles=dict(required=False),
grant_option=dict(required=False, type='bool', grant_option=dict(required=False, type='bool',
aliases=['admin_option']), aliases=['admin_option']),
host=dict(default='', aliases=['login_host']), host=dict(default='', aliases=['login_host']),
@ -884,11 +961,23 @@ def main():
else: else:
module.warn("Role '%s' does not exist, nothing to do" % roles[0].strip()) 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( changed = conn.manipulate_privs(
obj_type=p.type, obj_type=p.type,
privs=privs, privs=privs,
objs=objs, objs=objs,
roles=roles, roles=roles,
target_roles=target_roles,
state=p.state, state=p.state,
grant_option=p.grant_option, grant_option=p.grant_option,
schema_qualifier=p.schema, schema_qualifier=p.schema,

View file

@ -787,6 +787,10 @@
# Test postgresql_facts module: # Test postgresql_facts module:
- include: postgresql_facts.yml - 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 # dump/restore tests per format
# ============================================================ # ============================================================
- include: state_dump_restore.yml test_fixture=user file=dbdata.sql - include: state_dump_restore.yml test_fixture=user file=dbdata.sql

View file

@ -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 }}"