diff --git a/lib/ansible/modules/database/postgresql/postgresql_pg_hba.py b/lib/ansible/modules/database/postgresql/postgresql_pg_hba.py new file mode 100644 index 00000000000..12aceec95b6 --- /dev/null +++ b/lib/ansible/modules/database/postgresql/postgresql_pg_hba.py @@ -0,0 +1,709 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2019, Sebastiaan Mannem (@sebasmannem) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +''' +This module is used to manage postgres pg_hba files with Ansible. +''' + +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: postgresql_pg_hba +short_description: Add, remove or modifie a rule in a pg_hba file +description: + - The fundamental function of the module is to create, or delete lines in pg_hba files. + - The lines in the file should be in a typical pg_hba form and lines should be unique per key (type, databases, users, source). + If they are not unique and the SID is 'the one to change', only one for C(state=present) or none for C(state=absent) of the SID's will remain. +extends_documentation_fragment: files +version_added: "2.8" +options: + address: + description: + - The source address/net where the connections could come from. + - Will not be used for entries of I(type)=C(local). + - You can also use keywords C(all), C(samehost), and C(samenet). + default: samehost + type: str + aliases: [ source, src ] + backup: + description: + - If set, create a backup of the C(pg_hba) file before it is modified. + The location of the backup is returned in the (backup) variable by this module. + default: false + type: bool + backup_file: + description: + - Write backup to a specific backupfile rather than a temp file. + type: str + create: + description: + - Create an C(pg_hba) file if none exists. + - When set to false, an error is raised when the C(pg_hba) file doesn't exist. + default: false + type: bool + contype: + description: + - Type of the rule. If not set, C(postgresql_pg_hba) will only return contents. + type: str + choices: [ local, host, hostnossl, hostssl ] + databases: + description: + - Databases this line applies to. + default: all + type: str + dest: + description: + - Path to C(pg_hba) file to modify. + type: path + required: true + method: + description: + - Authentication method to be used. + type: str + choices: [ cert, gss, ident, krb5, ldap, md5, pam, password, peer, radius, reject, scram-sha-256 , sspi, trust ] + default: md5 + netmask: + description: + - The netmask of the source address. + type: str + options: + description: + - Additional options for the authentication I(method). + type: str + order: + description: + - The entries will be written out in a specific order. + - With this option you can control by which field they are ordered first, second and last. + - s=source, d=databases, u=users. + default: sdu + choices: [ sdu, sud, dsu, dus, usd, uds ] + state: + description: + - The lines will be added/modified when C(state=present) and removed when C(state=absent). + default: present + choices: [ absent, present ] + users: + description: + - Users this line applies to. + default: all + +notes: + - The default authentication assumes that on the host, you are either logging in as or + sudo'ing to an account with appropriate permissions to read and modify the file. + - This module also returns the pg_hba info. You can use this module to only retrieve it by only specifying I(dest). + The info kan be found in the returned data under key pg_hba, being a list, containing a dict per rule. + - This module will sort resulting C(pg_hba) files if a rule change is required. + This could give unexpected results with manual created hba files, if it was improperly sorted. + For example a rule was created for a net first and for a ip in that net range next. + In that situation, the 'ip specific rule' will never hit, it is in the C(pg_hba) file obsolete. + After the C(pg_hba) file is rewritten by the M(pg_hba) module, the ip specific rule will be sorted above the range rule. + And then it will hit, which will give unexpected results. + - With the 'order' parameter you can control which field is used to sort first, next and last. + - The module supports a check mode and a diff mode. + +requirements: + - ipaddress + +author: Sebastiaan Mannem (@sebasmannem) +''' + +EXAMPLES = ''' +- name: Grant users joe and simon access to databases sales and logistics from ipv6 localhost ::1/128 using peer authentication. + postgresql_pg_hba: + dest=/var/lib/postgres/data/pg_hba.conf + contype=host + users=joe,simon + source=::1 + databases=sales,logistics + method=peer + create=true + +- name: Grant user replication from network 192.168.0.100/24 access for replication with client cert authentication. + postgresql_pg_hba: + dest=/var/lib/postgres/data/pg_hba.conf + contype=host + users=replication + source=192.168.0.100/24 + databases=replication + method=cert + +- name: Revoke access from local user mary on database mydb. + postgresql_pg_hba: + dest=/var/lib/postgres/data/pg_hba.conf + contype=local + users=mary + databases=mydb + state=absent +''' + +RETURN = r''' +msgs: + description: List of textual messages what was done + returned: always + type: list + sample: + "msgs": [ + "Removing", + "Changed", + "Writing" + ] +backup_file: + description: File that the original pg_hba file was backed up to + returned: changed + type: str + sample: /tmp/pg_hba_jxobj_p +pg_hba: + description: List of the pg_hba rules as they are configured in the specified hba file + returned: always + type: list + sample: + "pg_hba": [ + { + "db": "all", + "method": "md5", + "src": "samehost", + "type": "host", + "usr": "all" + } + ] +''' + +import os +import re +import traceback + +IPADDRESS_IMP_ERR = None +try: + import ipaddress + HAS_IPADDRESS = True +except ImportError: + IPADDRESS_IMP_ERR = traceback.format_exc() + HAS_IPADDRESS = False +else: + HAS_IPADDRESS = True + +import tempfile +import shutil +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +# from ansible.module_utils.postgres import postgres_common_argument_spec + +PG_HBA_METHODS = ["trust", "reject", "md5", "password", "gss", "sspi", "krb5", "ident", "peer", + "ldap", "radius", "cert", "pam", "scram-sha-256"] +PG_HBA_TYPES = ["local", "host", "hostssl", "hostnossl"] +PG_HBA_ORDERS = ["sdu", "sud", "dsu", "dus", "usd", "uds"] +PG_HBA_HDR = ['type', 'db', 'usr', 'src', 'mask', 'method', 'options'] + +WHITESPACES_RE = re.compile(r'\s+') + + +class PgHbaError(Exception): + ''' + This exception is raised when parsing the pg_hba file ends in an error. + ''' + + +class PgHbaRuleError(PgHbaError): + ''' + This exception is raised when parsing the pg_hba file ends in an error. + ''' + + +class PgHbaRuleChanged(PgHbaRuleError): + ''' + This exception is raised when a new parsed rule is a changed version of an existing rule. + ''' + + +class PgHbaValueError(PgHbaError): + ''' + This exception is raised when a new parsed rule is a changed version of an existing rule. + ''' + + +class PgHbaRuleValueError(PgHbaRuleError): + ''' + This exception is raised when a new parsed rule is a changed version of an existing rule. + ''' + + +class PgHba(object): + """ + PgHba object to read/write entries to/from. + pg_hba_file - the pg_hba file almost always /etc/pg_hba + """ + def __init__(self, pg_hba_file=None, order="sdu", backup=False, create=False): + if order not in PG_HBA_ORDERS: + msg = "invalid order setting {0} (should be one of '{1}')." + raise PgHbaError(msg.format(order, "', '".join(PG_HBA_ORDERS))) + self.pg_hba_file = pg_hba_file + self.rules = None + self.comment = None + self.order = order + self.backup = backup + self.last_backup = None + self.create = create + self.unchanged() + # self.databases will be update by add_rule and gives some idea of the number of databases + # (at least that are handled by this pg_hba) + self.databases = set(['postgres', 'template0', 'template1']) + + # self.databases will be update by add_rule and gives some idea of the number of users + # (at least that are handled by this pg_hba) since this migth also be groups with multiple + # users, this migth be totally off, but at least it is some info... + self.users = set(['postgres']) + + self.read() + + def unchanged(self): + ''' + This method resets self.diff to a empty default + ''' + self.diff = {'before': {'file': self.pg_hba_file, 'pg_hba': []}, + 'after': {'file': self.pg_hba_file, 'pg_hba': []}} + + def read(self): + ''' + Read in the pg_hba from the system + ''' + self.rules = {} + self.comment = [] + # read the pg_hbafile + try: + file = open(self.pg_hba_file, 'r') + for line in file: + line = line.strip() + # uncomment + if '#' in line: + line, comment = line.split('#', 1) + self.comment.append('#' + comment) + try: + self.add_rule(PgHbaRule(line=line)) + except PgHbaRuleError: + pass + file.close() + self.unchanged() + except IOError: + pass + + def write(self, backup_file=''): + ''' + This method writes the PgHba rules (back) to a file. + ''' + if not self.changed(): + return False + + if self.pg_hba_file: + if not (os.path.isfile(self.pg_hba_file) or self.create): + raise PgHbaError("pg_hba file '{0}' doesn't exist. " + "Use create option to autocreate.".format(self.pg_hba_file)) + if self.backup and os.path.isfile(self.pg_hba_file): + if backup_file: + self.last_backup = backup_file + else: + __backup_file_h, self.last_backup = tempfile.mkstemp(prefix='pg_hba') + shutil.copy(self.pg_hba_file, self.last_backup) + fileh = open(self.pg_hba_file, 'w') + else: + filed, __path = tempfile.mkstemp(prefix='pg_hba') + fileh = os.fdopen(filed, 'w') + + fileh.write(self.render()) + self.unchanged() + fileh.close() + return True + + def add_rule(self, rule): + ''' + This method can be used to add a rule to the list of rules in this PgHba object + ''' + key = rule.key() + try: + try: + oldrule = self.rules[key] + except KeyError: + raise PgHbaRuleChanged + ekeys = set(list(oldrule.keys()) + list(rule.keys())) + ekeys.remove('line') + for k in ekeys: + if oldrule[k] != rule[k]: + raise PgHbaRuleChanged('{0} changes {1}'.format(rule, oldrule)) + except PgHbaRuleChanged: + self.rules[key] = rule + self.diff['after']['pg_hba'].append(rule.line()) + if rule['db'] not in ['all', 'samerole', 'samegroup', 'replication']: + databases = set(rule['db'].split(',')) + self.databases.update(databases) + if rule['usr'] != 'all': + user = rule['usr'] + if user[0] == '+': + user = user[1:] + self.users.add(user) + + def remove_rule(self, rule): + ''' + This method can be used to find and remove a rule. It doesn't look for the exact rule, only + the rule with the same key. + ''' + keys = rule.key() + try: + del self.rules[keys] + self.diff['before']['pg_hba'].append(rule.line()) + except KeyError: + pass + + def get_rules(self, with_lines=False): + ''' + This method returns all the rules of the PgHba object + ''' + rules = sorted(self.rules.values(), + key=lambda rule: rule.weight(self.order, + len(self.users) + 1, + len(self.databases) + 1), + reverse=True) + for rule in rules: + ret = {} + for key, value in rule.items(): + ret[key] = value + if not with_lines: + if 'line' in ret: + del ret['line'] + else: + ret['line'] = rule.line() + + yield ret + + def render(self): + ''' + This method renders the content of the PgHba rules and comments. + The returning value can be used directly to write to a new file. + ''' + comment = '\n'.join(self.comment) + rule_lines = '\n'.join([rule['line'] for rule in self.get_rules(with_lines=True)]) + result = comment + '\n' + rule_lines + # End it properly with a linefeed (if not already). + if result and result[-1] not in ['\n', '\r']: + result += '\n' + return result + + def changed(self): + ''' + This method can be called to detect if the PgHba file has been changed. + ''' + return bool(self.diff['before']['pg_hba'] or self.diff['after']['pg_hba']) + + +class PgHbaRule(dict): + ''' + This class represents one rule as defined in a line in a PgHbaFile. + ''' + + def __init__(self, contype=None, databases=None, users=None, source=None, netmask=None, + method=None, options=None, line=None): + ''' + This function can be called with a comma seperated list of databases and a comma seperated + list of users and it will act as a generator that returns a expanded list of rules one by + one. + ''' + + super(PgHbaRule, self).__init__() + + if line: + # Read valies from line if parsed + self.fromline(line) + + # read rule cols from parsed items + rule = dict(zip(PG_HBA_HDR, [contype, databases, users, source, netmask, method, options])) + for key, value in rule.items(): + if value: + self[key] = value + + # Some sanity checks + for key in ['method', 'type']: + if key not in self: + raise PgHbaRuleError('Missing {0} in rule {1}'.format(key, self)) + + if self['method'] not in PG_HBA_METHODS: + msg = "invalid method {0} (should be one of '{1}')." + raise PgHbaRuleValueError(msg.format(self['method'], "', '".join(PG_HBA_METHODS))) + + if self['type'] not in PG_HBA_TYPES: + msg = "invalid connection type {0} (should be one of '{1}')." + raise PgHbaRuleValueError(msg.format(self['type'], "', '".join(PG_HBA_TYPES))) + + if self['type'] == 'local': + self.unset('src') + self.unset('mask') + elif 'src' not in self: + raise PgHbaRuleError('Missing src in rule {1}'.format(self)) + elif '/' in self['src']: + self.unset('mask') + else: + self['src'] = str(self.source()) + self.unset('mask') + + def unset(self, key): + ''' + This method is used to unset certain columns if they exist + ''' + if key in self: + del self[key] + + def line(self): + ''' + This method can be used to return (or generate) the line + ''' + try: + return self['line'] + except KeyError: + self['line'] = "\t".join([self[k] for k in PG_HBA_HDR if k in self.keys()]) + return self['line'] + + def fromline(self, line): + ''' + split into 'type', 'db', 'usr', 'src', 'mask', 'method', 'options' cols + ''' + if WHITESPACES_RE.sub('', line) == '': + # empty line. skip this one... + return + cols = WHITESPACES_RE.split(line) + if len(cols) < 4: + msg = "Rule {0} has too few columns." + raise PgHbaValueError(msg.format(line)) + if cols[0] not in PG_HBA_TYPES: + msg = "Rule {0} has unknown type: {1}." + raise PgHbaValueError(msg.format(line, cols[0])) + if cols[0] == 'local': + if cols[3] not in PG_HBA_METHODS: + raise PgHbaValueError("Rule {0} of 'local' type has invalid auth-method {1}" + "on 4th column ".format(line, cols[3])) + cols.insert(3, None) + cols.insert(3, None) + else: + if len(cols) < 6: + cols.insert(4, None) + elif cols[5] not in PG_HBA_METHODS: + cols.insert(4, None) + if len(cols) < 7: + cols.insert(7, None) + if cols[5] not in PG_HBA_METHODS: + raise PgHbaValueError("Rule {0} has no valid method.".format(line)) + rule = dict(zip(PG_HBA_HDR, cols[:7])) + for key, value in rule.items(): + if value: + self[key] = value + + def key(self): + ''' + This method can be used to get the key from a rule. + ''' + if self['type'] == 'local': + source = 'local' + else: + source = str(self.source()) + return (source, self['db'], self['usr']) + + def source(self): + ''' + This method is used to get the source of a rule as an ipaddress object if possible. + ''' + if 'mask' in self.keys(): + try: + ipaddress.ip_address(u'{0}'.format(self['src'])) + except ValueError: + raise PgHbaValueError('Mask was specified, but source "{0}" ' + 'is no valid ip'.format(self['src'])) + # ipaddress module cannot work with ipv6 netmask, so lets convert it to prefixlen + # furthermore ipv4 with bad netmask throws 'Rule {} doesnt seem to be an ip, but has a + # mask error that doesn't seem to describe what is going on. + try: + mask_as_ip = ipaddress.ip_address(u'{0}'.format(self['mask'])) + except ValueError: + raise PgHbaValueError('Mask {0} seems to be invalid'.format(self['mask'])) + binvalue = "{0:b}".format(int(mask_as_ip)) + if '01' in binvalue: + raise PgHbaValueError('IP mask {0} seems invalid ' + '(binary value has 1 after 0)'.format(self['mask'])) + prefixlen = binvalue.count('1') + sourcenw = '{0}/{1}'.format(self['src'], prefixlen) + try: + return ipaddress.ip_network(u'{0}'.format(sourcenw), strict=False) + except ValueError: + raise PgHbaValueError('{0} is no valid address range'.format(sourcenw)) + + try: + return ipaddress.ip_network(u'{0}'.format(self['src']), strict=False) + except ValueError: + return self['src'] + + def weight(self, order, numusers, numdbs): + ''' + For networks, every 1 in 'netmask in binary' makes the subnet more specific. + Therefore I chose to use prefix as the weight. + So a single IP (/32) should have twice the weight of a /16 network. + To keep everything in the same weight scale, + - for ipv6, we use a weight scale of 0 (all possible ipv6 addresses) to 128 (single ip) + - for ipv4, we use a weight scale of 0 (all possible ipv4 addresses) to 128 (single ip) + Therefore for ipv4, we use prefixlen (0-32) * 4 for weight, + which corresponds to ipv6 (0-128). + ''' + if order not in PG_HBA_ORDERS: + raise PgHbaRuleError('{0} is not a valid order'.format(order)) + + if self['type'] == 'local': + sourceobj = '' + # local is always 'this server' and therefore considered /32 + srcweight = 130 # (Sort local on top of all) + else: + sourceobj = self.source() + if isinstance(sourceobj, ipaddress.IPv4Network): + srcweight = sourceobj.prefixlen * 4 + elif isinstance(sourceobj, ipaddress.IPv6Network): + srcweight = sourceobj.prefixlen + elif isinstance(sourceobj, str): + # You can also write all to match any IP address, + # samehost to match any of the server's own IP addresses, + # or samenet to match any address in any subnet that the server is connected to. + if sourceobj == 'all': + # (all is considered the full range of all ips, which has a weight of 0) + srcweight = 0 + elif sourceobj == 'samehost': + # (sort samehost second after local) + srcweight = 129 + elif sourceobj == 'samenet': + # Might write some fancy code to determine all prefix's + # from all interfaces and find a sane value for this one. + # For now, let's assume IPv4/24 or IPv6/96 (both have weight 96). + srcweight = 96 + elif sourceobj[0] == '.': + # suffix matching (domain name), let's asume a very large scale + # and therefore a very low weight IPv4/16 or IPv6/64 (both have weight 64). + srcweight = 64 + else: + # hostname, let's asume only one host matches, which is + # IPv4/32 or IPv6/128 (both have weight 128) + srcweight = 128 + + if self['db'] == 'all': + dbweight = numdbs + elif self['db'] == 'replication': + dbweight = 0 + elif self['db'] in ['samerole', 'samegroup']: + dbweight = 1 + else: + dbweight = 1 + self['db'].count(',') + + if self['usr'] == 'all': + uweight = numusers + else: + uweight = 1 + + ret = [] + for character in order: + if character == 'u': + ret.append(uweight) + elif character == 's': + ret.append(srcweight) + elif character == 'd': + ret.append(dbweight) + ret.append(sourceobj) + + return tuple(ret) + + +def main(): + ''' + This function is the main function of this module + ''' + # argument_spec = postgres_common_argument_spec() + argument_spec = dict() + argument_spec.update( + address=dict(type='str', default='samehost', aliases=['source', 'src']), + backup_file=dict(type='str'), + contype=dict(type='str', default=None, choices=PG_HBA_TYPES), + create=dict(type='bool', default=False), + databases=dict(type='str', default='all'), + dest=dict(type='path', required=True), + method=dict(type='str', default='md5', choices=PG_HBA_METHODS), + netmask=dict(type='str'), + options=dict(type='str'), + order=dict(type='str', default="sdu", choices=PG_HBA_ORDERS), + state=dict(type='str', default="present", choices=["absent", "present"]), + users=dict(type='str', default='all') + ) + module = AnsibleModule( + argument_spec=argument_spec, + add_file_common_args=True, + supports_check_mode=True + ) + if not HAS_IPADDRESS: + module.fail_json(msg=missing_required_lib('psycopg2'), exception=IPADDRESS_IMP_ERR) + + contype = module.params["contype"] + create = bool(module.params["create"] or module.check_mode) + if module.check_mode: + backup = False + else: + backup = module.params['backup'] + backup_file = module.params['backup_file'] + databases = module.params["databases"] + dest = module.params["dest"] + + method = module.params["method"] + netmask = module.params["netmask"] + options = module.params["options"] + order = module.params["order"] + source = module.params["address"] + state = module.params["state"] + users = module.params["users"] + + ret = {'msgs': []} + try: + pg_hba = PgHba(dest, order, backup=backup, create=create) + except PgHbaError as error: + module.fail_json(msg='Error reading file:\n{0}'.format(error)) + + if contype: + try: + for database in databases.split(','): + for user in users.split(','): + rule = PgHbaRule(contype, database, user, source, netmask, method, options) + if state == "present": + ret['msgs'].append('Adding') + pg_hba.add_rule(rule) + else: + ret['msgs'].append('Removing') + pg_hba.remove_rule(rule) + except PgHbaError as error: + module.fail_json(msg='Error modifying rules:\n{0}'.format(error)) + file_args = module.load_file_common_arguments(module.params) + ret['changed'] = changed = pg_hba.changed() + if changed: + ret['msgs'].append('Changed') + ret['diff'] = pg_hba.diff + + if not module.check_mode: + ret['msgs'].append('Writing') + try: + if pg_hba.write(backup_file): + module.set_fs_attributes_if_different(file_args, True, pg_hba.diff, + expand=False) + except PgHbaError as error: + module.fail_json(msg='Error writing file:\n{0}'.format(error)) + if pg_hba.last_backup: + ret['backup_file'] = pg_hba.last_backup + + ret['pg_hba'] = [rule for rule in pg_hba.get_rules()] + module.exit_json(**ret) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/postgresql/defaults/main.yml b/test/integration/targets/postgresql/defaults/main.yml index 339a76e72d3..861355dced8 100644 --- a/test/integration/targets/postgresql/defaults/main.yml +++ b/test/integration/targets/postgresql/defaults/main.yml @@ -9,3 +9,16 @@ db_default: 'postgres' tmp_dir: '/tmp' db_session_role1: 'session_role1' db_session_role2: 'session_role2' + +pg_hba_test_ips: +- contype: local + users: 'all,postgres' +- source: '0000:ffff::' + netmask: 'ffff:fff0::' +- source: '192.168.0.0/24' + netmask: '' + databases: 'all,replication' +- source: '0000:ff00::' + netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00' +- source: '172.16.0.0' + netmask: '255.255.0.0' diff --git a/test/integration/targets/postgresql/tasks/main.yml b/test/integration/targets/postgresql/tasks/main.yml index e4f5a203d27..908b1070c15 100644 --- a/test/integration/targets/postgresql/tasks/main.yml +++ b/test/integration/targets/postgresql/tasks/main.yml @@ -851,6 +851,9 @@ # ============================================================ - include: state_dump_restore.yml file=dbdata.tar test_fixture=admin +# postgres_pg_hba module checks +# ============================================================ +- include: postgresql_pg_hba.yml # # Cleanup # diff --git a/test/integration/targets/postgresql/tasks/postgresql_pg_hba.yml b/test/integration/targets/postgresql/tasks/postgresql_pg_hba.yml new file mode 100644 index 00000000000..2bb6a8e3ab3 --- /dev/null +++ b/test/integration/targets/postgresql/tasks/postgresql_pg_hba.yml @@ -0,0 +1,143 @@ +- name: Make sure file does not exist + file: + dest: /tmp/pg_hba.conf + state: absent + +- name: check_mode run + postgresql_pg_hba: + dest: /tmp/pg_hba.conf + contype: host + source: '0000:ffff::' + netmask: 'ffff:fff0::' + method: md5 + backup: true + order: sud + state: "{{item}}" + check_mode: yes + with_items: + - present + - absent + +- name: check_mode check + stat: + path: /tmp/pg_hba.conf + register: pg_hba_checkmode_check + +- name: Remove several ip addresses for idempotency check + postgresql_pg_hba: + contype: "{{item.contype|default('host')}}" + databases: "{{item.databases|default('all')}}" + dest: /tmp/pg_hba.conf + method: md5 + netmask: "{{item.netmask|default('')}}" + order: sud + source: "{{item.source|default('')}}" + state: absent + users: "{{item.users|default('all')}}" + with_items: "{{pg_hba_test_ips}}" + register: pg_hba_idempotency_check1 + +- name: idempotency not creating file check + stat: + path: /tmp/pg_hba.conf + register: pg_hba_idempotency_file_check + +- name: Add several ip addresses + postgresql_pg_hba: + backup: true + contype: "{{item.contype|default('host')}}" + create: true + databases: "{{item.databases|default('all')}}" + dest: /tmp/pg_hba.conf + method: md5 + netmask: "{{item.netmask|default('')}}" + order: sud + source: "{{item.source|default('')}}" + state: present + users: "{{item.users|default('all')}}" + register: pg_hba_change + with_items: "{{pg_hba_test_ips}}" + +- name: read pg_hba rules + postgresql_pg_hba: + dest: /tmp/pg_hba.conf + register: pg_hba + +- name: Add several ip addresses again for idempotency check + postgresql_pg_hba: + contype: "{{item.contype|default('host')}}" + databases: "{{item.databases|default('all')}}" + dest: /tmp/pg_hba.conf + method: md5 + netmask: "{{item.netmask|default('')}}" + order: sud + source: "{{item.source|default('')}}" + state: present + users: "{{item.users|default('all')}}" + with_items: "{{pg_hba_test_ips}}" + register: pg_hba_idempotency_check2 + +- name: pre-backup stat + stat: + path: /tmp/pg_hba.conf + register: prebackupstat + +- name: Add new ip address for backup check and netmask_sameas_prefix check + postgresql_pg_hba: + backup: true + contype: host + dest: /tmp/pg_hba.conf + method: md5 + netmask: 255.255.255.0 + order: sud + source: '172.21.0.0' + state: present + register: pg_hba_backup_check2 + +- name: Add new ip address for netmask_sameas_prefix check + postgresql_pg_hba: + backup: true + contype: host + dest: /tmp/pg_hba.conf + method: md5 + order: sud + source: '172.21.0.0/24' + state: present + register: netmask_sameas_prefix_check + +- name: post-backup stat + stat: + path: "{{pg_hba_backup_check2.backup_file}}" + register: postbackupstat + +- name: Dont allow netmask for src in [all, samehost, samenet] + postgresql_pg_hba: + contype: host + dest: /tmp/pg_hba.conf + method: md5 + netmask: '255.255.255.255' + order: sud + source: all + state: present + register: pg_hba_fail_src_all_with_netmask + ignore_errors: yes + +- assert: + that: + - 'pg_hba.pg_hba == [ + { "db": "all", "method": "md5", "type": "local", "usr": "all" }, + { "db": "all", "method": "md5", "type": "local", "usr": "postgres" }, + { "db": "all", "method": "md5", "src": "0:ff00::/120", "type": "host", "usr": "all" }, + { "db": "all", "method": "md5", "src": "192.168.0.0/24", "type": "host", "usr": "all" }, + { "db": "replication", "method": "md5", "src": "192.168.0.0/24", "type": "host", "usr": "all" }, + { "db": "all", "method": "md5", "src": "172.16.0.0/16", "type": "host", "usr": "all" }, + { "db": "all", "method": "md5", "src": "0:fff0::/28", "type": "host", "usr": "all" } + ]' + - 'pg_hba_change is changed' + - 'pg_hba_checkmode_check.stat.exists == false' + - 'not pg_hba_idempotency_check1 is changed' + - 'not pg_hba_idempotency_check2 is changed' + - 'pg_hba_idempotency_file_check.stat.exists == false' + - 'prebackupstat.stat.checksum == postbackupstat.stat.checksum' + - 'pg_hba_fail_src_all_with_netmask is failed' + - 'not netmask_sameas_prefix_check is changed'