Issue 56430 (#57147)

* Adding integration test for 127.0.0.1/32 and ::1/128.

* Making sure file is not corrupted when render fails

* Fixes #56430

* Adding changelog for MR 57147/Issue 56430
This commit is contained in:
Sebastiaan Mannem 2019-05-31 12:32:29 +02:00 committed by Dag Wieers
parent e1f1f238b6
commit 223f509ea3
4 changed files with 131 additions and 80 deletions

View file

@ -0,0 +1,2 @@
bugfixes:
- "postgresql_pg_hba - Fix TypeError after which pg_hba.conf is wiped (https://github.com/ansible/ansible/issues/56430)"

View file

@ -82,8 +82,10 @@ options:
order: order:
description: description:
- The entries will be written out in a specific order. - 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. With this option you can control by which field they are ordered first, second and last.
- s=source, d=databases, u=users. s=source, d=databases, u=users.
This option is deprecated since 2.9 and will be removed in 2.11.
Sortorder is now hardcoded to sdu.
default: sdu default: sdu
choices: [ sdu, sud, dsu, dus, usd, uds ] choices: [ sdu, sud, dsu, dus, usd, uds ]
state: state:
@ -301,6 +303,7 @@ class PgHba(object):
if not self.changed(): if not self.changed():
return False return False
contents = self.render()
if self.pg_hba_file: if self.pg_hba_file:
if not (os.path.isfile(self.pg_hba_file) or self.create): if not (os.path.isfile(self.pg_hba_file) or self.create):
raise PgHbaError("pg_hba file '{0}' doesn't exist. " raise PgHbaError("pg_hba file '{0}' doesn't exist. "
@ -316,7 +319,7 @@ class PgHba(object):
filed, __path = tempfile.mkstemp(prefix='pg_hba') filed, __path = tempfile.mkstemp(prefix='pg_hba')
fileh = os.fdopen(filed, 'w') fileh = os.fdopen(filed, 'w')
fileh.write(self.render()) fileh.write(contents)
self.unchanged() self.unchanged()
fileh.close() fileh.close()
return True return True
@ -364,11 +367,7 @@ class PgHba(object):
''' '''
This method returns all the rules of the PgHba object This method returns all the rules of the PgHba object
''' '''
rules = sorted(self.rules.values(), 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: for rule in rules:
ret = {} ret = {}
for key, value in rule.items(): for key, value in rule.items():
@ -544,8 +543,12 @@ class PgHbaRule(dict):
except ValueError: except ValueError:
return self['src'] return self['src']
def weight(self, order, numusers, numdbs): def __lt__(self, other):
''' """This function helps sorted to decide how to sort.
It just checks itself against the other and decides on some key values
if it should be sorted higher or lower in the list.
The way it works:
For networks, every 1 in 'netmask in binary' makes the subnet more specific. For networks, every 1 in 'netmask in binary' makes the subnet more specific.
Therefore I chose to use prefix as the weight. Therefore I chose to use prefix as the weight.
So a single IP (/32) should have twice the weight of a /16 network. So a single IP (/32) should have twice the weight of a /16 network.
@ -554,69 +557,100 @@ class PgHbaRule(dict):
- for ipv4, we use a weight scale of 0 (all possible ipv4 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, Therefore for ipv4, we use prefixlen (0-32) * 4 for weight,
which corresponds to ipv6 (0-128). which corresponds to ipv6 (0-128).
''' """
if order not in PG_HBA_ORDERS: myweight = self.source_weight()
raise PgHbaRuleError('{0} is not a valid order'.format(order)) hisweight = other.source_weight()
if myweight != hisweight:
return myweight > hisweight
myweight = self.db_weight()
hisweight = other.db_weight()
if myweight != hisweight:
return myweight < hisweight
myweight = self.user_weight()
hisweight = other.user_weight()
if myweight != hisweight:
return myweight < hisweight
try:
return self['src'] < other['src']
except TypeError:
return self.source_type_weight() < other.source_type_weight()
errormessage = 'We have two rules ({1}, {2})'.format(self, other)
errormessage += ' with exact same weight. Please file a bug.'
raise PgHbaValueError(errormessage)
def source_weight(self):
"""Report the weight of this source net.
Basically this is the netmask, where IPv4 is normalized to IPv6
(IPv4/32 has the same weight as IPv6/128).
"""
if self['type'] == 'local': if self['type'] == 'local':
sourceobj = '' return 130
# 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
sourceobj = self.source()
if isinstance(sourceobj, ipaddress.IPv4Network):
return sourceobj.prefixlen * 4
if isinstance(sourceobj, ipaddress.IPv6Network):
return sourceobj.prefixlen
if 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)
return 0
if sourceobj == 'samehost':
# (sort samehost second after local)
return 129
if 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).
return 96
if 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).
return 64
# hostname, let's asume only one host matches, which is
# IPv4/32 or IPv6/128 (both have weight 128)
return 128
raise PgHbaValueError('Cannot deduct the source weight of this source {1}'.format(sourceobj))
def source_type_weight(self):
"""Give a weight on the type of this source.
Basically make sure that IPv6Networks are sorted higher than IPv4Networks.
This is a 'when all else fails' solution in __lt__.
"""
sourceobj = self.source()
if isinstance(sourceobj, ipaddress.IPv4Network):
return 2
if isinstance(sourceobj, ipaddress.IPv6Network):
return 1
if isinstance(sourceobj, str):
return 0
raise PgHbaValueError('This source {1} is of an unknown type...'.format(sourceobj))
def db_weight(self):
"""Report the weight of the database.
Normally, just 1, but for replication this is 0, and for 'all', this is more than 2.
"""
if self['db'] == 'all': if self['db'] == 'all':
dbweight = numdbs return 100000
elif self['db'] == 'replication': if self['db'] == 'replication':
dbweight = 0 return 0
elif self['db'] in ['samerole', 'samegroup']: if self['db'] in ['samerole', 'samegroup']:
dbweight = 1 return 1
else: return 1 + self['db'].count(',')
dbweight = 1 + self['db'].count(',')
def user_weight(self):
"""Report weight when comparing users."""
if self['usr'] == 'all': if self['usr'] == 'all':
uweight = numusers return 1000000
else: return 1
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(): def main():

View file

@ -18,10 +18,20 @@ pg_hba_test_ips:
- source: '192.168.0.0/24' - source: '192.168.0.0/24'
netmask: '' netmask: ''
databases: 'all,replication' databases: 'all,replication'
- source: '192.168.1.0/24'
netmask: ''
databases: 'all'
method: reject
- source: '127.0.0.1/32'
netmask: ''
- source: '::1/128'
netmask: ''
- source: '0000:ff00::' - source: '0000:ff00::'
netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00' netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00'
method: scram-sha-256
- source: '172.16.0.0' - source: '172.16.0.0'
netmask: '255.255.0.0' netmask: '255.255.0.0'
method: trust
# defaults for test SSL # defaults for test SSL
ssl_db: 'ssl_db' ssl_db: 'ssl_db'

View file

@ -10,7 +10,7 @@
source: '0000:ffff::' source: '0000:ffff::'
netmask: 'ffff:fff0::' netmask: 'ffff:fff0::'
method: md5 method: md5
backup: true backup: 'True'
order: sud order: sud
state: "{{item}}" state: "{{item}}"
check_mode: yes check_mode: yes
@ -28,7 +28,7 @@
contype: "{{item.contype|default('host')}}" contype: "{{item.contype|default('host')}}"
databases: "{{item.databases|default('all')}}" databases: "{{item.databases|default('all')}}"
dest: /tmp/pg_hba.conf dest: /tmp/pg_hba.conf
method: md5 method: "{{item.method|default('md5')}}"
netmask: "{{item.netmask|default('')}}" netmask: "{{item.netmask|default('')}}"
order: sud order: sud
source: "{{item.source|default('')}}" source: "{{item.source|default('')}}"
@ -44,12 +44,12 @@
- name: Add several ip addresses - name: Add several ip addresses
postgresql_pg_hba: postgresql_pg_hba:
backup: true backup: 'True'
contype: "{{item.contype|default('host')}}" contype: "{{item.contype|default('host')}}"
create: true create: 'True'
databases: "{{item.databases|default('all')}}" databases: "{{item.databases|default('all')}}"
dest: /tmp/pg_hba.conf dest: /tmp/pg_hba.conf
method: md5 method: "{{item.method|default('md5')}}"
netmask: "{{item.netmask|default('')}}" netmask: "{{item.netmask|default('')}}"
order: sud order: sud
source: "{{item.source|default('')}}" source: "{{item.source|default('')}}"
@ -68,7 +68,7 @@
contype: "{{item.contype|default('host')}}" contype: "{{item.contype|default('host')}}"
databases: "{{item.databases|default('all')}}" databases: "{{item.databases|default('all')}}"
dest: /tmp/pg_hba.conf dest: /tmp/pg_hba.conf
method: md5 method: "{{item.method|default('md5')}}"
netmask: "{{item.netmask|default('')}}" netmask: "{{item.netmask|default('')}}"
order: sud order: sud
source: "{{item.source|default('')}}" source: "{{item.source|default('')}}"
@ -84,7 +84,7 @@
- name: Add new ip address for backup check and netmask_sameas_prefix check - name: Add new ip address for backup check and netmask_sameas_prefix check
postgresql_pg_hba: postgresql_pg_hba:
backup: true backup: 'True'
contype: host contype: host
dest: /tmp/pg_hba.conf dest: /tmp/pg_hba.conf
method: md5 method: md5
@ -96,7 +96,7 @@
- name: Add new ip address for netmask_sameas_prefix check - name: Add new ip address for netmask_sameas_prefix check
postgresql_pg_hba: postgresql_pg_hba:
backup: true backup: 'True'
contype: host contype: host
dest: /tmp/pg_hba.conf dest: /tmp/pg_hba.conf
method: md5 method: md5
@ -122,17 +122,22 @@
register: pg_hba_fail_src_all_with_netmask register: pg_hba_fail_src_all_with_netmask
ignore_errors: yes ignore_errors: yes
- debug:
var: pg_hba.pg_hba
- assert: - assert:
that: that:
- 'pg_hba.pg_hba == [ - 'pg_hba.pg_hba == [
{ "db": "all", "method": "md5", "type": "local", "usr": "all" },
{ "db": "all", "method": "md5", "type": "local", "usr": "postgres" }, { "db": "all", "method": "md5", "type": "local", "usr": "postgres" },
{ "db": "all", "method": "md5", "src": "0:ff00::/120", "type": "host", "usr": "all" }, { "db": "all", "method": "md5", "type": "local", "usr": "all" },
{ "db": "all", "method": "md5", "src": "192.168.0.0/24", "type": "host", "usr": "all" }, { "db": "all", "method": "md5", "src": "127.0.0.1/32", "type": "host", "usr": "all" },
{ "db": "all", "method": "md5", "src": "::1/128", "type": "host", "usr": "all" },
{ "db": "all", "method": "scram-sha-256", "src": "0:ff00::/120", "type": "host", "usr": "all" },
{ "db": "replication", "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": "192.168.0.0/24", "type": "host", "usr": "all" },
{ "db": "all", "method": "reject", "src": "192.168.1.0/24", "type": "host", "usr": "all" },
{ "db": "all", "method": "trust", "src": "172.16.0.0/16", "type": "host", "usr": "all" },
{ "db": "all", "method": "md5", "src": "0:fff0::/28", "type": "host", "usr": "all" } { "db": "all", "method": "md5", "src": "0:fff0::/28", "type": "host", "usr": "all" }
]' ]'
- 'pg_hba_change is changed' - 'pg_hba_change is changed'
- 'pg_hba_checkmode_check.stat.exists == false' - 'pg_hba_checkmode_check.stat.exists == false'
- 'not pg_hba_idempotency_check1 is changed' - 'not pg_hba_idempotency_check1 is changed'