Support for postgresql default privileges (#32987)
* Support for postgresql default privileges fix the following issues: * #29701 * #23657 * The ALTER DEFAULT PRIVILEGES is implemented with type 'default_privs' * Added a Query Builder for simplification * Some minor lint * Fixed Lint Issue in doc Fixed misspelled method name * Removed the damned empty space on line 243 ! (within the doc) x| * Kept Compat in string interpolation for old beloved python 2.6
This commit is contained in:
parent
3a3869f4c8
commit
5385de4bfd
1 changed files with 175 additions and 29 deletions
|
@ -7,12 +7,10 @@
|
||||||
from __future__ import absolute_import, division, print_function
|
from __future__ import absolute_import, division, print_function
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||||
'status': ['stableinterface'],
|
'status': ['stableinterface'],
|
||||||
'supported_by': 'community'}
|
'supported_by': 'community'}
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = """
|
DOCUMENTATION = """
|
||||||
---
|
---
|
||||||
module: postgresql_privs
|
module: postgresql_privs
|
||||||
|
@ -44,7 +42,8 @@ options:
|
||||||
- Type of database object to set privileges on.
|
- Type of database object to set privileges on.
|
||||||
default: table
|
default: table
|
||||||
choices: [table, sequence, function, database,
|
choices: [table, sequence, function, database,
|
||||||
schema, language, tablespace, group]
|
schema, language, tablespace, group,
|
||||||
|
default_privs]
|
||||||
objs:
|
objs:
|
||||||
description:
|
description:
|
||||||
- Comma separated list of database objects to set privileges on.
|
- Comma separated list of database objects to set privileges on.
|
||||||
|
@ -227,6 +226,37 @@ EXAMPLES = """
|
||||||
privs: ALL
|
privs: ALL
|
||||||
type: database
|
type: database
|
||||||
role: librarian
|
role: librarian
|
||||||
|
|
||||||
|
# ALTER DEFAULT PRIVILEGES ON DATABASE library TO librarian
|
||||||
|
# Objs must be set, ALL_DEFAULT to TABLES/SEQUENCES/TYPES/FUNCTIONS
|
||||||
|
# ALL_DEFAULT works only with privs=ALL
|
||||||
|
# For specific
|
||||||
|
- postgresql_privs:
|
||||||
|
db: library
|
||||||
|
objs: ALL_DEFAULT
|
||||||
|
privs: ALL
|
||||||
|
type: default_privs
|
||||||
|
role: librarian
|
||||||
|
grant_option: yes
|
||||||
|
|
||||||
|
# ALTER DEFAULT PRIVILEGES ON DATABASE library TO reader
|
||||||
|
# Objs must be set, ALL_DEFAULT to TABLES/SEQUENCES/TYPES/FUNCTIONS
|
||||||
|
# ALL_DEFAULT works only with privs=ALL
|
||||||
|
# For specific
|
||||||
|
- postgresql_privs:
|
||||||
|
db: library
|
||||||
|
objs: TABLES,SEQUENCES
|
||||||
|
privs: SELECT
|
||||||
|
type: default_privs
|
||||||
|
role: reader
|
||||||
|
|
||||||
|
- postgresql_privs:
|
||||||
|
db: library
|
||||||
|
objs: TYPES
|
||||||
|
privs: USAGE
|
||||||
|
type: default_privs
|
||||||
|
role: reader
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
|
@ -240,12 +270,15 @@ except ImportError:
|
||||||
# import module snippets
|
# import module snippets
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
from ansible.module_utils.database import pg_quote_identifier
|
from ansible.module_utils.database import pg_quote_identifier
|
||||||
from ansible.module_utils._text import to_native, to_text
|
from ansible.module_utils._text import to_native
|
||||||
|
|
||||||
|
|
||||||
VALID_PRIVS = frozenset(('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE',
|
VALID_PRIVS = frozenset(('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE',
|
||||||
'REFERENCES', 'TRIGGER', 'CREATE', 'CONNECT',
|
'REFERENCES', 'TRIGGER', 'CREATE', 'CONNECT',
|
||||||
'TEMPORARY', 'TEMP', 'EXECUTE', 'USAGE', 'ALL', 'USAGE'))
|
'TEMPORARY', 'TEMP', 'EXECUTE', 'USAGE', 'ALL', 'USAGE'))
|
||||||
|
VALID_DEFAULT_OBJS = {'TABLES': ('ALL', 'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'REFERENCES', 'TRIGGER'),
|
||||||
|
'SEQUENCES': ('ALL', 'SELECT', 'UPDATE', 'USAGE'),
|
||||||
|
'FUNCTIONS': ('ALL', 'EXECUTE'),
|
||||||
|
'TYPES': ('ALL', 'USAGE')}
|
||||||
|
|
||||||
|
|
||||||
class Error(Exception):
|
class Error(Exception):
|
||||||
|
@ -255,10 +288,12 @@ class Error(Exception):
|
||||||
# We don't have functools.partial in Python < 2.5
|
# We don't have functools.partial in Python < 2.5
|
||||||
def partial(f, *args, **kwargs):
|
def partial(f, *args, **kwargs):
|
||||||
"""Partial function application"""
|
"""Partial function application"""
|
||||||
|
|
||||||
def g(*g_args, **g_kwargs):
|
def g(*g_args, **g_kwargs):
|
||||||
new_kwargs = kwargs.copy()
|
new_kwargs = kwargs.copy()
|
||||||
new_kwargs.update(g_kwargs)
|
new_kwargs.update(g_kwargs)
|
||||||
return f(*(args + g_args), **g_kwargs)
|
return f(*(args + g_args), **g_kwargs)
|
||||||
|
|
||||||
g.f = f
|
g.f = f
|
||||||
g.args = args
|
g.args = args
|
||||||
g.kwargs = kwargs
|
g.kwargs = kwargs
|
||||||
|
@ -410,6 +445,14 @@ class Connection(object):
|
||||||
self.cursor.execute(query, (groups,))
|
self.cursor.execute(query, (groups,))
|
||||||
return self.cursor.fetchall()
|
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,))
|
||||||
|
return [t[0] for t in self.cursor.fetchall()]
|
||||||
|
|
||||||
# Manipulating privileges
|
# Manipulating privileges
|
||||||
|
|
||||||
def manipulate_privs(self, obj_type, privs, objs, roles,
|
def manipulate_privs(self, obj_type, privs, objs, roles,
|
||||||
|
@ -449,6 +492,8 @@ class Connection(object):
|
||||||
get_status = self.get_database_acls
|
get_status = self.get_database_acls
|
||||||
elif obj_type == 'group':
|
elif obj_type == 'group':
|
||||||
get_status = self.get_group_memberships
|
get_status = self.get_group_memberships
|
||||||
|
elif obj_type == 'default_privs':
|
||||||
|
get_status = partial(self.get_default_privs, schema_qualifier)
|
||||||
else:
|
else:
|
||||||
raise Error('Unsupported database object type "%s".' % obj_type)
|
raise Error('Unsupported database object type "%s".' % obj_type)
|
||||||
|
|
||||||
|
@ -474,6 +519,9 @@ class Connection(object):
|
||||||
# Either group membership or privileges on objects of a certain type
|
# Either group membership or privileges on objects of a certain type
|
||||||
if obj_type == 'group':
|
if obj_type == 'group':
|
||||||
set_what = ','.join(pg_quote_identifier(i, 'role') for i in obj_ids)
|
set_what = ','.join(pg_quote_identifier(i, 'role') for i in obj_ids)
|
||||||
|
elif obj_type == 'default_privs':
|
||||||
|
# We don't want privs to be quoted here
|
||||||
|
set_what = ','.join(privs)
|
||||||
else:
|
else:
|
||||||
# function types are already quoted above
|
# function types are already quoted above
|
||||||
if obj_type != 'function':
|
if obj_type != 'function':
|
||||||
|
@ -490,30 +538,114 @@ class Connection(object):
|
||||||
for_whom = ','.join(pg_quote_identifier(r, 'role') for r in roles)
|
for_whom = ','.join(pg_quote_identifier(r, 'role') for r in roles)
|
||||||
|
|
||||||
status_before = get_status(objs)
|
status_before = get_status(objs)
|
||||||
if state == 'present':
|
|
||||||
if grant_option:
|
|
||||||
if obj_type == 'group':
|
|
||||||
query = 'GRANT %s TO %s WITH ADMIN OPTION'
|
|
||||||
else:
|
|
||||||
query = 'GRANT %s TO %s WITH GRANT OPTION'
|
|
||||||
else:
|
|
||||||
query = 'GRANT %s TO %s'
|
|
||||||
self.cursor.execute(query % (set_what, for_whom))
|
|
||||||
|
|
||||||
# Only revoke GRANT/ADMIN OPTION if grant_option actually is False.
|
query = QueryBuilder(state) \
|
||||||
if grant_option is False:
|
.for_objtype(obj_type) \
|
||||||
if obj_type == 'group':
|
.with_grant_option(grant_option) \
|
||||||
query = 'REVOKE ADMIN OPTION FOR %s FROM %s'
|
.for_whom(for_whom) \
|
||||||
else:
|
.for_schema(schema_qualifier) \
|
||||||
query = 'REVOKE GRANT OPTION FOR %s FROM %s'
|
.set_what(set_what) \
|
||||||
self.cursor.execute(query % (set_what, for_whom))
|
.for_objs(objs) \
|
||||||
else:
|
.build()
|
||||||
query = 'REVOKE %s FROM %s'
|
|
||||||
self.cursor.execute(query % (set_what, for_whom))
|
self.cursor.execute(query)
|
||||||
status_after = get_status(objs)
|
status_after = get_status(objs)
|
||||||
return status_before != status_after
|
return status_before != status_after
|
||||||
|
|
||||||
|
|
||||||
|
class QueryBuilder(object):
|
||||||
|
def __init__(self, state):
|
||||||
|
self._grant_option = None
|
||||||
|
self._for_whom = None
|
||||||
|
self._set_what = None
|
||||||
|
self._obj_type = None
|
||||||
|
self._state = state
|
||||||
|
self._schema = None
|
||||||
|
self._objs = None
|
||||||
|
self.query = []
|
||||||
|
|
||||||
|
def for_objs(self, objs):
|
||||||
|
self._objs = objs
|
||||||
|
return self
|
||||||
|
|
||||||
|
def for_schema(self, schema):
|
||||||
|
self._schema = schema
|
||||||
|
return self
|
||||||
|
|
||||||
|
def with_grant_option(self, option):
|
||||||
|
self._grant_option = option
|
||||||
|
return self
|
||||||
|
|
||||||
|
def for_whom(self, who):
|
||||||
|
self._for_whom = who
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_what(self, what):
|
||||||
|
self._set_what = what
|
||||||
|
return self
|
||||||
|
|
||||||
|
def for_objtype(self, objtype):
|
||||||
|
self._obj_type = objtype
|
||||||
|
return self
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
if self._state == 'present':
|
||||||
|
self.build_present()
|
||||||
|
elif self._state == 'absent':
|
||||||
|
self.build_absent()
|
||||||
|
else:
|
||||||
|
self.build_absent()
|
||||||
|
return '\n'.join(self.query)
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
def add_grant_option(self):
|
||||||
|
if self._grant_option:
|
||||||
|
if self._obj_type == 'group':
|
||||||
|
self.query[-1] += ' WITH ADMIN OPTION;'
|
||||||
|
else:
|
||||||
|
self.query[-1] += ' WITH GRANT OPTION;'
|
||||||
|
else:
|
||||||
|
self.query[-1] += ';'
|
||||||
|
if self._obj_type == 'group':
|
||||||
|
self.query.append('REVOKE ADMIN OPTION FOR {0} FROM {1};'.format(self._set_what, self._for_whom))
|
||||||
|
elif not self._obj_type == 'default_privs':
|
||||||
|
self.query.append('REVOKE GRANT OPTION FOR {0} FROM {1};'.format(self._set_what, self._for_whom))
|
||||||
|
|
||||||
|
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))
|
||||||
|
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))
|
||||||
|
self.add_grant_option()
|
||||||
|
|
||||||
|
def build_present(self):
|
||||||
|
if self._obj_type == 'default_privs':
|
||||||
|
self.add_default_revoke()
|
||||||
|
self.add_default_priv()
|
||||||
|
else:
|
||||||
|
self.query.append('GRANT {0} TO {1}'.format(self._set_what, self._for_whom))
|
||||||
|
self.add_grant_option()
|
||||||
|
|
||||||
|
def build_absent(self):
|
||||||
|
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))
|
||||||
|
else:
|
||||||
|
self.query.append('REVOKE {0} FROM {1};'.format(self._set_what, self._for_whom))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
module = AnsibleModule(
|
module = AnsibleModule(
|
||||||
argument_spec=dict(
|
argument_spec=dict(
|
||||||
|
@ -528,7 +660,8 @@ def main():
|
||||||
'schema',
|
'schema',
|
||||||
'language',
|
'language',
|
||||||
'tablespace',
|
'tablespace',
|
||||||
'group']),
|
'group',
|
||||||
|
'default_privs']),
|
||||||
objs=dict(required=False, aliases=['obj']),
|
objs=dict(required=False, aliases=['obj']),
|
||||||
schema=dict(required=False),
|
schema=dict(required=False),
|
||||||
roles=dict(required=True, aliases=['role']),
|
roles=dict(required=True, aliases=['role']),
|
||||||
|
@ -539,7 +672,8 @@ def main():
|
||||||
unix_socket=dict(default='', aliases=['login_unix_socket']),
|
unix_socket=dict(default='', aliases=['login_unix_socket']),
|
||||||
login=dict(default='postgres', aliases=['login_user']),
|
login=dict(default='postgres', aliases=['login_user']),
|
||||||
password=dict(default='', aliases=['login_password'], no_log=True),
|
password=dict(default='', aliases=['login_password'], no_log=True),
|
||||||
ssl_mode=dict(default="prefer", choices=['disable', 'allow', 'prefer', 'require', 'verify-ca', 'verify-full']),
|
ssl_mode=dict(default="prefer",
|
||||||
|
choices=['disable', 'allow', 'prefer', 'require', 'verify-ca', 'verify-full']),
|
||||||
ssl_rootcert=dict(default=None)
|
ssl_rootcert=dict(default=None)
|
||||||
),
|
),
|
||||||
supports_check_mode=True
|
supports_check_mode=True
|
||||||
|
@ -547,9 +681,8 @@ def main():
|
||||||
|
|
||||||
# Create type object as namespace for module params
|
# Create type object as namespace for module params
|
||||||
p = type('Params', (), module.params)
|
p = type('Params', (), module.params)
|
||||||
|
|
||||||
# param "schema": default, allowed depends on param "type"
|
# param "schema": default, allowed depends on param "type"
|
||||||
if p.type in ['table', 'sequence', 'function']:
|
if p.type in ['table', 'sequence', 'function', 'default_privs']:
|
||||||
p.schema = p.schema or 'public'
|
p.schema = p.schema or 'public'
|
||||||
elif p.schema:
|
elif p.schema:
|
||||||
module.fail_json(msg='Argument "schema" is not allowed '
|
module.fail_json(msg='Argument "schema" is not allowed '
|
||||||
|
@ -594,12 +727,25 @@ def main():
|
||||||
module.fail_json(msg='Invalid privileges specified: %s' % privs.difference(VALID_PRIVS))
|
module.fail_json(msg='Invalid privileges specified: %s' % privs.difference(VALID_PRIVS))
|
||||||
else:
|
else:
|
||||||
privs = None
|
privs = None
|
||||||
|
|
||||||
# objs:
|
# objs:
|
||||||
if p.type == 'table' and p.objs == 'ALL_IN_SCHEMA':
|
if p.type == 'table' and p.objs == 'ALL_IN_SCHEMA':
|
||||||
objs = conn.get_all_tables_in_schema(p.schema)
|
objs = conn.get_all_tables_in_schema(p.schema)
|
||||||
elif p.type == 'sequence' and p.objs == 'ALL_IN_SCHEMA':
|
elif p.type == 'sequence' and p.objs == 'ALL_IN_SCHEMA':
|
||||||
objs = conn.get_all_sequences_in_schema(p.schema)
|
objs = conn.get_all_sequences_in_schema(p.schema)
|
||||||
|
elif p.type == 'default_privs':
|
||||||
|
if p.objs == 'ALL_DEFAULT':
|
||||||
|
objs = frozenset(VALID_DEFAULT_OBJS.keys())
|
||||||
|
else:
|
||||||
|
objs = frozenset(obj.upper() for obj in p.objs.split(','))
|
||||||
|
if not objs.issubset(VALID_DEFAULT_OBJS):
|
||||||
|
module.fail_json(
|
||||||
|
msg='Invalid Object set specified: %s' % objs.difference(VALID_DEFAULT_OBJS.keys()))
|
||||||
|
# Again, do we have valid privs specified for object type:
|
||||||
|
valid_objects_for_priv = frozenset(obj for obj in objs if privs.issubset(VALID_DEFAULT_OBJS[obj]))
|
||||||
|
if not valid_objects_for_priv == objs:
|
||||||
|
module.fail_json(
|
||||||
|
msg='Invalid priv specified. Valid object for priv: {0}. Objects: {1}'.format(
|
||||||
|
valid_objects_for_priv, objs))
|
||||||
else:
|
else:
|
||||||
objs = p.objs.split(',')
|
objs = p.objs.split(',')
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue