Repository: ansible/ansible

Issue:
UNENCRYPTED option is no longer supported in PG10
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
`postgresql_user` module

##### SUMMARY
It's not really a bug report, but an attempt to prevent one in the future. In the next version of PostgreSQL (to be more precise PG 10),  the `UNENCRYPTED` option is no longer supported for `CREATE/ALTER USER ... PASSSWORD` (see [release note](https://www.postgresql.org/docs/10/static/release-10.html) and [corresponding message](https://www.postgresql.org/message-id/flat/E1d7e0a-0006az-Bh%40gemulon.postgresql.org#E1d7e0a-0006az-Bh@gemulon.postgresql.org)). It means that default behaviour of `postgresql_user` command (`encrypted=no`, which leads to `CREATE USER WITH UNENCRYPTED PASSWORD ...`) will cause a problems with that version. I couldn't find any discussion/github issue about that in this repo, so maybe it makes sense to raise this question. Maybe it's a good idea to verify PG version in that case to provide backward compatible solution, or just force use `encrypted=yes`. 

##### STEPS TO REPRODUCE
Use the following step with PG10:

```yaml
- name: Create user
  postgresql_user:
    name={{ db_user }}
    password={{ db_pass }}
    role_attr_flags=CREATEDB,SUPERUSER
    state=present
```

##### EXPECTED RESULTS
A user should be created in a PostgreSQL database.

##### ACTUAL RESULTS
```
TASK [Create user] *************************************************************
fatal: [52.36.192.180]: FAILED! => {"changed": false, "failed": true, "module_stderr": "Traceback (most recent call last):\n  File \"/tmp/ansible__Aw4zB/ansible_module_postgresql_user.py\", line 661, in <module>\n    main()\n  File \"/tmp/ansible__Aw4zB/ansible_module_postgresql_user.py\", line 625, in main\n    changed = user_add(cursor, user, password, role_attr_flags, encrypted, expires)\n  File \"/tmp/ansible__Aw4zB/ansible_module_postgresql_user.py\", line 219, in user_add\n    cursor.execute(query, query_password_data)\n  File \"/usr/lib/python2.7/dist-packages/psycopg2/extras.py\", line 120, in execute\n    return super(DictCursor, self).execute(query, vars)\npsycopg2.NotSupportedError: UNENCRYPTED PASSWORD is no longer supported\nLINE 1: CREATE USER \"test\" WITH UNENCRYPTED PASSWORD 'some_password' SUPERU...\n                                ^\nHINT:  Remove UNENCRYPTED to store the password in encrypted form instead.\n\n", "module_stdout": "", "msg": "MODULE FAILURE", "parsed": false}
```



Discussion:
@erthalion Greetings! Thanks for taking the time to open this issue. In order for the community to handle your issue effectively, we need a bit more information. 

Here are the items we could not find in your description:
- ansible version

Please set the description of this issue with this template:
https://raw.githubusercontent.com/ansible/ansible/devel/.github/ISSUE_TEMPLATE.md

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: issue_missing_data --->
cc @nerzhul @matburt
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
I confirm this is an issue.
confirmed
:+1: confirmed issue
Confirming this too, but isn't it rather an incompatibility issue of [psycopg2](https://github.com/psycopg/psycopg2)? The error is in `/usr/lib/python3/dist-packages/psycopg2/extras.py`.

My psycopg2 version is 2.6.1. Don't have time now to test it with the latest version (2.7.3.1). Officially psycopg2 does not support PG10. See [here](http://initd.org/psycopg/docs/install.html). I don't find any GitHub issues regarding PG10 though.
Ah no forget what I said. Issue is in https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/database/postgresql/postgresql_user.py. In PG10 UNENCRYPTED is not supported.
If you set `encrypted` to `yes` it works.

``` yaml
- postgresql_user:
    db: acme
    name: django
    password: ceec4eif7ya
    encrypted: yes
```
@erthalion You have not responded to information requests in this issue so we will assume it no longer affects you. If you are still interested in this, please create a new issue with the requested information.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: needs_info_base --->
Issue still present in master, not clear why this has been closed, especially considering that there is a trivial fix (changing the default of 'encrypted' to True).
@erthalion You have not responded to information requests in this issue so we will assume it no longer affects you. If you are still interested in this, please create a new issue with the requested information.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: needs_info_base --->
Related to https://github.com/ansible/ansible/issues/33753

Resolving patch:
diff --git a/lib/ansible/modules/database/postgresql/postgresql_user.py b/lib/ansible/modules/database/postgresql/postgresql_user.py
--- a/lib/ansible/modules/database/postgresql/postgresql_user.py
+++ b/lib/ansible/modules/database/postgresql/postgresql_user.py
@@ -7,9 +7,11 @@
 __metaclass__ = type
 
 
-ANSIBLE_METADATA = {'metadata_version': '1.1',
-                    'status': ['stableinterface'],
-                    'supported_by': 'community'}
+ANSIBLE_METADATA = {
+    'metadata_version': '1.1',
+    'status': ['stableinterface'],
+    'supported_by': 'community'
+}
 
 
 DOCUMENTATION = '''
@@ -34,60 +36,70 @@
 options:
   name:
     description:
-      - name of the user (role) to add or remove
+      - Name of the user (role) to add or remove.
     required: true
   password:
     description:
-      - set the user's password, before 1.4 this was required.
-      - >
-        When passing an encrypted password, the encrypted parameter must also be true, and 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\\" | md5`\\"). Note that if the provided password string is already in
-        MD5-hashed format, then it is used as-is, regardless of encrypted parameter.
+      - Set the user's password, before 1.4 this was required.
+      - Password can be passed unhashed or hashed (MD5-hashed).
+      - Unhashed password will automatically be hashed when saved into the
+        database if C(encrypted) parameter is set, otherwise it will be save in
+        plain text format.
+      - 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:
     description:
-      - name of database where permissions will be granted
+      - Name of database where permissions will be granted.
   fail_on_user:
     description:
-      - if C(yes), fail when user can't be removed. Otherwise just log and continue
-    type: bool
+      - If C(yes), fail when user can't be removed. Otherwise just log and
+        continue.
     default: 'yes'
+    type: bool
   port:
     description:
       - Database port to connect to.
     default: 5432
   login_user:
     description:
-      - User (role) used to authenticate with PostgreSQL
+      - User (role) used to authenticate with PostgreSQL.
     default: postgres
   login_password:
     description:
-      - Password used to authenticate with PostgreSQL
+      - Password used to authenticate with PostgreSQL.
   login_host:
     description:
       - Host running PostgreSQL.
     default: localhost
   login_unix_socket:
     description:
-      - Path to a Unix domain socket for local connections
+      - Path to a Unix domain socket for local connections.
   priv:
     description:
-      - "PostgreSQL privileges string in the format: C(table:priv1,priv2)"
+      - "PostgreSQL privileges string in the format: C(table:priv1,priv2)."
   role_attr_flags:
     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.
-    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:
     description:
-      - The user (role) state
+      - The user (role) state.
     default: present
-    choices: [ present, absent ]
+    choices: ["present", "absent"]
   encrypted:
     description:
-      - whether the password is stored hashed in the database. boolean. Passwords can be passed already hashed or unhashed, and postgresql ensures the
-        stored password is hashed when encrypted is set.
+      - Whether the password is stored hashed in the database. Passwords can be
+        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'
   expires:
     description:
@@ -97,22 +109,26 @@
     version_added: '1.4'
   no_password_changes:
     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.
-    type: bool
     default: 'no'
+    type: bool
     version_added: '2.0'
   ssl_mode:
     description:
-      - Determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server.
-      - See https://www.postgresql.org/docs/current/static/libpq-ssl.html for more information on the modes.
+      - Determines whether or with what priority a secure SSL TCP/IP connection
+        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: prefer
-    choices: [disable, allow, prefer, require, verify-ca, verify-full]
+    choices: ["disable", "allow", "prefer", "require", "verify-ca", "verify-full"]
     version_added: '2.3'
   ssl_rootcert:
     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.
     version_added: '2.3'
   conn_limit:
@@ -128,14 +144,11 @@
      PostgreSQL must also be installed on the remote host. For Ubuntu-based
      systems, install the postgresql, libpq-dev, and python-psycopg2 packages
      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
      to all users. You may not specify password or role_attr_flags when the
      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 ]
 author: "Ansible Core Team"
 '''
@@ -148,10 +161,11 @@
     password: ceec4eif7ya
     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:
     name: rails
-    password: secret
+    password: md59543f1d82624df2b31672ec0f7050460
     role_attr_flags: CREATEDB,NOSUPERUSER
 
 # Remove test user privileges from acme
@@ -184,7 +198,7 @@
 - postgresql_user:
     db: test
     user: test
-    password: NULL
+    password: ""
 '''
 
 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 = ['CREATE USER %(user)s' %
              {"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("PASSWORD %(password)s")
     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?
     pwchanging = False
     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
         #  3: The size of the 'md5' prefix
         # When the provided password looks like a MD5-hash, value of
         # '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']:
                 pwchanging = True
         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')}]
         if pwchanging:
-            alter.append("WITH %(crypt)s" % {"crypt": encrypted})
-            alter.append("PASSWORD %(password)s")
+            if password != '':
+                alter.append("WITH %(crypt)s" % {"crypt": encrypted})
+                alter.append("PASSWORD %(password)s")
+            else:
+                alter.append("WITH PASSWORD NULL")
             alter.append(role_attr_flags)
         elif role_attr_flags:
             alter.append('WITH %s' % role_attr_flags)
@@ -715,7 +737,7 @@ def main():
             port=dict(default='5432'),
             fail_on_user=dict(type='bool', default='yes'),
             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'),
             expires=dict(default=None),
             ssl_mode=dict(default='prefer', choices=[


Test patch:
diff --git a/test/integration/targets/postgresql/tasks/test_password.yml b/test/integration/targets/postgresql/tasks/test_password.yml
--- a/test/integration/targets/postgresql/tasks/test_password.yml
+++ b/test/integration/targets/postgresql/tasks/test_password.yml
@@ -161,6 +161,52 @@
 
     - <<: *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'
 
   - block:
@@ -201,6 +247,52 @@
 
     - <<: *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'
 
   - name: Remove user
diff --git a/test/sanity/validate-modules/ignore.txt b/test/sanity/validate-modules/ignore.txt
--- a/test/sanity/validate-modules/ignore.txt
+++ b/test/sanity/validate-modules/ignore.txt
@@ -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_user.py E322
 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/proxysql/proxysql_backend_servers.py E322
 lib/ansible/modules/database/proxysql/proxysql_backend_servers.py E325