postgresql_user: set encrypted as default and fix empty password reporting changed (#36931)

* Set encrypted as default and fix empty password reporting changed

* Starting with Postgres 10 `UNENCRYPTED` passwords are removed and
because of that this module fails with the default `encrypted=no`.
Also encrypted passwords are suported since version 7.2
(https://www.postgresql.org/docs/7.2/static/sql-createuser.html) which
went EOL in 2007 and since 7.3 it is the default. Because of this it
makes a lot more sense to make `encrypted=yes` the default. This won't
break backward compatibility, the module would just update the user's
password in the DB in the hashed format and everything else will work
like before. It's also a security bad practice to store passwords in
plain text. fixes #25823
* There was also a bug with `encrypted=yes` and an empty password always
reported as changed.
* Improved documentation for `encrypted`/`password` parameters, and
removed some obsolete notes about passlib.

* Fix clearing user's password to work with all versions of Postgres

* Add tests for clearing the user password

* Fix documentation atfer rebase

* Add changelog fragment
pull/40513/head
Strahinja Kustudic 7 years ago committed by Toshio Kuratomi
parent 062f0444a1
commit 32d6a354d7

@ -19,6 +19,11 @@ See [Porting Guide](https://docs.ansible.com/ansible/devel/porting_guides/portin
* Removed restriction from protocol in cloudflare_dns module to allow other protocols than tcp and udp to be specified. * Removed restriction from protocol in cloudflare_dns module to allow other protocols than tcp and udp to be specified.
* Ansible 2.6 and onwards, `target_id` parameter in `vmware_target_canonical_facts` module is an optional parameter. * Ansible 2.6 and onwards, `target_id` parameter in `vmware_target_canonical_facts` module is an optional parameter.
* `postgresql_user` module changed `encrypted=yes` to be the default. This
shouldn't break any current playbooks, the module will just store passwords
hashed by default. This change was done because Postgres 10 dropped support for
`UNENCRYPTED` passwords and because all versions since Postgres 7.2 support
storing encrypted passwords.
#### Removed modules (previously deprecated) #### Removed modules (previously deprecated)

@ -0,0 +1,7 @@
---
minor_changes:
- "`postgresql_user` module changed `encrypted=yes` to be the default. This
shouldn't break any current playbooks, the module will just store passwords
hashed by default. This change was done because Postgres 10 dropped support for
`UNENCRYPTED` passwords and because all versions since Postgres 7.2 support
storing encrypted passwords."

@ -7,9 +7,11 @@ 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 = '''
@ -34,60 +36,70 @@ version_added: "0.6"
options: options:
name: name:
description: description:
- name of the user (role) to add or remove - Name of the user (role) to add or remove.
required: true required: true
password: password:
description: description:
- set the user's password, before 1.4 this was required. - Set the user's password, before 1.4 this was required.
- > - Password can be passed unhashed or hashed (MD5-hashed).
When passing an encrypted password, the encrypted parameter must also be true, and it must be generated with the format - Unhashed password will automatically be hashed when saved into the
C('str[\\"md5\\"] + md5[ password + username ]'), resulting in a total of 35 characters. An easy way to do this is: database if C(encrypted) parameter is set, otherwise it will be save in
C(echo \\"md5`echo -n \\"verysecretpasswordJOE\\" | md5`\\"). Note that if the provided password string is already in plain text format.
MD5-hashed format, then it is used as-is, regardless of encrypted parameter. - When passing a hashed password it must be generated with the format
C('str["md5"] + md5[ password + username ]'), resulting in a total of
35 characters. An easy way to do this is C(echo "md5$(echo -n
'verysecretpasswordJOE' | md5sum)").
- Note that if the provided password string is already in MD5-hashed
format, then it is used as-is, regardless of C(encrypted) parameter.
db: db:
description: description:
- name of database where permissions will be granted - Name of database where permissions will be granted.
fail_on_user: fail_on_user:
description: description:
- if C(yes), fail when user can't be removed. Otherwise just log and continue - If C(yes), fail when user can't be removed. Otherwise just log and
type: bool continue.
default: 'yes' default: 'yes'
type: bool
port: port:
description: description:
- Database port to connect to. - Database port to connect to.
default: 5432 default: 5432
login_user: login_user:
description: description:
- User (role) used to authenticate with PostgreSQL - User (role) used to authenticate with PostgreSQL.
default: postgres default: postgres
login_password: login_password:
description: description:
- Password used to authenticate with PostgreSQL - Password used to authenticate with PostgreSQL.
login_host: login_host:
description: description:
- Host running PostgreSQL. - Host running PostgreSQL.
default: localhost default: localhost
login_unix_socket: login_unix_socket:
description: description:
- Path to a Unix domain socket for local connections - Path to a Unix domain socket for local connections.
priv: priv:
description: description:
- "PostgreSQL privileges string in the format: C(table:priv1,priv2)" - "PostgreSQL privileges string in the format: C(table:priv1,priv2)."
role_attr_flags: role_attr_flags:
description: description:
- "PostgreSQL role attributes string in the format: CREATEDB,CREATEROLE,SUPERUSER" - "PostgreSQL role attributes string in the format: CREATEDB,CREATEROLE,SUPERUSER."
- Note that '[NO]CREATEUSER' is deprecated. - Note that '[NO]CREATEUSER' is deprecated.
default: ""
choices: ["[NO]SUPERUSER", "[NO]CREATEROLE", "[NO]CREATEDB", "[NO]INHERIT", "[NO]LOGIN", "[NO]REPLICATION", "[NO]BYPASSRLS"] choices: ["[NO]SUPERUSER", "[NO]CREATEROLE", "[NO]CREATEDB", "[NO]INHERIT", "[NO]LOGIN", "[NO]REPLICATION", "[NO]BYPASSRLS"]
state: state:
description: description:
- The user (role) state - The user (role) state.
default: present default: present
choices: [ present, absent ] choices: ["present", "absent"]
encrypted: encrypted:
description: description:
- whether the password is stored hashed in the database. boolean. Passwords can be passed already hashed or unhashed, and postgresql ensures the - Whether the password is stored hashed in the database. Passwords can be
stored password is hashed when encrypted is set. passed already hashed or unhashed, and postgresql ensures the stored
password is hashed when C(encrypted) is set.
- "Note: Postgresql 10 and newer doesn't support unhashed passwords."
- Previous to Ansible 2.6, this was C(no) by default.
default: 'yes'
type: bool
version_added: '1.4' version_added: '1.4'
expires: expires:
description: description:
@ -97,22 +109,26 @@ options:
version_added: '1.4' version_added: '1.4'
no_password_changes: no_password_changes:
description: description:
- if C(yes), don't inspect database for password changes. Effective when C(pg_authid) is not accessible (such as AWS RDS). Otherwise, make - If C(yes), don't inspect database for password changes. Effective when
C(pg_authid) is not accessible (such as AWS RDS). Otherwise, make
password changes as necessary. password changes as necessary.
type: bool
default: 'no' default: 'no'
type: bool
version_added: '2.0' version_added: '2.0'
ssl_mode: ssl_mode:
description: description:
- Determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server. - Determines whether or with what priority a secure SSL TCP/IP connection
- See https://www.postgresql.org/docs/current/static/libpq-ssl.html for more information on the modes. will be negotiated with the server.
- See U(https://www.postgresql.org/docs/current/static/libpq-ssl.html) for
more information on the modes.
- Default of C(prefer) matches libpq default. - Default of C(prefer) matches libpq default.
default: prefer default: prefer
choices: [disable, allow, prefer, require, verify-ca, verify-full] choices: ["disable", "allow", "prefer", "require", "verify-ca", "verify-full"]
version_added: '2.3' version_added: '2.3'
ssl_rootcert: ssl_rootcert:
description: description:
- Specifies the name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be - Specifies the name of a file containing SSL certificate authority (CA)
certificate(s). If the file exists, the server's certificate will be
verified to be signed by one of these authorities. verified to be signed by one of these authorities.
version_added: '2.3' version_added: '2.3'
conn_limit: conn_limit:
@ -128,14 +144,11 @@ notes:
PostgreSQL must also be installed on the remote host. For Ubuntu-based PostgreSQL must also be installed on the remote host. For Ubuntu-based
systems, install the postgresql, libpq-dev, and python-psycopg2 packages systems, install the postgresql, libpq-dev, and python-psycopg2 packages
on the remote host before using this module. on the remote host before using this module.
- If the passlib library is installed, then passwords that are encrypted
in the DB but not encrypted when passed as arguments can be checked for
changes. If the passlib library is not installed, unencrypted passwords
stored in the DB encrypted will be assumed to have changed.
- If you specify PUBLIC as the user, then the privilege changes will apply - If you specify PUBLIC as the user, then the privilege changes will apply
to all users. You may not specify password or role_attr_flags when the to all users. You may not specify password or role_attr_flags when the
PUBLIC user is specified. PUBLIC user is specified.
- The ssl_rootcert parameter requires at least Postgres version 8.4 and I(psycopg2) version 2.4.3. - The ssl_rootcert parameter requires at least Postgres version 8.4 and
I(psycopg2) version 2.4.3.
requirements: [ psycopg2 ] requirements: [ psycopg2 ]
author: "Ansible Core Team" author: "Ansible Core Team"
''' '''
@ -148,10 +161,11 @@ EXAMPLES = '''
password: ceec4eif7ya password: ceec4eif7ya
priv: "CONNECT/products:ALL" priv: "CONNECT/products:ALL"
# Create rails user, grant privilege to create other databases and demote rails from super user status # Create rails user, set its password (MD5-hashed) and grant privilege to create other
# databases and demote rails from super user status
- postgresql_user: - postgresql_user:
name: rails name: rails
password: secret password: md59543f1d82624df2b31672ec0f7050460
role_attr_flags: CREATEDB,NOSUPERUSER role_attr_flags: CREATEDB,NOSUPERUSER
# Remove test user privileges from acme # Remove test user privileges from acme
@ -184,7 +198,7 @@ EXAMPLES = '''
- postgresql_user: - postgresql_user:
db: test db: test
user: test user: test
password: NULL password: ""
''' '''
import itertools import itertools
@ -248,7 +262,7 @@ def user_add(cursor, user, password, role_attr_flags, encrypted, expires, conn_l
query_password_data = dict(password=password, expires=expires) query_password_data = dict(password=password, expires=expires)
query = ['CREATE USER %(user)s' % query = ['CREATE USER %(user)s' %
{"user": pg_quote_identifier(user, 'role')}] {"user": pg_quote_identifier(user, 'role')}]
if password is not None: if password is not None and password != '':
query.append("WITH %(crypt)s" % {"crypt": encrypted}) query.append("WITH %(crypt)s" % {"crypt": encrypted})
query.append("PASSWORD %(password)s") query.append("PASSWORD %(password)s")
if expires is not None: if expires is not None:
@ -277,11 +291,16 @@ def user_should_we_change_password(current_role_attrs, user, password, encrypted
# Do we actually need to do anything? # Do we actually need to do anything?
pwchanging = False pwchanging = False
if password is not None: if password is not None:
# Empty password means that the role shouldn't have a password, which
# means we need to check if the current password is None.
if password == '':
if current_role_attrs['rolpassword'] is not None:
pwchanging = True
# 32: MD5 hashes are represented as a sequence of 32 hexadecimal digits # 32: MD5 hashes are represented as a sequence of 32 hexadecimal digits
# 3: The size of the 'md5' prefix # 3: The size of the 'md5' prefix
# When the provided password looks like a MD5-hash, value of # When the provided password looks like a MD5-hash, value of
# 'encrypted' is ignored. # 'encrypted' is ignored.
if ((password.startswith('md5') and len(password) == 32 + 3) or encrypted == 'UNENCRYPTED'): elif (password.startswith('md5') and len(password) == 32 + 3) or encrypted == 'UNENCRYPTED':
if password != current_role_attrs['rolpassword']: if password != current_role_attrs['rolpassword']:
pwchanging = True pwchanging = True
elif encrypted == 'ENCRYPTED': elif encrypted == 'ENCRYPTED':
@ -360,8 +379,11 @@ def user_alter(db_connection, module, user, password, role_attr_flags, encrypted
alter = ['ALTER USER %(user)s' % {"user": pg_quote_identifier(user, 'role')}] alter = ['ALTER USER %(user)s' % {"user": pg_quote_identifier(user, 'role')}]
if pwchanging: if pwchanging:
if password != '':
alter.append("WITH %(crypt)s" % {"crypt": encrypted}) alter.append("WITH %(crypt)s" % {"crypt": encrypted})
alter.append("PASSWORD %(password)s") alter.append("PASSWORD %(password)s")
else:
alter.append("WITH PASSWORD NULL")
alter.append(role_attr_flags) alter.append(role_attr_flags)
elif role_attr_flags: elif role_attr_flags:
alter.append('WITH %s' % role_attr_flags) alter.append('WITH %s' % role_attr_flags)
@ -715,7 +737,7 @@ def main():
port=dict(default='5432'), port=dict(default='5432'),
fail_on_user=dict(type='bool', default='yes'), fail_on_user=dict(type='bool', default='yes'),
role_attr_flags=dict(default=''), role_attr_flags=dict(default=''),
encrypted=dict(type='bool', default='no'), encrypted=dict(type='bool', default='yes'),
no_password_changes=dict(type='bool', default='no'), no_password_changes=dict(type='bool', default='no'),
expires=dict(default=None), expires=dict(default=None),
ssl_mode=dict(default='prefer', choices=[ ssl_mode=dict(default='prefer', choices=[

@ -161,6 +161,52 @@
- <<: *changed - <<: *changed
- name: 'Using MD5-hashed password: check that password changed when clearing the password'
<<: *task_parameters
postgresql_user:
<<: *parameters
password: ''
encrypted: 'yes'
environment:
PGCLIENTENCODING: 'UTF8'
- <<: *changed
- name: 'Using MD5-hashed password: check that password not changed when clearing the password again'
<<: *task_parameters
postgresql_user:
<<: *parameters
password: ''
encrypted: 'yes'
environment:
PGCLIENTENCODING: 'UTF8'
PGOPTIONS: '-c default_transaction_read_only=on' # ensure 'alter user' query isn't executed
- <<: *not_changed
- name: 'Using cleartext password: check that password not changed when clearing the password again'
<<: *task_parameters
postgresql_user:
<<: *parameters
password: ''
encrypted: 'no'
environment:
PGCLIENTENCODING: 'UTF8'
PGOPTIONS: '-c default_transaction_read_only=on' # ensure 'alter user' query isn't executed
- <<: *not_changed
- name: 'Using MD5-hashed password: check that password changed when using a cleartext password'
<<: *task_parameters
postgresql_user:
<<: *parameters
password: '{{ db_password1 }}'
encrypted: 'yes'
environment:
PGCLIENTENCODING: 'UTF8'
- <<: *changed
when: encrypted == 'yes' when: encrypted == 'yes'
- block: - block:
@ -201,6 +247,52 @@
- <<: *changed - <<: *changed
- name: 'Using cleartext password: check that password changed when clearing the password'
<<: *task_parameters
postgresql_user:
<<: *parameters
password: ''
encrypted: 'no'
environment:
PGCLIENTENCODING: 'UTF8'
- <<: *changed
- name: 'Using cleartext password: check that password not changed when clearing the password again'
<<: *task_parameters
postgresql_user:
<<: *parameters
password: ''
encrypted: 'no'
environment:
PGCLIENTENCODING: 'UTF8'
PGOPTIONS: '-c default_transaction_read_only=on' # ensure 'alter user' query isn't executed
- <<: *not_changed
- name: 'Using MD5-hashed password: check that password not changed when clearing the password again'
<<: *task_parameters
postgresql_user:
<<: *parameters
password: ''
encrypted: 'yes'
environment:
PGCLIENTENCODING: 'UTF8'
PGOPTIONS: '-c default_transaction_read_only=on' # ensure 'alter user' query isn't executed
- <<: *not_changed
- name: 'Using cleartext password: check that password changed when using cleartext password'
<<: *task_parameters
postgresql_user:
<<: *parameters
password: "{{ db_password1 }}"
encrypted: 'no'
environment:
PGCLIENTENCODING: 'UTF8'
- <<: *changed
when: encrypted == 'no' when: encrypted == 'no'
- name: Remove user - name: Remove user

@ -643,7 +643,6 @@ lib/ansible/modules/database/postgresql/postgresql_schema.py E322
lib/ansible/modules/database/postgresql/postgresql_schema.py E324 lib/ansible/modules/database/postgresql/postgresql_schema.py E324
lib/ansible/modules/database/postgresql/postgresql_user.py E322 lib/ansible/modules/database/postgresql/postgresql_user.py E322
lib/ansible/modules/database/postgresql/postgresql_user.py E324 lib/ansible/modules/database/postgresql/postgresql_user.py E324
lib/ansible/modules/database/postgresql/postgresql_user.py E325
lib/ansible/modules/database/postgresql/postgresql_user.py E326 lib/ansible/modules/database/postgresql/postgresql_user.py E326
lib/ansible/modules/database/proxysql/proxysql_backend_servers.py E322 lib/ansible/modules/database/proxysql/proxysql_backend_servers.py E322
lib/ansible/modules/database/proxysql/proxysql_backend_servers.py E325 lib/ansible/modules/database/proxysql/proxysql_backend_servers.py E325

Loading…
Cancel
Save