#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:
parent
7b44bc1ac9
commit
bb61d7527f
4 changed files with 203 additions and 13 deletions
|
@ -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)"
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }}"
|
Loading…
Reference in a new issue